Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/canister-declarations.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Canister declarations
sidebar:
order: 8
order: 9
---

PicJS creates actors from a canister's Candid interface: an `idlFactory` that encodes and decodes calls, and a `_SERVICE` type that describes them. Generate both from the canister's `.did` file with [`@icp-sdk/bindgen`](https://js.icp.build/bindgen/latest/):
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/canister-snapshots.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Canister snapshots
sidebar:
order: 7
order: 8
---

A [canister snapshot](https://docs.internetcomputer.org/guides/canister-management/snapshots/) captures a canister's state: its Wasm module, heap and stable memory, and chunk store. Taking, loading, deleting and uploading snapshots must be done by a controller of the canister. Listing and downloading them is also allowed for other principals if the canister's `snapshotVisibility` setting permits it.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/more-examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: More Examples
next: false
sidebar:
order: 9
order: 10
---

All examples are written in [TypeScript](https://www.typescriptlang.org/) with [Jest](https://jestjs.io/) or [Vitest](https://vitest.dev/) as the test runner,
Expand Down
10 changes: 10 additions & 0 deletions docs/src/content/docs/guides/running-tests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ const picServer = await PocketIcServer.start({
});
```

These include the logs of each instance's replica, which only logs warnings and errors by default. To see more, set its log level with the `logLevel` option when creating the instance, one of `critical`, `error`, `warn`, `info`, `debug` or `trace`:

```ts
const pic = await PocketIc.create(picServer.getUrl(), {
logLevel: 'debug',
});
```

## Testing interleaved calls

While a call waits at an `await`, for example for an inter-canister call, the canister can execute other calls. If the first call checked some state before the `await` and updates it after, another call can act on the same state in between, which can cause reentrancy bugs.
Expand All @@ -103,4 +111,6 @@ await pic.stopLive();

The gateway can be configured with the `httpGateway` option, for example `pic.makeLive({ httpGateway: { port: 8080 } })`.

To create an instance that is live from the start, set the `autoProgress` option, for example `PocketIc.create(url, { autoProgress: {} })`. Its time follows the real time right away, and `makeLive` starts the HTTP gateway for it.

To test how a client handles slower calls, for example its loading states or timeouts, set a minimum delay between rounds with `artificialDelayMs`, for example `pic.makeLive({ artificialDelayMs: 500 })`. This delays calls that wait for live mode to execute them: calls made through the HTTP gateway, and calls submitted with `pic.submitCall` whose result is checked with `pic.ingressStatus`. Calls made with an actor, a deferred actor, `pic.updateCall` or `pic.awaitCall` execute their rounds themselves and are not delayed.
115 changes: 115 additions & 0 deletions docs/src/content/docs/guides/working-with-bitcoin.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: Working with Bitcoin
sidebar:
order: 7
---

The `bitcoin` ICP feature deploys the Bitcoin canister under the Bitcoin testnet canister ID `g4xu7-jiaaa-aaaan-aaaaq-cai`, configured for the regtest network. It gets its blocks from a local `bitcoind` node in regtest mode, so your tests control the chain: they mine blocks, send transactions, and check how your canister reacts. The Bitcoin subnet it runs on is created automatically.

It doesn't deploy ckBTC. To test against ckBTC, deploy the ckBTC ledger yourself and make an identity you control its minter, so the tests can mint ckBTC.

## Running bitcoind

Start `bitcoind` in regtest mode, for example with the [Bitcoin Core Docker image](https://hub.docker.com/r/bitcoin/bitcoin), and expose its P2P port `18444`:

```shell
docker run -d --name bitcoind -p 18444:18444 bitcoin/bitcoin:latest bitcoind -regtest
```

Then mine blocks to an address. Coinbase rewards can only be spent after 100 confirmations, so mine at least 101 blocks to be able to spend the first reward:

```shell
docker exec --user bitcoin bitcoind bitcoin-cli -regtest createwallet test
docker exec --user bitcoin bitcoind bitcoin-cli -regtest getnewaddress
docker exec --user bitcoin bitcoind bitcoin-cli -regtest generatetoaddress 101 <address>
```

## Connecting the Bitcoin canister

Enable the `bitcoin` ICP feature and pass the address of `bitcoind` with `bitcoindAddrs`. The address must be reachable from the machine running the PocketIC server:

```ts
import { IcpFeaturesConfig, PocketIc } from '@dfinity/pic';

const pic = await PocketIc.create(process.env.PIC_URL, {
icpFeatures: { bitcoin: IcpFeaturesConfig.DefaultConfig },
bitcoindAddrs: ['127.0.0.1:18444'],
initialTime: new Date(),
});
```

`bitcoind` timestamps blocks with the real time, so the instance has to start at the current time to accept them, which `initialTime: new Date()` does. Alternatively, create the instance with `autoProgress: {}`, which makes it live and follow the real time, see [Live mode](./running-tests#live-mode).

If your canister signs Bitcoin transactions with threshold ECDSA or Schnorr, also create the test threshold keys subnet with `testThresholdKeys: { state: { type: SubnetStateType.New } }`, which holds the `test_key_1` keys.

## Waiting for the canister to sync

The Bitcoin canister fetches blocks from `bitcoind` as the instance executes rounds, and rejects calls with `Canister state is not fully synced` until it has caught up. Before asserting on your canister, tick until the Bitcoin canister reports the expected state, for example the balance of the address you mined to:

```ts
import { IDL } from '@icp-sdk/core/candid';
import { Principal } from '@icp-sdk/core/principal';

const BITCOIN_CANISTER_ID = Principal.fromText('g4xu7-jiaaa-aaaan-aaaaq-cai');

const GetBalanceRequest = IDL.Record({
address: IDL.Text,
network: IDL.Variant({
mainnet: IDL.Null,
testnet: IDL.Null,
regtest: IDL.Null,
}),
min_confirmations: IDL.Opt(IDL.Nat32),
});

async function getBalance(address: string): Promise<bigint> {
const res = await pic.queryCall({
canisterId: BITCOIN_CANISTER_ID,
method: 'bitcoin_get_balance_query',
arg: new Uint8Array(
IDL.encode(
[GetBalanceRequest],
[{ address, network: { regtest: null }, min_confirmations: [] }],
),
),
});

return IDL.decode([IDL.Nat64], res)[0] as bigint;
}

async function waitForBalance(address: string, expected: bigint) {
for (;;) {
try {
if ((await getBalance(address)) >= expected) {
return;
}
} catch (error) {
if (!String(error).includes('Canister state is not fully synced')) {
throw error;
}
}
await pic.tick();
}
}

// 101 blocks mined to the address, with a reward of 50 BTC each.
await waitForBalance(address, 101n * 50n * 100_000_000n);
```

After mining more blocks or sending transactions with `bitcoin-cli`, wait again before the next assertion.

## Dogecoin

The `dogecoin` ICP feature works the same way. It deploys the Dogecoin canister under its mainnet canister ID `gordg-fyaaa-aaaan-aaadq-cai`, configured for the regtest network, and gets its blocks from the `dogecoind` nodes given by `dogecoindAddrs`:

```ts
const pic = await PocketIc.create(process.env.PIC_URL, {
icpFeatures: { dogecoin: IcpFeaturesConfig.DefaultConfig },
dogecoindAddrs: ['127.0.0.1:18444'],
initialTime: new Date(),
});
```

Start `dogecoind` with `dogecoind -regtest`, using a [Dogecoin Core release](https://github.com/dogecoin/dogecoin/releases), and mine blocks with `dogecoin-cli -regtest generatetoaddress`. The Dogecoin canister's interface differs from the Bitcoin canister's: its methods are prefixed with `dogecoin_`, such as `dogecoin_get_balance_query`, its network is `variant { mainnet; regtest }`, and amounts are `nat` rather than `nat64`.

The Bitcoin and Dogecoin canisters can run on the same instance, but `bitcoindAddrs` and `dogecoindAddrs` can't be combined, as the PocketIC server fails to create an instance with both.
27 changes: 16 additions & 11 deletions docs/src/content/docs/guides/working-with-the-nns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ const pic = await PocketIc.create(process.env.PIC_URL, {

`IcpFeaturesConfig.DefaultConfig` configures each feature to resemble mainnet as closely as possible. The available features are:

| Feature | Deploys |
| --------------- | ------------------------------------------------------------------------------------------------ |
| `registry` | The NNS registry canister, kept in sync with PocketIC's internal registry |
| `cyclesMinting` | The cycles minting canister, with the ICP/XDR conversion rate set |
| `icpToken` | The ICP ledger and index canisters; the anonymous principal starts with 1,000,000,000 ICP |
| `cyclesToken` | The cycles ledger and index canisters; the anonymous principal starts with 2^127 - 1 cycles |
| `nnsGovernance` | The NNS governance and root canisters, with a 1 ICP neuron controlled by the anonymous principal |
| `sns` | The SNS-W and aggregator canisters, with the SNS canister WASMs uploaded |
| `ii` | The Internet Identity backend and frontend canisters |
| `nnsUi` | The NNS dapp; requires `cyclesMinting`, `icpToken`, `nnsGovernance`, `sns` and `ii` |
| Feature | Deploys |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| `registry` | The NNS registry canister, kept in sync with PocketIC's internal registry |
| `cyclesMinting` | The cycles minting canister, with the ICP/XDR conversion rate set |
| `icpToken` | The ICP ledger and index canisters; the anonymous principal starts with 1,000,000,000 ICP |
| `cyclesToken` | The cycles ledger and index canisters; the anonymous principal starts with 2^127 - 1 cycles |
| `nnsGovernance` | The NNS governance and root canisters, with a 1 ICP neuron controlled by the anonymous principal |
| `sns` | The SNS-W and aggregator canisters, with the SNS canister WASMs uploaded |
| `ii` | The Internet Identity backend and frontend canisters |
| `nnsUi` | The NNS dapp; requires `cyclesMinting`, `icpToken`, `nnsGovernance`, `sns` and `ii` |
| `canisterMigration` | The canister migration orchestrator canister |
| `bitcoin` | The Bitcoin canister for the regtest network, see [Working with Bitcoin](./working-with-bitcoin) |
| `dogecoin` | The Dogecoin canister for the regtest network, see [Working with Bitcoin](./working-with-bitcoin#dogecoin) |

`ii` and `nnsUi` also deploy frontends, which PocketIC serves through an HTTP gateway created together with the instance. Set the `httpGateway` option, then make the instance live to serve them:

Expand All @@ -44,6 +47,8 @@ const iiUrl = `http://uqzsh-gqaaa-aaaaq-qaada-cai.localhost:${gatewayPort}`;

Without `httpGateway`, creating an instance with either feature throws an error.

The subnets that host these canisters must be empty: leave them unconfigured or use `SubnetStateType.New`, as PocketIC rejects state loaded with `SubnetStateType.FromPath`. Enabling `cyclesMinting` also moves the instance's default time to 10 May 2021, to match the cycles minting canister's state.
The subnets that host these canisters must be empty: leave them unconfigured or use `SubnetStateType.New`, as PocketIC rejects state loaded with `SubnetStateType.FromPath`. Enabling `cyclesMinting` also moves the instance's default time to 10 May 2021, to match the cycles minting canister's state. To start at a later time, set the `initialTime` option, for example `initialTime: new Date()`. Earlier times are rejected.

The NNS subnet gets a random subnet ID. If your canisters rely on the ID of the NNS subnet on mainnet, set `mainnetNnsSubnetId: true` to create it with that ID.

Check out the [NNS Proxy](https://github.com/dfinity/pic-js/tree/main/examples/nns_proxy) example for a full test that creates neurons and proposals against NNS governance.
82 changes: 81 additions & 1 deletion packages/pic/src/pocket-ic-client-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
isNotNil,
} from './util';
import { HttpGatewayRequiredError, TopologyValidationError } from './error';
import { CanisterCyclesCostSchedule, SenderInfo } from './pocket-ic-types';
import {
AutoProgressConfig,
CanisterCyclesCostSchedule,
LogLevel,
SenderInfo,
} from './pocket-ic-types';

export { CanisterCyclesCostSchedule };

Expand All @@ -33,6 +38,12 @@ export interface CreateInstanceRequest {
icpFeatures?: IcpFeatures;
disableIngressValidation?: boolean;
httpGateway?: HttpGatewayConfig;
initialTime?: Date | number;
autoProgress?: AutoProgressConfig;
bitcoindAddrs?: string[];
dogecoindAddrs?: string[];
mainnetNnsSubnetId?: boolean;
logLevel?: LogLevel;
}

export interface SubnetConfig<
Expand Down Expand Up @@ -118,6 +129,9 @@ export interface IcpFeatures {
sns?: IcpFeaturesConfig;
ii?: IcpFeaturesConfig;
nnsUi?: IcpFeaturesConfig;
bitcoin?: IcpFeaturesConfig;
dogecoin?: IcpFeaturesConfig;
canisterMigration?: IcpFeaturesConfig;
}

export interface HttpGatewayConfig {
Expand All @@ -136,6 +150,45 @@ export interface EncodedCreateInstanceRequest {
icp_features?: EncodedIcpFeatures;
disable_ingress_validation?: boolean;
http_gateway_config?: EncodedInstanceHttpGatewayConfig;
initial_time?: EncodedInitialTime;
bitcoind_addr?: string[];
dogecoind_addr?: string[];
mainnet_nns_subnet_id?: boolean;
log_level?: LogLevel;
}

export type EncodedInitialTime =
| { Timestamp: { nanos_since_epoch: bigint } }
| { AutoProgress: { artificial_delay_ms?: number } };

function encodeInitialTime({
initialTime,
autoProgress,
}: CreateInstanceRequest): EncodedInitialTime | undefined {
if (!isNil(initialTime) && !isNil(autoProgress)) {
throw new Error(
'The initialTime and autoProgress options cannot be combined, as an instance created with autoProgress follows the real time',
);
}

if (!isNil(initialTime)) {
const millis =
initialTime instanceof Date ? initialTime.getTime() : initialTime;

return {
Timestamp: {
nanos_since_epoch: BigInt(millis) * NANOS_PER_MILLISECOND,
},
};
}

if (!isNil(autoProgress)) {
return {
AutoProgress: { artificial_delay_ms: autoProgress.artificialDelayMs },
};
}

return undefined;
}

export interface EncodedInstanceHttpGatewayConfig {
Expand Down Expand Up @@ -186,6 +239,9 @@ export interface EncodedIcpFeatures {
sns?: EncodedIcpFeaturesConfig;
ii?: EncodedIcpFeaturesConfig;
nns_ui?: EncodedIcpFeaturesConfig;
bitcoin?: EncodedIcpFeaturesConfig;
dogecoin?: EncodedIcpFeaturesConfig;
canister_migration?: EncodedIcpFeaturesConfig;
}

export interface EncodedSubnetConfig {
Expand Down Expand Up @@ -327,6 +383,15 @@ function encodeIcpFeatures(icpFeatures: IcpFeatures): EncodedIcpFeatures {
nns_ui: icpFeatures.nnsUi
? encodeIcpFeaturesConfig(icpFeatures.nnsUi)
: undefined,
bitcoin: icpFeatures.bitcoin
? encodeIcpFeaturesConfig(icpFeatures.bitcoin)
: undefined,
dogecoin: icpFeatures.dogecoin
? encodeIcpFeaturesConfig(icpFeatures.dogecoin)
: undefined,
canister_migration: icpFeatures.canisterMigration
? encodeIcpFeaturesConfig(icpFeatures.canisterMigration)
: undefined,
};
}

Expand Down Expand Up @@ -384,8 +449,23 @@ export function encodeCreateInstanceRequest(
http_gateway_config: defaultOptions.httpGateway
? encodeHttpGatewayConfig(defaultOptions.httpGateway)
: undefined,
initial_time: encodeInitialTime(defaultOptions),
bitcoind_addr: defaultOptions.bitcoindAddrs,
dogecoind_addr: defaultOptions.dogecoindAddrs,
mainnet_nns_subnet_id: defaultOptions.mainnetNnsSubnetId,
log_level: defaultOptions.logLevel,
};

// The PocketIC server panics when it starts both adapters for one instance.
if (
!isNil(defaultOptions.bitcoindAddrs) &&
!isNil(defaultOptions.dogecoindAddrs)
) {
throw new Error(
'The bitcoindAddrs and dogecoindAddrs options cannot be combined, as the PocketIC server fails to create an instance with both',
);
}

const { ii, nnsUi } = defaultOptions.icpFeatures ?? {};
if ((ii || nnsUi) && isNil(defaultOptions.httpGateway)) {
throw new HttpGatewayRequiredError(ii ? 'ii' : 'nnsUi');
Expand Down
Loading
Loading