02 - Chain, Ag-solo, Wallet, and Client
Previous read
Next read
Chapter 2 covers general information about the Agoric stack, as well as the installation and setup of the SDK
Understanding the stack
While an in-depth explanation the SDK is out-of-scope for this guide, understanding what each component does and how they interact with each other can go a long way when designing, developing, and debugging Agoric applications.
At the lowest level is the Agoric simulated chain, which simulates the blockchain on which smart contracts are executed and serves as a network that can interact with Ag-solo processes. Ag-solo processes are off-chain Agoric VMs. They have their own UI and way of communicating with chains (including multiple chains and network connections). An Ag-solo process can host a wallet, which is a user's trusted agent. A wallet enables or disables inbound connections from Dapps and approves or declines proposals from those Dapps.
Lastly, the client web app is able to communicate with the wallet and smart contracts via a web-socket connection known as Local Bridge. The client provides a way to visualize and interact with blockchain data, as well as local wallets.
Getting the SDK
To install the Agoric SDK, clone the Agoric repo, run yarn install & build, then link its path appropriately
node --version # 14.15.0 or higher
npm install --global yarn
git clone https://github.com/Agoric/agoric-sdk
cd agoric-sdk
yarn install
yarn build
yarn link-cli ~/bin/agoric
agoric --version
Quick start
Before looking at contract deployment, it’s important to get familiar with the commands involved in starting up the services needed for Agoric to dapps to work. The basic and easiest development cycle is as follows:
# in agoric directory
agoric install
agoric start --reset -v
agoric open
agoric deploy <contract-deploy-script.js> <api-deploy-script>
# in frontend directory
yarn && yarn start
- Agoric install will install any packages defined within the agoric directory. You can add a new package via ‘yarn add
’ or by modifying the package.json file directly, then running agoric install (note that yarn install will not work in agoric subdirectories). - Agoric start will start an instance of the simulated chain, and deploy an Ag-solo process serving as a wallet.
- Agoric open will open the wallet interface on a browser.
- The last command involves running the frontend and will vary depending on your framework of choice.
These four commands are sufficient for running a chain with a single Ag-solo process, deploying our contracts to it, and opening the wallet interface. For a more flexible setup, each component can be run separately, as shown below.
Chain
The chain is the process in which Agoric services run, including Ag-solo machines. This guide will only use of Agoric’s simulation-chain, for integration with other chains see Dynamic IBC (Inter-Blockchain Communication Protocol), aka dIBC. The simplest way to start an instance of a chain is via
agoric start local-chain --reset --verbose
This will start a local simulated chain, allowing other processes to run separately. Omit the —reset option if you want the chain state to be preserved across restarts. The —verbose flag will result in more detailed logs that can be useful when debugging.
Ag-solo
The Ag-solo process represents a user’s wallet. It is possible to deploy multiple Ag-solos to the same local chain, allowing multiple wallets to run locally on a single machine. To start an Ag-solo use the following command:
agoric start local-solo 8000 --reset --verbose
Make sure the chain process is running when deploying Ag-solos. The argument after local-solo allows you to choose the port used by the Ag-solo, in order to use multiple Ag-solo processes you must use different ports (default is 8000).
Wallet
Once the simulated chain is running, and the Ag-solo is deployed, the wallet interface can be opened via
agoric open --hostport=localhost:8000
This command will open your browser and navigate to localhost:8000. For security reasons, the wallet interface is protected by an access token, meaning that manually navigating to localhost:8000 will not work. You must use the URL shown by the agoric open command:
❯ agoric open
Launching wallet...
http://127.0.0.1:8000/wallet/#accessToken=s6Cl_JVmL3DE0E7iaFZdTj9lbCKemrFhkzo9d3oj8M0srIUfRsHy-4yIyO0iZuMQ
Note: the same port must be used in the start and open commands.
Multiple User Setup
The commands above make it possible to deploy multiple Ag-solos to a single simulated local chain, for ease of use, let’s write a simple make file that can help out with this
chain-reset:
agoric start local-chain --reset --verbose
solo0-reset:
agoric start local-solo 8000 --reset --verbose
solo1-reset:
agoric start local-solo 8001 --reset --verbose
chain:
agoric start local-chain --verbose
solo0:
agoric start local-solo 8000 --verbose
solo1:
agoric start local-solo 8001 --verbose
wallet0:
agoric open --hostport=localhost:8000 --no-browser
wallet1:
agoric open --hostport=localhost:8001 --no-browser
The following make commands to start a two-user setup:
> make chain-reset
(new terminal)
> make solo0-reset
(new terminal)
> make solo1-reset
(new terminal)
> make wallet1
> make wallet2
Writing and deploying smart contracts
Smart contracts are located under the /contract subdirectory and are written in JavaScript. Chapter 3 will cover contract structure and syntax in more detail, for now, let's explore how they are deployed to Zoe, and how to interact with them from the frontend.
To deploy a contract, you must write a deployment script (in Javascript) to pass to the agoric deploy command, it takes our contract code, installs it on Zoe, and makes the installation publicly available. Most Agoric project templates include this script already, and often times there is no need to make adjustments to it. Here’s a documented example:
// @ts-check
import fs from 'fs';
import '@agoric/zoe/exported.js';
import { E } from '@endo/eventual-send';
import { resolve } from 'import-meta-resolve';
export default async function deployContract(
homePromise,
{ bundleSource, pathResolve },
) {
// Your off-chain machine (what we call an ag-solo) starts off with
// a number of references, some of which are shared objects on chain, and
// some of which are objects that only exist on your machine.
// Let's wait for the promise to resolve.
const home = await homePromise;
// Unpack the references.
const {
// *** ON-CHAIN REFERENCES ***
// Zoe lives on-chain and is shared by everyone who has access to
// the chain. In this demo, that's just you, but on our testnet,
// everyone has access to the same Zoe.
zoe,
// The board is an on-chain object that is used to make private
// on-chain objects public to everyone else on-chain. These
// objects get assigned a unique string id. Given the id, other
// people can access the object through the board. Ids and values
// have a one-to-one bidirectional mapping. If a value is added a
// second time, the original id is just returned.
board,
} = home;
// First, we must bundle up our contract code (./src/contract.js)
// and install it on Zoe. This returns an installationHandle, an
// opaque, unforgeable identifier for our contract code that we can
// reuse again and again to create new, live contract instances.
const bundle = await bundleSource(pathResolve(`./src/index.js`));
const installation = await E(zoe).install(bundle);
// Let's share this installation with other people, so that
// they can run our contract code by making a contract
// instance (see the api deploy script in this repo to see an
// example of how to use the installation to make a new contract
// instance.)
// To share the installation, we're going to put it in the
// board. The board is a shared, on-chain object that maps
// strings to objects.
const CONTRACT_NAME = 'NAME';
const INSTALLATION_BOARD_ID = await E(board).getId(installation);
console.log('- SUCCESS! contract code installed on Zoe');
console.log(`-- Contract Name: ${CONTRACT_NAME}`);
console.log(`-- Installation Board Id: ${INSTALLATION_BOARD_ID}`);
// Save the constants somewhere where the UI and api can find it.
const dappConstants = {
CONTRACT_NAME,
INSTALLATION_BOARD_ID,
};
const defaultsFolder = pathResolve(`../../frontend/src/service/conf`);
const defaultsFile = pathResolve(
`../../frontend/src/service/conf/installation-constants.js`,
);
console.log('writing', defaultsFile);
const defaultsContents = `\
// GENERATED FROM ${pathResolve('./deploy.js')}
export default ${JSON.stringify(dappConstants, undefined, 2)};
`;
await fs.promises.mkdir(defaultsFolder, { recursive: true });
await fs.promises.writeFile(defaultsFile, defaultsContents);
}
This can look daunting before becoming familiar with the concepts of Zoe and the Board, but what’s important to understand is:
- Installing contract code does not mean you start a new instance of that contract, it simply makes it available to actors wishing to start one (done in the next step)
- References to deployed contracts are stored in a JS file for other parts of the stack to locate. You are welcome to export this file to any location, but keep in mind it will need to be accessed by the API deployment script, as well as the frontend app
After the contract code is installed and references to it are exported to a file, you must run the API deployment script in order to create an instance of the contract using the reference file created in the previous step. The API deployment script is located in the /api subdirectory and provides a way to run custom startup logic for your contracts. Depending on your contract, the code to initialize an instance may vary, but let's look at a simple example:
// @ts-check
// Agoric Dapp api deployment script
import fs from 'fs';
import { E } from '@endo/eventual-send';
import '@agoric/zoe/exported.js';
import installationConstants from '../ui/public/conf/installationConstants.js';
// deploy.js runs in an ephemeral Node.js outside of swingset.
// Once the deploy.js script ends, connections to any of
// its objects are severed.
const API_PORT = process.env.API_PORT || '8000';
export default async function deployApi(
homePromise,
{ bundleSource, pathResolve },
) {
// Let's wait for the promise to resolve.
const home = await homePromise;
// Unpack the references.
const {
// *** ON-CHAIN REFERENCES ***
// Zoe lives on-chain and is shared by everyone who has access to
// the chain. In this demo, that's just you, but on our testnet,
// everyone has access to the same Zoe.
zoe,
// The board is an on-chain object that is used to make private
// on-chain objects public to everyone else on-chain. These
// objects get assigned a unique string id. Given the id, other
// people can access the object through the board. Ids and values
// have a one-to-one bidirectional mapping. If a value is added a
// second time, the original id is just returned.
board,
} = home;
// To get the backend of our dapp up and running, first we need to
// grab the installation that our contract deploy script put
// in the public board.
const { INSTALLATION_BOARD_ID, CONTRACT_NAME } = installationConstants;
const installation = await E(board).getValue(INSTALLATION_BOARD_ID);
// Second, we can use the installation to create a new instance of
// our contract code on Zoe. A contract instance is a running
// program that can take offers through Zoe. Making an instance will
// give us a `creatorFacet` that will let us make invitations we can
// send to users.
const { creatorFacet, instance, publicFacet } = await E(zoe).startInstance(
installation,
);
console.log('- SUCCESS! contract instance is running on Zoe');
const [INSTANCE_BOARD_ID] =
await Promise.all([
E(board).getId(instance),
]);
console.log(`-- Contract Name: ${CONTRACT_NAME}`);
console.log(`-- INSTANCE_BOARD_ID: ${INSTANCE_BOARD_ID}`);
const API_URL = process.env.API_URL || `http://127.0.0.1:${API_PORT || 8000}`;
// Re-save the constants somewhere where the UI and api can find it.
const dappConstants = {
INSTANCE_BOARD_ID,
INSTALLATION_BOARD_ID,
// BRIDGE_URL: 'agoric-lookup:https://local.agoric.com?append=/bridge',
BRIDGE_URL: 'http://127.0.0.1:8000',
API_URL,
};
const defaultsFile = pathResolve(`../ui/public/conf/defaults.js`);
console.log('writing', defaultsFile);
const defaultsContents = `\
// GENERATED FROM ${pathResolve('./deploy.js')}
export default ${JSON.stringify(dappConstants, undefined, 2)};
`;
await fs.promises.writeFile(defaultsFile, defaultsContents);
}
The pattern is very similar to the contract deploy script, only instead of installing contract code, it uses a reference to the installation and start a contract instance. Once again, fs stores a reference to that instance in an external file, letting the frontend know which contract(s) to interact with.
Frontend Integration
Once deployment of contracts and API is complete, all that’s left is to configure the frontend to interact with the wallet and Zoe. The local bridge handles this via a web-socket connection and, at the time of writing, there is no official or recommended way to set up the frontend for this, in part due to the fact that Agoric can be used with any web framework, or plain HTML, CSS and JS. That said, there are a few dapp examples on Agoric’s Github that can be used for inspiration:
- (React + TS) KREAd Character Builder: This is the dapp that will be covered in chapter 3 and it's configured to use React and typescript.
- (React + JS) Baseball Card Store: This is an example dapp provided by Agoric build using React and written in JS.
- (HTML + CSS + JS) Fungible Facet: example dapp by Agoric, built using plain HTML, CSS, and JS.