HeliosLang/compiler

TxOutput.value.get() throws error

Closed this issue · 8 comments

This is probably a user error (Helios v0.6.5), but I am having trouble with .value.get(). My script is as follows:

minting voting_ballot

const POLLS_CLOSE: Time = Time::new(${pollsClose})
const REFERENCE_POLICY_HASH : MintingPolicyHash = MintingPolicyHash::new("${referencePolicyId}".serialize())

enum Redeemer {
  Mint
}

func tx_output_spent(voting_asset: AssetClass, tx_out: TxOutput) -> Bool {
  print(voting_asset.serialize().show());
  print(tx_out.value.serialize().show());
  print(tx_out.value.get(voting_asset).show());   // <-- THIS LINE THROWS AN ERROR
  print("I don't show up in the error log");
  tx_out.value.get(voting_asset) == 1
}

func assets_were_spent(minted_assets: Value, policy: MintingPolicyHash, outputs: []TxOutput) -> Bool {
  tx_sends_to_self: Bool = minted_assets.get_policy(policy).all((asset_id: ByteArray, amount: Int) -> Bool {
    print("FOO");
    print(REFERENCE_POLICY_HASH.serialize().show());
    print("BAR");
    print(asset_id.show());
    voting_asset: AssetClass = AssetClass::new(REFERENCE_POLICY_HASH, asset_id);
    //print(voting_asset.serialize().show());
    outputs.fold((spent: Bool, tx_out: TxOutput) -> Bool { tx_output_spent(voting_asset, tx_out) || spent }, false) && amount == 1
  });
  if (tx_sends_to_self) {
    true
  } else {
    print("The minted assets were never sent to self");
    false
  }
}

func polls_are_still_open(time_range: TimeRange) -> Bool {
  tx_during_polls_open: Bool = time_range.is_before(POLLS_CLOSE);
  if (tx_during_polls_open) {
    true
  } else {
    print("Invalid time range: " + time_range.serialize().show() + " (polls close at " + POLLS_CLOSE.serialize().show() + ")");
    false
  }
}

func main(redeemer: Redeemer, ctx: ScriptContext) -> Bool {
  redeemer.switch {
    Mint =>  {
      tx: Tx = ctx.tx;
      minted_policy: MintingPolicyHash = ctx.get_current_minting_policy_hash();

      polls_are_still_open(tx.time_range) && assets_were_spent(tx.minted, minted_policy, tx.outputs)
    }
  }
}

The error/trace returned from compilation is:

{
 "ScriptFailures": {
 "mint:0": [
 {
 "validatorFailed": {
 "error": "An error has occurred: User error:\
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.",
 "traces": [
 "FOO",
 "583a58383536666439336566386562653463373336343561636164616634373737313663653937333339303530323434363434636566333235373431",
 "BAR",
 "57696c6454616e677a2032",
 "d8799f583a583835366664393365663865626534633733363435616361646166343737373136636539373333393035303234343634346365663332353734314b57696c6454616e677a2032ff",
 "a240a1401a0011d28a581c25d7da6b446fee355edc48c07eefae55b4cdbcd534bea9c7dbd48f07a14b57696c6454616e677a203201"
 ]
 }
 }
 ]
 }
}

Yeah, value.get(asset_class) throws an error if asset_class isn't found. Maybe it's too restrictive though and should just return 0, or I can add a method called get_safe

I remember there being a few cases where it made sense to implement it like this, but maybe in general the return value should be 0

Ah that makes sense. This could just be a doc fix too. Per the Chapter 4 documentation:

// Gets the amount of a specific AssetClass contained in a Value.
func get(self, asset_class: AssetClass) -> Int

No mention of error there so I was assuming it would just return 0. I will use .contains() instead.

Can you advise on .contains()? It is returning false, but it should be returning true:

const POLLS_CLOSE: Time = Time::new(${pollsClose})
const REFERENCE_POLICY_HASH: MintingPolicyHash = MintingPolicyHash::new("${referencePolicyId}".serialize())
const SINGLE_NFT: Int = 1

func tx_outputs_contain(voting_asset: AssetClass, outputs: []TxOutput) -> Bool {
      outputs.fold((spent: Bool, tx_out: TxOutput) -> Bool {
        print("Searching...");
        print(voting_asset.serialize().show());
        print(tx_out.value.serialize().show());
        tx_out.value.contains(Value::new(voting_asset, SINGLE_NFT)) || spent
      }, false)
    }

    func assets_were_spent(minted_assets: Value, policy: MintingPolicyHash, outputs: []TxOutput) -> Bool {
      tx_sends_to_self: Bool = minted_assets.get_policy(policy).all((asset_id: ByteArray, amount: Int) -> Bool {
        voting_asset: AssetClass = AssetClass::new(REFERENCE_POLICY_HASH, asset_id);
        tx_outputs_contain(voting_asset, outputs) && amount == SINGLE_NFT
      });
      if (tx_sends_to_self) {
        true
      } else {
        print("The NFTs with voting power for the ballots were never sent-to-self");
        false
      }
    }

The traces are:

