# 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.
export const start = () => {
Let's start with a contract with a simple greet
function:
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
:
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:
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 (opens new window) framework.
They start with @endo/init
to establish a Hardened JavaScript environment:
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:
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 for state.
export const start = () => {
let value = 'Hello, World!';
const get = () => value;
const set = v => (value = v);
return {
publicFacet: Far('ValueCell', { get, set }),
};
};
Using set
changes the results of the following call to get
:
test('state', async t => {
const { publicFacet } = state.start();
t.is(await E(publicFacet).get(), 'Hello, World!');
await E(publicFacet).set(2);
t.is(await E(publicFacet).get(), 2);
});
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.
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:
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.