Skip to content

Commit

Permalink
docs(mocknet): document mocknet
Browse files Browse the repository at this point in the history
  • Loading branch information
egasimus committed Jul 7, 2022
1 parent 7fc20e1 commit 22f2515
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 76 deletions.
45 changes: 44 additions & 1 deletion doc/docs/mocknet.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
# Fast full-stack testing of production builds with Fadroma Mocknet

## Literate tests with Ensuite
Testing the production builds of smart contracts, and the associated client classes and deploy
scripts, can be slow and awkward. Testnets are permanent and public; devnets can be temporary, but
transactions are still throttled by the rate at which blocks are processed.

Smart contracts are just WASM programs. They are written with the expectation that they will
be run by the CosmWasm blockchain's WASM runtime (`compute` module), but WASM itself is a
portable, environment-independent format, and Node.js has native support for running WASM modules.

From this, it follows that by providing implementations of the contract-facing CosmWasm API,
we could run the production builds of smart contracts inside a simulated blockchain-like
environment.

Such an environment would not be bound to the distrubuted consensus mechanisms of a blockchan,
and would thus allow the contracts to be tested more quickly. This is especially useful in CI
environments, where launching a devnet container might not be possible or desirable.

## Enable mocknet

The easiest way to use Mocknet is to set `FADROMA_CHAIN=Mocknet` in your environment, and
run your deploy scripts as usual. They should work just the same - only way faster.

## Example: mocknet-only test command

Continuing our Fadroma Ops example from the previous chapter, let's add a `test` command
which only ever runs on Mocknet.

```typescript
import assert from 'assert'

Fadroma.command('test',
() => { process.env.FADROMA_CHAIN = 'Mocknet' },
...common,
Fadroma.Deploy.New,
deployMyContract,
configureMyContract,
async function testMyContract (context) {
assert(await context.myContract.q2(), "some expected value")
}
)
```

The extra step before `...common` modifies the environment of the running Node process.
The subsequent `Fadroma.Chain.FromEnv` step contained in `common` will populate the context
with an instance of `Mocknet` instead of the chain that would otherwise be used.
175 changes: 100 additions & 75 deletions doc/docs/ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,9 @@ the deployment of smart contracts is a workflow from their subsequent operation.
The following is a guide to understanding and using the smart contract deployment system,
Fadroma Ops.

## Add static `init` method to client class
## Preparation

Client classes operate with contracts that are already instantiated: while `execute` and `query`
correspond to a contract's `handle` and `query`, the `init` method of the contract does **not**
correspond to the `constructor` of the client.

So, completely optionally, you could add a static `init` method on your client
like so:

```typescript
import { Client, Uint128, Address } from '@fadroma/client'

interface MyContractInit {
param1: Uint128
param2: Address
}

class MyContract extends Client {
static init (param1: Uint128, param2: Address): MyContractInit {
/* ...custom preparation, validation, etc... */
return { param1, param2 }
}
/* ...tx and query methods... */
}
```

It would only generate an init message, but not broadcast it.
Alternatively, you could just generate the init message during
the deploy procedure, and not write a static `init` method.

## Add Fadroma and your script to `package.json`
### Add Fadroma and your script to `package.json`

```json
// /path/to/your/project/package.json
Expand All @@ -58,11 +30,10 @@ the deploy procedure, and not write a static `init` method.
:::info
Fadroma will use [Ganesha](https://github.com/hackbg/ganesha) to compile
deployment scripts on each run. You can use TypeScript seamlessly in your
deploy procedures, which is really helpful when they start getting complex
in their own right.
deploy procedures.
:::

## Set up the deploy script
### Set up the deploy script

Here's a starter template for a deploy script:

Expand Down Expand Up @@ -118,7 +89,7 @@ is invaluable.

Let's break this down as we prepare to implement `deployMyContract`.

## Import Fadroma
### Import Fadroma

The default export of Fadroma contains a collection of **operations**.
Those are functions that populate the **operation context** for performing
Expand All @@ -128,16 +99,10 @@ a deployment action.
import Fadroma, { OperationContext } from '@hackbg/fadroma'
```

