Distributed application groundwork developed at Hack.bg. Fadroma is a scriptable orchestrator for building next-generation dApps on CosmWasm-enabled backends.
Component | Package | Description |
---|---|---|
@hackbg/fadroma | View docs. Cross-chain connector and deployer. | |
@hackbg/fadroma | View docs. Core API model. | |
@fadroma/scrt | View docs. Secret Network support. | |
@fadroma/cw | View docs. Other CosmWasm chain support. | |
@fadroma/create | View docs. Project setup utility. | |
@fadroma/compile | View docs. Smart contact compilation helper. | |
@fadroma/devnet | View docs. Local instances of chains for integration testing. | |
@fadroma/schema | View docs. Local instances of chains for integration testing. | |
fadroma-dsl | Macro-based smart contract DSL. | |
fadroma | Library for smart contracts. |
# Create a project:
$ npx @hackbg/fadroma@latest create
# Create a project using a specific version of Fadroma:
$ npx @hackbg/[email protected] create
The newly created project will contain the following modules:
-
api.ts is the root module of your project's TypeScript SDK. It contains
Client
subclasses that correspond to your contracts, and aDeployment
subclass which describes how the contracts relate to each other. See the Fadroma Agent API documentation for details. -
config.ts is your project's deploy configuration. Here, you can customize the build/upload/deploy procedures and define project-specific commands that you can then access from the Fadroma CLI.
-
test.ts is where you can write integration tests for your project.
# Build all contracts in the project:
$ npm run fadroma build
# Build a single contract:
$ npm run fadroma build some-contract
# Build multiple contracts:
$ npm run fadroma build some-contract another-contract a-third-contract
# Build contract by path:
$ npm run fadroma /path/to/crate
By default, builds happen in a Docker container. Set FADROMA_BUILD_RAW=1
to instead use
your local Rust toolchain.
The production builds of your contracts are stored as .wasm
binaries in your project's
wasm/
directory. Every binary has a corresponding .wasm.sha256
checksum file whose contents
correspond to the on-chain code hash.
To rebuild a contract, do one of the following:
- delete the contract and its checksum from
wasm/
; - use the
rebuild
command instead ofbuild
; - set the
FADROMA_REBUILD=1
when callingbuild
,upload
ordeploy
.
# Rebuild all contracts:
$ npm run fadroma rebuild
The supported deploy targets are mainnet
, testnet
, and devnet
. Projects created by Fadroma
define NPM scripts to select them:
# Deploy to mainnet
$ npm run mainnet deploy
# Deploy to testnet
$ npm run testnet deploy
# Deploy to devnet
$ npm run devnet deploy
In the examples below, we will use those interchangeably.
Alternatively, use the FADROMA_CHAIN
environment variable with npm run fadroma
.
See Fadroma Connect for a list of supported values.
Fadroma allows you to easily run local instances of the supported chains, in order to test your contracts without uploading them to testnet.
# Pause the devnet
$ npm run devnet pause
# Export a snapshot of the devnet to a new Docker image
$ npm run devnet export
# Resume the devnet
$ npm run devnet resume
# Stop the devnet and erase all state
$ npm run devnet reset
An exported devnet snapshot is a great way to provide a standardized dev build of your project that can be run locally by frontend devs, by your CI pipeline, etc.
# Build and upload all contracts in the project
$ npm testnet upload
# Build and upload a single contract
$ npm testnet upload some-contract
# Build and upload multiple contracts
$ npm testnet upload some-contract another-contract a-third-contract
If contract binaries are not present, the upload command will try to build them first.
Uploading a contract adds an upload receipt in state/$CHAIN_ID/uploads/$CODE_ID.json
.
This prevents duplicate uploads.
To force a reupload, either use the reupload
command (in place of upload
), or set
FADROMA_REUPLOAD=1
(e.g. when invoking upload
or deploy
).
# Reupload all contracts, getting new code ids:
$ npm testnet reupload
# Redeploy with new code ids
$ FADROMA_REUPLOAD=1 npm testnet redeploy
Use the deploy
command to deploy your project:
# Deploy your project to testnet
$ npm run testnet deploy [...ARGS]
When deploying, Fadroma will automatically build and upload any contracts that are specified in the deployment and are not already built or uploaded to the given chain.
Running deploy
on a completed deployment will do nothing (unless you've updated the
description of the deployment, in which case it will try to apply the updates).
To deploy everything anew, use redeploy
:
# Deploy everything anew
$ npm run testnet redeploy [...ARGS]
If deploying fails, you should be able to re-run deploy
and continue where you left off.
Deploying a project results in a deploy receipt being created -
a simple file containing the state of the deployment. You can have more than one of
these, corresponding to multiple independent deployments of the same code. To see
a list of them, use the list
command:
# List deployments in this project
$ npm run testnet list
After a deploy, the newly created deployment will be marked as active. To switch
to another deployment, use the select
command:
# Select another deployment
$ npm run testnet select my-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.
# Export the state of the active testnet deployment to ./my-deployment_@_timestamp.json
$ npm run testnet export
# Export state to ./some-directory/my-deployment_@_timestamp.json
$ npm run testnet export ./some-directory
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.
// TODO
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:
// TODO
Or, to connect to individual contracts from the stored deployment:
// TODO
Migrations can be implemented as static or regular methods
of Deployment
classes.
// TODO
-
Fadroma Agent Core, our core API defining portable user agents for smart contract-based backends.
-
Example: Fadroma Workshop repo, a step-by-step guide on how to build smart contracts using the Fadroma Rust crate.
-
Example: Fadroma Factory, a guide to deploying your Rust contracts using the Fadroma TypeScript package, via a factory pattern that enables your users to instantiate contracts in a controlled way.
This package is the core of Fadroma. It defines a TypeScript API for interacting with blockchains based on Tendermint and CosmWasm.
Since different chains provide different client libraries and connection methods, you need a concrete implementation of Fadroma Agent to actually talk to a chain:
- @fadroma/scrt for Secret Network.
- @fadroma/namada for Namada.
- @fadroma/cw for generic CosmWasm-enabled chains, such as:
Instances of the Chain class represents blockchains.
A chain may exists in one of several modes, represented by the chain.mode property and the ChainMode enum:
- mainnet is a production chain storing real value;
- testnet is a persistent remote chain used for testing;
- devnet is a locally run chain node in a Docker container;
- mocknet is a mock implementation of a chain.
The Chain.mainnet, Chain.testnet, Chain.devnet and Chain.mocknet static methods construct a chain in the given mode.
You can also check whether a chain is in a given mode using the chain.isMainnet, chain.isTestnet, chain.isDevnet and chain.isMocknet read-only boolean properties.
The chain.devMode property is true when the chain is a devnet or mocknet. Devnets and mocknets are under your control - i.e. you can delete them and start over. On the other hand, mainnet and testnet are global and persistent.
The chain.id property is a string that uniquely identifies a given blockchain.
Examples are secret-4
(Secret Network mainnet), pulsar-3
(Secret Network testnet),
or okp4-nemeton-1
(OKP4 testnet). Chains in different modes usually have distinct IDs.
The same chain may be accessible via different URLs. The chain.url property identifies the URL to which requests are sent.
Since the underlying API classes (e.g. CosmWasmClient
or SecretNetworkClient
) are
initialized asynchronously, and JavaScript does not have async constructors, chains start
out in an unitialized state, where the chain.api property is not populated. Awaiting the
chain.ready one-shot promise returns the same chain object, but with the API client populated.
Normally, this is done automatically when calling the chain's async methods; but if you want to
access the API handle directly, you would need to await chain.ready. This is useful if you
want to access a chain-specific feature that is not part of the Fadroma Agent API
Examples:
const { api } = await chain.ready
The chain.height getter returns a Promise wrapping the current block height.
The chain.nextBlock getter returns a Promise which resolves when the block height increments, and contains the new block height.
Examples:
// Get the current block height
const height = await chain.height
// Wait until the block height increments
await chain.nextBlock
The Chain.defaultDenom and chain.defaultDenom properties contain the default denomination of the chain's native token.
The chain.getBalance(denom, address) async method queries the balance of a given address in a given token.
Examples:
// TODO
The chain.query(contract, message) async method calls a read-only query method of a smart contract.
The chain.getCodeId(address), chain.getHash(addressOrCodeId) and chain.getLabel(address) async methods query the corresponding metadata of a smart contract.
The chain.checkHash(address, codeHash) method warns if the code hash of a contract is not the expected one.
Examples:
// TODO
To transact on a given chain, you need to authorize an Agent. This is done using the chain.authenticate(...) method, which synchonously returns a new Agent instance for the given chain.
Instantiating multiple agents allows the same program to interact with the chain from multiple distinct identities.
This method may be called with one of the following signatures:
- chain.authenticate(options)
- chain.authenticate(CustomAgentClass, options)
- chain.authenticate(CustomAgentClass)
The returned Agent starts out uninitialized. Awaiting the agent.ready property makes sure the agent is initialized. Usually, agents are initialized the first time you call one of the async methods described below.
If you don't pass a mnemonic, a random mnemonic and address will be generated.
Examples:
// TODO
The agent.address property is the on-chain address that uniquely identifies the agent.
The agent.name property is a user-friendly name for an agent. On devnet, the name is also used to access the initial accounts that are created during devnet genesis.
The agent.height and agent.nextBlock methods are equivalent to the same methods on the chain object, and are replicated on the Agent class purely for convenience.
const height = await agent.height
await agent.nextBlock
The agent.getBalance(denom, address) async method works the same as chain.getBalance(...) but defaults to the agent's address.
The agent.balance readonly property is a shorthand for querying the current agent's balance in the chain's main native token.
The agent.send(address, amounts, options) async method sends one or more amounts of native tokens to the specified address.
The agent.sendMany([[address, coin], [address, coin]...]) async method sends native tokens to multiple addresses.
Examples:
await agent.balance // In the default native token
await agent.getBalance() // In the default native token
await agent.getBalance('token') // In a non-default native token
await agent.send('recipient-address', 1000)
await agent.send('recipient-address', '1000')
await agent.send('recipient-address', [
{denom:'token1', amount: '1000'}
{denom:'token2', amount: '2000'}
])
The agent.upload(...) uploads a contract binary to the chain.
The agent.instantiate(...) async method takes a code ID and returns a contract instance.
The agent.instantiateMany(...) async method instantiates multiple contracts within the same transaction.
On Secret Network, it's not possible to send multiple separate upload transactions within the same block. Therefore, when uploading multiple contracts, agent.nextBlock needs to be awaited between them. agent.uploadMany(...) does this automatically.
Examples:
import { examples } from './fixtures/Fixtures.ts.md'
import { readFileSync } from 'node:fs'
// uploading from a Buffer
await agent.upload(readFileSync(examples['KV'].path), {
// optional metadata
codePath: examples['KV'].path
})
// Uploading from a filename
await agent.upload('example.wasm') // TODO
// Uploading an Uploadable object
await agent.upload({ artifact: './example.wasm', codeHash: 'expectedCodeHash' }) // TODO
const c1 = await agent.instantiate({
codeId: '1',
codeHash: 'verify!',
label: 'unique1',
initMsg: { arg: 'val' }
})
const [ c2, c3 ] = await agent.instantiateMany([
{ codeId: '2', label: 'unique2', initMsg: { arg: 'values' } },
{ codeId: '3', label: 'unique3', initMsg: { arg: 'values' } }
])
The agent.query(contract, message) async method calls a query method of a smart contract. This is equivalent to chain.query(...).
The agent.execute(contract, message) async method calls a transaction method of a smart contract, signing the transaction as the given agent.
Examples:
const response = await agent.query(c1, { get: { key: '1' } })
assert.rejects(agent.query(c1, { invalid: "query" }))
const result = await agent.execute(c1, { set: { key: '1', value: '2' } })
assert.rejects(agent.execute(c1, { invalid: "tx" }))
The agent.batch(...) method creates an instance of Batch.
Conceptually, you can view a batch as a kind of agent that does not execute transactions immediately - it collects them, and waits for the batch.broadcast() method. You can pass a batch anywhere you can pass an agent.
The main difference between a batch and and agent is that you cannot query from a batch. This is because a batch is an atomic action, and queries made inbetween individual transactions of a batch would return the state as it was before all the transactions. Therefore, to avoid confusion and outdated state, the query methods of the batch "agent" throw errors. If you need to perform queries, use a regular agent before or after the batch.
Instead of broadcasting, you can also export an unsigned batch, and pass it around manually as part of a multisig transaction.
To create and submit a batch in a single expression,
you can use batch.wrap(async (batch) => { ... })
:
Examples:
const results = await agent.batch(async batch=>{
await batch.execute(c1, { del: { key: '1' } })
await batch.execute(c2, { set: { key: '3', value: '4' } })
}).run()
Transacting creates load on the network, which incurs costs on node operators. Compensations for transactions are represented by the gas metric.
You can specify default gas limits for each method by defining the fees: Record<string, IFee>
property of your client class:
const fee1 = new Fee('100000', 'uscrt')
client.fees['my_method'] = fee1
assert.deepEqual(client.getFee('my_method'), fee1)
assert.deepEqual(client.getFee({'my_method':{'parameter':'value'}}), fee1)
You can also specify one fee for all transactions, using client.withFee({ gas, amount: [...] })
.
This method works by returning a copy of client
with fees overridden by the provided value.
const fee2 = new Fee('200000', 'uscrt')
assert.deepEqual(await client.withFee(fee2).getFee('my_method'), fee2)
The Client class represents a handle to a smart contract deployed to a given chain.
To provide a robust SDK to users of your project, simply publish a NPM package containing subclasses of Client that correspond to your contracts and invoke their methods.
To operate a smart contract through a Client
,
you need an agent
, an address
, and a codeHash
:
Example:
import { Client } from '@hackbg/fadroma'
class MyClient extends Client {
myMethod = (param) => this.execute({
my_method: { param }
})
myQuery = (param) => this.query({
my_query: { param }
})
}
let address = Symbol('some-addr')
let codeHash = Symbol('some-hash')
let client: Client = new MyClient({ agent, address, codeHash })
assert.equal(client.agent, agent)
assert.equal(client.address, address)
assert.equal(client.codeHash, codeHash)
client = agent.getClient(MyClient, address, codeHash)
await client.execute({ my_method: {} })
await client.query({ my_query: {} })
By default, the Client
's agent
property is equal to the agent
which deployed the contract. This property determines the address from
which subsequent transactions with that Client
will be sent.
In case you want to deploy the contract as one identity, then interact
with it from another one as part of the same procedure, you can set agent
to another instance of Agent
:
assert.equal(client.agent, agent)
client.agent = await chain.authenticate()
assert.notEqual(client.agent, agent)
Similarly to withFee
, the as
method returns a new instance of your
client class, bound to a different agent
, thus allowing you to execute
transactions as a different identity.
const agent1 = await chain.authenticate(/*...*/)
const agent2 = await chain.authenticate(/*...*/)
client = agent1.getClient(Client, "...")
// executed by agent1:
client.execute({ my_method: {} })
// executed by agent2
client.withAgent(agent2).execute({ my_method: {} })
The original Contract
object from which the contract
was deployed can be found on the optional meta
property of the Client
.
import { Contract } from '@hackbg/fadroma'
assert.ok(client.meta instanceof Contract)
Fetching metadata:
import { fetchLabel } from '@hackbg/fadroma'
await fetchCodeId(client, agent)
await fetchLabel(client, agent)
The code ID is a unique identifier for compiled code uploaded to a chain.
The code hash also uniquely identifies for the code that underpins a contract. However, unlike the code ID, which is opaque, the code hash corresponds to the actual content of the code. Uploading the same code multiple times will give you different code IDs, but the same code hash.
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 '@hackbg/fadroma'
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:
- If it's not compiled yet, this will build it.
- If it's not uploaded yet, it will upload it.
- If it's not instantiated yet, it will instantiate it.
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
}
}
The Contract
class describes an individual smart contract instance and uniquely identifies it
within the Deployment
.
import { Contract } from '@hackbg/fadroma'
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 '@hackbg/fadroma'
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()
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
.
// TODO
You can pass either an array or an object to template.instances
.
// TODO
To build, the compiler
property must be set to a valid Compiler
.
When obtaining instances from a Deployment
, the compiler
property
is provided automatically from deployment.compiler
.
// TODO
You can build a Template
(or its subclass, Contract
) by awaiting the
built
property or the return value of the build()
method.
// TODO
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
.
// TODO
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 compiler are present, this will also try to build the contract.
// TODO