The Fadroma Guide
Homepage Documentation Github Crates.io NPM Hack.bg

Fadroma Deploy API

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.

Deployment

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: {}
  })

}

Preparing

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 */)

Deploying everything

Then, call its deploy method:

await deployment.deploy()

For each contract defined in the deployment, this will do the following:

Expecting contracts to be deployed

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.

Deploying individual contracts with dependencies

By awaiting 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.

Deploying with custom logic

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
  }
}

Storing and exporting deployment state

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.

Exporting the deployment

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 exporting 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 })
  }
}

Connecting to an exported deployment

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)

Adding custom migrations

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()

Template

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.

Deploying multiple contracts from a template

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)
))

Building from source code

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.

Uploading binaries

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.

Contract

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 () => ({})
})

Naming and labels

The chain requires labels to be unique. Labels generated by Fadroma are of the format ${deployment.name}/${contract.name}.

Lazy init

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.

Deploying contract instances

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'