Let's look at some pre-defined operations; we'll see what's in `OperationContext`
when we get to implementing `deployMyContract`.

## Enable building and uploading

The most common operations are `Fadroma.Build.Scrt`, `Fadroma.Chain.FromEnv`, and
`Fadroma.Upload.FromFile`.
### Enable building and uploading

Since we may have multiple deploy commands, and all of them will need to be able to build and
upload contracts, we collect these three steps in an array, `common`.
upload contracts, we collect the pre-defined preparation steps in an array, `common`.

```typescript
const common = [
Expand All @@ -147,18 +112,20 @@ const common = [
]
```

Their names are more or less self-explanatory: they enable building smart contracts in a
Secret Network-specific build container, and uploading them to the Secret Network instance
(mainnet, testnet, devnet, or mocknet) that is specified by an environment variable.
The most common pre-defined operations are:
* `Fadroma.Build.Scrt`: subsequent steps will be able to `build` contracts for Secret Network
* `Fadroma.Chain.FromEnv`: subsequent steps will have access to the `chain` and `agent` defined
by the `FADROMA_CHAIN` and `SCRT_AGENT_MNEMONIC` environment variables.
* `Fadroma.Upload.FromFile`: subsequent steps will be able to `upload` contracts to the selected
`chain`.

The environment variable used by `Fadroma.Chain.FromEnv` is `FADROMA_CHAIN` and it supports
the following values:
`FADROMA_CHAIN` and supports the following values:

* `Mocknet`
* `LegacyScrtMainnet`, `LegacyScrtTestnet`, `LegacyScrtDevnet`
* `ScrtMainnet`, `ScrtTestnet`, `ScrtDevnet`

## Define commands
### Define commands

Up next are two `Fadroma.command(name, ...operations)` invocations. The `Fadroma.command` function
defines a command which is then exposed to the outside world by the final `Fadroma.module` call.
Expand All @@ -183,7 +150,7 @@ what we're doing here but shadows the `deploy` command defined in scripts. Make
`pnpm run deploy` and not `pnpm deploy`.
:::

### Deployment receipts
### About deployment receipts

