Skip to content

Complete Contract Walk-Through

Let's look at the contract from the basic dapp in some detail.

Bundling a Contract

In deploying the basic dapp contract, the first step was to bundle all of its modules into a single artifact. We used the agoric run command in that case. The core mechanism used in agoric run is a call to bundleSource().

In the contract directory of the dapp, run test-bundle-source.js following ava conventions:

sh
cd contract
yarn ava test/test-bundle-source.js

The results look something like...

console
  ✔ bundleSource() bundles the contract for use with zoe (2.7s)
    ℹ 1e1aeca9d3ebc0bd39130fe5ef6fbb077177753563db522d6623886da9b43515816df825f7ebcb009cbe86dcaf70f93b9b8595d1a87c2ab9951ee7a32ad8e572
    ℹ Object @Alleged: BundleInstallation {}


  1 test passed
Test Setup

The test uses createRequire from the node module API to resolve the main module specifier:

js
import bundleSource from '@endo/bundle-source';
import { createRequire } from 'module';
js
const myRequire = createRequire(import.meta.url);
const contractPath = myRequire.resolve(`../src/gameAssetContract.js`);

bundleSource() returns a bundle object with moduleFormat, a hash, and the contents:

js
const bundle = await bundleSource(contractPath);
t.is(bundle.moduleFormat, 'endoZipBase64');
t.log(bundle.endoZipBase64Sha512);
t.true(bundle.endoZipBase64.length > 10_000);
Getting the zip file from inside a bundle

An endo bundle is a zip file inside JSON. To get it back out:

sh
jq -r .endoZipBase64 bundle-xyz.json | base64 -d >xyz.zip

You can then, for example, look at its contents:

sh
unzip -l xyz.zip

Contract Installation

To identify the code of contracts that parties consent to participate in, Zoe uses Installation objects.

Let's try it with the contract from our basic dapp:

sh
yarn ava test/test-contract.js -m 'Install the contract'
  ✔ Install the contract
    ℹ Object @Alleged: BundleInstallation {}
Test Setup

The test starts by using makeZoeKitForTest to set up zoe for testing:

js
import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js';
js
const { zoeService: zoe } = makeZoeKitForTest();

It gets an installation using a bundle as in the previous section:

js
const installation = await E(zoe).install(bundle);
t.log(installation);
t.is(typeof installation, 'object');

The installation identifies the basic contract that we'll go over in detail in the sections below.

gameAssetContract.js listing
js
/** @file Contract to mint and sell Place NFTs for a hypothetical game. */
// @ts-check

import { Far } from '@endo/far';
import { M, getCopyBagEntries } from '@endo/patterns';
import { AmountMath, AssetKind } from '@agoric/ertp/src/amountMath.js';
import { AmountShape } from '@agoric/ertp/src/typeGuards.js';
import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js';
import '@agoric/zoe/exported.js';

import { makeTracer } from './debug.js';

const { Fail, quote: q } = assert;

const trace = makeTracer('Game', true);

/** @param {Amount<'copyBag'>} amt */
const bagValueSize = amt => {
  /** @type {[unknown, bigint][]} */
  const entries = getCopyBagEntries(amt.value); // XXX getCopyBagEntries returns any???
  const total = entries.reduce((acc, [_place, qty]) => acc + qty, 0n);
  return total;
};

/**
 * @param {ZCF<{joinPrice: Amount}>} zcf
 */
export const start = async zcf => {
  const { joinPrice } = zcf.getTerms();

  const { zcfSeat: gameSeat } = zcf.makeEmptySeatKit();
  const mint = await zcf.makeZCFMint('Place', AssetKind.COPY_BAG);

  /** @param {ZCFSeat} playerSeat */
  const joinHandler = playerSeat => {
    const { give, want } = playerSeat.getProposal();
    trace('join', 'give', give, 'want', want.Places.value);

    AmountMath.isGTE(give.Price, joinPrice) ||
      Fail`${q(give.Price)} below joinPrice of ${q(joinPrice)}}`;

    bagValueSize(want.Places) <= 3n || Fail`only 3 places allowed when joining`;

    const tmp = mint.mintGains(want);
    atomicRearrange(
      zcf,
      harden([
        [playerSeat, gameSeat, give],
        [tmp, playerSeat, want],
      ]),
    );

    playerSeat.exit(true);
    return 'welcome to the game';
  };

  const joinShape = harden({
    give: { Price: AmountShape },
    want: { Places: AmountShape },
    exit: M.any(),
  });

  const publicFacet = Far('API', {
    makeJoinInvitation: () =>
      zcf.makeInvitation(joinHandler, 'join', undefined, joinShape),
  });

  return { publicFacet };
};
harden(start);

Starting a Contract Instance

Now we're ready to start an instance of the basic dapp contract:

sh
yarn ava test/test-contract.js -m 'Start the contract'
  ✔ Start the contract (652ms)
    ℹ terms: {
        joinPrice: {
          brand: Object @Alleged: PlayMoney brand {},
          value: 5n,
        },
      }
    ℹ Object @Alleged: InstanceHandle {}

Contracts can be parameterized by terms. The price of joining the game is not fixed in the source code of this contract, but rather chosen when starting an instance of the contract. Likewise, when starting an instance, we can choose which asset issuers the contract should use for its business:

js
const money = makeIssuerKit('PlayMoney');
const issuers = { Price: money.issuer };
const terms = { joinPrice: AmountMath.make(money.brand, 5n) };
t.log('terms:', terms);

/** @type {ERef<Installation<GameContractFn>>} */
const installation = E(zoe).install(bundle);
const { instance } = await E(zoe).startInstance(installation, issuers, terms);
t.log(instance);
t.is(typeof instance, 'object');

