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 by writing a simple contract that returns a greetings message. We will simply call it hello-world smart contract.
A contract is defined by a JavaScript module that exports a start
function. For our hello-world smart contract, the declaration of start
function looks like this:
export const start = () => {
For the hello-world smart contract, we will have a simple greet
function apart from start
function. The greet
function takes a string as a parameter (for example, name of the person calling the function) and returns a customized greeting message.
const greet = who => `Hello, ${who}!`;
The greet
function, along with any other public function, must be made accessible through the publicFacet
of the contract. The start
function returns an object with a publicFacet
property. In the hello-world contract, the start
function exposes the greet
function by defining it as a method of the contract's publicFacet
, as shown below:
return {
publicFacet: Far('Hello', { greet }),
};
We wrap the value of the publicFacet
property in a Far(...)
call to safely expose it as a remote object, accessible from outside the contract. This also gives it a suggestive interface name Hello
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 }),
};
};
Let us save this code to a file named 01-hello.js
inside src
directory.
Using, testing a contract
Agoric contracts are typically tested using the ava framework. The test file begins with an import @endo/init
to establish a Hardened JavaScript environment. We also import E()
in order to make asynchronous method calls and test
function from ava
. We'll talk more about using E()
for async method calls later. Following these import
statements, we write a simple test that validates that the greet
method works as expected.
Putting it all together:
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';
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!');
});
Let's save this code in a file named test-01-hello.js
in a test
directory. Both src
and test
directories should lie in the same contract
directory. Let us run the following command to execute the test:
yarn ava --match="contract greets by name"
You should see the following line towards the end of the output:
1 test passed
Congratulations! You have written and tested your first smart contract. Our next goal is to learn about the state of a smart contract.
See also:
State
Contracts can use ordinary variables and data structures for state.
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
:
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.
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.