"traces": [
 "Searching...",
 "d8799f583a583835366664393365663865626534633733363435616361646166343737373136636539373333393035303234343634346365663332353734314b57696c6454616e677a2032ff",
 "a240a1401a0011d28a581c0c33803c89eb26bf0094f7e09a211d4deb8bfb4e38b0af0191d0a4a5a14b57696c6454616e677a203201",
 "Searching...",
 "d8799f583a583835366664393365663865626534633733363435616361646166343737373136636539373333393035303234343634346365663332353734314b57696c6454616e677a2032ff",
 "a240a1401a0011d28a581c56fd93ef8ebe4c73645acadaf477716ce97339050244644cef325741a14b57696c6454616e677a203201",
 "Searching...",
 "d8799f583a583835366664393365663865626534633733363435616361646166343737373136636539373333393035303234343634346365663332353734314b57696c6454616e677a2032ff",
 "a440a1401a001c6920581c56fd93ef8ebe4c73645acadaf477716ce97339050244644cef325741a24c57696c6454616e677a203130014b57696c6454616e677a203301581c698d6cab8b61576b3b0becdf9f8ba041efe4006cfd29451280c7a95ea24c57696c6454616e677a203130014b57696c6454616e677a203501581cddbbf4ecd96a0237941bdf6e64c39db9d9d4d4088fa2e0eb9c7609e6a2581a436c756d73792056616c6c6579204c616e6420506c6f7420233201581a436c756d73792056616c6c6579204c616e6420506c6f7420233401",
 "Searching..."
 "d8799f583a583835366664393365663865626534633733363435616361646166343737373136636539373333393035303234343634346365663332353734314b57696c6454616e677a2032ff",
 "a140a1401b0000000248bfc434",
 ]

I believe the third UTxO in the trace is the one that contains the send-to-self but I might be doing something wrong.

I think it has to do with my MintingPolicyHash::new I'm not finding much documentation on that though. The original Plutus Core says something about SHA256? I have this in JS code:

const REFERENCE_POLICY_ID = '56fd93ef8ebe4c73645acadaf477716ce97339050244644cef325741';

That SHA256 stuff is wrong (and a known documentation issue that IOG should've been fixed by now). MintingPolicyHash should be blake2b-224 hash of [0x02, ...scriptbytes] (for PlutusV2).

Indeed looks like an issue with MintingPolicyHash::new(). You should use const REFERENCE_POLICY_ID = #56fd93ef8ebe4c73645acadaf477716ce97339050244644cef325741 (bytearray, no semicolon). And then MintingPolicyHash::new(REFERENCE_POLICY_ID) (not serialize!)

MintingPolicyHash and PolicyID are the same thing (sorry for the confusion, but that's the fault of IOG for using different names for the same thing throughout their codebase, I just ended up picking the most verbose name).

Minor recommendation: you can use outputs.any instead of outputs.fold (is a bit more efficient because it quits the loop early).

Great. Works. Here's the final JS code with inline Helios. Closing.

const SINGLE_NFT = 1n;
const POLLS_OPEN = 1664462000000;
const POLLS_CLOSE = 1666242000000; // 10/20/22 00:00:00
const REFERENCE_POLICY_ID = '56fd93ef8ebe4c73645acadaf477716ce97339050244644cef325741';
const TEN_MINS = 600000;

function getBallotSourceCodeStr(referencePolicyId, pollsClose) {
  return `
    minting voting_ballot

    const POLLS_CLOSE: Time = Time::new(${pollsClose})
    const REFERENCE_POLICY_HASH: MintingPolicyHash = MintingPolicyHash::new(#${referencePolicyId})
    const SINGLE_NFT: Int = 1

    enum Redeemer {
      Mint
    }

    func tx_outputs_contain(voting_asset: AssetClass, outputs: []TxOutput) -> Bool {
      outputs.any((tx_out: TxOutput) -> Bool {
        print("Searching...");
        print(voting_asset.serialize().show());
        print(tx_out.value.serialize().show());
        tx_out.value.contains(Value::new(voting_asset, SINGLE_NFT))
      })
    }

    func assets_were_spent(minted_assets: Value, policy: MintingPolicyHash, outputs: []TxOutput) -> Bool {
      tx_sends_to_self: Bool = minted_assets.get_policy(policy).all((asset_id: ByteArray, amount: Int) -> Bool {
        voting_asset: AssetClass = AssetClass::new(REFERENCE_POLICY_HASH, asset_id);
        tx_outputs_contain(voting_asset, outputs) && amount == SINGLE_NFT
      });
      if (tx_sends_to_self) {
        true
      } else {
        print("The NFTs with voting power for the ballots were never sent-to-self");
        false
      }
    }

    func polls_are_still_open(time_range: TimeRange) -> Bool {
      tx_during_polls_open: Bool = time_range.is_before(POLLS_CLOSE);
      if (tx_during_polls_open) {
        true
      } else {
        print("Invalid time range: " + time_range.serialize().show() + " (polls close at " + POLLS_CLOSE.serialize().show() + ")");
        false
      }
    }

    func main(redeemer: Redeemer, ctx: ScriptContext) -> Bool {
      redeemer.switch {
        Mint =>  {
          tx: Tx = ctx.tx;
          minted_policy: MintingPolicyHash = ctx.get_current_minting_policy_hash();

          polls_are_still_open(tx.time_range) && assets_were_spent(tx.minted, minted_policy, tx.outputs)
        }
      }
    }
  `;
}

I've also added a notice in the docs for the value.get() method that an error is thrown if asset_class isn't found (and a recommendation to use value.contains() if the goal is simply to check if value contains something).