makeIssuerKit and AmountMath.make are covered in the ERTP section, along with makeEmptyPurse, mintPayment, and getAmountOf below.

See also E(zoe).startInstance(...).

Let's take a look at what happens in the contract when it starts. A facet of Zoe, the Zoe Contract Facet, is passed to the contract start function. The contract uses this zcf to get its terms. Likewise it uses zcf to make a gameSeat where it can store assets that it receives in trade as well as a mint for making assets consisting of collections (bags) of Places:

js
export const start = async zcf => {
  const { joinPrice } = zcf.getTerms();

  const { zcfSeat: gameSeat } = zcf.makeEmptySeatKit();
  const mint = await zcf.makeZCFMint('Place', AssetKind.COPY_BAG);

It defines a joinShape and joinHandler but doesn't do anything with them yet. They will come into play later. It defines and returns its publicFacet and stands by.

js
return { publicFacet };

Trading with Offer Safety

Our basic dapp includes a test of trading:

sh
yarn ava test/test-contract.js -m 'Alice trades*'
  ✔ Alice trades: give some play money, want some game places (674ms)
    ℹ Object @Alleged: InstanceHandle {}
    ℹ Alice gives {
        Price: {
          brand: Object @Alleged: PlayMoney brand {},
          value: 5n,
        },
      }
    ℹ Alice payout brand Object @Alleged: Place brand {}
    ℹ Alice payout value Object @copyBag {
        payload: [
          [
            'Park Place',
            1n,
          ],
          [
            'Boardwalk',
            1n,
          ],
        ],
      }

We start by putting some money in a purse for Alice:

js
const alicePurse = money.issuer.makeEmptyPurse();
const amountOfMoney = AmountMath.make(money.brand, 10n);
const moneyPayment = money.mint.mintPayment(amountOfMoney);
alicePurse.deposit(moneyPayment);

Then we pass the contract instance and the purse to our code for alice:

js
await alice(t, zoe, instance, alicePurse);

Alice starts by using the instance to get the contract's publicFacet and terms from Zoe:

js
const publicFacet = E(zoe).getPublicFacet(instance);
const terms = await E(zoe).getTerms(instance);
const { issuers, brands, joinPrice } = terms;

Then she constructs a proposal to give the joinPrice in exchange for 1 Park Place and 1 Boardwalk, denominated in the game's Place brand; and she withdraws a payment from her purse:

js
const choices = ['Park Place', 'Boardwalk'];
const choiceBag = makeCopyBag(choices.map(name => [name, 1n]));
const proposal = {
  give: { Price: joinPrice },
  want: { Places: AmountMath.make(brands.Place, choiceBag) },
};
const Price = await E(purse).withdraw(joinPrice);
t.log('Alice gives', proposal.give);

She then requests an invitation to join the game; makes an offer with (a promise for) this invitation, her proposal, and her payment; and awaits her Places payout:

js
const toJoin = E(publicFacet).makeJoinInvitation();

const seat = E(zoe).offer(toJoin, proposal, { Price });
const places = await E(seat).getPayout('Places');
Troubleshooting missing brands in offers

If you see...

Error#1: key Object [Alleged: IST brand] {} not found in collection brandToIssuerRecord

then it may be that your offer uses brands that are not known to the contract. Use E(zoe).getTerms() to find out what issuers are known to the contract.

If you're writing or instantiating the contract, you can tell the contract about issuers when you are creating an instance or by using zcf.saveIssuer().

The contract gets Alice's E(publicFacet).makeJoinInvitation() call and uses zcf to make an invitation with an associated handler, description, and proposal shape. Zoe gets Alice's E(zoe).offer(...) call, checks the proposal against the proposal shape, escrows the payment, and invokes the handler.

js
const joinShape = harden({
  give: { Price: AmountShape },
  want: { Places: AmountShape },
  exit: M.any(),
});

const publicFacet = Far('API', {
  makeJoinInvitation: () =>
    zcf.makeInvitation(joinHandler, 'join', undefined, joinShape),
});

The offer handler is invoked with a seat representing the party making the offer. It extracts the give and want from the party's offer and checks that they are giving at least the joinPrice and not asking for too many places in return.

With all these prerequisites met, the handler instructs zcf to mint the requested Place assets, allocate what the player is giving into its own gameSeat, and allocate the minted places to the player. Finally, it concludes its business with the player.

js
/** @param {ZCFSeat} playerSeat */
const joinHandler = playerSeat => {
  const { give, want } = playerSeat.getProposal();
  trace('join', 'give', give, 'want', want.Places.value);

  AmountMath.isGTE(give.Price, joinPrice) ||
    Fail`${q(give.Price)} below joinPrice of ${q(joinPrice)}}`;

  bagValueSize(want.Places) <= 3n || Fail`only 3 places allowed when joining`;

  const tmp = mint.mintGains(want);
  atomicRearrange(
    zcf,
    harden([
      [playerSeat, gameSeat, give],
      [tmp, playerSeat, want],
    ]),
  );

  playerSeat.exit(true);
  return 'welcome to the game';
};

Zoe checks that the contract's instructions are consistent with the offer and with conservation of assets. Then it allocates the escrowed payment to the contract's gameSeat and pays out the place NFTs to Alice in response to the earlier getPayout(...) call.

Alice asks the Place issuer what her payout is worth and tests that it's what she wanted.

js
const actual = await E(issuers.Place).getAmountOf(places);
t.log('Alice payout brand', actual.brand);
t.log('Alice payout value', actual.value);
t.deepEqual(actual, proposal.want.Places);