From 8104820bfc3078b33016596d12f7218da413f6b4 Mon Sep 17 00:00:00 2001 From: Adam Avramov Date: Wed, 18 Oct 2023 14:52:49 +0300 Subject: [PATCH] wip: refactor+test: simplifying documentation structure - spec/Agent.spec.ts.md -> agent/README.md - spec/Agent.test.ts -> agent/agent.test.ts etc. --- README.md | 1005 ++++++++++++++++- agent/README.md | 559 ++++++++- spec/Agent.test.ts => agent/agent.test.ts | 8 +- connect/README.md | 51 + connect/connect.test.ts | 6 + connect/cw/README.md | 111 +- spec/CW.test.ts => connect/cw/cw.test.ts | 0 connect/cw/okp4/README.md | 63 ++ connect/scrt/README.md | 632 +++++++++++ .../Scrt.test.ts => connect/scrt/scrt.test.ts | 42 + {spec => cookbook}/Factory.spec.ts.md | 0 {spec => cookbook}/Implementing.spec.ts.md | 0 {spec => cookbook}/tsconfig.json | 0 ensuite.yml | 1 + spec/Build.test.ts => fadroma-build.test.ts | 0 spec/Deploy.test.ts => fadroma-deploy.test.ts | 0 spec/Devnet.test.ts => fadroma-devnet.test.ts | 1 + spec/Upload.test.ts => fadroma-upload.test.ts | 0 .../Project.test.ts => fadroma-wizard.test.ts | 0 fadroma.test.ts | 84 ++ {spec/fixtures => fixtures}/.gitignore | 0 {spec/fixtures => fixtures}/Fixtures.ts.md | 0 {spec/fixtures => fixtures}/README.md | 0 .../_mock-devnet-init.mjs | 0 {spec/fixtures => fixtures}/empty.sh | 0 {spec/fixtures => fixtures}/empty.wasm | 0 .../fadroma-example-echo@HEAD.wasm | Bin .../fadroma-example-echo@HEAD.wasm.sha256 | 0 .../fadroma-example-kv@HEAD.wasm | Bin .../fadroma-example-kv@HEAD.wasm.sha256 | 0 .../fadroma-example-legacy@HEAD.wasm | Bin .../fadroma-example-legacy@HEAD.wasm.sha256 | 0 {spec/fixtures => fixtures}/null.wasm | 0 {spec/fixtures => fixtures}/tsconfig.json | 0 package.json | 26 +- spec/Agent.spec.ts.md | 420 ------- spec/Build.spec.ts.md | 249 ---- spec/CW.spec.ts.md | 46 - spec/Connect.spec.ts.md | 52 - spec/Connect.test.ts | 4 - spec/Deploy.spec.ts.md | 429 ------- spec/Devnet.spec.ts.md | 300 ----- spec/Mocknet.spec.ts.md | 136 --- spec/Mocknet.test.ts | 41 - spec/Project.spec.ts.md | 148 --- spec/Scrt.spec.ts.md | 198 ---- spec/Snip20.spec.ts.md | 293 ----- spec/Upload.spec.ts.md | 63 -- spec/Util.spec.ts.md | 76 -- spec/testIndex.ts | 18 - spec/testSelector.ts | 37 - 51 files changed, 2484 insertions(+), 2615 deletions(-) rename spec/Agent.test.ts => agent/agent.test.ts (97%) create mode 100644 connect/connect.test.ts rename spec/CW.test.ts => connect/cw/cw.test.ts (100%) create mode 100644 connect/cw/okp4/README.md rename spec/Scrt.test.ts => connect/scrt/scrt.test.ts (73%) rename {spec => cookbook}/Factory.spec.ts.md (100%) rename {spec => cookbook}/Implementing.spec.ts.md (100%) rename {spec => cookbook}/tsconfig.json (100%) rename spec/Build.test.ts => fadroma-build.test.ts (100%) rename spec/Deploy.test.ts => fadroma-deploy.test.ts (100%) rename spec/Devnet.test.ts => fadroma-devnet.test.ts (99%) rename spec/Upload.test.ts => fadroma-upload.test.ts (100%) rename spec/Project.test.ts => fadroma-wizard.test.ts (100%) create mode 100644 fadroma.test.ts rename {spec/fixtures => fixtures}/.gitignore (100%) rename {spec/fixtures => fixtures}/Fixtures.ts.md (100%) rename {spec/fixtures => fixtures}/README.md (100%) rename {spec/fixtures => fixtures}/_mock-devnet-init.mjs (100%) rename {spec/fixtures => fixtures}/empty.sh (100%) rename {spec/fixtures => fixtures}/empty.wasm (100%) rename {spec/fixtures => fixtures}/fadroma-example-echo@HEAD.wasm (100%) rename {spec/fixtures => fixtures}/fadroma-example-echo@HEAD.wasm.sha256 (100%) rename {spec/fixtures => fixtures}/fadroma-example-kv@HEAD.wasm (100%) rename {spec/fixtures => fixtures}/fadroma-example-kv@HEAD.wasm.sha256 (100%) rename {spec/fixtures => fixtures}/fadroma-example-legacy@HEAD.wasm (100%) rename {spec/fixtures => fixtures}/fadroma-example-legacy@HEAD.wasm.sha256 (100%) rename {spec/fixtures => fixtures}/null.wasm (100%) rename {spec/fixtures => fixtures}/tsconfig.json (100%) delete mode 100644 spec/Agent.spec.ts.md delete mode 100644 spec/Build.spec.ts.md delete mode 100644 spec/CW.spec.ts.md delete mode 100644 spec/Connect.spec.ts.md delete mode 100644 spec/Connect.test.ts delete mode 100644 spec/Deploy.spec.ts.md delete mode 100644 spec/Devnet.spec.ts.md delete mode 100644 spec/Mocknet.spec.ts.md delete mode 100644 spec/Mocknet.test.ts delete mode 100644 spec/Project.spec.ts.md delete mode 100644 spec/Scrt.spec.ts.md delete mode 100644 spec/Snip20.spec.ts.md delete mode 100644 spec/Upload.spec.ts.md delete mode 100644 spec/Util.spec.ts.md delete mode 100644 spec/testIndex.ts delete mode 100644 spec/testSelector.ts diff --git a/README.md b/README.md index 70e00a0ce0c..10362b464af 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-[![Fadroma](https://fadroma.tech/logo.svg)](https://fadroma.tech) +[![Fadroma](./banner2.svg)](https://fadroma.tech) Distributed application framework developed at [**Hack.bg**](https://hack.bg). @@ -22,3 +22,1006 @@ contracts using the Fadroma Rust crate, and the [**Fadroma Factory Example**](ht for a guide to deploying your Rust contracts using the Fadroma TypeScript package.
+ +--- + +# Getting started + +## Creating a project and defining contracts + +```shell +# Create a project: +$ npx @hackbg/fadroma@latest create + +# Create a project using a specific version of Fadroma: +$ npx @hackbg/fadroma@1.5.6 create + +# Add a contract to the project: +$ npm exec fadroma add +``` + +## Building contracts + +```shell +# Build a contract from the project: +$ npm exec fadroma build some-contract + +# Build multiple contracts from the project in parallel: +$ npm exec fadroma build some-contract another-contract a-third-contract + +# Build all contracts in the project: +$ npm exec fadroma build +``` + +Checksums of compiled contracts by version are stored in the build state +directory, `wasm/`. + +## Uploading contracts + +```shell +# Upload a contract: +$ npm exec fadroma upload CONTRACT [...CONTRACT] +``` + +If contract binaries are not present, the upload command will try to build them first. +Every successful upload logs the transaction as a file called an **upload receipt** under +`state/$CHAIN_ID/upload.`. This contains info about the upload transaction. + +The `UploadStore` loads a collection of upload receipts and tells the `Uploader` if a +binary has already been uploaded, so it can prevent duplicate uploads. + +## Deploying the project + +```shell +$ npm exec fadroma deploy [...ARGS] +``` + +Commencing a deployment creates a corresponding file under `state/$CHAIN_ID/deploy`, called +a **deploy receipt**. As contracts are deployed as part of this deployment, their details +will be appended to this file so that they can be found later. + +When a deploy receipt is created, that deployment is made active. This is so you can easily +find and interact with the contract you just deployed. The default deploy procedure is +dependency-based, so if the deployment fails, re-running `deploy` should try to resume +where you left off. Running `deploy` on a completed deployment will do nothing. + +To start over, use the `redeploy` command: + +```shell +$ npm exec fadroma redeploy [...ARGS] +``` + +This will create and activate a new deployment, and deploy everything anew. + +Keeping receipts of your primary mainnet/testnet deployments in your version control system +will let you keep track of your project's footprint on public networks. + +During development, receipts for deployments of a project are kept in a +human- and VCS-friendly YAML format. When publishing an API client, +you may want to include individual deployments as JSON files... TODO + +### 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: + +```sh +npm run mainnet select NAME +npm run 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: + +```typescript +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. + +```typescript +// 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: + +```typescript +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: + +```typescript +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. + +```typescript +// 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`. + +```typescript +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`. + +```typescript +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`. + +```typescript +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. + +```typescript +await deployment.t.built +// -or- +await deployment.t.build() +``` + +See [the **build guide**](./build.html) 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`. + +```typescript +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. + +```typescript +await deployment.t.uploaded +// -or- +await deployment.t.upload() +``` + +See [the **upload guide**](./upload.html) for more info. + +## Contract + +The `Contract` class describes an individual smart contract instance and uniquely identifies it +within the `Deployment`. + +```typescript +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`. + +```typescript +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`. + +```typescript +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. + +```typescript +await deployment.a.uploaded +await deployment.a.upload() + +await deployment.a.built +await deployment.a.build() +``` + +```typescript +import './Deploy.test.ts' +``` + +--- + +# Fadroma Guide: Devnet + +Fadroma enables fully local development of projects - no remote testnet needed! +This feature is known as **Fadroma Devnet**. + +Normally, you would interact with a devnet no different than any other +`Chain`: through your `Deployment` subclass. + +When using the Fadroma CLI, `Chain` instances are provided automatically +to instances `Deployment` subclasses. + +So, when `FADROMA_CHAIN` is set to `ScrtDevnet`, your deployment will +be instantiated alongside a local devnet, ready to operate! + +As a shortcut, projects created via the Fadroma CLI contain the `devnet` +NPM script, which is an alias to `FADROMA_CHAIN=ScrtDevnet fadroma`. + +So, to deploy your project to a local devnet, you would just run: + +```sh +$ npm run devnet deploy +``` + +## Advanced usage + +Fadroma Devnet includes container images based on `localsecret`, +for versions of Secret Network 1.2 to 1.9. Under the hood, the +implementation uses the library [`@hackbg/dock`](https://www.npmjs.com/package/@hackbg/dock) +to manage Docker images and containers. There is also experimental +support for Podman. + +### Creating the devnet + +When scripting with the Fadroma API outside of the standard CLI/deployment +context, you can use the `getDevnet` method to configure and obtain a `Devnet` +instance. + +```typescript +import { getDevnet } from '@hackbg/fadroma' + +const devnet = getDevnet(/* { options } */) +``` + +`getDevnet` supports the following options; their default values can be +set through environment variables. + +At this point you have prepared a *description* of a devnet. +To actually launch it, use the `create` then the `start` method: + +```typescript +await devnet.create() +await devnet.start() +``` + +At this point, you should have a devnet container running, +its state represented by files in your project's `state/` directory. + +To operate on the devnet thus created, you will need to wrap it +in a **Chain** object and obtain the usual **Agent** instance. + +For this, the **Devnet** class has the **getChain** method. + +```typescript +const chain = devnet.getChain() +``` + +A `Chain` object which represents a devnet has the following additional API: + +|name|description| +|-|-| +|**chain.mode**|**ChainMode**: `"Devnet"` when the chain in question is a devnet| +|**chain.isDevnet**|**boolean:** `true` when the chain in question is a devnet| +|**chain.devnet**|**DevnetHandle**: allows devnet internals to be controlled from your script| +|**chain.devnet.running**|**boolean**: `true` if the devnet container is running| +|**chain.devnet.start()**|**()⇒Promise\**: starts the devnet container| +|**chain.devnet.getAccount(name)**|**(string)⇒Promise\\>**: returns info about a genesis account| +|**chain.devnet.assertPresence()**|**()⇒Promise\**: throws if the devnet container ID is known, but the container itself is not found| + +```typescript +assert(chain.mode === 'Devnet') +assert(chain.isDevnet) +assert(chain.devnet === devnet) +``` + +### Devnet accounts + +Devnet state is independent from the state of mainnet or testnet. +That means existing wallets and faucets don't exist. Instead, you +have access to multiple **genesis accounts**, which are provided +with initial balance to cover gas costs for your contracts. + +When getting an **Agent** on the devnet, use the `name` property +to specify which genesis account to use. Default genesis account +names are `Admin`, `Alice`, `Bob`, `Charlie`, and `Mallory`. + +```typescript +const alice = chain.getAgent({ name: 'Alice' }) +await alice.ready +``` + +This will populate the created Agent with the mnemonic for that +genesis account. + +```typescript +assert( + alice instanceof Agent +) + +assert.equal( + alice.name, + 'Alice' +) + +assert.equal( + alice.address, + $(chain.devnet.stateDir, 'wallet', 'Alice.json').as(JSONFile).load().address, +) + +assert.equal( + alice.mnemonic, + $(chain.devnet.stateDir, 'wallet', 'Alice.json').as(JSONFile).load().mnemonic, +) +``` + +That's it! You are now set to use the standard Fadroma Agent API +to operate on the local devnet as the specified identity. + +#### Custom devnet accounts + +You can also specify custom genesis accounts by passing an array +of account names to the `accounts` parameter of the **getDevnet** +function. + +```typescript +const anotherDevnet = getDevnet({ + accounts: [ 'Alice', 'Bob' ], +}) + +assert.deepEqual( + anotherDevnet.accounts, + [ 'Alice', 'Bob' ] +) + +await anotherDevnet.delete() +``` + +### Pausing the devnet + +You can pause the devnet by stopping the container: + +```typescript +await devnet.pause() +await devnet.start() +await devnet.pause() +``` + +### Exporting a devnet snapshot + +An exported devnet snapshot is a great way to provide a +standardized development build of your project. For example, +you can use one to test the frontend/contracts stack as a +step of your integration pipeline. + +To create a snapshot, use the **export** method of the **Devnet** class: + +```typescript +await devnet.export() +``` + +When the active chain is a devnet, the `export` command, +which exports a list of contracts in the current deployment, +also saves the current state of the devnet as **a new container image**. + +```sh +$ npm run devnet export +``` + +### Cleaning up + +Devnets are local-only and thus temporary. + +To delete an individual devnet, the **Devnet** class +provides the **delete** method. This will stop and remove +the devnet container, then delete all devnet state in your +project's state directory. + +```typescript +await devnet.delete() +``` + +To delete all devnets in a project, the **Project** class +provides the **resetDevnets** method: + +```typescript +import Project from '@hackbg/fadroma' +const project = new Project() +project.resetDevnets() +``` + +The to call **resetDevnets** from the command line, use the +`reset` command: + +```sh +$ npm run devnet reset +``` + +## Devnet state + +Each **devnet** is a stateful local instance of a chain node +(such as `secretd` or `okp4d`), and consists of two things: + +1. A container named `fadroma-KIND-ID`, where: + + * `KIND` is what kind of devnet it is. For now, the only valid + value is `devnet`. In future releases, this will be changed to + contain the chain name and maybe the chain version. + + * `ID` is a random 8-digit hex number. This way, when you have + multiple devnets of the same kind, you can distinguish them + from one another. + + * The name of the container corresponds to the chain ID of the + contained devnet. + +```typescript +assert.ok( + chain.id.match(/fadroma-devnet-[0-9a-f]{8}/) +) + +assert.equal( + chain.id, + chain.devnet.chainId +) + +assert.equal( + (await chain.devnet.container).name, + `/${chain.id}` +) +``` + +2. State files under `your-project/state/fadroma-KIND-ID/`: + + * `devnet.json` contains metadata about the devnet, such as + the chain ID, container ID, connection port, and container + image to use. + + * `wallet/` contains JSON files with the addresses and mnemonics + of the **genesis accounts** that are created when the devnet + is initialized. These are the initial holders of the devnet's + native token, and you can use them to execute transactions. + + * `upload/` and `deploy/` contain **upload and deploy receipts**. + These work the same as for remote testnets and mainnets, + and enable reuse of uploads and deployments. + +```typescript +await devnet.create() +await devnet.start() +await devnet.pause() + +assert.equal( + $(chain.devnet.stateDir).name, + chain.id +) + +assert.deepEqual( + $(chain.devnet.stateDir, 'devnet.json').as(JSONFile).load(), + { + chainId: chain.id, + containerId: chain.devnet.containerId, + port: chain.devnet.port, + imageTag: chain.devnet.imageTag + } +) + +assert.deepEqual( + $(chain.devnet.stateDir, 'wallet').as(JSONDirectory).list(), + chain.devnet.accounts +) + +await devnet.delete() +``` + +--- + +```typescript +import { Chain, Agent } from '@fadroma/agent' +import $, { JSONFile, JSONDirectory } from '@hackbg/file' +import { Devnet } from '@hackbg/fadroma' +``` + +--- + +# Building contracts from source + +When deploying, Fadroma automatically builds the `Contract`s specified in the deployment, +using a procedure based on [secret-contract-optimizer](https://hub.docker.com/r/enigmampc/secret-contract-optimizer). + +This either with your local Rust/WASM toolchain, +or in a pre-defined [build container](https://github.com/hackbg/fadroma/pkgs/container/fadroma). +The latter option requires Docker (which you also need for the devnet). + +By default, optimized builds are output to the `wasm` subdirectory of your project. +Checksums of build artifacts are emitted as `wasm/*.wasm.sha256`: these checksums +should be equal to the code hashes returned by the chain. + +We advise you to keep these +**build receipts** in version control. This gives you a quick way to keep track of the +correspondence between changes to source and resulting changes to code hashes. + +Furthermore, when creating a `Project`, you'll be asked to define one or more `Template`s +corresponding to the contract crates of your project. You can + +Fadroma implements **reproducible compilation** of contracts. +What to compile is specified using the primitives defined in [Fadroma Core](../client/README.md). + +## Build CLI + +```shell +$ fadroma build CONTRACT # nop if already built +$ fadroma rebuild CONTRACT # always rebuilds +``` + + * **`CONTRACT`**: one of the contracts defined in the [project](../project/Project.spec.ts), + *or* a path to a crate assumed to contain a single contract. + +### Builder configuration + +## Build API + +* **BuildRaw**: runs the build in the current environment +* **BuildContainer**: runs the build in a container for enhanced reproducibility + +### Getting a builder + +```typescript +import { getBuilder } from '@hackbg/fadroma' +const builder = getBuilder(/* { ...options... } */) + +import { Builder } from '@hackbg/fadroma' +assert(builder instanceof Builder) +``` + +#### BuildContainer + +By default, you get a `BuildContainer`, +which runs the build procedure in a container +provided by either Docker or Podman (as selected +by the `FADROMA_BUILD_PODMAN` environment variable). + +```typescript +import { BuildContainer } from '@hackbg/fadroma' +assert.ok(getBuilder({ raw: false }) instanceof BuildContainer) +``` + +`BuildContainer` uses [`@hackbg/dock`](https://www.npmjs.com/package/@hackbg/dock) to +operate the container engine. + +```typescript +import * as Dokeres from '@hackbg/dock' +assert.ok(getBuilder({ raw: false }).docker instanceof Dokeres.Engine) +``` + +Use `FADROMA_DOCKER` or the `dockerSocket` option to specify a non-default Docker socket path. + +```typescript +getBuilder({ raw: false, dockerSocket: 'test' }) +``` + +The `BuildContainer` runs the build procedure defined by the `FADROMA_BUILD_SCRIPT` +in a container based on the `FADROMA_BUILD_IMAGE`, resulting in optimized WASM build artifacts +being output to the `FADROMA_ARTIFACTS` directory. + +#### BuildRaw + +If you want to execute the build procedure in your +current environment, you can switch to `BuildRaw` +by passing `raw: true` or setting `FADROMA_BUILD_RAW`. + +```typescript +const rawBuilder = getBuilder({ raw: true }) + +import { BuildRaw } from '@hackbg/fadroma' +assert.ok(rawBuilder instanceof BuildRaw) +``` + +### Building a contract + +Now that we've obtained a `Builder`, let's compile a contract from source into a WASM binary. + +#### Building a named contract from the project + +Building asynchronously returns `Template` instances. +A `Template` is an undeployed contract. You can upload +it once, and instantiate any number of `Contract`s from it. + +```typescript +for (const raw of [true, false]) { + const builder = getBuilder({ raw }) +``` + +To build a single crate with the builder: + +```typescript + const contract_0 = await builder.build({ crate: 'examples/kv' }) +``` + +To build multiple crates in parallel: + +```typescript + const [contract_1, contract_2] = await builder.buildMany([ + { crate: 'examples/admin' }, + { crate: 'examples/killswitch' } + ]) +``` + +For built contracts, the following holds true: + +```typescript + for (const [contract, index] of [ contract_0, contract_1, contract_2 ].map((c,i)=>[c,i]) { +``` + +* Build result will contain code hash and path to binary: + +```typescript + assert(typeof contract.codeHash === 'string', `contract_${index}.codeHash is set`) + assert(contract.artifact instanceof URL, `contract_${index}.artifact is set`) +``` + +* Build result will contain info about build inputs: + +```typescript + assert(contract.workspace, `contract_${index}.workspace is set`) + assert(contract.crate, `contract_${index}.crate is set`) + assert(contract.revision, `contract_${index}.revision is set`) +``` + +The above holds true equally for contracts produced +by `BuildContainer` and `BuildRaw`. + +```typescript + } +} +``` + +#### Specifying a contract to build + +The `Template` and `Contract` classes have the following properties for specifying the source: + +|field|type|description| +|-|-|-| +|**`repository`**|Path or URL|Points to the Git repository containing the contract sources. This is all you need if your smart contract is a single crate.| +|**`workspace`**|Path or URL|Cargo workspace containing the contract sources. May or may not be equal to `contract.repo`. May be empty if the contract is a single crate.| +|**`crate`**|string|Name of the Cargo crate containing the individual contract source. Required if `contract.workspace` is set.| +|**`revision`**|string|Git reference (branch or tag). Defaults to `HEAD`, otherwise builds a commit from history.| + +The outputs of builds are called **artifact**s, and are represented by two properties: + +|field|type|description| +|-|-|-| +|**`artifact`**|URL|Canonical location of the compiled binary.| +|**`codeHash`**|string|SHA256 checksum of artifact. should correspond to **template.codeHash** and **instance.codeHash** properties of uploaded and instantiated contracts| + +```typescript +import { Contract } from '@fadroma/agent' +const contract: Contract = new Contract({ builder, crate: 'fadroma-example-kv' }) +await contract.compiled +``` + +```typescript +import { Template } from '@fadroma/agent' +const template = new Template({ builder, crate: 'fadroma-example-kv' }) +await template.compiled +``` + +### Building past commits of contracts + +* `DotGit`, a helper for finding the contents of Git history + where Git submodules are involved. This works in tandem with + `build.impl.mjs` to enable: + * **building any commit** from a project's history, and therefore + * **pinning versions** for predictability during automated one-step deployments. + +If `.git` directory is present, builders can check out and build a past commits of the repo, +as specifier by `contract.revision`. + +```typescript +import { Contract } from '@fadroma/agent' +import { getGitDir, DotGit } from '@hackbg/fadroma' + +assert.throws(()=>getGitDir(new Contract())) + +const contractWithSource = new Contract({ + repository: 'REPO', + revision: 'REF', + workspace: 'WORKSPACE' + crate: 'CRATE' +}) + +assert.ok(getGitDir(contractWithSource) instanceof DotGit) +``` + +### Build caching + +When build caching is enabled, each build call first checks in `FADROMA_ARTIFACTS` +for a corresponding pre-existing build and reuses it if present. + +Setting `FADROMA_REBUILD` disables build caching. + +## Implementation details + +### The build procedure + +The ultimate build procedure, i.e. actual calls to `cargo` and such, +is implemented in the standalone script `FADROMA_BUILD_SCRIPT` (default: `build.impl.mjs`), +which is launched by the builders. + +### Builders + +The subclasses of the abstract base class `Builder` in Fadroma Core +implement the compilation procedure for contracts. + +--- + +```typescript +import { fileURLToPath } from 'url' +``` + +--- + +# Fadroma Upload + +Fadroma takes care of **uploading WASM files to get code IDs**. + +Like builds, uploads are *idempotent*: if the same code hash is +known to already be uploaded to the same chain (as represented by +an upload receipt in `state/$CHAIN/uploads/$CODE_HASH.json`, +Fadroma will skip the upload and reues the existing code ID. + +## Upload CLI + +The `fadroma upload` command (available through `npm run $MODE upload` +in the default project structure) lets you access Fadroma's `Uploader` +implementation from the command line. + +```shell +$ fadroma upload CONTRACT # nil if same contract is already uploaded +$ fadroma reupload CONTRACT # always reupload +``` + +## Upload API + +The client package, `@fadroma/agent`, exposes a base `Uploader` class, +which the global `fetch` method to obtain code from any supported URL +(`file:///` or otherwise). + +This `fetch`-based implementation only supports temporary, in-memory +upload caching: if you ask it to upload the same contract many times, +it will upload it only once - but it will forget all about that +as soon as you refresh the page. + +The backend package, `@hackbg/fadroma`, provides `FSUploader`. +This extension of `Uploader` uses Node's `fs` API instead, and +writes upload receipts into the upload state directory for the +given chain (e.g. `state/$CHAIN/uploads/`). + +Let's try uploading an example WASM binary: + +```typescript +import { fixture } from './fixtures/Fixtures.ts.md' +const artifact = fixture('fadroma-example-kv@HEAD.wasm') // replace with path to your binary +``` + +* Uploading with default configuration (from environment variables): + +```typescript +import { upload } from '@hackbg/fadroma' +await upload({ artifact }) +``` + +* Passing custom options to the uploader: + +```typescript +import { getUploader } from '@hackbg/fadroma' +await getUploader({ /* options */ }).upload({ artifact }) +``` + +--- + +# Configuration + +|Env var|Default path|Description| +|-|-|-| +|`FADROMA_ROOT` |current working directory |Root directory of project| +|`FADROMA_PROJECT` |`@/ops.ts` |Project command entrypoint| +|`FADROMA_BUILD_STATE` |`@/wasm` |Checksums of compiled contracts by version| +|`FADROMA_UPLOAD_STATE`|`@/state/uploads.csv` |Receipts of uploaded contracts| +|`FADROMA_DEPLOY_STATE`|`@/state/deployments.csv` |Receipts of instantiated (deployed) contracts| + +|name|env var|description| +|-|-|-| +|**chainId**|`FADROMA_DEVNET_CHAIN_ID`|**string**: chain ID (set to reconnect to existing devnet)| +|**platform**|`FADROMA_DEVNET_PLATFORM`|**string**: what kind of devnet to instantiate (e.g. `scrt_1.9`)| +|**deleteOnExit**|`FADROMA_DEVNET_REMOVE_ON_EXIT`|**boolean**: automatically remove the container and state when your script exits| +|**keepRunning**|`FADROMA_DEVNET_KEEP_RUNNING`|**boolean**: don't pause the container when your script exits| +|**host**|`FADROMA_DEVNET_HOST`|**string**: hostname where the devnet is running| +|**port**|`FADROMA_DEVNET_PORT`|**string**: port on which to connect to the devnet| + +|env var|type|description| +|-|-|-| +|**`FADROMA_BUILD_VERBOSE`**|flag|more log output +|**`FADROMA_BUILD_QUIET`**|flag|less log output +|**`FADROMA_BUILD_SCRIPT`**|path to script|build implementation +|**`FADROMA_BUILD_RAW`**|flag|run the build script in the current environment instead of container +|**`FADROMA_DOCKER`**|host:port or socket|non-default docker socket address +|**`FADROMA_BUILD_IMAGE`**|docker image tag|image to run +|**`FADROMA_BUILD_DOCKERFILE`**|path to dockerfile|dockerfile to build image if missing +|**`FADROMA_BUILD_PODMAN`**|flag|whether to use podman instead of docker +|**`FADROMA_PROJECT`**|path|root of project +|**`FADROMA_ARTIFACTS`**|path|project artifact cache +|**`FADROMA_REBUILD`**|flag|builds always run, artifact cache is ignored diff --git a/agent/README.md b/agent/README.md index aea89df112a..d14859a4d58 100644 --- a/agent/README.md +++ b/agent/README.md @@ -1,28 +1,551 @@ -
- -# Fadroma Agent +# [Fadroma](https://fadroma.tech) Agent: Scriptable User Agents for the Blockchain [![](https://img.shields.io/npm/v/@fadroma/agent?color=%2365b34c&label=%40fadroma%2Fagent&style=for-the-badge)](https://www.npmjs.com/package/@fadroma/agent) -Base layer for isomorphic dAPI clients. +The *Fadroma Agent API* is Fadroma's imperative API for interacting with smart contract +platforms. It's designed for expressing smart contract operations in a concise and readable manner. + +The API is specified by the [**@fadroma/agent**](https://www.npmjs.com/package/@fadroma/agent) +package. In effect, it's a reduced and simplified vocabulary that covers the common ground +between different implementations of smart contract-enabled chains. + +Since different chains provide different client libraries and connection methods, +the concrete implementations of the Fadroma Agent API are contained in separate +packages: + +* [**@fadroma/scrt**](https://www.npmjs.com/package/@fadroma/scrt) for Secret Network; +* [**@fadroma/cw**](https://www.npmjs.com/package/@fadroma/cw) for other CosmWasm-enabled chains, + such as OKP4. + +[**@fadroma/connect**](https://www.npmjs.com/package/@fadroma/connect) reexports all available +Fadroma Agent API implementations. It's recommended to use **@fadroma/connect** when depending +on more than one of the above. + +The overarching goal of Fadroma Agent is to enable developers to learn only a single client +library for all supported blockchains and client platforms. + +## Connecting to a chain + +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: + +```typescript +const { api } = await chain.ready +``` + +### Block height + +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: + +```typescript +// Get the current block height +const height = await chain.height + +// Wait until the block height increments +await chain.nextBlock +``` + +### Native tokens + +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: + +```typescript +// TODO +``` + +### Querying contracts + +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: + +```typescript +// TODO +``` + +## Authenticating an agent + +To transact on a given chain, you need to authorize an **Agent**. +This is done using the **chain.getAgent(...)** 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.getAgent(options)** +* **chain.getAgent(CustomAgentClass, options)** +* **chain.getAgent(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: + +```typescript +// TODO +``` + +### Agent identity + +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. + +### Agents and block height + +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. + +```typescript +const height = await agent.height + +await agent.nextBlock +``` + +### Native token transactions + +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: + +```typescript +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'} +]) +``` + +### Uploading and instantiating contracts + +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: + +```typescript +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 + artifact: 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 + +// Uploading multiple pieces of code: +await agent.uploadMany([ + 'example.wasm', + readFileSync('example.wasm'), + { artifact: './example.wasm', codeHash: 'expectedCodeHash' } +]) + +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' } } +]) +``` + +### Executing transactions and performing queries + +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: + +```typescript +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" })) +``` + +### Batching transactions + +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: + +```typescript +const results = await agent.batch(async batch=>{ + await batch.execute(c1, { del: { key: '1' } }) + await batch.execute(c2, { set: { key: '3', value: '4' } }) +}).run() +``` + +## Contract clients + +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: + +```typescript +import { Client } from '@fadroma/agent' + +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: {} }) +``` + +### Client agent + +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`: + +```typescript +assert.equal(client.agent, agent) +client.agent = await chain.getAgent() +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. + +```typescript +const agent1 = await chain.getAgent(/*...*/) +const agent2 = await chain.getAgent(/*...*/) + +client = agent1.getClient(Client, "...") + +// executed by agent1: +client.execute({ my_method: {} }) + +// executed by agent2 +client.withAgent(agent2).execute({ my_method: {} }) +``` + +### Client metadata + +The original `Contract` object from which the contract +was deployed can be found on the optional `meta` property of the `Client`. + +```typescript +import { Contract } from '@hackbg/fadroma' +assert.ok(client.meta instanceof Contract) +``` + +Fetching metadata: + +```typescript +import { fetchCodeHash, fetchCodeId, fetchLabel, assertCodeHash, codeHashOf } from '@fadroma/agent' + +await fetchCodeHash(client, agent) +await fetchCodeId(client, agent) +await fetchLabel(client, agent) +codeHashOf({ codeHash: 'hash' }) +codeHashOf({ code_hash: 'hash' }) +``` + +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. + +## Gas fees + +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` +property of your client class: + +```typescript +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. + +```typescript +const fee2 = new Fee('200000', 'uscrt') + +assert.deepEqual(await client.withFee(fee2).getFee('my_method'), fee2) +``` + +## Declarative deployments + +# 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`. + +```typescript +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: + +```typescript +// 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. + +```typescript +import { getDeployment } from '@hackbg/fadroma' +deployment = getDeployment(DeploymentA, /* ...constructor args */) +``` + +### Deploying everything + +Then, call its `deploy` method: + +```typescript +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. + +### 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). + +```typescript +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 -Defines the core operational model and type vocabulary -of the Fadroma dApp framework. +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)". -All other NPM packages in the Fadroma ecosystem -build upon this one, and either: +```typescript +assert(await deployment.kv1.deployed instanceof Client) +assert(await deployment.kv2.deployed instanceof Client) +``` -* Provide platform-specific implementations of these abstractions - (such as an Agent that is specifically for the Secret Network, - or a Builder that executes builds specifically in a Docker container), or +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. -* Build atop the abstract object model to deliver new features with - the appropriate degree of cross-platform support. +### Deploying with custom logic -The `@fadroma/agent` package itself is written in a platform-independent way -(basic [isomorphic JavaScript](https://en.wikipedia.org/wiki/Isomorphic_JavaScript)). -and should contain no Node-specifics or other engine-specific features. +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: -See https://fadroma.tech for more info. +```typescript +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 + } +} +``` diff --git a/spec/Agent.test.ts b/agent/agent.test.ts similarity index 97% rename from spec/Agent.test.ts rename to agent/agent.test.ts index 755ff0b0681..fb1d2b29450 100644 --- a/spec/Agent.test.ts +++ b/agent/agent.test.ts @@ -1,9 +1,8 @@ -import { StubChain as Chain, StubAgent as Agent, Batch, Error, Console } from '@fadroma/agent' +import { StubChain as Chain, StubAgent as Agent, Batch, Error, Console } from './agent' import assert from 'node:assert' -import testEntrypoint from './testSelector' +import { testEntrypoint, testSuite } from '@hackbg/ensuite' export default testEntrypoint(import.meta.url, { - 'docs': () => import('./Agent.spec.ts.md'), 'obtain': testAgentObtain, 'batch': testAgentBatch, 'errors': testAgentErrors, @@ -69,7 +68,7 @@ export async function testAgentBatch () { //assert.throws(()=>batch.nextBlock) //assert.throws(()=>batch.balance) - let chain: Chain = Chain.mocknet() + let chain: Chain = new Chain({ id: 'stub' }) let agent: Agent = await chain.getAgent({ address: 'testing1agent0' }) let batch: Batch @@ -151,3 +150,4 @@ export async function testAgentConsole () { if (typeof method==='function') try { method.bind(log)() } catch (e) { console.warn(e) } } } + diff --git a/connect/README.md b/connect/README.md index 750ed56a309..34f73401ee6 100644 --- a/connect/README.md +++ b/connect/README.md @@ -9,3 +9,54 @@ Catalog of Fadroma Agent implementations. See https://fadroma.tech for more info. + +This package acts as a hub for the available Fadroma Agent API implementations. +In practical terms, it allows you to connect to every chain (or other backend) +that Fadroma supports. + +## Connect CLI + +```sh +$ fadroma chain list +``` + +### Connection configuration + +## Connect API + +```typescript +import connect from '@fadroma/connect' + +for (const platform of ['secretjs', 'secretcli']) { + for (const mode of ['mainnet', 'testnet', 'devnet', 'mocknet']) { + const { chain, agent } = connect({ platform, mode, mnemonic: '...' }) + } +} +``` + +## Configuration + +```typescript +import { ConnectConfig } from '@fadroma/connect' +const config = new ConnectConfig() +config.getChain() +config.getChain(null) +assert.throws(()=>config.getChain('NoSuchChain')) +config.getAgent() +config.listChains() +``` + +## Errors + +```typescript +import { ConnectError, ConnectConsole } from '@fadroma/connect' +new ConnectError.NoChainSelected() +new ConnectError.UnknownChainSelected() +new ConnectConsole().selectedChain() +``` + +--- + +```typescript +import assert from 'node:assert' +``` diff --git a/connect/connect.test.ts b/connect/connect.test.ts new file mode 100644 index 00000000000..c2b5ca64e0e --- /dev/null +++ b/connect/connect.test.ts @@ -0,0 +1,6 @@ +import { testEntrypoint } from '@hackbg/ensuite' + +export default testEntrypoint(import.meta.url, { + 'docs': () => import('../spec/Connect.spec.ts.md'), +}) + diff --git a/connect/cw/README.md b/connect/cw/README.md index 0453ad5dd09..83d580664d1 100644 --- a/connect/cw/README.md +++ b/connect/cw/README.md @@ -12,75 +12,60 @@ See https://fadroma.tech for more info. ## Supported networks -### [OKP4](https://okp4.network/) +### Others -OKP4 is an Open Knowledge Platform For decentralized ontologies. +There is basic support for all chains accessible via `@cosmjs/stargate`. +However, chain-specific features may not be directly available. ```typescript -import { OKP4 } from '@fadroma/cw' - -// select chain and authenticate -const okp4 = await OKP4.testnet().getAgent({ mnemonic: '...' }).ready - -// deploy cognitarium -const { address: cognitariumAddress } = await okp4.instantiate(OKP4.Cognitarium.init()) - -// select cognitarium -const cognitarium = okp4.cognitarium(cognitariumAddress, okpAgent) - -// insert triples -await cognitarium.insert('turtle', ` - @prefix rdf: . - @prefix dc: . - @prefix ex: . - - - dc:title "RDF/XML Syntax Specification (Revised)" ; - ex:editor [ - ex:fullname "Dave Beckett"; - ex:homePage - ] . -`) - -// query triples -const result = await cognitarium.select(1, [ - /* prefixes */ -], [ - /* selected variables */ -], [ - /* where clauses */ -]) - -// deploy objectarium -const { address: objectariumAddress } = await okp4.instantiate(OKP4.Objectarium.init('fadroma')) - -// select objectarium -const objectarium = okp4.objectarium(objectariumAddress, okpAgent) - -// use objectarium -const { id: objectariumDataId } = await objectarium.store(false, 'somedatainbase64') -await objectarium.pin(id) -await objectarium.unpin(id) -await objectarium.forget(id) - -// deploy law stone -const { address: lawStoneAddress } = await okp4.instantiate(OKP4.LawStone.init('okp1...', ` - admin_addr('${okp4.address}'). -`)) - -// select law stone -const lawStone = okp4.lawStone(lawStoneAddress, okpAgent) - -// use law stone -await lawStone.ask(`admin_addr(${okp4.address})`) -await lawStone.break() +// TODO add example for connecting to generic CW-enabled chain ``` -### Others +--- -There is basic support for all chains accessible via `@cosmjs/stargate`. -However, chain-specific features may not be directly available. +# Generic CosmWasm-enabled chains ```typescript -// TODO add example for connecting to generic CW-enabled chain +import assert from 'node:assert' +``` + +Fadroma supports connecting to any chain that supports CosmWasm. + +For this, we currently use our own fork of the `@cosmjs/*` packages, +unified into a single package, [`@hackbg/cosmjs-esm`](https://www.npmjs.com/package/@hackbg/cosmjs-esm). + +## OKP4 support + +In these tests, we'll connect to a local OKP4 devnet +managed by Fadroma on your local Docker installation. +(Make sure you can call `docker` without `sudo`!) + +```typescript +import '@hackbg/fadroma' // installs devnet support +import { OKP4 } from '@fadroma/connect' + +const okp4 = await OKP4.devnet({ deleteOnExit: true }).ready +assert(okp4 instanceof OKP4.Chain) +``` + +You can use the `cognitaria`, `objectaria` and `lawStones` methods +to get lists of the corresponding contracts. + +```typescript +console.log(await okp4.cognitaria()) +console.log(await okp4.objectaria()) +console.log(await okp4.lawStones()) +``` + +To interact with them, you need to authenticate. This is done with +the `getAgent` method. The returned `OKP4Agent` has the same listing +methods - only this time the contracts are returned ready to use. + +```typescript +const signer = { /* get this from keplr */ } +const agent = await okp4.getAgent({ signer }).ready + +console.log(await agent.cognitaria()) +console.log(await agent.objectaria()) +console.log(await agent.lawStones()) ``` diff --git a/spec/CW.test.ts b/connect/cw/cw.test.ts similarity index 100% rename from spec/CW.test.ts rename to connect/cw/cw.test.ts diff --git a/connect/cw/okp4/README.md b/connect/cw/okp4/README.md new file mode 100644 index 00000000000..eb5b9745b80 --- /dev/null +++ b/connect/cw/okp4/README.md @@ -0,0 +1,63 @@ +# [OKP4](https://okp4.network/) support in [Fadroma](https://fadroma.tech) + +OKP4 is an Open Knowledge Platform For decentralized ontologies. + +```typescript +import { OKP4 } from '@fadroma/cw' + +// select chain and authenticate +const okp4 = await OKP4.testnet().getAgent({ mnemonic: '...' }).ready + +// deploy cognitarium +const { address: cognitariumAddress } = await okp4.instantiate(OKP4.Cognitarium.init()) + +// select cognitarium +const cognitarium = okp4.cognitarium(cognitariumAddress, okpAgent) + +// insert triples +await cognitarium.insert('turtle', ` + @prefix rdf: . + @prefix dc: . + @prefix ex: . + + + dc:title "RDF/XML Syntax Specification (Revised)" ; + ex:editor [ + ex:fullname "Dave Beckett"; + ex:homePage + ] . +`) + +// query triples +const result = await cognitarium.select(1, [ + /* prefixes */ +], [ + /* selected variables */ +], [ + /* where clauses */ +]) + +// deploy objectarium +const { address: objectariumAddress } = await okp4.instantiate(OKP4.Objectarium.init('fadroma')) + +// select objectarium +const objectarium = okp4.objectarium(objectariumAddress, okpAgent) + +// use objectarium +const { id: objectariumDataId } = await objectarium.store(false, 'somedatainbase64') +await objectarium.pin(id) +await objectarium.unpin(id) +await objectarium.forget(id) + +// deploy law stone +const { address: lawStoneAddress } = await okp4.instantiate(OKP4.LawStone.init('okp1...', ` + admin_addr('${okp4.address}'). +`)) + +// select law stone +const lawStone = okp4.lawStone(lawStoneAddress, okpAgent) + +// use law stone +await lawStone.ask(`admin_addr(${okp4.address})`) +await lawStone.break() +``` diff --git a/connect/scrt/README.md b/connect/scrt/README.md index 36ae1825fec..4241159b828 100644 --- a/connect/scrt/README.md +++ b/connect/scrt/README.md @@ -141,3 +141,635 @@ Fadroma tries to fetch the block limit from the chain: ```typescript console.log((await new Scrt().getAgent()).fees) ``` + +--- + +# Secret Network + +To use Fadroma Agent with SecretJS, you need the `@fadroma/scrt` package. +This package implements the core Fadroma Agent API with SecretJS. +It also exposes SN-specifics, such as a `Snip20` token client +and a `ViewingKeyClient`. + +* Like `@fadroma/agent`, this package aims to be *isomorphic*: + one of its design goals is to be usable in Node and browsers without modification. + +* `@hackbg/fadroma` automatically has this package through `@fadroma/connect` + +```typescript +import { Scrt } from '@fadroma/connect' +import { Devnet } from '@hackbg/fadroma' +import assert from 'node:assert' +``` + +## Configuring + +Several options are exposed as environment variables. + +```typescript +const config = new Scrt.Config() +``` + +|ScrtConfig property|Env var|Description| +|-|-|-| +|agentName |FADROMA_SCRT_AGENT_NAME |agent name| +|agentMnemonic |FADROMA_SCRT_AGENT_MNEMONIC |agent mnemonic for scrt only| +|mainnetChainId|FADROMA_SCRT_MAINNET_CHAIN_ID|chain id for mainnet| +|testnetChainId|FADROMA_SCRT_TESTNET_CHAIN_ID|chain id for mainnet| +|mainnetUrl |FADROMA_SCRT_MAINNET_URL |mainnet URL| +|testnetUrl |FADROMA_SCRT_TESTNET_URL |testnet URL| + +## Connecting and authenticating + +To connect to Secret Network with Fadroma, use one of the following: + +```typescript +const mainnet = Scrt.Chain.mainnet({ url: 'test' }) +const testnet = Scrt.Chain.testnet({ url: 'test' }) +const devnet = new Devnet({ platform: 'scrt_1.9' }).getChain(Scrt.Chain) +const mocknet = Scrt.Chain.mocknet({ url: 'test' }) +``` + +This will give you a `Scrt` instance (subclass of `Chain`): + +```typescript +import { Chain } from '@fadroma/agent' +for (const chain of [mainnet, testnet]) { + assert.ok(chain instanceof Chain && chain instanceof Scrt.Chain) +} +``` + +To interact with Secret Network, you need to authenticate as an `Agent`: + +### Fresh wallet + +This gives you a randomly generated mnemonic. + +```typescript +const agent0 = await mainnet.getAgent().ready +assert.ok(agent0 instanceof Scrt.Agent) +assert.ok(agent0.chain instanceof Scrt.Chain) +assert.ok(agent0.mnemonic) +assert.ok(agent0.address) +``` + +The `mnemonic` property of `Agent` will be hidden to prevent leakage. + +### By mnemonic + +```typescript +const mnemonic = 'define abandon palace resource estate elevator relief stock order pool knock myth brush element immense task rapid habit angry tiny foil prosper water news' +const agent1 = await mainnet.getAgent({ mnemonic }).ready + +ok(agent1 instanceof Scrt.Agent) +ok(agent1.chain instanceof Scrt.Chain) +ok(agent1.mnemonic) +ok(agent1.address) +``` + +### Keplr + +```typescript +// TODO: +// const agent2 = await mainnet.fromKeplr().ready +// ok(agent2 instanceof Scrt.Agent) +// ok(agent2.chain instanceof Scrt.Chain) +// ok(agent2.mnemonic) +// ok(agent2.address) +``` + +### secretcli + +```typescript +// TODO: +// const agent3 = await mainnet.fromSecretCli() +// ok(agent3 instanceof Scrt.Agent) +// ok(agent3.chain instanceof Scrt.Chain) +// ok(agent3.mnemonic) +// ok(agent3.address) +``` + +## Querying + +The `SecretJS` module used by a `ScrtChain` is available on the `SecretJS` property. + +```typescript +for (const chain of [mainnet, testnet, devnet, mocknet]) { + await chain.api + + // FIXME: need mock + //await chain.block + //await chain.height + + // FIXME: rejects with "#" ?! + // await chain.getBalance('scrt', 'address') + // await chain.getLabel() + // await chain.getCodeId() + // await chain.getHash() + // await chain.fetchLimits() + + // FIXME: Queries should be possible without an Agent. + assert.rejects(()=>chain.query()) +} +``` + +The `api`, `wallet`, and `encryptionUtils` properties of `ScrtAgent` +expose the `SecretNetworkClient`, `Wallet`, and `EncryptionUtils` (`EnigmaUtils`) +instances. + +```typescript +await agent1.ready +ok(agent1.api) +``` + +```typescript +const agent = agent0 +const address = 'some-addr' +const codeHash = 'some-hash' +``` + +## Tokens + +`@fadroma/scrt` exports a `Snip20` client class with most of the SNIP-20 methods exposed. + +```typescript +const token = new Scrt.Snip20({ agent, address, codeHash }) +``` + +There is also a `Snip721` stub client. See [#172](https://github.com/hackbg/fadroma/issues/172) +if you want to contribute a SNIP-721 client implementation: + +```typescript +const nft = new Scrt.Snip721({ agent, address, codeHash }) +``` + +## Viewing keys + +`@fadroma/scrt` exports the **`ViewingKeyClient`** class. + +```typescript +const client = new Scrt.ViewingKeyClient({ agent, address, codeHash }) +``` + +This is meant for embedding into your own `Client` classes +for contracts that implement the SNIP20-compatible viewing key API. + +```typescript +class MyClient extends Client { + get vk () { return new Scrt.ViewingKeyClient(this) } +} +``` + +Each `Snip20` instance already has a `vk` property that is a `ViewingKeyClient`. + +```typescript +assert(token.vk instanceof Scrt.ViewingKeyClient) +``` + +This is an example of composing client APIs by ownership rather than inheritance, +as shown above. + +## Query permits + +```typescript +// TODO add docs +``` + +--- + +```typescript +import { ok } from 'node:assert' +import { Client } from '@fadroma/agent' +import './Scrt.test.ts' +``` +FIXME: compare client implementation with actual snip20 spec + +--- + +# Using Fadroma with blockchain tokens + +Tokens are one core primitive of smart contract-based systems. +Fadroma provides several APIs for interfacing with tokens. + +## Token descriptors + +In the CosmWasm ecosystem, there's a distinction between **native** and **custom** tokens. + +* A **native token** is implemented by the chain's `bank` module. + Usually, you can pay gas fees with it. +* A **custom token** is implemented by a smart contract on the chain's `compute` module, + and can do more things than the native token. The core specification for custom tokens on + Secret Network is called [SNIP-20](https://docs.scrt.network/secret-network-documentation/development/snips/snip-20-spec-private-fungible-tokens). + +```typescript +import type { + + Token, // NativeToken|CustomToken + NativeToken // { native_token: { denom } } + CustomToken // { custom_token: { contract_addr, token_code_hash? } } + +} from '@fadroma/tokens' +``` + +`@fadroma/tokens` represents references to tokens as plain serializable objects. +These are useful when you want to pass info about a token from TypeScript to a contract +(where parsing is stricter). You can create them like this: + +```typescript +import { + + TokenKind, // Enumeration with two members, Custom and Native. + nativeToken, // Create a token descriptor specifying a native token + customToken, // Create a token descriptor specifying a custom token + +} from '@fadroma/tokens' + +const native: Token = nativeToken('scrt') // Native token: SCRT +const custom: Token = customToken('addr', 'hash') // SNIP-20 custom token: SecretSCRT (SSCRT) +``` + +And validate them like this: + +```typescript +import { + + isTokenDescriptor, // isNativeToken|isCustomToken + isNativeToken, // True iff native token + isCustomToken, // True iff custom token + +} from '@fadroma/tokens' + +ok(isTokenDescriptor(native)) +ok(isTokenDescriptor(custom)) + +ok(isNativeToken(native) && !isCustomToken(native)) +ok(isCustomToken(custom) && !isNativeToken(custom)) +``` + +And read their properties like this: + +```typescript +import { + + getTokenKind, // Return the kind of the token. + getTokenId, // Return either the native token's name or the custom token's address + +} from '@fadroma/tokens' + +equal(getTokenKind(native), TokenKind.Native) +equal(getTokenKind(custom), TokenKind.Custom) + +equal(getTokenId(native), 'native') +equal(getTokenId(custom), 'addr') +throws(()=>getTokenId(customToken())) +``` + +### Token amount descriptors + +To specify an integer amount of a token, use `TokenAmount`. + +* **NOTE:** Token amounts are always integers to avoid errors with precision, + so you need to add the appropriate amount of decimals. + +```typescript +import { + + TokenAmount, // An object representing an integer amount of a native or custom token + +} from '@fadroma/tokens' + +const native100 = new TokenAmount(native, '100') // 100 uSCRT +const custom100 = new TokenAmount(custom, '100') // 100 uSSCRT +deepEqual(native100.asNativeBalance, [{denom: "scrt", amount: "100"}]) +throws(()=>custom100.asNativeBalance) +``` + +### Token pair descriptors + +To describe a pair of tokens that can be exchanged against each other, +you can use `TokenPair`. + +Token pairs have the `reverse` property which returns a +new token pair with the places of the tow tokens swapped. + +```typescript +import { + + TokenPair, // A pair of tokens + +} from '@fadroma/tokens' + +deepEqual( + new TokenPair(native, custom).reverse, + new TokenPair(custom, native) +) +``` + +Respectively, `TokenPairAmount` establishes +an equivalence in value, e.g. `100 TOKENA = 200 TOKENB`. + +```typescript +import { + + TokenPairAmount // A pair of tokens with specified amounts + +} from '@fadroma/tokens' + +deepEqual( + new TokenPairAmount(new TokenPair(native, custom), "100", "200").reverse, + new TokenPairAmount(new TokenPair(custom, native), "200", "100") +) + +new TokenPairAmount(new TokenPair(native, custom), "100", "200").asNativeBalance +new TokenPairAmount(new TokenPair(custom, native), "100", "200").asNativeBalance + +throws(()=>new TokenPairAmount(new TokenPair(native, native), "100", "200").asNativeBalance) +``` + +## Token contract client + +To interact with a SNIP-20 token from TypeScript, you can use the `Snip20` client class. + +```typescript +import { + + Snip20 // The Client class for SNIP-20 tokens + +} from '@fadroma/tokens' + +import * as some from './mocks' +// Let's mock out the info returned from the backend + +// Create a client to a token contract +const yourToken = new Snip20(some.agent, some.address, some.codeHash) + +// A Snip20 instance can be converted into a descriptor, +// in order to pass info about that contract to some other contract or JSON API. +deepEqual(yourToken.asDescriptor, { + custom_token: { + contract_addr: some.address, + token_code_hash: some.codeHash + } +}) + +// And the converse: creating Snip20 clients +// from descriptors received over the wire: +ok( + Snip20.fromDescriptor(null, yourToken.asDescriptor) instanceof Snip20 +) + +deepEqual( + Snip20.fromDescriptor(null, yourToken.asDescriptor).asDescriptor, + descriptor +) +``` + +### Populating token metadata + +A token's address uniquely identifies it (for the given chain, of course). +However, to interact with a token on the chain, as a minimum you also need +its code hash; and `Snip20` client instances also keep track of other token metadata, +such as decimals. + +Those metadata fields start out as empty, and you can fetch their values +by calling `Snip20#populate`: + +```typescript +await yourToken.populate() +equal(yourToken.codeHash, 'fetchedCodeHash') +equal(yourToken.tokenName, name) +equal(yourToken.symbol, symbol) +equal(yourToken.decimals, decimals) +equal(yourToken.totalSupply, total_supply) +``` + +### Querying balance and sending amounts + +```typescript +const amount = Symbol() +yourToken.agent.query = async () => ({ balance: { amount } }) +yourToken.agent.execute = async (x, y) => y + +equal(await yourToken.getBalance('address', 'vk'), amount) + +deepEqual( + await yourToken.send('amount', 'recipient', { callback:'test' }), + { send: { amount: 'amount', recipient: 'recipient', msg: 'eyJjYWxsYmFjayI6InRlc3QifQ==' } } +) +``` + +### Deploying tokens + +```typescript +// This generates an init message for the standard SNIP-20 implementation +ok(Snip20.init()) + +// TODO show instantiate +``` + +### Query permits + +Fadroma supports generating [SNIP-24](https://docs.scrt.network/secret-network-documentation/development/snips/snip-24-query-permits-for-snip-20-tokens) +query permits. + +```typescript +import { + + createPermitMsg // Create a permit message + +} from '@fadroma/tokens' + +assert.deepEqual( + JSON.stringify(createPermitMsg('q', 'p')), + '{"with_permit":{"query":"q","permit":"p"}}' +) +``` + +## Token manager + +The Token Manager is an object that serves as a registry +of token contracts in a deployment. It keeps track of tokens, +and allows you to specify the cases where a contract in your project +depends on a custom token. + +When working on devnet, Fadroma can deploy mocks of third-party tokens. + +```typescript +import { Deployment } from '@fadroma/agent' +import { TokenManager, TokenPair, TokenError } from '@fadroma/tokens' + +const context: Deployment = new Deployment({ + name: 'test', + state: {} + agent: { + address: 'agent-address', + getHash: async () => 'Hash' + }, +}) + +const manager = new TokenManager(context) + +assert.throws(()=>manager.get()) +assert.throws(()=>manager.get('UNKNOWN')) + +const token = { address: 'a1', codeHash: 'c2' } +assert.ok(manager.add('KNOWN', token)) +assert.ok(manager.has('KNOWN')) +assert.equal(manager.get('KNOWN').address, token.address) +assert.equal(manager.get('KNOWN').codeHash, token.codeHash) + +assert.ok(manager.define('DEPLOY', { address: 'a2', codeHash: 'c2' })) +assert.ok(manager.pair('DEPLOY-KNOWN') instanceof TokenPair) + +new TokenError() + +import { ContractSlot, Template } from '@fadroma/agent' +const manager2 = new TokenManager({ + template: (options) => new ContractSlot(options) +}, new Template({ + crate: 'snip20' +})) +``` + +```typescript +import { ok, equal, deepEqual, throws } from 'assert' +``` + +--- + +# Fadroma Guide: Mocknet + +Testing the production builds of smart contracts can be slow and awkward. +Testnets are permanent and public; devnets can be temporary, but transactions +are still throttled by the block rate. + +Mocknet is a lightweight functioning mock of a CosmWasm-capable +platform, structured as an implementation of the Fadroma Chain API. +It emulates the APIs that a CosmWasm contract expects to see when +running in production, on top of the JavaScript engine's built-in +WebAssembly runtime. + +This way, you can run your real smart contracts without a real blockchain, +and quickly test their user-facing functionality and interoperation +in a customizable environment. + +## Table of contents + +* [Getting started with mocknet](#getting-started-with-mocknet) +* [Testing contracts on mocknet](#testing-contracts-on-mocknet) +* [Implementation details](#implementation-details) + +## Getting started with mocknet + +You can interact with a mocknet from TypeScript, the same way you interact with any other chain - +through the Fadroma Client API. + +* More specifically, `Mocknet` is an implementation of the `Chain` + abstract class which represents connection info for chains. +* **NOTE:** Mocknets are currently not persistent. + +```typescript +import { Mocknet } from '@fadroma/agent' +let chain = new Mocknet.Chain() +let agent = await chain.getAgent() + +import { Chain, Agent, Mocknet } from '@fadroma/agent' +assert.ok(chain instanceof Chain) +assert.ok(agent instanceof Agent) +assert.ok(agent instanceof Mocknet.Agent) +``` + +When creating a mocknet, the block height starts at 0. +You can increment it manually to represent the passing of block time. + +Native token balances also start at 0. You can give native tokens to agents by +setting the `Mocknet#balances` property: + +```typescript +assert.equal(await chain.height, 0) + +chain.balances[agent.address] = 1000 +assert.equal(await chain.getBalance(agent.address), 1000) + +assert.equal(agent.defaultDenom, chain.defaultDenom) +assert.ok(await agent.account) +assert.ok(!await agent.send()) +assert.ok(!await agent.sendMany()) +``` + +## Testing contracts on mocknet + +Uploading WASM blob will return the expected monotonously incrementing code ID... + +```typescript +import { pathToFileURL } from 'url' +import { examples } from './fixtures/Fixtures.ts.md' + +assert.equal(chain.lastCodeId, 0) + +const uploaded_a = await agent.upload(examples['KV'].data.load(), examples['KV']) +assert.equal(uploaded_a.codeId, 1) +assert.equal(chain.lastCodeId, 1) + +const uploaded_b = await agent.upload(examples['Legacy'].data.load(), examples['Legacy']) +assert.equal(uploaded_b.codeId, 2) +assert.equal(chain.lastCodeId, 2) +``` + +...which you can use to instantiate the contract. + +```typescript +const contract_a = uploaded_a.instance({ agent, name: 'kv', initMsg: { fail: false } }) +const client_a = await contract_a.deployed + +const contract_b = uploaded_b.instance({ agent, name: 'legacy', initMsg: { fail: false } }) +const client_b = await contract_b.deployed + +assert.deepEqual( + await client_a.query({get: {key: "foo"}}), + [null, null] // value returned from the contract +) + +assert.ok(await client_a.execute({set: {key: "foo", value: "bar"}})) + +const [data, meta] = await client_a.query({get: {key: "foo"} }) +assert.equal(data, 'bar') +assert.ok(meta) + +await chain.getLabel(client_a.address) +await chain.getHash(client_a.address) +await chain.getCodeId(client_a.codeHash) +``` + +## Backwards compatibility + +Mocknet supports contracts compiled for CosmWasm 0.x or 1.x. + +```typescript +assert.equal(chain.contracts[contract_a.address].cwVersion, '1.x') +assert.equal(chain.contracts[contract_b.address].cwVersion, '0.x') +``` + +## Snapshots + +Currently, **Mocknet is not stateful:** it only exists for the duration of the script run. + +You can instantiate Mocknet with pre-uploaded contracts: + +```typescript +chain = new Mocknet.Chain({ + uploads: { + 1: new Uint8Array(), + 234: new Uint8Array() + 567: new Uint8Array() + } +}) + +assert.equal(chain.lastCodeId, 567) +``` + +--- + +```typescript +import assert from 'node:assert' +``` diff --git a/spec/Scrt.test.ts b/connect/scrt/scrt.test.ts similarity index 73% rename from spec/Scrt.test.ts rename to connect/scrt/scrt.test.ts index 8572ea14b2c..876efefd277 100644 --- a/spec/Scrt.test.ts +++ b/connect/scrt/scrt.test.ts @@ -140,3 +140,45 @@ async function testScrtConsole () { .submittingBatchFailed(new Error()) } + +import * as assert from 'node:assert' +import { Mocknet } from '@fadroma/scrt' +import { randomBech32 } from '@fadroma/agent' + +import testEntrypoint from './testSelector' +export default testEntrypoint(import.meta.url, { + 'docs': () => import('./Mocknet.spec.ts.md'), + 'other': testMocknet() +}) + +export async function testMocknet () { + new Mocknet.Console().log('...') + new Mocknet.Console().trace('...') + new Mocknet.Console().debug('...') + + // **Base64 I/O:** Fields that are of type `Binary` (query responses and the `data` field of handle + // responses) are returned by the contract as Base64-encoded strings + // If `to_binary` is used to produce the `Binary`, it's also JSON encoded through Serde. + // These functions are used by the mocknet code to encode/decode the base64. + assert.equal(Mocknet.b64toUtf8('IkVjaG8i'), '"Echo"') + assert.equal(Mocknet.utf8toB64('"Echo"'), 'IkVjaG8i') + + let key: string + let value: string + let data: string +} + +export function mockEnv () { + const height = 0 + const time = 0 + const chain_id = "mock" + const sender = randomBech32('mocked') + const address = randomBech32('mocked') + return { + block: { height, time, chain_id }, + message: { sender: sender, sent_funds: [] }, + contract: { address }, + contract_key: "", + contract_code_hash: "" + } +} diff --git a/spec/Factory.spec.ts.md b/cookbook/Factory.spec.ts.md similarity index 100% rename from spec/Factory.spec.ts.md rename to cookbook/Factory.spec.ts.md diff --git a/spec/Implementing.spec.ts.md b/cookbook/Implementing.spec.ts.md similarity index 100% rename from spec/Implementing.spec.ts.md rename to cookbook/Implementing.spec.ts.md diff --git a/spec/tsconfig.json b/cookbook/tsconfig.json similarity index 100% rename from spec/tsconfig.json rename to cookbook/tsconfig.json diff --git a/ensuite.yml b/ensuite.yml index 4c426c1bd10..5e79d6d52fb 100644 --- a/ensuite.yml +++ b/ensuite.yml @@ -1,6 +1,7 @@ coverage: exclude: - "**/*.dist.*" + - "**/*.test.*" - "*.dist.*" - "*/coverage/**/*" - "build.impl.mjs" diff --git a/spec/Build.test.ts b/fadroma-build.test.ts similarity index 100% rename from spec/Build.test.ts rename to fadroma-build.test.ts diff --git a/spec/Deploy.test.ts b/fadroma-deploy.test.ts similarity index 100% rename from spec/Deploy.test.ts rename to fadroma-deploy.test.ts diff --git a/spec/Devnet.test.ts b/fadroma-devnet.test.ts similarity index 99% rename from spec/Devnet.test.ts rename to fadroma-devnet.test.ts index 30d2b7057c4..91e4aaa0681 100644 --- a/spec/Devnet.test.ts +++ b/fadroma-devnet.test.ts @@ -203,3 +203,4 @@ export async function testDevnetContainer () { "devnet assert presence rejects if container is removed" ) } + diff --git a/spec/Upload.test.ts b/fadroma-upload.test.ts similarity index 100% rename from spec/Upload.test.ts rename to fadroma-upload.test.ts diff --git a/spec/Project.test.ts b/fadroma-wizard.test.ts similarity index 100% rename from spec/Project.test.ts rename to fadroma-wizard.test.ts diff --git a/fadroma.test.ts b/fadroma.test.ts new file mode 100644 index 00000000000..3d49d04f976 --- /dev/null +++ b/fadroma.test.ts @@ -0,0 +1,84 @@ +import * as assert from 'node:assert' +import { into, intoArray, intoRecord } from '@fadroma/agent' +import { testEntrypoint, testSuite } from '@hackbg/ensuite' +export default testEntrypoint(import.meta.url, { + + 'agent': testSuite('./agent/agent.test'), + + 'connect': testSuite('./connect/connect.test'), + 'cw': testSuite('./connect/cw/cw.test'), + 'scrt': testSuite('./connect/scrt/scrt.test'), + + 'build': testSuite('./fadroma-build.test'), + 'deploy': testSuite('./fadroma-deploy.test'), + 'devnet': testSuite('./fadroma-devnet.test'), + 'upload': testSuite('./fadroma-upload.test'), + 'wizard': testSuite('./fadroma-wizard.test'), + + //'factory': () => import ('./Factory.spec.ts.md'), + //'impl': () => import('./Implementing.spec.ts.md'), +}) + +export async function testCollections () { + + assert.equal(await into(1), 1) + assert.equal(await into(Promise.resolve(1)), 1) + assert.equal(await into(()=>1), 1) + assert.equal(await into(async ()=>1), 1) + + assert.deepEqual( + await intoArray([1, ()=>1, Promise.resolve(1), async () => 1]), + [1, 1, 1, 1] + ) + + assert.deepEqual(await intoRecord({ + ready: 1, + getter: () => 2, + promise: Promise.resolve(3), + asyncFn: async () => 4 + }), { + ready: 1, + getter: 2, + promise: 3, + asyncFn: 4 + }) +} + +export async function testProject () { + const { default: Project } = await import('@hackbg/fadroma') + const { tmpDir } = await import('./fixtures/Fixtures.ts.md') + + const root = tmpDir() + + let project: Project = new Project({ + root: `${root}/test-project-1`, + name: 'test-project-1', + templates: { + test1: { crate: 'test1' }, + test2: { crate: 'test2' }, + } + }) + .create() + .status() + .cargoUpdate() + + const test1 = project.getTemplate('test1') + assert(test1 instanceof Template) + + const test3 = project.setTemplate('test3', { crate: 'test2' }) + assert(test3 instanceof Template) + await project.build() + await project.build('test1') + await project.upload() + await project.upload('test2') + await project.deploy(/* any deploy arguments, if you've overridden the deploy procedure */) + await project.redeploy(/* ... */) + await project.exportDeployment('state') +} + +export async function testDeploy () { +import { Deployment, Template, Contract, Client } from '@fadroma/agent' +let deployment: Deployment +let template: Template +let contract: Contract +} diff --git a/spec/fixtures/.gitignore b/fixtures/.gitignore similarity index 100% rename from spec/fixtures/.gitignore rename to fixtures/.gitignore diff --git a/spec/fixtures/Fixtures.ts.md b/fixtures/Fixtures.ts.md similarity index 100% rename from spec/fixtures/Fixtures.ts.md rename to fixtures/Fixtures.ts.md diff --git a/spec/fixtures/README.md b/fixtures/README.md similarity index 100% rename from spec/fixtures/README.md rename to fixtures/README.md diff --git a/spec/fixtures/_mock-devnet-init.mjs b/fixtures/_mock-devnet-init.mjs similarity index 100% rename from spec/fixtures/_mock-devnet-init.mjs rename to fixtures/_mock-devnet-init.mjs diff --git a/spec/fixtures/empty.sh b/fixtures/empty.sh similarity index 100% rename from spec/fixtures/empty.sh rename to fixtures/empty.sh diff --git a/spec/fixtures/empty.wasm b/fixtures/empty.wasm similarity index 100% rename from spec/fixtures/empty.wasm rename to fixtures/empty.wasm diff --git a/spec/fixtures/fadroma-example-echo@HEAD.wasm b/fixtures/fadroma-example-echo@HEAD.wasm similarity index 100% rename from spec/fixtures/fadroma-example-echo@HEAD.wasm rename to fixtures/fadroma-example-echo@HEAD.wasm diff --git a/spec/fixtures/fadroma-example-echo@HEAD.wasm.sha256 b/fixtures/fadroma-example-echo@HEAD.wasm.sha256 similarity index 100% rename from spec/fixtures/fadroma-example-echo@HEAD.wasm.sha256 rename to fixtures/fadroma-example-echo@HEAD.wasm.sha256 diff --git a/spec/fixtures/fadroma-example-kv@HEAD.wasm b/fixtures/fadroma-example-kv@HEAD.wasm similarity index 100% rename from spec/fixtures/fadroma-example-kv@HEAD.wasm rename to fixtures/fadroma-example-kv@HEAD.wasm diff --git a/spec/fixtures/fadroma-example-kv@HEAD.wasm.sha256 b/fixtures/fadroma-example-kv@HEAD.wasm.sha256 similarity index 100% rename from spec/fixtures/fadroma-example-kv@HEAD.wasm.sha256 rename to fixtures/fadroma-example-kv@HEAD.wasm.sha256 diff --git a/spec/fixtures/fadroma-example-legacy@HEAD.wasm b/fixtures/fadroma-example-legacy@HEAD.wasm similarity index 100% rename from spec/fixtures/fadroma-example-legacy@HEAD.wasm rename to fixtures/fadroma-example-legacy@HEAD.wasm diff --git a/spec/fixtures/fadroma-example-legacy@HEAD.wasm.sha256 b/fixtures/fadroma-example-legacy@HEAD.wasm.sha256 similarity index 100% rename from spec/fixtures/fadroma-example-legacy@HEAD.wasm.sha256 rename to fixtures/fadroma-example-legacy@HEAD.wasm.sha256 diff --git a/spec/fixtures/null.wasm b/fixtures/null.wasm similarity index 100% rename from spec/fixtures/null.wasm rename to fixtures/null.wasm diff --git a/spec/fixtures/tsconfig.json b/fixtures/tsconfig.json similarity index 100% rename from spec/fixtures/tsconfig.json rename to fixtures/tsconfig.json diff --git a/package.json b/package.json index 00d2ce653b2..99d87422c0c 100644 --- a/package.json +++ b/package.json @@ -27,36 +27,24 @@ "license": "AGPL-3.0-only", "scripts": { "repl": "./fadroma.cli.cjs repl", + "prepare": "husky install", "clean": "rm -rf *.cjs.js *.esm.js *.d.ts packages/*/dist packages/*/types", "ubik": "pnpm i && pnpm clean && pnpm check && ubik", "check": "time tsc --noEmit", "ci": "node --version && npm --version && pnpm --version && pwd && ls -al && pnpm cloc && pnpm clean && concurrently npm:check npm:cov:all && pnpm ubik --dry compile && ls -al", "cloc": "cloc --verbose=2 --fullpath --not-match-d=node_modules --not-match-f=pnpm-lock.yaml --exclude-dir=.github,.husky,ensuite,cosmjs-esm,secretjs-esm,coverage,state .", + "build": "./fadroma.cli.cjs build", "build:example": "FADROMA_REBUILD=1 FADROMA_BUILD_WORKSPACE_ROOT=. FADROMA_BUILD_WORKSPACE_MANIFEST=_Cargo.toml FADROMA_BUILD_OUTPUT_DIR=fixtures ./fadroma.cli.cjs build", + "docs:dev": "ensuite-dev", "docs:render": "time ensuite/ensuite-render.cli.mjs", "docs:typedoc": "time typedoc --customCss ./typedoc.css --tsconfig ./tsconfig.json --entryPointStrategy legacy-packages --entryPoints agent --entryPoints connect/* --entryPoints connect --entryPoints .", - "docs:vp:dev": "vitepress dev", - "docs:vp:build": "vitepress build", - "docs:vp:serve": "vitepress serve", - "test": "time ensuite spec/testIndex.ts", - "test:guide": "time ensuite GUIDE.ts.md", - "test:agent": "time ensuite spec/Agent.test.ts", - "test:build": "time ensuite spec/Build.test.ts", - "test:deploy": "time ensuite spec/Deploy.test.ts", - "test:devnet": "time ensuite spec/Devnet.test.ts", - "test:factory": "time ensuite spec/Factory.spec.ts.md", - "test:project": "time ensuite spec/Project.test.ts", - "test:scrt": "time ensuite spec/Scrt.test.ts", - "test:cw": "time ensuite spec/CW.test.ts", - "cov": "time ensuite-cov spec/testIndex.ts", - "cov:all": "time ensuite-cov spec/testIndex.ts all", - "cov:guide": "time ensuite-cov GUIDE.ts.md", - "cov:build": "time ensuite-cov spec/Build.test.ts", - "cov:factory": "time ensuite-cov spec/Factory.spec.ts.md", - "cov:devnet": "time ensuite-cov spec/Devnet.test.ts" + + "test": "time ensuite fadroma.test.ts", + "cov": "time ensuite-cov fadroma.test.ts", + "cov:all": "time ensuite-cov fadroma.test.ts all" }, "dependencies": { "@fadroma/agent": "workspace:2.0.0-rc.1", diff --git a/spec/Agent.spec.ts.md b/spec/Agent.spec.ts.md deleted file mode 100644 index 3fd2bfaf6d2..00000000000 --- a/spec/Agent.spec.ts.md +++ /dev/null @@ -1,420 +0,0 @@ -# Fadroma Agent: Scriptable User Agents for the Blockchain - -The *Fadroma Agent API* is Fadroma's imperative API for interacting with smart contract -platforms. It's designed for expressing smart contract operations in a concise and readable manner. - -The API is specified by the [**@fadroma/agent**](https://www.npmjs.com/package/@fadroma/agent) -package. In effect, it's a reduced and simplified vocabulary that covers the common ground -between different implementations of smart contract-enabled chains. - -Since different chains provide different client libraries and connection methods, -the concrete implementations of the Fadroma Agent API are contained in separate -packages: - -* [**@fadroma/scrt**](https://www.npmjs.com/package/@fadroma/scrt) for Secret Network; -* [**@fadroma/cw**](https://www.npmjs.com/package/@fadroma/cw) for other CosmWasm-enabled chains, - such as OKP4. - -[**@fadroma/connect**](https://www.npmjs.com/package/@fadroma/connect) reexports all available -Fadroma Agent API implementations. It's recommended to use **@fadroma/connect** when depending -on more than one of the above. - -The overarching goal of Fadroma Agent is to enable developers to learn only a single client -library for all supported blockchains and client platforms. - -## Connecting to a chain - -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: - -```typescript -const { api } = await chain.ready -``` - -### Block height - -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: - -```typescript -// Get the current block height -const height = await chain.height - -// Wait until the block height increments -await chain.nextBlock -``` - -### Native tokens - -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: - -```typescript -// TODO -``` - -### Querying contracts - -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: - -```typescript -// TODO -``` - -## Authenticating an agent - -To transact on a given chain, you need to authorize an **Agent**. -This is done using the **chain.getAgent(...)** 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.getAgent(options)** -* **chain.getAgent(CustomAgentClass, options)** -* **chain.getAgent(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: - -```typescript -// TODO -``` - -### Agent identity - -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. - -### Agents and block height - -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. - -```typescript -const height = await agent.height - -await agent.nextBlock -``` - -### Native token transactions - -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: - -```typescript -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'} -]) -``` - -### Uploading and instantiating contracts - -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: - -```typescript -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 - artifact: 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 - -// Uploading multiple pieces of code: -await agent.uploadMany([ - 'example.wasm', - readFileSync('example.wasm'), - { artifact: './example.wasm', codeHash: 'expectedCodeHash' } -]) - -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' } } -]) -``` - -### Executing transactions and performing queries - -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: - -```typescript -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" })) -``` - -### Batching transactions - -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: - -```typescript -const results = await agent.batch(async batch=>{ - await batch.execute(c1, { del: { key: '1' } }) - await batch.execute(c2, { set: { key: '3', value: '4' } }) -}).run() -``` - -## Contract clients - -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: - -```typescript -import { Client } from '@fadroma/agent' - -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: {} }) -``` - -### Client agent - -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`: - -```typescript -assert.equal(client.agent, agent) -client.agent = await chain.getAgent() -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. - -```typescript -const agent1 = await chain.getAgent(/*...*/) -const agent2 = await chain.getAgent(/*...*/) - -client = agent1.getClient(Client, "...") - -// executed by agent1: -client.execute({ my_method: {} }) - -// executed by agent2 -client.withAgent(agent2).execute({ my_method: {} }) -``` - -### Client metadata - -The original `Contract` object from which the contract -was deployed can be found on the optional `meta` property of the `Client`. - -```typescript -import { Contract } from '@hackbg/fadroma' -assert.ok(client.meta instanceof Contract) -``` - -Fetching metadata: - -```typescript -import { fetchCodeHash, fetchCodeId, fetchLabel, assertCodeHash, codeHashOf } from '@fadroma/agent' - -await fetchCodeHash(client, agent) -await fetchCodeId(client, agent) -await fetchLabel(client, agent) -codeHashOf({ codeHash: 'hash' }) -codeHashOf({ code_hash: 'hash' }) -``` - -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. - -## Gas fees - -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` -property of your client class: - -```typescript -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. - -```typescript -const fee2 = new Fee('200000', 'uscrt') - -assert.deepEqual(await client.withFee(fee2).getFee('my_method'), fee2) -``` diff --git a/spec/Build.spec.ts.md b/spec/Build.spec.ts.md deleted file mode 100644 index 46ac50430b8..00000000000 --- a/spec/Build.spec.ts.md +++ /dev/null @@ -1,249 +0,0 @@ -# Building contracts from source - -When deploying, Fadroma automatically builds the `Contract`s specified in the deployment, -using a procedure based on [secret-contract-optimizer](https://hub.docker.com/r/enigmampc/secret-contract-optimizer). - -This either with your local Rust/WASM toolchain, -or in a pre-defined [build container](https://github.com/hackbg/fadroma/pkgs/container/fadroma). -The latter option requires Docker (which you also need for the devnet). - -By default, optimized builds are output to the `wasm` subdirectory of your project. -Checksums of build artifacts are emitted as `wasm/*.wasm.sha256`: these checksums -should be equal to the code hashes returned by the chain. - -We advise you to keep these -**build receipts** in version control. This gives you a quick way to keep track of the -correspondence between changes to source and resulting changes to code hashes. - -Furthermore, when creating a `Project`, you'll be asked to define one or more `Template`s -corresponding to the contract crates of your project. You can - -Fadroma implements **reproducible compilation** of contracts. -What to compile is specified using the primitives defined in [Fadroma Core](../client/README.md). - -## Build CLI - -```shell -$ fadroma build CONTRACT # nop if already built -$ fadroma rebuild CONTRACT # always rebuilds -``` - - * **`CONTRACT`**: one of the contracts defined in the [project](../project/Project.spec.ts), - *or* a path to a crate assumed to contain a single contract. - -### Builder configuration - -|env var|type|description| -|-|-|-| -|**`FADROMA_BUILD_VERBOSE`**|flag|more log output -|**`FADROMA_BUILD_QUIET`**|flag|less log output -|**`FADROMA_BUILD_SCRIPT`**|path to script|build implementation -|**`FADROMA_BUILD_RAW`**|flag|run the build script in the current environment instead of container -|**`FADROMA_DOCKER`**|host:port or socket|non-default docker socket address -|**`FADROMA_BUILD_IMAGE`**|docker image tag|image to run -|**`FADROMA_BUILD_DOCKERFILE`**|path to dockerfile|dockerfile to build image if missing -|**`FADROMA_BUILD_PODMAN`**|flag|whether to use podman instead of docker -|**`FADROMA_PROJECT`**|path|root of project -|**`FADROMA_ARTIFACTS`**|path|project artifact cache -|**`FADROMA_REBUILD`**|flag|builds always run, artifact cache is ignored - -## Build API - -* **BuildRaw**: runs the build in the current environment -* **BuildContainer**: runs the build in a container for enhanced reproducibility - -### Getting a builder - -```typescript -import { getBuilder } from '@hackbg/fadroma' -const builder = getBuilder(/* { ...options... } */) - -import { Builder } from '@hackbg/fadroma' -assert(builder instanceof Builder) -``` - -#### BuildContainer - -By default, you get a `BuildContainer`, -which runs the build procedure in a container -provided by either Docker or Podman (as selected -by the `FADROMA_BUILD_PODMAN` environment variable). - -```typescript -import { BuildContainer } from '@hackbg/fadroma' -assert.ok(getBuilder({ raw: false }) instanceof BuildContainer) -``` - -`BuildContainer` uses [`@hackbg/dock`](https://www.npmjs.com/package/@hackbg/dock) to -operate the container engine. - -```typescript -import * as Dokeres from '@hackbg/dock' -assert.ok(getBuilder({ raw: false }).docker instanceof Dokeres.Engine) -``` - -Use `FADROMA_DOCKER` or the `dockerSocket` option to specify a non-default Docker socket path. - -```typescript -getBuilder({ raw: false, dockerSocket: 'test' }) -``` - -The `BuildContainer` runs the build procedure defined by the `FADROMA_BUILD_SCRIPT` -in a container based on the `FADROMA_BUILD_IMAGE`, resulting in optimized WASM build artifacts -being output to the `FADROMA_ARTIFACTS` directory. - -#### BuildRaw - -If you want to execute the build procedure in your -current environment, you can switch to `BuildRaw` -by passing `raw: true` or setting `FADROMA_BUILD_RAW`. - -```typescript -const rawBuilder = getBuilder({ raw: true }) - -import { BuildRaw } from '@hackbg/fadroma' -assert.ok(rawBuilder instanceof BuildRaw) -``` - -### Building a contract - -Now that we've obtained a `Builder`, let's compile a contract from source into a WASM binary. - -#### Building a named contract from the project - -Building asynchronously returns `Template` instances. -A `Template` is an undeployed contract. You can upload -it once, and instantiate any number of `Contract`s from it. - -```typescript -for (const raw of [true, false]) { - const builder = getBuilder({ raw }) -``` - -To build a single crate with the builder: - -```typescript - const contract_0 = await builder.build({ crate: 'examples/kv' }) -``` - -To build multiple crates in parallel: - -```typescript - const [contract_1, contract_2] = await builder.buildMany([ - { crate: 'examples/admin' }, - { crate: 'examples/killswitch' } - ]) -``` - -For built contracts, the following holds true: - -```typescript - for (const [contract, index] of [ contract_0, contract_1, contract_2 ].map((c,i)=>[c,i]) { -``` - -* Build result will contain code hash and path to binary: - -```typescript - assert(typeof contract.codeHash === 'string', `contract_${index}.codeHash is set`) - assert(contract.artifact instanceof URL, `contract_${index}.artifact is set`) -``` - -* Build result will contain info about build inputs: - -```typescript - assert(contract.workspace, `contract_${index}.workspace is set`) - assert(contract.crate, `contract_${index}.crate is set`) - assert(contract.revision, `contract_${index}.revision is set`) -``` - -The above holds true equally for contracts produced -by `BuildContainer` and `BuildRaw`. - -```typescript - } -} -``` - -#### Specifying a contract to build - -The `Template` and `Contract` classes have the following properties for specifying the source: - -|field|type|description| -|-|-|-| -|**`repository`**|Path or URL|Points to the Git repository containing the contract sources. This is all you need if your smart contract is a single crate.| -|**`workspace`**|Path or URL|Cargo workspace containing the contract sources. May or may not be equal to `contract.repo`. May be empty if the contract is a single crate.| -|**`crate`**|string|Name of the Cargo crate containing the individual contract source. Required if `contract.workspace` is set.| -|**`revision`**|string|Git reference (branch or tag). Defaults to `HEAD`, otherwise builds a commit from history.| - -The outputs of builds are called **artifact**s, and are represented by two properties: - -|field|type|description| -|-|-|-| -|**`artifact`**|URL|Canonical location of the compiled binary.| -|**`codeHash`**|string|SHA256 checksum of artifact. should correspond to **template.codeHash** and **instance.codeHash** properties of uploaded and instantiated contracts| - -```typescript -import { Contract } from '@fadroma/agent' -const contract: Contract = new Contract({ builder, crate: 'fadroma-example-kv' }) -await contract.compiled -``` - -```typescript -import { Template } from '@fadroma/agent' -const template = new Template({ builder, crate: 'fadroma-example-kv' }) -await template.compiled -``` - -### Building past commits of contracts - -* `DotGit`, a helper for finding the contents of Git history - where Git submodules are involved. This works in tandem with - `build.impl.mjs` to enable: - * **building any commit** from a project's history, and therefore - * **pinning versions** for predictability during automated one-step deployments. - -If `.git` directory is present, builders can check out and build a past commits of the repo, -as specifier by `contract.revision`. - -```typescript -import { Contract } from '@fadroma/agent' -import { getGitDir, DotGit } from '@hackbg/fadroma' - -assert.throws(()=>getGitDir(new Contract())) - -const contractWithSource = new Contract({ - repository: 'REPO', - revision: 'REF', - workspace: 'WORKSPACE' - crate: 'CRATE' -}) - -assert.ok(getGitDir(contractWithSource) instanceof DotGit) -``` - -### Build caching - -When build caching is enabled, each build call first checks in `FADROMA_ARTIFACTS` -for a corresponding pre-existing build and reuses it if present. - -Setting `FADROMA_REBUILD` disables build caching. - -## Implementation details - -### The build procedure - -The ultimate build procedure, i.e. actual calls to `cargo` and such, -is implemented in the standalone script `FADROMA_BUILD_SCRIPT` (default: `build.impl.mjs`), -which is launched by the builders. - -### Builders - -The subclasses of the abstract base class `Builder` in Fadroma Core -implement the compilation procedure for contracts. - ---- - -```typescript -import assert from 'node:assert' -import { fileURLToPath } from 'url' -``` diff --git a/spec/CW.spec.ts.md b/spec/CW.spec.ts.md deleted file mode 100644 index 462f1ff55a3..00000000000 --- a/spec/CW.spec.ts.md +++ /dev/null @@ -1,46 +0,0 @@ -# Generic CosmWasm-enabled chains - -```typescript -import assert from 'node:assert' -``` - -Fadroma supports connecting to any chain that supports CosmWasm. - -For this, we currently use our own fork of the `@cosmjs/*` packages, -unified into a single package, [`@hackbg/cosmjs-esm`](https://www.npmjs.com/package/@hackbg/cosmjs-esm). - -## OKP4 support - -In these tests, we'll connect to a local OKP4 devnet -managed by Fadroma on your local Docker installation. -(Make sure you can call `docker` without `sudo`!) - -```typescript -import '@hackbg/fadroma' // installs devnet support -import { OKP4 } from '@fadroma/connect' - -const okp4 = await OKP4.devnet({ deleteOnExit: true }).ready -assert(okp4 instanceof OKP4.Chain) -``` - -You can use the `cognitaria`, `objectaria` and `lawStones` methods -to get lists of the corresponding contracts. - -```typescript -console.log(await okp4.cognitaria()) -console.log(await okp4.objectaria()) -console.log(await okp4.lawStones()) -``` - -To interact with them, you need to authenticate. This is done with -the `getAgent` method. The returned `OKP4Agent` has the same listing -methods - only this time the contracts are returned ready to use. - -```typescript -const signer = { /* get this from keplr */ } -const agent = await okp4.getAgent({ signer }).ready - -console.log(await agent.cognitaria()) -console.log(await agent.objectaria()) -console.log(await agent.lawStones()) -``` diff --git a/spec/Connect.spec.ts.md b/spec/Connect.spec.ts.md deleted file mode 100644 index 6b3d750a462..00000000000 --- a/spec/Connect.spec.ts.md +++ /dev/null @@ -1,52 +0,0 @@ -# Connecting - -This package acts as a hub for the available Fadroma Agent API implementations. -In practical terms, it allows you to connect to every chain (or other backend) -that Fadroma supports. - -## Connect CLI - -```sh -$ fadroma chain list -``` - -### Connection configuration - -## Connect API - -```typescript -import connect from '@fadroma/connect' - -for (const platform of ['secretjs', 'secretcli']) { - for (const mode of ['mainnet', 'testnet', 'devnet', 'mocknet']) { - const { chain, agent } = connect({ platform, mode, mnemonic: '...' }) - } -} -``` - -## Configuration - -```typescript -import { ConnectConfig } from '@fadroma/connect' -const config = new ConnectConfig() -config.getChain() -config.getChain(null) -assert.throws(()=>config.getChain('NoSuchChain')) -config.getAgent() -config.listChains() -``` - -## Errors - -```typescript -import { ConnectError, ConnectConsole } from '@fadroma/connect' -new ConnectError.NoChainSelected() -new ConnectError.UnknownChainSelected() -new ConnectConsole().selectedChain() -``` - ---- - -```typescript -import assert from 'node:assert' -``` diff --git a/spec/Connect.test.ts b/spec/Connect.test.ts deleted file mode 100644 index 13e3a46309e..00000000000 --- a/spec/Connect.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -import testEntrypoint from './testSelector' -export default testEntrypoint(import.meta.url, { - 'docs': () => import('./Connect.spec.ts.md'), -}) diff --git a/spec/Deploy.spec.ts.md b/spec/Deploy.spec.ts.md deleted file mode 100644 index a21f513a137..00000000000 --- a/spec/Deploy.spec.ts.md +++ /dev/null @@ -1,429 +0,0 @@ -# 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`. - -```typescript -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: - -```typescript -// 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. - -```typescript -import { getDeployment } from '@hackbg/fadroma' -deployment = getDeployment(DeploymentA, /* ...constructor args */) -``` - -### Deploying everything - -Then, call its `deploy` method: - -```typescript -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. - -### 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). - -```typescript -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 `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)". - -```typescript -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: - -```typescript -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: - -```sh -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: - -```typescript -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. - -```typescript -// 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: - -```typescript -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: - -```typescript -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. - -```typescript -// 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`. - -```typescript -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`. - -```typescript -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`. - -```typescript -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. - -```typescript -await deployment.t.built -// -or- -await deployment.t.build() -``` - -See [the **build guide**](./build.html) 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`. - -```typescript -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. - -```typescript -await deployment.t.uploaded -// -or- -await deployment.t.upload() -``` - -See [the **upload guide**](./upload.html) for more info. - -## Contract - -The `Contract` class describes an individual smart contract instance and uniquely identifies it -within the `Deployment`. - -```typescript -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`. - -```typescript -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`. - -```typescript -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. - -```typescript -await deployment.a.uploaded -await deployment.a.upload() - -await deployment.a.built -await deployment.a.build() -``` - -```typescript -import assert from 'node:assert' -import './Deploy.test.ts' -``` diff --git a/spec/Devnet.spec.ts.md b/spec/Devnet.spec.ts.md deleted file mode 100644 index a27b79be1aa..00000000000 --- a/spec/Devnet.spec.ts.md +++ /dev/null @@ -1,300 +0,0 @@ -# Fadroma Guide: Devnet - -Fadroma enables fully local development of projects - no remote testnet needed! -This feature is known as **Fadroma Devnet**. - -Normally, you would interact with a devnet no different than any other -`Chain`: through your `Deployment` subclass. - -When using the Fadroma CLI, `Chain` instances are provided automatically -to instances `Deployment` subclasses. - -So, when `FADROMA_CHAIN` is set to `ScrtDevnet`, your deployment will -be instantiated alongside a local devnet, ready to operate! - -As a shortcut, projects created via the Fadroma CLI contain the `devnet` -NPM script, which is an alias to `FADROMA_CHAIN=ScrtDevnet fadroma`. - -So, to deploy your project to a local devnet, you would just run: - -```sh -$ npm run devnet deploy -``` - -## Advanced usage - -Fadroma Devnet includes container images based on `localsecret`, -for versions of Secret Network 1.2 to 1.9. Under the hood, the -implementation uses the library [`@hackbg/dock`](https://www.npmjs.com/package/@hackbg/dock) -to manage Docker images and containers. There is also experimental -support for Podman. - -### Creating the devnet - -When scripting with the Fadroma API outside of the standard CLI/deployment -context, you can use the `getDevnet` method to configure and obtain a `Devnet` -instance. - -```typescript -import { getDevnet } from '@hackbg/fadroma' - -const devnet = getDevnet(/* { options } */) -``` - -`getDevnet` supports the following options; their default values can be -set through environment variables. - -|name|env var|description| -|-|-|-| -|**chainId**|`FADROMA_DEVNET_CHAIN_ID`|**string**: chain ID (set to reconnect to existing devnet)| -|**platform**|`FADROMA_DEVNET_PLATFORM`|**string**: what kind of devnet to instantiate (e.g. `scrt_1.9`)| -|**deleteOnExit**|`FADROMA_DEVNET_REMOVE_ON_EXIT`|**boolean**: automatically remove the container and state when your script exits| -|**keepRunning**|`FADROMA_DEVNET_KEEP_RUNNING`|**boolean**: don't pause the container when your script exits| -|**host**|`FADROMA_DEVNET_HOST`|**string**: hostname where the devnet is running| -|**port**|`FADROMA_DEVNET_PORT`|**string**: port on which to connect to the devnet| - -At this point you have prepared a *description* of a devnet. -To actually launch it, use the `create` then the `start` method: - -```typescript -await devnet.create() -await devnet.start() -``` - -At this point, you should have a devnet container running, -its state represented by files in your project's `state/` directory. - -To operate on the devnet thus created, you will need to wrap it -in a **Chain** object and obtain the usual **Agent** instance. - -For this, the **Devnet** class has the **getChain** method. - -```typescript -const chain = devnet.getChain() -``` - -A `Chain` object which represents a devnet has the following additional API: - -|name|description| -|-|-| -|**chain.mode**|**ChainMode**: `"Devnet"` when the chain in question is a devnet| -|**chain.isDevnet**|**boolean:** `true` when the chain in question is a devnet| -|**chain.devnet**|**DevnetHandle**: allows devnet internals to be controlled from your script| -|**chain.devnet.running**|**boolean**: `true` if the devnet container is running| -|**chain.devnet.start()**|**()⇒Promise\**: starts the devnet container| -|**chain.devnet.getAccount(name)**|**(string)⇒Promise\\>**: returns info about a genesis account| -|**chain.devnet.assertPresence()**|**()⇒Promise\**: throws if the devnet container ID is known, but the container itself is not found| - -```typescript -assert(chain.mode === 'Devnet') -assert(chain.isDevnet) -assert(chain.devnet === devnet) -``` - -### Devnet accounts - -Devnet state is independent from the state of mainnet or testnet. -That means existing wallets and faucets don't exist. Instead, you -have access to multiple **genesis accounts**, which are provided -with initial balance to cover gas costs for your contracts. - -When getting an **Agent** on the devnet, use the `name` property -to specify which genesis account to use. Default genesis account -names are `Admin`, `Alice`, `Bob`, `Charlie`, and `Mallory`. - -```typescript -const alice = chain.getAgent({ name: 'Alice' }) -await alice.ready -``` - -This will populate the created Agent with the mnemonic for that -genesis account. - -```typescript -assert( - alice instanceof Agent -) - -assert.equal( - alice.name, - 'Alice' -) - -assert.equal( - alice.address, - $(chain.devnet.stateDir, 'wallet', 'Alice.json').as(JSONFile).load().address, -) - -assert.equal( - alice.mnemonic, - $(chain.devnet.stateDir, 'wallet', 'Alice.json').as(JSONFile).load().mnemonic, -) -``` - -That's it! You are now set to use the standard Fadroma Agent API -to operate on the local devnet as the specified identity. - -#### Custom devnet accounts - -You can also specify custom genesis accounts by passing an array -of account names to the `accounts` parameter of the **getDevnet** -function. - -```typescript -const anotherDevnet = getDevnet({ - accounts: [ 'Alice', 'Bob' ], -}) - -assert.deepEqual( - anotherDevnet.accounts, - [ 'Alice', 'Bob' ] -) - -await anotherDevnet.delete() -``` - -### Pausing the devnet - -You can pause the devnet by stopping the container: - -```typescript -await devnet.pause() -await devnet.start() -await devnet.pause() -``` - -### Exporting a devnet snapshot - -An exported devnet snapshot is a great way to provide a -standardized development build of your project. For example, -you can use one to test the frontend/contracts stack as a -step of your integration pipeline. - -To create a snapshot, use the **export** method of the **Devnet** class: - -```typescript -await devnet.export() -``` - -When the active chain is a devnet, the `export` command, -which exports a list of contracts in the current deployment, -also saves the current state of the devnet as **a new container image**. - -```sh -$ npm run devnet export -``` - -### Cleaning up - -Devnets are local-only and thus temporary. - -To delete an individual devnet, the **Devnet** class -provides the **delete** method. This will stop and remove -the devnet container, then delete all devnet state in your -project's state directory. - -```typescript -await devnet.delete() -``` - -To delete all devnets in a project, the **Project** class -provides the **resetDevnets** method: - -```typescript -import Project from '@hackbg/fadroma' -const project = new Project() -project.resetDevnets() -``` - -The to call **resetDevnets** from the command line, use the -`reset` command: - -```sh -$ npm run devnet reset -``` - -## Devnet state - -Each **devnet** is a stateful local instance of a chain node -(such as `secretd` or `okp4d`), and consists of two things: - -1. A container named `fadroma-KIND-ID`, where: - - * `KIND` is what kind of devnet it is. For now, the only valid - value is `devnet`. In future releases, this will be changed to - contain the chain name and maybe the chain version. - - * `ID` is a random 8-digit hex number. This way, when you have - multiple devnets of the same kind, you can distinguish them - from one another. - - * The name of the container corresponds to the chain ID of the - contained devnet. - -```typescript -assert.ok( - chain.id.match(/fadroma-devnet-[0-9a-f]{8}/) -) - -assert.equal( - chain.id, - chain.devnet.chainId -) - -assert.equal( - (await chain.devnet.container).name, - `/${chain.id}` -) -``` - -2. State files under `your-project/state/fadroma-KIND-ID/`: - - * `devnet.json` contains metadata about the devnet, such as - the chain ID, container ID, connection port, and container - image to use. - - * `wallet/` contains JSON files with the addresses and mnemonics - of the **genesis accounts** that are created when the devnet - is initialized. These are the initial holders of the devnet's - native token, and you can use them to execute transactions. - - * `upload/` and `deploy/` contain **upload and deploy receipts**. - These work the same as for remote testnets and mainnets, - and enable reuse of uploads and deployments. - -```typescript -await devnet.create() -await devnet.start() -await devnet.pause() - -assert.equal( - $(chain.devnet.stateDir).name, - chain.id -) - -assert.deepEqual( - $(chain.devnet.stateDir, 'devnet.json').as(JSONFile).load(), - { - chainId: chain.id, - containerId: chain.devnet.containerId, - port: chain.devnet.port, - imageTag: chain.devnet.imageTag - } -) - -assert.deepEqual( - $(chain.devnet.stateDir, 'wallet').as(JSONDirectory).list(), - chain.devnet.accounts -) - -await devnet.delete() -``` - ---- - -```typescript -import assert from 'node:assert' -import { Chain, Agent } from '@fadroma/agent' -import $, { JSONFile, JSONDirectory } from '@hackbg/file' -import { Devnet } from '@hackbg/fadroma' -``` diff --git a/spec/Mocknet.spec.ts.md b/spec/Mocknet.spec.ts.md deleted file mode 100644 index 83212b89cff..00000000000 --- a/spec/Mocknet.spec.ts.md +++ /dev/null @@ -1,136 +0,0 @@ -# Fadroma Guide: Mocknet - -Testing the production builds of smart contracts can be slow and awkward. -Testnets are permanent and public; devnets can be temporary, but transactions -are still throttled by the block rate. - -Mocknet is a lightweight functioning mock of a CosmWasm-capable -platform, structured as an implementation of the Fadroma Chain API. -It emulates the APIs that a CosmWasm contract expects to see when -running in production, on top of the JavaScript engine's built-in -WebAssembly runtime. - -This way, you can run your real smart contracts without a real blockchain, -and quickly test their user-facing functionality and interoperation -in a customizable environment. - -## Table of contents - -* [Getting started with mocknet](#getting-started-with-mocknet) -* [Testing contracts on mocknet](#testing-contracts-on-mocknet) -* [Implementation details](#implementation-details) - -## Getting started with mocknet - -You can interact with a mocknet from TypeScript, the same way you interact with any other chain - -through the Fadroma Client API. - -* More specifically, `Mocknet` is an implementation of the `Chain` - abstract class which represents connection info for chains. -* **NOTE:** Mocknets are currently not persistent. - -```typescript -import { Mocknet } from '@fadroma/agent' -let chain = new Mocknet.Chain() -let agent = await chain.getAgent() - -import { Chain, Agent, Mocknet } from '@fadroma/agent' -assert.ok(chain instanceof Chain) -assert.ok(agent instanceof Agent) -assert.ok(agent instanceof Mocknet.Agent) -``` - -When creating a mocknet, the block height starts at 0. -You can increment it manually to represent the passing of block time. - -Native token balances also start at 0. You can give native tokens to agents by -setting the `Mocknet#balances` property: - -```typescript -assert.equal(await chain.height, 0) - -chain.balances[agent.address] = 1000 -assert.equal(await chain.getBalance(agent.address), 1000) - -assert.equal(agent.defaultDenom, chain.defaultDenom) -assert.ok(await agent.account) -assert.ok(!await agent.send()) -assert.ok(!await agent.sendMany()) -``` - -## Testing contracts on mocknet - -Uploading WASM blob will return the expected monotonously incrementing code ID... - -```typescript -import { pathToFileURL } from 'url' -import { examples } from './fixtures/Fixtures.ts.md' - -assert.equal(chain.lastCodeId, 0) - -const uploaded_a = await agent.upload(examples['KV'].data.load(), examples['KV']) -assert.equal(uploaded_a.codeId, 1) -assert.equal(chain.lastCodeId, 1) - -const uploaded_b = await agent.upload(examples['Legacy'].data.load(), examples['Legacy']) -assert.equal(uploaded_b.codeId, 2) -assert.equal(chain.lastCodeId, 2) -``` - -...which you can use to instantiate the contract. - -```typescript -const contract_a = uploaded_a.instance({ agent, name: 'kv', initMsg: { fail: false } }) -const client_a = await contract_a.deployed - -const contract_b = uploaded_b.instance({ agent, name: 'legacy', initMsg: { fail: false } }) -const client_b = await contract_b.deployed - -assert.deepEqual( - await client_a.query({get: {key: "foo"}}), - [null, null] // value returned from the contract -) - -assert.ok(await client_a.execute({set: {key: "foo", value: "bar"}})) - -const [data, meta] = await client_a.query({get: {key: "foo"} }) -assert.equal(data, 'bar') -assert.ok(meta) - -await chain.getLabel(client_a.address) -await chain.getHash(client_a.address) -await chain.getCodeId(client_a.codeHash) -``` - -## Backwards compatibility - -Mocknet supports contracts compiled for CosmWasm 0.x or 1.x. - -```typescript -assert.equal(chain.contracts[contract_a.address].cwVersion, '1.x') -assert.equal(chain.contracts[contract_b.address].cwVersion, '0.x') -``` - -## Snapshots - -Currently, **Mocknet is not stateful:** it only exists for the duration of the script run. - -You can instantiate Mocknet with pre-uploaded contracts: - -```typescript -chain = new Mocknet.Chain({ - uploads: { - 1: new Uint8Array(), - 234: new Uint8Array() - 567: new Uint8Array() - } -}) - -assert.equal(chain.lastCodeId, 567) -``` - ---- - -```typescript -import assert from 'node:assert' -``` diff --git a/spec/Mocknet.test.ts b/spec/Mocknet.test.ts deleted file mode 100644 index d9fd756a9f3..00000000000 --- a/spec/Mocknet.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as assert from 'node:assert' -import { Mocknet } from '@fadroma/scrt' -import { randomBech32 } from '@fadroma/agent' - -import testEntrypoint from './testSelector' -export default testEntrypoint(import.meta.url, { - 'docs': () => import('./Mocknet.spec.ts.md'), - 'other': testMocknet() -}) - -export async function testMocknet () { - new Mocknet.Console().log('...') - new Mocknet.Console().trace('...') - new Mocknet.Console().debug('...') - - // **Base64 I/O:** Fields that are of type `Binary` (query responses and the `data` field of handle - // responses) are returned by the contract as Base64-encoded strings - // If `to_binary` is used to produce the `Binary`, it's also JSON encoded through Serde. - // These functions are used by the mocknet code to encode/decode the base64. - assert.equal(Mocknet.b64toUtf8('IkVjaG8i'), '"Echo"') - assert.equal(Mocknet.utf8toB64('"Echo"'), 'IkVjaG8i') - - let key: string - let value: string - let data: string -} - -export function mockEnv () { - const height = 0 - const time = 0 - const chain_id = "mock" - const sender = randomBech32('mocked') - const address = randomBech32('mocked') - return { - block: { height, time, chain_id }, - message: { sender: sender, sent_funds: [] }, - contract: { address }, - contract_key: "", - contract_code_hash: "" - } -} diff --git a/spec/Project.spec.ts.md b/spec/Project.spec.ts.md deleted file mode 100644 index 28a41748608..00000000000 --- a/spec/Project.spec.ts.md +++ /dev/null @@ -1,148 +0,0 @@ -# Project API - -## Configuration - -|Env var|Default path|Description| -|-|-|-| -|`FADROMA_ROOT` |current working directory |Root directory of project| -|`FADROMA_PROJECT` |`@/ops.ts` |Project command entrypoint| -|`FADROMA_BUILD_STATE` |`@/wasm` |Checksums of compiled contracts by version| -|`FADROMA_UPLOAD_STATE`|`@/state/uploads.csv` |Receipts of uploaded contracts| -|`FADROMA_DEPLOY_STATE`|`@/state/deployments.csv` |Receipts of instantiated (deployed) contracts| - -## Creating a project - -```shell -$ npx @hackbg/fadroma@latest create -``` - -```typescript -import Project from '@hackbg/fadroma' - -const root = tmpDir() - -let project: Project = new Project({ - root: `${root}/test-project-1`, - name: 'test-project-1', - templates: { - test1: { crate: 'test1' }, - test2: { crate: 'test2' }, - } -}) - .create() - .status() - .cargoUpdate() -``` - -## Defining new contracts - -```shell -$ npm exec fadroma add -``` - -```typescript -const test1 = project.getTemplate('test1') -assert(test1 instanceof Template) - -const test3 = project.setTemplate('test3', { crate: 'test2' }) -assert(test3 instanceof Template) -``` - -## Building - -```shell -$ npm exec fadroma build CONTRACT [...CONTRACT] -``` - -Building all templates in the project: - -```typescript -await project.build() -``` - -Building some templates from the project: - -```typescript -await project.build('test1') -``` - -Checksums of compiled contracts by version are stored in the build state -directory, `wasm/`. - -## Uploading - -```shell -$ npm exec fadroma upload CONTRACT [...CONTRACT] -``` - -Uploading all templates in the project: - -```typescript -await project.upload() -``` - -Uploading some templates from the project: - -```typescript -await project.upload('test2') -``` - -If contract binaries are not present, the upload command will try to build them first. -Every successful upload logs the transaction as a file called an **upload receipt** under -`state/$CHAIN_ID/upload.`. This contains info about the upload transaction. - -The `UploadStore` loads a collection of upload receipts and tells the `Uploader` if a -binary has already been uploaded, so it can prevent duplicate uploads. - -## Deploying - -```shell -$ npm exec fadroma deploy [...ARGS] -``` - -Deploying the project: - -```typescript -await project.deploy(/* any deploy arguments, if you've overridden the deploy procedure */) -``` - -Commencing a deployment creates a corresponding file under `state/$CHAIN_ID/deploy`, called -a **deploy receipt**. As contracts are deployed as part of this deployment, their details -will be appended to this file so that they can be found later. - -When a deploy receipt is created, that deployment is made active. This is so you can easily -find and interact with the contract you just deployed. The default deploy procedure is -dependency-based, so if the deployment fails, re-running `deploy` should try to resume -where you left off. Running `deploy` on a completed deployment will do nothing. - -To start over, use the `redeploy` command: - -```shell -$ npm exec fadroma redeploy [...ARGS] -``` - -```typescript -await project.redeploy(/* ... */) -``` - -This will create and activate a new deployment, and deploy everything anew. - -Keeping receipts of your primary mainnet/testnet deployments in your version control system -will let you keep track of your project's footprint on public networks. - -During development, receipts for deployments of a project are kept in a -human- and VCS-friendly YAML format. When publishing an API client, -you may want to include individual deployments as JSON> - -```typescript -await project.exportDeployment('state') -``` - ---- - -```typescript -import assert from 'node:assert' -import { Template } from '@fadroma/agent' -import { tmpDir } from './fixtures/Fixtures.ts.md' -import './Project.test' -``` diff --git a/spec/Scrt.spec.ts.md b/spec/Scrt.spec.ts.md deleted file mode 100644 index cb5401319ab..00000000000 --- a/spec/Scrt.spec.ts.md +++ /dev/null @@ -1,198 +0,0 @@ -# Secret Network - -To use Fadroma Agent with SecretJS, you need the `@fadroma/scrt` package. -This package implements the core Fadroma Agent API with SecretJS. -It also exposes SN-specifics, such as a `Snip20` token client -and a `ViewingKeyClient`. - -* Like `@fadroma/agent`, this package aims to be *isomorphic*: - one of its design goals is to be usable in Node and browsers without modification. - -* `@hackbg/fadroma` automatically has this package through `@fadroma/connect` - -```typescript -import { Scrt } from '@fadroma/connect' -import { Devnet } from '@hackbg/fadroma' -import assert from 'node:assert' -``` - -## Configuring - -Several options are exposed as environment variables. - -```typescript -const config = new Scrt.Config() -``` - -|ScrtConfig property|Env var|Description| -|-|-|-| -|agentName |FADROMA_SCRT_AGENT_NAME |agent name| -|agentMnemonic |FADROMA_SCRT_AGENT_MNEMONIC |agent mnemonic for scrt only| -|mainnetChainId|FADROMA_SCRT_MAINNET_CHAIN_ID|chain id for mainnet| -|testnetChainId|FADROMA_SCRT_TESTNET_CHAIN_ID|chain id for mainnet| -|mainnetUrl |FADROMA_SCRT_MAINNET_URL |mainnet URL| -|testnetUrl |FADROMA_SCRT_TESTNET_URL |testnet URL| - -## Connecting and authenticating - -To connect to Secret Network with Fadroma, use one of the following: - -```typescript -const mainnet = Scrt.Chain.mainnet({ url: 'test' }) -const testnet = Scrt.Chain.testnet({ url: 'test' }) -const devnet = new Devnet({ platform: 'scrt_1.9' }).getChain(Scrt.Chain) -const mocknet = Scrt.Chain.mocknet({ url: 'test' }) -``` - -This will give you a `Scrt` instance (subclass of `Chain`): - -```typescript -import { Chain } from '@fadroma/agent' -for (const chain of [mainnet, testnet]) { - assert.ok(chain instanceof Chain && chain instanceof Scrt.Chain) -} -``` - -To interact with Secret Network, you need to authenticate as an `Agent`: - -### Fresh wallet - -This gives you a randomly generated mnemonic. - -```typescript -const agent0 = await mainnet.getAgent().ready -assert.ok(agent0 instanceof Scrt.Agent) -assert.ok(agent0.chain instanceof Scrt.Chain) -assert.ok(agent0.mnemonic) -assert.ok(agent0.address) -``` - -The `mnemonic` property of `Agent` will be hidden to prevent leakage. - -### By mnemonic - -```typescript -const mnemonic = 'define abandon palace resource estate elevator relief stock order pool knock myth brush element immense task rapid habit angry tiny foil prosper water news' -const agent1 = await mainnet.getAgent({ mnemonic }).ready - -ok(agent1 instanceof Scrt.Agent) -ok(agent1.chain instanceof Scrt.Chain) -ok(agent1.mnemonic) -ok(agent1.address) -``` - -### Keplr - -```typescript -// TODO: -// const agent2 = await mainnet.fromKeplr().ready -// ok(agent2 instanceof Scrt.Agent) -// ok(agent2.chain instanceof Scrt.Chain) -// ok(agent2.mnemonic) -// ok(agent2.address) -``` - -### secretcli - -```typescript -// TODO: -// const agent3 = await mainnet.fromSecretCli() -// ok(agent3 instanceof Scrt.Agent) -// ok(agent3.chain instanceof Scrt.Chain) -// ok(agent3.mnemonic) -// ok(agent3.address) -``` - -## Querying - -The `SecretJS` module used by a `ScrtChain` is available on the `SecretJS` property. - -```typescript -for (const chain of [mainnet, testnet, devnet, mocknet]) { - await chain.api - - // FIXME: need mock - //await chain.block - //await chain.height - - // FIXME: rejects with "#" ?! - // await chain.getBalance('scrt', 'address') - // await chain.getLabel() - // await chain.getCodeId() - // await chain.getHash() - // await chain.fetchLimits() - - // FIXME: Queries should be possible without an Agent. - assert.rejects(()=>chain.query()) -} -``` - -The `api`, `wallet`, and `encryptionUtils` properties of `ScrtAgent` -expose the `SecretNetworkClient`, `Wallet`, and `EncryptionUtils` (`EnigmaUtils`) -instances. - -```typescript -await agent1.ready -ok(agent1.api) -``` - -```typescript -const agent = agent0 -const address = 'some-addr' -const codeHash = 'some-hash' -``` - -## Tokens - -`@fadroma/scrt` exports a `Snip20` client class with most of the SNIP-20 methods exposed. - -```typescript -const token = new Scrt.Snip20({ agent, address, codeHash }) -``` - -There is also a `Snip721` stub client. See [#172](https://github.com/hackbg/fadroma/issues/172) -if you want to contribute a SNIP-721 client implementation: - -```typescript -const nft = new Scrt.Snip721({ agent, address, codeHash }) -``` - -## Viewing keys - -`@fadroma/scrt` exports the **`ViewingKeyClient`** class. - -```typescript -const client = new Scrt.ViewingKeyClient({ agent, address, codeHash }) -``` - -This is meant for embedding into your own `Client` classes -for contracts that implement the SNIP20-compatible viewing key API. - -```typescript -class MyClient extends Client { - get vk () { return new Scrt.ViewingKeyClient(this) } -} -``` - -Each `Snip20` instance already has a `vk` property that is a `ViewingKeyClient`. - -```typescript -assert(token.vk instanceof Scrt.ViewingKeyClient) -``` - -This is an example of composing client APIs by ownership rather than inheritance, -as shown above. - -## Query permits - -```typescript -// TODO add docs -``` - ---- - -```typescript -import { ok } from 'node:assert' -import { Client } from '@fadroma/agent' -import './Scrt.test.ts' -``` diff --git a/spec/Snip20.spec.ts.md b/spec/Snip20.spec.ts.md deleted file mode 100644 index 1b2241228b1..00000000000 --- a/spec/Snip20.spec.ts.md +++ /dev/null @@ -1,293 +0,0 @@ -FIXME: compare client implementation with actual snip20 spec - ---- - -# Using Fadroma with blockchain tokens - -Tokens are one core primitive of smart contract-based systems. -Fadroma provides several APIs for interfacing with tokens. - -## Token descriptors - -In the CosmWasm ecosystem, there's a distinction between **native** and **custom** tokens. - -* A **native token** is implemented by the chain's `bank` module. - Usually, you can pay gas fees with it. -* A **custom token** is implemented by a smart contract on the chain's `compute` module, - and can do more things than the native token. The core specification for custom tokens on - Secret Network is called [SNIP-20](https://docs.scrt.network/secret-network-documentation/development/snips/snip-20-spec-private-fungible-tokens). - -```typescript -import type { - - Token, // NativeToken|CustomToken - NativeToken // { native_token: { denom } } - CustomToken // { custom_token: { contract_addr, token_code_hash? } } - -} from '@fadroma/tokens' -``` - -`@fadroma/tokens` represents references to tokens as plain serializable objects. -These are useful when you want to pass info about a token from TypeScript to a contract -(where parsing is stricter). You can create them like this: - -```typescript -import { - - TokenKind, // Enumeration with two members, Custom and Native. - nativeToken, // Create a token descriptor specifying a native token - customToken, // Create a token descriptor specifying a custom token - -} from '@fadroma/tokens' - -const native: Token = nativeToken('scrt') // Native token: SCRT -const custom: Token = customToken('addr', 'hash') // SNIP-20 custom token: SecretSCRT (SSCRT) -``` - -And validate them like this: - -```typescript -import { - - isTokenDescriptor, // isNativeToken|isCustomToken - isNativeToken, // True iff native token - isCustomToken, // True iff custom token - -} from '@fadroma/tokens' - -ok(isTokenDescriptor(native)) -ok(isTokenDescriptor(custom)) - -ok(isNativeToken(native) && !isCustomToken(native)) -ok(isCustomToken(custom) && !isNativeToken(custom)) -``` - -And read their properties like this: - -```typescript -import { - - getTokenKind, // Return the kind of the token. - getTokenId, // Return either the native token's name or the custom token's address - -} from '@fadroma/tokens' - -equal(getTokenKind(native), TokenKind.Native) -equal(getTokenKind(custom), TokenKind.Custom) - -equal(getTokenId(native), 'native') -equal(getTokenId(custom), 'addr') -throws(()=>getTokenId(customToken())) -``` - -### Token amount descriptors - -To specify an integer amount of a token, use `TokenAmount`. - -* **NOTE:** Token amounts are always integers to avoid errors with precision, - so you need to add the appropriate amount of decimals. - -```typescript -import { - - TokenAmount, // An object representing an integer amount of a native or custom token - -} from '@fadroma/tokens' - -const native100 = new TokenAmount(native, '100') // 100 uSCRT -const custom100 = new TokenAmount(custom, '100') // 100 uSSCRT -deepEqual(native100.asNativeBalance, [{denom: "scrt", amount: "100"}]) -throws(()=>custom100.asNativeBalance) -``` - -### Token pair descriptors - -To describe a pair of tokens that can be exchanged against each other, -you can use `TokenPair`. - -Token pairs have the `reverse` property which returns a -new token pair with the places of the tow tokens swapped. - -```typescript -import { - - TokenPair, // A pair of tokens - -} from '@fadroma/tokens' - -deepEqual( - new TokenPair(native, custom).reverse, - new TokenPair(custom, native) -) -``` - -Respectively, `TokenPairAmount` establishes -an equivalence in value, e.g. `100 TOKENA = 200 TOKENB`. - -```typescript -import { - - TokenPairAmount // A pair of tokens with specified amounts - -} from '@fadroma/tokens' - -deepEqual( - new TokenPairAmount(new TokenPair(native, custom), "100", "200").reverse, - new TokenPairAmount(new TokenPair(custom, native), "200", "100") -) - -new TokenPairAmount(new TokenPair(native, custom), "100", "200").asNativeBalance -new TokenPairAmount(new TokenPair(custom, native), "100", "200").asNativeBalance - -throws(()=>new TokenPairAmount(new TokenPair(native, native), "100", "200").asNativeBalance) -``` - -## Token contract client - -To interact with a SNIP-20 token from TypeScript, you can use the `Snip20` client class. - -```typescript -import { - - Snip20 // The Client class for SNIP-20 tokens - -} from '@fadroma/tokens' - -import * as some from './mocks' -// Let's mock out the info returned from the backend - -// Create a client to a token contract -const yourToken = new Snip20(some.agent, some.address, some.codeHash) - -// A Snip20 instance can be converted into a descriptor, -// in order to pass info about that contract to some other contract or JSON API. -deepEqual(yourToken.asDescriptor, { - custom_token: { - contract_addr: some.address, - token_code_hash: some.codeHash - } -}) - -// And the converse: creating Snip20 clients -// from descriptors received over the wire: -ok( - Snip20.fromDescriptor(null, yourToken.asDescriptor) instanceof Snip20 -) - -deepEqual( - Snip20.fromDescriptor(null, yourToken.asDescriptor).asDescriptor, - descriptor -) -``` - -### Populating token metadata - -A token's address uniquely identifies it (for the given chain, of course). -However, to interact with a token on the chain, as a minimum you also need -its code hash; and `Snip20` client instances also keep track of other token metadata, -such as decimals. - -Those metadata fields start out as empty, and you can fetch their values -by calling `Snip20#populate`: - -```typescript -await yourToken.populate() -equal(yourToken.codeHash, 'fetchedCodeHash') -equal(yourToken.tokenName, name) -equal(yourToken.symbol, symbol) -equal(yourToken.decimals, decimals) -equal(yourToken.totalSupply, total_supply) -``` - -### Querying balance and sending amounts - -```typescript -const amount = Symbol() -yourToken.agent.query = async () => ({ balance: { amount } }) -yourToken.agent.execute = async (x, y) => y - -equal(await yourToken.getBalance('address', 'vk'), amount) - -deepEqual( - await yourToken.send('amount', 'recipient', { callback:'test' }), - { send: { amount: 'amount', recipient: 'recipient', msg: 'eyJjYWxsYmFjayI6InRlc3QifQ==' } } -) -``` - -### Deploying tokens - -```typescript -// This generates an init message for the standard SNIP-20 implementation -ok(Snip20.init()) - -// TODO show instantiate -``` - -### Query permits - -Fadroma supports generating [SNIP-24](https://docs.scrt.network/secret-network-documentation/development/snips/snip-24-query-permits-for-snip-20-tokens) -query permits. - -```typescript -import { - - createPermitMsg // Create a permit message - -} from '@fadroma/tokens' - -assert.deepEqual( - JSON.stringify(createPermitMsg('q', 'p')), - '{"with_permit":{"query":"q","permit":"p"}}' -) -``` - -## Token manager - -The Token Manager is an object that serves as a registry -of token contracts in a deployment. It keeps track of tokens, -and allows you to specify the cases where a contract in your project -depends on a custom token. - -When working on devnet, Fadroma can deploy mocks of third-party tokens. - -```typescript -import { Deployment } from '@fadroma/agent' -import { TokenManager, TokenPair, TokenError } from '@fadroma/tokens' - -const context: Deployment = new Deployment({ - name: 'test', - state: {} - agent: { - address: 'agent-address', - getHash: async () => 'Hash' - }, -}) - -const manager = new TokenManager(context) - -assert.throws(()=>manager.get()) -assert.throws(()=>manager.get('UNKNOWN')) - -const token = { address: 'a1', codeHash: 'c2' } -assert.ok(manager.add('KNOWN', token)) -assert.ok(manager.has('KNOWN')) -assert.equal(manager.get('KNOWN').address, token.address) -assert.equal(manager.get('KNOWN').codeHash, token.codeHash) - -assert.ok(manager.define('DEPLOY', { address: 'a2', codeHash: 'c2' })) -assert.ok(manager.pair('DEPLOY-KNOWN') instanceof TokenPair) - -new TokenError() - -import { ContractSlot, Template } from '@fadroma/agent' -const manager2 = new TokenManager({ - template: (options) => new ContractSlot(options) -}, new Template({ - crate: 'snip20' -})) -``` - -```typescript -import { ok, equal, deepEqual, throws } from 'assert' -``` - diff --git a/spec/Upload.spec.ts.md b/spec/Upload.spec.ts.md deleted file mode 100644 index a0f1aa00ec2..00000000000 --- a/spec/Upload.spec.ts.md +++ /dev/null @@ -1,63 +0,0 @@ -# Fadroma Upload - -Fadroma takes care of **uploading WASM files to get code IDs**. - -Like builds, uploads are *idempotent*: if the same code hash is -known to already be uploaded to the same chain (as represented by -an upload receipt in `state/$CHAIN/uploads/$CODE_HASH.json`, -Fadroma will skip the upload and reues the existing code ID. - -## Upload CLI - -The `fadroma upload` command (available through `npm run $MODE upload` -in the default project structure) lets you access Fadroma's `Uploader` -implementation from the command line. - -```shell -$ fadroma upload CONTRACT # nil if same contract is already uploaded -$ fadroma reupload CONTRACT # always reupload -``` - -## Upload API - -The client package, `@fadroma/agent`, exposes a base `Uploader` class, -which the global `fetch` method to obtain code from any supported URL -(`file:///` or otherwise). - -This `fetch`-based implementation only supports temporary, in-memory -upload caching: if you ask it to upload the same contract many times, -it will upload it only once - but it will forget all about that -as soon as you refresh the page. - -The backend package, `@hackbg/fadroma`, provides `FSUploader`. -This extension of `Uploader` uses Node's `fs` API instead, and -writes upload receipts into the upload state directory for the -given chain (e.g. `state/$CHAIN/uploads/`). - -Let's try uploading an example WASM binary: - -```typescript -import { fixture } from './fixtures/Fixtures.ts.md' -const artifact = fixture('fadroma-example-kv@HEAD.wasm') // replace with path to your binary -``` - -* Uploading with default configuration (from environment variables): - -```typescript -import { upload } from '@hackbg/fadroma' -await upload({ artifact }) -``` - -* Passing custom options to the uploader: - -```typescript -import { getUploader } from '@hackbg/fadroma' -await getUploader({ /* options */ }).upload({ artifact }) -``` - ---- - -```typescript -import assert from 'node:assert' -//await import('./Upload.test.ts') -``` diff --git a/spec/Util.spec.ts.md b/spec/Util.spec.ts.md deleted file mode 100644 index fde298c8d66..00000000000 --- a/spec/Util.spec.ts.md +++ /dev/null @@ -1,76 +0,0 @@ -# Fadroma Guide: Utilities - -### Lazy evaluation - -### Generic collections - -```typescript -import { into, intoArray, intoRecord } from '@fadroma/agent' - -assert.equal(await into(1), 1) -assert.equal(await into(Promise.resolve(1)), 1) -assert.equal(await into(()=>1), 1) -assert.equal(await into(async ()=>1), 1) - -assert.deepEqual( - await intoArray([1, ()=>1, Promise.resolve(1), async () => 1]), - [1, 1, 1, 1] -) - -assert.deepEqual(await intoRecord({ - ready: 1, - getter: () => 2, - promise: Promise.resolve(3), - asyncFn: async () => 4 -}), { - ready: 1, - getter: 2, - promise: 3, - asyncFn: 4 -}) -``` - -### Validation against expected value - -Case-insensitive. - -```typescript -import { validated } from '@fadroma/agent' -assert.ok(validated('test', 1)) -assert.ok(validated('test', 1, 1)) -assert.ok(validated('test', 'a', 'A')) -assert.throws(()=>validated('test', 1, 2)) -assert.throws(()=>validated('test', 'a', 'b')) -``` - -### Overrides and fallbacks - -Only work on existing properties. - -```typescript -import { override, fallback } from '@fadroma/agent' -assert.deepEqual( - override({ a: 1, b: 2 }, { b: 3, c: 4 }), - { a: 1, b: 3 } -) -assert.deepEqual( - fallback({ a: 1, b: undefined }, { a: undefined, b: 3, c: 4 }), - { a: 1, b: 3 } -) -``` - -### Tabular alignment - -For more legible output. - -```typescript -assert.equal(getMaxLength(['a', 'ab', 'abcd', 'abc', 'b']), 4) -function getMaxLength (strings: string[]): number { - return Math.max(...strings.map(string=>string.length)) -} -``` - -```typescript -import assert from 'node:assert' -``` - diff --git a/spec/testIndex.ts b/spec/testIndex.ts deleted file mode 100644 index dfcee7190a5..00000000000 --- a/spec/testIndex.ts +++ /dev/null @@ -1,18 +0,0 @@ -import testEntrypoint, { testSuite } from './testSelector' - -export default testEntrypoint(import.meta.url, { - 'agent': testSuite('./Agent.test'), - 'build': testSuite('./Build.test'), - 'connect': testSuite('./Connect.test'), - 'cw': testSuite('./CW.test'), - 'deploy': testSuite('./Deploy.test'), - 'devnet': testSuite('./Devnet.test'), - 'factory': () => import ('./Factory.spec.ts.md'), - //'impl': () => import('./Implementing.spec.ts.md'), - 'mocknet': testSuite('./Mocknet.test'), - 'project': testSuite('./Project.test'), - 'scrt': testSuite('./Scrt.test'), - //'snip20': () => import('./Snip20.spec.ts.md'), - 'upload': testSuite('./Upload.test'), - 'util': () => import('./Util.spec.ts.md'), -}) diff --git a/spec/testSelector.ts b/spec/testSelector.ts deleted file mode 100644 index 4cce583504a..00000000000 --- a/spec/testSelector.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { resolve, dirname } from 'node:path' -import { fileURLToPath } from 'node:url' - -export const repoRoot = dirname(dirname(resolve(fileURLToPath(import.meta.url)))) - -export default function testEntrypoint (url: string, tests: Record) { - const entrypoint = resolve(process.argv[2]) - const mainScript = resolve(fileURLToPath(url)) - if (entrypoint === mainScript) return pickTest(tests) - return tests -} - -export async function pickTest (tests: Record, picked = process.argv[3]) { - if (picked === 'all') return testAll(tests) - const test = tests[picked] - if (test) return await test() - console.log('\nSpecify suite to run:') - console.log(` all`) - for (const test of Object.keys(tests)) { - console.log(` ${test}`) - } - console.log() - process.exit(1) -} - -export async function testAll (tests: Record) { - const runs = Object.values(tests).map(test=>test()) - return await Promise.all(runs) -} - -export function testSuite (path: string) { - return async () => { - console.log('Testing:', path) - const { default: suite } = await import(path) - return pickTest(suite, process.argv[4]) - } -}