Helios Testing

Part 1: Coin Selection

ยท

5 min read

How did I get here

So recently I have been working with a great team of people developing a Cardano Marketplace called Dropspot. To develop this platform we needed to create Smart Contracts. Haskell is hard! I've been writing code for 20+ years, but it has always been procedural, and it has always been Java-like (think curly brackets etc), the Math like constructs of Haskell I just found too much for my brain! So although I did complete the Plutus Pioneers 3rd Cohort, and develop a working Plutus Smart Contract using Haskell it was too slow and difficult to perform Integration tests (sure it would be possible but I didn't have that sort of time!).

Along came Vasil and we needed to update our Plutus V1 contract to take advantage of the improvements that were being made for Plutus V2. I didn't want to have to rewrite our contract again (also the business was coming up with some amazing ideas which made me quite concerned as to how I was going to be able to implement them with Haskell). I decided to have a look around and discovered that some very clever people were developing DSLs (Domain Specific Languages) for Plutus Smart Contracts! Helios is the language that we decided to move with. It had its Transaction Builder (the Helios API) and the Language itself, while a little different from what I was used to was close enough that I could with the help of the rather well-written Helios Book, create a feature equivalent contract with a simple API that could be integrated into our existing project in just a few days. Boom! I now could properly support the business in developing how it would like to.

Testing

Helios recently released what I think is a leveling up of the ecosystem; the NetworkEmulator class. Helios already has a pretty solid Plutus VM built into the API (it will for instance calculate the ExUnits for you as part of the Tx::finalize method, pretty impressive).

Simple Coin Selection Testing

StackBlitz - Coin Selection Test

The first example I have for utilising the NetworkEmulator and to a lesser degree the WalletEmulator is my Simple Coin Selection program.

import { bytesToHex, UTxO, Value } from '@hyperionbt/helios';
import { CoinSelectionError } from './CoinSelectionError';

export function coinSelection(utxos: UTxO[], target: Value): UTxO[] {
  const selected: Set<UTxO> = new Set();

  // Loop thru the target assets and select any UTxOs that have that asset
  target.assets.mintingPolicies.forEach((policy) => {
    target.assets.getTokenNames(policy).forEach((tokenName) => {
      const quantity = target.assets.get(policy, tokenName);

      // Get all UTxO's that have this token;
      // Sort them by quantity in descending order
      // Select the combination of UTxO's that add up to the quantity we need

      const assetCovered = utxos
        .filter((utxo) => utxo.value.assets.get(policy, tokenName) >= 0)
        .sort((a, b) =>
          a.value.assets.get(policy, tokenName) >
          b.value.assets.get(policy, tokenName)
            ? -1
            : 1
        )
        .some((utxo) => {
          selected.add(utxo);

          // Check we have selected enough to cover the Asset
          const selectedQty = Array.from(selected).reduce((acc, utxo) => {
            acc += utxo.value.assets.get(policy, tokenName);
            return acc;
          }, BigInt(0));

          return selectedQty >= quantity;
        });

      // If the asset is not covered then throw and Not enough coins Error
      if (!assetCovered) {
        throw new CoinSelectionError({
          message: `${policy.hex} ${bytesToHex(
            tokenName
          )} (${quantity}) - Not found in UTxO set`,
          code: 1,
        });
      }
    });
  });

  // Finally work out if we have enough Lovelace to cover the target
  const selectedLovelace = Array.from(selected).reduce(
    (acc, utxo) => acc + utxo.value.lovelace,
    BigInt(0)
  );

  if (target.lovelace > selectedLovelace) {
    // We don't have enough Lovelace, so we need to select some more UTxOs
    let remainingLovelace = target.lovelace - selectedLovelace;

    const selectedRemainingUTxOs = utxos
      .filter((utxo) => !selected.has(utxo))
      .sort((a, b) => (a.value.lovelace > b.value.lovelace ? -1 : 1))
      .some((utxo) => {
        selected.add(utxo);
        // Reduce Remaining Lovelace
        remainingLovelace -= utxo.value.lovelace;
        return remainingLovelace <= BigInt(0);
      });

    if (!selectedRemainingUTxOs) {
      throw new CoinSelectionError({
        message: `Not enough Lovelace to cover target - (${remainingLovelace})`,
        code: 2,
      });
    }
  }

  return Array.from(selected);
}

A lot is going on here but the TLDR is:
1. Developer needs to get a list of UTxOs probably from a users Wallet
2. Developer decides upon the Value that they would like to cover with the selected coins (Value is made up of Lovelace and Native Assets).

Now the program will attempt to look through the supplied UTxOs and select the ones that will eventually provide enough Coins (I think a Coin is another term for a UTxO) to cover the value. If we are unable to do that then we will throw a CoinSelectionError and provide some information in the message.

To test the above we need to create a list of UTxOs and then validate that we can select the correct ones to provide the correct Value for a transaction that we are attempting to build.

import { NetworkEmulator } from '@hyperionbt/helios';

test('Something', async () => {
    // Create an Instance of NetworkEmulator
    const network = new NetworkEmulator();
    // Create a Test Asset (๐Ÿง)
    const a1 = new Assets();
    a1.addComponent(
        MintingPolicyHash.fromHex(
          '16aa5486dab6527c4697387736ae449411c03dcd20a3950453e6779c'
        ),
        Array.from(new TextEncoder().encode('PodgyPenguin1041')),
        BigInt(1)
    );

    // Create a Wallet - we add 5ADA to start
    const alice = network.createWallet(BigInt(5_000_000), a1);

    // Lets add a second UTxO with some more ADA and another Asset
    const a2 = new Assets();
    a1.addComponent(
        MintingPolicyHash.fromHex(
          '16aa5486dab6527c4697387736ae449411c03dcd20a3950453e6779c'
        ),
        Array.from(new TextEncoder().encode('PodgyPenguin1047')),
        BigInt(1)
    );
    // We create this UTxO into alice's wallet
    network.createUtxo(alice, BigInt(23_000_000), a2);

    // Now lets tick the network on 10 slots,
    // this will allow the UTxOs to be created from Genisis
    network.tick(BigInt(10));

    // Now we are able to get the UTxOs in Alices Wallet
    // Note we provide the Address here, this is useful 
    // for getting the UTxOs from a script etc (later!)
    const utxos = await network.getUtxos(alice.address);

    // Now lets create an Assets object containing the 
    // assets we would like to select
    const wantAssets = new Assets();
    wantAssets.addComponent(
        MintingPolicyHash.fromHex(
          '16aa5486dab6527c4697387736ae449411c03dcd20a3950453e6779c'
        ),
        Array.from(new TextEncoder().encode('PodgyPenguin1047')),
        BigInt(1)
    );

    // Lets run our Coin Selection, we want 10ADA and a Dodgy ๐Ÿง
    const result = coinSelection(utxos, new Value(BigInt(10_000_000), wantAssets));

    // I've created a small helper function (see the StackBlitz 
    // for detail, but TLDR it checks if the resulting assets
    // cover the required ADA and Assets.
    fundsAreSufficient(result, BigInt(10_000_000), wantAssets);
})

Next up I will be looking at how I have gone about testing Transactions. Let me know if this is useful at all!

ย