The Deploy API revolves around the Deployment
class,
the Template
and Contract
classes, and the associated
implementations of Client
, Builder
, Uploader
, and DeployStore
.
import { Deployment, Template, Contract, Client } from '@fadroma/agent'
let deployment: Deployment
let template: Template
let contract: Contract
These classes are used for describing systems consisting of multiple smart contracts,
such as when deploying them from source. By defining such a system as one or more
subclasses of Deployment
, Fadroma enables declarative, idempotent, and reproducible
smart contract deployments.
The Deployment
class represents a set of interrelated contracts.
To define your deployment, extend the Deployment
class, and use the
this.template({...})
and this.contract({...})
methods to specify
what contracts to deploy:
// in your project's api.ts:
import { Deployment } from '@fadroma/agent'
export class DeploymentA extends Deployment {
kv1 = this.contract({
name: 'kv1',
crate: 'examples/kv',
initMsg: {}
})
kv2 = this.contract({
name: 'kv2',
crate: 'examples/kv',
initMsg: {}
})
}
To prepare a deployment for deploying, use getDeployment
.
This will provide a populated instance of your deployment class.
import { getDeployment } from '@hackbg/fadroma'
deployment = getDeployment(DeploymentA, /* ...constructor args */)
Then, call its deploy
method:
await deployment.deploy()
For each contract defined in the deployment, this will do the following:
Having deployed a contract, you want to obtain a Client
instance
that points to it, so you can call the contract's methods.
Using the contract.expect()
method you can get an instance
of the Client
specified in the contract options, provided
the contract is already deployed (i.e. its address is known).
assert(deployment.kv1.expect() instanceof Client)
assert(deployment.kv2.expect() instanceof Client)
This is the recommended method for passing handles to contracts to your UI code after deploying or connecting to a stored deployment (see below).
If the address of the request contract is not available, this will throw an error.
By await
ing a Contract
's deployed
property, you say:
"give me a handle to this contract; if it's not deployed,
deploy it, and all of its dependencies (as specified by the initMsg
method)".
assert(await deployment.kv1.deployed instanceof Client)
assert(await deployment.kv2.deployed instanceof Client)
Since this does not call the deployment's deploy
method,
it only deploys the requested contract and its dependencies
but not any other contracts defined in the deployment.
The deployment.deploy
method simply instantiates
all contracts in order. You are free to override it
and deploy the defined contracts according to some
custom logic:
class DeploymentB extends Deployment {
kv1 = this.contract({ crate: 'examples/kv', name: 'kv1', initMsg: {} })
kv2 = this.contract({ crate: 'examples/kv', name: 'kv2', initMsg: {} })
deploy = async (deployBoth: boolean = false) => {
await this.kv1.deployed
if (deployBoth) await this.kv2.deployed
return this
}
}
By default, the list of contracts in each deployment created by Fadroma
is stored in state/${CHAIN_ID}/deploy/${DEPLOYMENT}.yml
.
The deployment currently selected as "active" by the CLI
(usually, the latest created deployment) is symlinked at
state/${CHAIN_ID}/deploy/.active.yml
.
Deployments in YAML multi-document format are human-readable
and version control-friendly. When a list of contracts in JSON
is desired, you can use the export
command to export a JSON
snapshot of the active deployment.
For example, to select and export a mainnet deployment:
pnpm mainnet select NAME
pnpm mainnet export [DIRECTORY]
This will create a file named NAME_@_TIMESTAMP.json
in the current working directory (or another specified).
Internally, the data for the export is generated by the
deployment.snapshot
getter:
assert.deepEqual(
Object.keys(deployment.snapshot.contracts),
['kv1', 'kv2']
)
In a standard Fadroma project, where the Rust contracts
and TypeScript API client live in the same repo, by export
ing
the latest mainnet and testnet deployments to JSON files
during the TypeScript build process, and adding them to your
API client package, you can publish an up-to-date "address book"
of your project's active contracts as part of your API client library.
// in your project's api.ts:
import { Deployment } from '@fadroma/agent'
// you would load snapshots as JSON, e.g.:
// const testnet = await (await fetch('./testnet_v4.json')).json()
export const mainnet = deployment.snapshot
export const testnet = deployment.snapshot
// and create instances of your deployment with preloaded
// "address books" of contracts. for example here we restore
// a different snapshot depending on whether we're passed a
// mainnet or testnet connection.
class DeploymentC extends Deployment {
kv1 = this.contract({ crate: 'examples/kv', name: 'kv1', initMsg: {} })
kv2 = this.contract({ crate: 'examples/kv', name: 'kv2', initMsg: {} })
static connect = (agent: Agent) => {
if (agent?.chain?.isMainnet) return new this({ ...mainnet, agent })
if (agent?.chain?.isTestnet) return new this({ ...testnet, agent })
return new this({ agent })
}
}
Having been deployed once, contracts may be used continously.
The Deployment
's connect
method loads stored data about
the contracts in the deployment, populating the contained
Contract
instances.
With the above setup you can automatically connect to
your project in mainnet or testnet mode, depending on
what Agent
you pass:
const mainnetAgent = { chain: { isMainnet: true } } // mock
const testnetAgent = { chain: { isTestnet: true } } // mock
const onMainnet = DeploymentC.connect(mainnetAgent)
const onTestnet = DeploymentC.connect(testnetAgent)
assert(onMainnet.isMainnet)
assert(onTestnet.isTestnet)
assert.deepEqual(Object.keys(onMainnet.contracts), ['kv1', 'kv2'])
assert.deepEqual(Object.keys(onTestnet.contracts), ['kv1', 'kv2'])
Or, to connect to individual contracts from the stored deployment:
const kv1 = DeploymentC.connect(mainnetAgent).kv1.expect()
assert(kv1 instanceof Client)
const kv2 = DeploymentC.connect(testnetAgent).kv2.expect()
assert(kv2 instanceof Client)
Migrations can be implemented as static or regular methods
of Deployment
classes.
// in your project's api.ts:
import { Deployment } from '@fadroma/agent'
class DeploymentD extends DeploymentC {
kv3 = this.contract({ crate: 'examples/kv', name: 'kv3', initMsg: {} })
// simplest client-side migration is to just instantiate
// a new deployment with the data from the old deployment.
static upgrade = (previous: DeploymentC) =>
new this({ ...previous })
}
// simplest chain-side migration is to just call default deploy,
// which should reuse kv1 and kv2 and only deploy kv3.
deployment = await DeploymentD.upgrade(deployment).deploy()
The Template
class represents a smart contract's source, compilation,
binary, and upload. It can have a codeHash
and codeId
but not an
address
.
Instantiating a template refers to calling the template.instance
method (or its plural, template.instances
), which returns Contract
,
which represents a particular smart contract instance, which can have
an address
.
The deployment.template
method adds a Template
to the Deployment
.
class Deployment4 extends Deployment {
t = this.template({ crate: 'examples/kv' })
a = this.t.instance({ name: 'a', initMsg: {} })
b = this.t.instances([
{name:'b1',initMsg:{}},
{name:'b2',initMsg:{}},
{name:'b3',initMsg:{}}
])
c = this.t.instances({
c1:{name:'c1',initMsg:{}},
c2:{name:'c2',initMsg:{}},
c3:{name:'c3',initMsg:{}}
})
}
You can pass either an array or an object to template.instances
.
deployment = await getDeployment(Deployment4).deploy()
assert(deployment.t instanceof Template)
assert([
deployment.a,
...Object.values(deployment.b)
...Object.values(deployment.c)
].every(
c=>(c instanceof Contract) && (c.expect() instanceof Client)
))
To build, the builder
property must be set to a valid Builder
.
When obtaining instances from a Deployment
, the builder
property
is provided automatically from deployment.builder
.
import { Builder } from '@fadroma/agent'
assert(deployment.t.builder instanceof Builder)
assert.equal(deployment.t.builder, deployment.builder)
You can build a Template
(or its subclass, Contract
) by awaiting the
built
property or the return value of the build()
method.
await deployment.t.built
// -or-
await deployment.t.build()
See the build guide for more info.
To upload, the uploader
property must be set to a valid Uploader
.
When obtaining instances from a Deployment
, the uploader
property
is provided automatically from deployment.uploader
.
import { Uploader } from '@fadroma/agent'
assert(deployment.t.uploader instanceof Uploader)
assert.equal(deployment.t.uploader, deployment.uploader)
You can upload a Template
(or its subclass, Contract
) by awaiting the
uploaded
property or the return value of the upload()
method.
If a WASM binary is not present (template.artifact
is empty),
but a source and a builder are present, this will also try to build the contract.
await deployment.t.uploaded
// -or-
await deployment.t.upload()
See the upload guide for more info.
The Contract
class describes an individual smart contract instance and uniquely identifies it
within the Deployment
.
import { Contract } from '@fadroma/agent'
new Contract({
repository: 'REPO',
revision: 'REF',
workspace: 'WORKSPACE'
crate: 'CRATE',
artifact: 'ARTIFACT',
chain: { /* ... */ },
agent: { /* ... */ },
deployment: { /* ... */ },
codeId: 0,
codeHash: 'CODEHASH'
client: Client,
name: 'NAME',
initMsg: async () => ({})
})
The chain requires labels to be unique.
Labels generated by Fadroma are of the format ${deployment.name}/${contract.name}
.
The initMsg
property of Contract
can be a function returning the actual message.
This function is only called during instantiation, and can be used to generate init
messages on the fly, such as when passing the address of one contract to another.
To instantiate a Contract
, its agent
property must be set to a valid Agent
.
When obtaining instances from a Deployment
, their agent
property is provided
from deployment.agent
.
import { Agent } from '@fadroma/agent'
assert(deployment.a.agent instanceof Agent)
assert.equal(deployment.a.agent, deployment.agent)
You can instantiate a Contract
by awaiting the deployed
property or the return value of the
deploy()
method. Since distributed ledgers are append-only, deployment is an idempotent operation,
so the deploy will run only once and subsequent calls will return the same Contract
with the
same address
.
await deployment.a.deploy()
await deployment.a.deployed
If contract.codeId
is not set but either source code or a WASM binary is present,
this will try to upload and build the code first.
await deployment.a.uploaded
await deployment.a.upload()
await deployment.a.built
await deployment.a.build()
import assert from 'node:assert'
import './Deploy.test.ts'