diff --git a/docs/src/content/docs/guides/canister-declarations.mdx b/docs/src/content/docs/guides/canister-declarations.mdx
index b589be1..139ba59 100644
--- a/docs/src/content/docs/guides/canister-declarations.mdx
+++ b/docs/src/content/docs/guides/canister-declarations.mdx
@@ -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/):
diff --git a/docs/src/content/docs/guides/canister-snapshots.mdx b/docs/src/content/docs/guides/canister-snapshots.mdx
index efe1d0a..2f62f2f 100644
--- a/docs/src/content/docs/guides/canister-snapshots.mdx
+++ b/docs/src/content/docs/guides/canister-snapshots.mdx
@@ -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.
diff --git a/docs/src/content/docs/guides/more-examples.mdx b/docs/src/content/docs/guides/more-examples.mdx
index 3703cbc..8e28a96 100644
--- a/docs/src/content/docs/guides/more-examples.mdx
+++ b/docs/src/content/docs/guides/more-examples.mdx
@@ -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,
diff --git a/docs/src/content/docs/guides/running-tests.mdx b/docs/src/content/docs/guides/running-tests.mdx
index 365579a..7844f52 100644
--- a/docs/src/content/docs/guides/running-tests.mdx
+++ b/docs/src/content/docs/guides/running-tests.mdx
@@ -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.
@@ -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.
diff --git a/docs/src/content/docs/guides/working-with-bitcoin.mdx b/docs/src/content/docs/guides/working-with-bitcoin.mdx
new file mode 100644
index 0000000..1b8440c
--- /dev/null
+++ b/docs/src/content/docs/guides/working-with-bitcoin.mdx
@@ -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
+```
+
+## 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 {
+ 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.
diff --git a/docs/src/content/docs/guides/working-with-the-nns.mdx b/docs/src/content/docs/guides/working-with-the-nns.mdx
index f4cbaa7..9a84c36 100644
--- a/docs/src/content/docs/guides/working-with-the-nns.mdx
+++ b/docs/src/content/docs/guides/working-with-the-nns.mdx
@@ -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:
@@ -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.
diff --git a/packages/pic/src/pocket-ic-client-types.ts b/packages/pic/src/pocket-ic-client-types.ts
index 7a542a3..8e5fde2 100644
--- a/packages/pic/src/pocket-ic-client-types.ts
+++ b/packages/pic/src/pocket-ic-client-types.ts
@@ -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 };
@@ -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<
@@ -118,6 +129,9 @@ export interface IcpFeatures {
sns?: IcpFeaturesConfig;
ii?: IcpFeaturesConfig;
nnsUi?: IcpFeaturesConfig;
+ bitcoin?: IcpFeaturesConfig;
+ dogecoin?: IcpFeaturesConfig;
+ canisterMigration?: IcpFeaturesConfig;
}
export interface HttpGatewayConfig {
@@ -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 {
@@ -186,6 +239,9 @@ export interface EncodedIcpFeatures {
sns?: EncodedIcpFeaturesConfig;
ii?: EncodedIcpFeaturesConfig;
nns_ui?: EncodedIcpFeaturesConfig;
+ bitcoin?: EncodedIcpFeaturesConfig;
+ dogecoin?: EncodedIcpFeaturesConfig;
+ canister_migration?: EncodedIcpFeaturesConfig;
}
export interface EncodedSubnetConfig {
@@ -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,
};
}
@@ -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');
diff --git a/packages/pic/src/pocket-ic-types.ts b/packages/pic/src/pocket-ic-types.ts
index 1dd2ba9..6218a5c 100644
--- a/packages/pic/src/pocket-ic-types.ts
+++ b/packages/pic/src/pocket-ic-types.ts
@@ -115,8 +115,83 @@ export interface CreateInstanceOptions {
* Defaults to `false`.
*/
disableIngressValidation?: boolean;
+
+ /**
+ * The initial time of the instance, as a `Date` or in milliseconds since the Unix epoch.
+ * Must be at least 6 May 2021 21:17:10 CEST, or 10 May 2021 10:00:01 CEST
+ * if the `cyclesMinting` ICP feature is enabled, which is also the default time then.
+ * Cannot be combined with {@link CreateInstanceOptions.autoProgress}.
+ */
+ initialTime?: Date | number;
+
+ /**
+ * Makes the instance live from creation: it executes rounds on its own and
+ * follows the real time, see {@link PocketIc.makeLive}.
+ * Creating the instance returns once its certified time has been set for the first time.
+ * Cannot be combined with {@link CreateInstanceOptions.initialTime}.
+ */
+ autoProgress?: AutoProgressConfig;
+
+ /**
+ * The addresses of `bitcoind` nodes, e.g. `127.0.0.1:18444`, that the Bitcoin
+ * canister deployed by the `bitcoin` ICP feature syncs with. The addresses
+ * must be reachable from the machine running the PocketIC server.
+ * Cannot be combined with {@link CreateInstanceOptions.dogecoindAddrs}, as the
+ * PocketIC server fails to create an instance with both.
+ */
+ bitcoindAddrs?: string[];
+
+ /**
+ * The addresses of `dogecoind` nodes, e.g. `127.0.0.1:18444`, that the Dogecoin
+ * canister deployed by the `dogecoin` ICP feature syncs with. The addresses
+ * must be reachable from the machine running the PocketIC server.
+ * Cannot be combined with {@link CreateInstanceOptions.bitcoindAddrs}, as the
+ * PocketIC server fails to create an instance with both.
+ */
+ dogecoindAddrs?: string[];
+
+ /**
+ * Creates the NNS subnet with the subnet ID of the NNS subnet on mainnet,
+ * `tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe`.
+ * Has no effect if the instance has no NNS subnet. Defaults to `false`.
+ */
+ mainnetNnsSubnetId?: boolean;
+
+ /**
+ * The log level of the PocketIC instance's replica, whose logs are written to the
+ * PocketIC server's standard output, see {@link StartServerOptions.showRuntimeLogs}.
+ * Defaults to `warn`.
+ */
+ logLevel?: LogLevel;
}
+/**
+ * Options for making an instance live when it is created,
+ * see {@link CreateInstanceOptions.autoProgress}.
+ *
+ * @category Types
+ */
+export interface AutoProgressConfig {
+ /**
+ * The minimum delay in milliseconds between consecutive rounds,
+ * see {@link MakeLiveOptions.artificialDelayMs}. Defaults to no delay.
+ */
+ artificialDelayMs?: number;
+}
+
+/**
+ * The log level of a PocketIC instance's replica.
+ *
+ * @category Types
+ */
+export type LogLevel =
+ | 'critical'
+ | 'error'
+ | 'warn'
+ | 'info'
+ | 'debug'
+ | 'trace';
+
/**
* Options for the HTTP gateway created together with a PocketIC instance.
*/
@@ -446,6 +521,25 @@ export interface IcpFeatures {
* `icpToken`, `nnsGovernance`, `sns` and `ii` features.
*/
nnsUi?: IcpFeaturesConfig;
+
+ /**
+ * Deploys the Bitcoin canister under the Bitcoin testnet canister ID
+ * `g4xu7-jiaaa-aaaan-aaaaq-cai`, configured for the regtest network.
+ * It syncs with the `bitcoind` nodes given by {@link CreateInstanceOptions.bitcoindAddrs}.
+ */
+ bitcoin?: IcpFeaturesConfig;
+
+ /**
+ * Deploys the Dogecoin canister under its mainnet canister ID
+ * `gordg-fyaaa-aaaan-aaadq-cai`, configured for the regtest network.
+ * It syncs with the `dogecoind` nodes given by {@link CreateInstanceOptions.dogecoindAddrs}.
+ */
+ dogecoin?: IcpFeaturesConfig;
+
+ /**
+ * Deploys the canister migration orchestrator canister on the NNS subnet.
+ */
+ canisterMigration?: IcpFeaturesConfig;
}
/**
diff --git a/packages/pic/src/pocket-ic.ts b/packages/pic/src/pocket-ic.ts
index 87a1845..731b88d 100644
--- a/packages/pic/src/pocket-ic.ts
+++ b/packages/pic/src/pocket-ic.ts
@@ -2058,7 +2058,8 @@ export class PocketIc {
/**
* Make the PocketIC instance live by enabling auto progress and starting an HTTP gateway.
* If the instance was created with {@link CreateInstanceOptions.httpGateway}, that gateway is used instead.
- * If the instance is already live, this method returns the port of its HTTP gateway.
+ * If the instance is already live, this method returns the port of its HTTP gateway,
+ * starting one first if the instance was created live with {@link CreateInstanceOptions.autoProgress}.
*
* @param options Options for making the instance live, see {@link MakeLiveOptions}.
* To change them on a live instance, call {@link stopLive} first.
@@ -2093,26 +2094,30 @@ export class PocketIc {
artificialDelayMs,
httpGateway,
}: MakeLiveOptions = {}): Promise {
+ if (!isNil(httpGateway) && !isNil(this.client.instanceHttpGatewayPort)) {
+ throw new Error(
+ 'The instance was created with an HTTP gateway, configure it with the httpGateway option of PocketIc.create instead',
+ );
+ }
+
const isLive = await this.client.autoProgressEnabled();
if (isLive) {
- if (isNil(this.httpGatewayPort)) {
- throw new Error(
- 'Inconsistent state, PocketIC server is live but no HTTP Gateway URL is known',
- );
- }
- if (!isNil(artificialDelayMs) || !isNil(httpGateway)) {
+ const hasGateway = !isNil(this.httpGatewayPort);
+ if (!isNil(artificialDelayMs) || (hasGateway && !isNil(httpGateway))) {
throw new Error(
'The instance is already live, call stopLive before making it live with new options',
);
}
- return this.httpGatewayPort;
- }
+ // An instance created with CreateInstanceOptions.autoProgress is live
+ // before it has an HTTP gateway.
+ if (isNil(this.httpGatewayPort)) {
+ this.httpGatewayPort =
+ this.client.instanceHttpGatewayPort ??
+ (await this.client.startHttpGateway(httpGateway));
+ }
- if (!isNil(httpGateway) && !isNil(this.client.instanceHttpGatewayPort)) {
- throw new Error(
- 'The instance was created with an HTTP gateway, configure it with the httpGateway option of PocketIc.create instead',
- );
+ return this.httpGatewayPort;
}
await this.client.autoProgress(artificialDelayMs);
diff --git a/packages/pic/tests/src/instance-options.spec.ts b/packages/pic/tests/src/instance-options.spec.ts
new file mode 100644
index 0000000..88087a9
--- /dev/null
+++ b/packages/pic/tests/src/instance-options.spec.ts
@@ -0,0 +1,194 @@
+import { Principal } from '@icp-sdk/core/principal';
+import {
+ CreateInstanceOptions,
+ IcpFeaturesConfig,
+ LogLevel,
+ PocketIc,
+ SubnetStateType,
+} from '../../src';
+
+const NEW_SUBNET = { state: { type: SubnetStateType.New } } as const;
+
+const BITCOIN_CANISTER_ID = 'g4xu7-jiaaa-aaaan-aaaaq-cai';
+const DOGECOIN_CANISTER_ID = 'gordg-fyaaa-aaaan-aaadq-cai';
+const CANISTER_MIGRATION_CANISTER_ID = 'sbzkb-zqaaa-aaaaa-aaaiq-cai';
+const MAINNET_NNS_SUBNET_ID =
+ 'tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe';
+
+describe('instance options', () => {
+ let pic: PocketIc | undefined;
+
+ async function create(options: CreateInstanceOptions): Promise {
+ pic = await PocketIc.create(process.env.PIC_URL, options);
+
+ return pic;
+ }
+
+ afterEach(async () => {
+ await pic?.stopLive();
+ await pic?.tearDown();
+ pic = undefined;
+ });
+
+ describe('initialTime', () => {
+ it('should set the initial time from a Date or milliseconds', async () => {
+ const initialTime = new Date('2030-01-02T03:04:05.000Z');
+
+ const fromDate = await create({
+ application: [NEW_SUBNET],
+ initialTime,
+ });
+ expect(await fromDate.getTime()).toBe(initialTime.getTime());
+ await fromDate.tearDown();
+
+ const fromMillis = await create({
+ application: [NEW_SUBNET],
+ initialTime: initialTime.getTime(),
+ });
+ expect(await fromMillis.getTime()).toBe(initialTime.getTime());
+ });
+
+ it('should reject a time before the earliest supported one', async () => {
+ await expect(
+ create({
+ application: [NEW_SUBNET],
+ initialTime: new Date('2021-05-06T19:17:09.000Z'),
+ }),
+ ).rejects.toThrow('must be no earlier than');
+ });
+
+ it('should not be combined with autoProgress', async () => {
+ await expect(
+ create({
+ application: [NEW_SUBNET],
+ initialTime: new Date(),
+ autoProgress: {},
+ }),
+ ).rejects.toThrow(
+ 'The initialTime and autoProgress options cannot be combined',
+ );
+ });
+ });
+
+ describe('autoProgress', () => {
+ it('should create a live instance that follows the real time', async () => {
+ const live = await create({
+ application: [NEW_SUBNET],
+ autoProgress: {},
+ });
+
+ expect(Math.abs((await live.getTime()) - Date.now())).toBeLessThan(
+ 10_000,
+ );
+
+ const port = await live.makeLive();
+ const res = await fetch(`http://localhost:${port}/api/v2/status`);
+ expect(res.status).toBe(200);
+ });
+
+ it('should reject a new artificial delay once live', async () => {
+ const live = await create({
+ application: [NEW_SUBNET],
+ autoProgress: { artificialDelayMs: 100 },
+ });
+
+ await expect(live.makeLive({ artificialDelayMs: 200 })).rejects.toThrow(
+ 'The instance is already live',
+ );
+ });
+ });
+
+ it('should create the NNS subnet with the mainnet subnet ID', async () => {
+ const instance = await create({
+ nns: NEW_SUBNET,
+ mainnetNnsSubnetId: true,
+ });
+
+ expect((await instance.getNnsSubnet())?.id.toText()).toBe(
+ MAINNET_NNS_SUBNET_ID,
+ );
+ });
+
+ it.each(['critical', 'error', 'warn', 'info', 'debug', 'trace'])(
+ 'should accept the %s log level',
+ async logLevel => {
+ const instance = await create({ application: [NEW_SUBNET], logLevel });
+
+ await instance.tick();
+ },
+ );
+
+ describe('ICP features', () => {
+ async function expectCanister(
+ instance: PocketIc,
+ canisterId: string,
+ subnetId: Principal | undefined,
+ ): Promise {
+ const id = Principal.fromText(canisterId);
+
+ expect(await instance.canisterExists(id)).toBe(true);
+ expect((await instance.getCanisterSubnetId(id))?.toText()).toBe(
+ subnetId?.toText(),
+ );
+ }
+
+ it('should deploy the Bitcoin canister to the Bitcoin subnet', async () => {
+ const instance = await create({
+ icpFeatures: { bitcoin: IcpFeaturesConfig.DefaultConfig },
+ });
+
+ const bitcoinSubnet = await instance.getBitcoinSubnet();
+ await expectCanister(instance, BITCOIN_CANISTER_ID, bitcoinSubnet?.id);
+ });
+
+ it('should deploy the Dogecoin canister to the Bitcoin subnet', async () => {
+ const instance = await create({
+ icpFeatures: { dogecoin: IcpFeaturesConfig.DefaultConfig },
+ });
+
+ const bitcoinSubnet = await instance.getBitcoinSubnet();
+ await expectCanister(instance, DOGECOIN_CANISTER_ID, bitcoinSubnet?.id);
+ });
+
+ it('should deploy the canister migration orchestrator to the NNS subnet', async () => {
+ const instance = await create({
+ icpFeatures: { canisterMigration: IcpFeaturesConfig.DefaultConfig },
+ });
+
+ const nnsSubnet = await instance.getNnsSubnet();
+ await expectCanister(
+ instance,
+ CANISTER_MIGRATION_CANISTER_ID,
+ nnsSubnet?.id,
+ );
+ });
+
+ it.each([
+ ['bitcoin', { bitcoindAddrs: ['127.0.0.1:18444'] }],
+ ['dogecoin', { dogecoindAddrs: ['127.0.0.1:18445'] }],
+ ] as const)(
+ 'should accept %s node addresses that nothing listens on yet',
+ async (feature, addrs) => {
+ await create({
+ icpFeatures: { [feature]: IcpFeaturesConfig.DefaultConfig },
+ ...addrs,
+ });
+ },
+ );
+
+ it('should reject bitcoind and dogecoind addresses together', async () => {
+ await expect(
+ create({
+ icpFeatures: {
+ bitcoin: IcpFeaturesConfig.DefaultConfig,
+ dogecoin: IcpFeaturesConfig.DefaultConfig,
+ },
+ bitcoindAddrs: ['127.0.0.1:18444'],
+ dogecoindAddrs: ['127.0.0.1:18445'],
+ }),
+ ).rejects.toThrow(
+ 'The bitcoindAddrs and dogecoindAddrs options cannot be combined',
+ );
+ });
+ });
+});