Appearance
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.