The `new` and `add` commands differ by one pre-defined step: `Fadroma.Deploy.New` vs
`Fadroma.Deploy.Append`. What's the difference?
Expand Down Expand Up @@ -220,18 +187,19 @@ when a particular contract was deployed. You can get the latter by looking at `i
deployment receipt, and querying that transaction in the transaction explorer.
:::

## The actual deploy procedure
## The deploy procedure

So far, we've set up an environment for deploying contracts and keeping track of our
actions. Let's see how to implement the custom operation, `deployMyContract`, in that
environment.

## The operation context
### About operations and the operation context

Operations are asynchronous functions that take a single argument, the `OperationContext`,
and may return an object containing updates to the operation context.

You can extend the `OperationContext` type to add custom parameters to your operations.
The convention is to name the extended context after the function that will be using it.

```typescript
interface DeployMyContract extends OperationContext {
Expand All @@ -244,7 +212,7 @@ async function deployMyContract (context: DeployMyContract) {
}
```

## Build for production
### Build for production

The first thing we need to do is compile a production build of our contract.
For this, we can use the `context.build(source: Source): Promise<Artifact>`
Expand All @@ -263,7 +231,7 @@ The result of the build is stored under `artifacts/[email protected]`,
and a checksum is stored at `artifacts/[email protected]`, relative
to your project root.

## Upload the code
### Upload the code

Now that we have a compiled artifact, we can use
`context.upload(artifact: Artifact): Promise<Template>`, provided by `Fadroma.Upload.FromFile`,
Expand Down Expand Up @@ -293,7 +261,7 @@ async function deployMyContract (context: DeployMyContract) {
}
```

## Instantiate the contract(s)
### Instantiate the contract(s)

Now that we have a `Template`, we are ready to create as many instances
of our contract as needed.
Expand All @@ -309,40 +277,42 @@ async function deployMyContract (context: DeployMyContract) {
}
```

Of course, this can also be written as a single expression:

```typescript
async function deployMyContract (context: DeployMyContract) {
const instance = await context.deploy(
await context.buildAndUpload(new Workspace(process.cwd()).crate('my-contract')),
"MyContract",
{ param1: context.param1, param2: context.param2 }
)
/* ... */
}
```

Invoking `npm run deploy new` will now deploy the contract to the chain
specified by `FADROMA_CHAIN`.

## Getting a contract client
### Get a contract client

If we want to run further procedures after deployment, such as configuring our
smart contract, we could obtain an instance of the corresponding client class.

```typescript
import { MyContract } from 'my-client-library'
async function deployMyContract (context: DeployMyContract) {
const myContract = context.agent.getClient(MyContract, await context.deploy(
const source = new Workspace(process.cwd()).crate('my-contract')
const template = await context.buildAndUpload(source)
const name = "My Contract"
const initMsg = { param1: context.param1, param2: context.param2 }
const instance = await context.deploy(template, name, initMsg)
const client = context.agent.getClient(MyContract, instance)
await client.tx2("123")
}
```

Of course, this can also be written as a single expression:

```typescript
async function deployMyContract (context: DeployMyContract) {
await context.agent.getClient(MyContract, await context.deploy(
await context.buildAndUpload(new Workspace(process.cwd()).crate('my-contract')),
"MyContract",
{ param1: context.param1, param2: context.param2 }
))
await myContract.tx2("123")
)).tx2("123")
}
```

## Stacking operations
## Deployment idioms

### Stacking operations

Returning an object from an operation updates the context for subsequent steps.
Hence, the following is valid:
Expand All @@ -354,6 +324,7 @@ async function deployMyContract (context: DeployMyContract) {
"MyContract",
{ param1: context.param1, param2: context.param2 }
))
await client.tx2("123")
return { myContract }
}

Expand All @@ -362,7 +333,7 @@ interface ConfigureMyContract extends OperationContext {
myValue: Uint128
}

async function configureMyContract (context: OperationContext) {
async function configureMyContract (context: ConfigureMyContract) {
await context.myContract.tx2(context.myValue)
}

Expand All @@ -374,11 +345,11 @@ Fadroma.command('deploy-and-configure',
)
```

## Working with an existing deployment
### Working with an existing deployment

Let's say you already deployed your contract and want to configure it at a later time.
Use `Fadroma.Deploy.Append` to get the active deployment in `context`, and
`context.deployment.getClient(agent, Client, name)` to get the client to the
`context.deployment.getClient(agent, Client, name)` to get a client for the
previously deployed contract.

```typescript
Expand All @@ -391,3 +362,57 @@ Fadroma.command('configure',
configureMyContract
)
```

### Destructuring `context`

When an operation is reused in multiple contexts, you may find it useful to provide
overridable defaults for every member of `context` like so:

```typescript
Fadroma.command('configure',
...common,
Fadroma.Deploy.Append,
configureMyContract
)

function configureMyContract (context: ConfigureMyContract) {
const {
agent,
deployment,
name = "My Contract",
myContract = deployment.getClient(agent, MyContract, name)
myValue = "default config value"
}
await myContract.tx2(myValue)
}
```

### Adding static `init` method to client class

Client classes operate with contracts that are already instantiated: while `execute` and `query`
correspond to a contract's `handle` and `query`, the `init` method of the contract does **not**
correspond to the `constructor` of the client.

So, completely optionally, you could add a static `init` method on your client
like so:

```typescript
import { Client, Uint128, Address } from '@fadroma/client'

interface MyContractInit {
param1: Uint128
param2: Address
}

class MyContract extends Client {
static init (param1: Uint128, param2: Address): MyContractInit {
/* ...custom preparation, validation, etc... */
return { param1, param2 }
}
/* ...tx and query methods... */
}
```

This would generate an init message, which you would then broadcast with `context.deploy`.
Alternatively, you could just generate the init message during
the deploy procedure, and not write a static `init` method.

0 comments on commit 22f2515

Please sign in to comment.