Skip to content

Smart Contract Basics

Before we look at how to make a contract such as the one in the basic dapp in the previous section, let's cover some basics.

A contract is defined by a JavaScript module that exports a start function that implements the contract's API.

js
export const start = () => {

Let's start with a contract with a simple greet function:

js
const greet = who => `Hello, ${who}!`;

The start function can expose the greet function as part of the contract API by making it a method of the contract's publicFacet:

js
return {
  publicFacet: Far('Hello', { greet }),
};

We mark it Far(...) to allow callers to use it from outside the contract and give it a suggestive interface name for debugging. We'll discuss Far in more detail later.

Putting it all together:

js
import { Far } from '@endo/far';

const greet = who => `Hello, ${who}!`;

export const start = () => {
  return {
    publicFacet: Far('Hello', { greet }),
  };
};

Using, testing a contract

Let's use some tests to explore how a contract is used.

Agoric contracts are typically tested using the ava framework. They start with @endo/init to establish a Hardened JavaScript environment:

js
import '@endo/init';
import { E } from '@endo/far';
// eslint-disable-next-line import/no-unresolved -- https://github.com/avajs/ava/issues/2951
import test from 'ava';

We'll talk more about using E() for async method calls later.

A test that the greet method works as expected looks like:

js
import { start } from '../src/01-hello.js';

test('contract greets by name', async t => {
  const { publicFacet } = start();
  const actual = await E(publicFacet).greet('Bob');
  t.is(actual, 'Hello, Bob!');
});

State

Contracts can use ordinary variables and data structures for state.

js
export const start = () => {
  const rooms = new Map();

  const getRoomCount = () => rooms.size;
  const makeRoom = id => {
    let count = 0;
    const room = Far('Room', {
      getId: () => id,
      incr: () => (count += 1),
      decr: () => (count -= 1),
    });
    rooms.set(id, room);
    return room;
  };

  return {
    publicFacet: Far('RoomMaker', { getRoomCount, makeRoom }),
  };
};

Using makeRoom changes the results of the following call to getRoomCount:

js
test('state', async t => {
  const { publicFacet } = state.start();
  const actual = await E(publicFacet).getRoomCount();
  t.is(actual, 0);
  await E(publicFacet).makeRoom(2);
  t.is(await E(publicFacet).getRoomCount(), 1);
});

Heap state is persistent

Ordinary heap state persists between contract invocations.

We'll discuss more explicit state management for large numbers of objects (virtual objects) and objects that last across upgrades (durable objects) later.

Access Control with Objects

We can limit the publicFacet API to read-only by omitting the set() method.

The creatorFacet is provided only to the caller who creates the contract instance.

js
import { Far } from '@endo/far';

export const start = () => {
  let value = 'Hello, World!';
  const get = () => value;
  const set = v => (value = v);

  return {
    publicFacet: Far('ValueView', { get }),
    creatorFacet: Far('ValueCell', { get, set }),
  };
};

Trying to set using the publicFacet throws, but using the creatorFacet works:

js
test('access control', async t => {
  const { publicFacet, creatorFacet } = access.start();
  t.is(await E(publicFacet).get(), 'Hello, World!');
  await t.throwsAsync(E(publicFacet).set(2), { message: /no method/ });
  await E(creatorFacet).set(2);
  t.is(await E(publicFacet).get(), 2);
});

Note that the set() method has no access check inside it. Access control is based on separation of powers between the publicFacet, which is expected to be shared widely, and the creatorFacet, which is closely held. We'll discuss this object capabilities approach more later.

Next, let's look at minting and trading assets with Zoe.