Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4fce746
chore(local): add a ckRED test token to the local network
yhabib Oct 27, 2026
8b886ec
chore(local): add a mock exchange rate canister and install the mocks…
yhabib Oct 27, 2026
34d06ba
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Oct 27, 2026
ae5dcea
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Oct 27, 2026
d9e36ef
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Oct 27, 2026
81d7073
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Oct 27, 2026
f3de59d
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Oct 27, 2026
52e67ae
fix(local): stop on a failed status request and keep the exchange rat…
yhabib Oct 27, 2026
40f1f9d
chore(local): seed the mock exchange rate at install
yhabib Oct 27, 2026
879b445
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Oct 27, 2026
9efd9a0
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Oct 27, 2026
10af372
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Oct 27, 2026
0bdbeb4
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Nov 2, 2026
61c5e41
fix(local): stop the start script when a canister status request fails
yhabib Nov 2, 2026
bd78f96
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Nov 3, 2026
80c8054
docs(local): explain the zero total value locked without a wasm
yhabib Nov 3, 2026
ea85318
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Nov 3, 2026
3a0a54b
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Nov 3, 2026
894c65a
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Nov 4, 2026
8108587
fix(spellcheck): remove the stray count line from the dictionary
yhabib Nov 4, 2026
8ad4a49
Merge branch 'governance-dfx-icp-cli-migration-bb2c0e' into feat/loca…
yhabib Nov 4, 2026
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
3 changes: 2 additions & 1 deletion .config/spellcheck.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
89
90
Monterey
IC
ICP
Expand Down Expand Up @@ -87,4 +87,5 @@ Cmd
Banxa
dfx
IC0542
ckRED
MCP
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,9 @@ jobs:
scripts/dfx-wasm-metadata-add.test --verbose
- name: Verify that metrics are present
run: scripts/nns-dapp/e2e-test-metrics-present
# TODO: Test the TVL again. scripts/nns-dapp/test-tvl needs a mock
# exchange rate canister, which the icp-cli network does not have.
- name: Test TVL
run: |
scripts/nns-dapp/test-tvl --nns_dapp_wasm nns-dapp.wasm.gz --nns_dapp_arg nns-dapp-arg-local.did
- name: Test import canister IDs
run: |
./scripts/canister_ids --import-from-index-html "$(scripts/dfx-canister-url --network local nns-dapp)/" --network local --create
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG-Nns-Dapp-unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ proposal is successful, the changes it released will be moved from this file to
test token, so the end-to-end tests that need them are skipped.
- The local test network has a mock of the ICP Swap price feed, so the app
shows token prices and the staking rewards.
- The local test network has a ckRED test token, so the import token
end-to-end tests run again.
- The local test network has a mock exchange rate canister, so the total value
locked works and its test runs again.

#### Deprecated

Expand Down
7 changes: 5 additions & 2 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ next `scripts/icp-network-start` begins from a clean state.

The local network has no SNS project and no ckBTC, ckETH or ckUSDC tokens. A
mock of the ICP Swap price feed sets the ICP price to 9 USD, see
[`mocks/icp-swap`](./mocks/icp-swap/README.md). The ICP ledger funds the test
account that the "Get ICP" menu uses.
[`mocks/icp-swap`](./mocks/icp-swap/README.md). A ckRED test token exists for
the import token tests, see [`mocks/ckred`](./mocks/ckred/README.md). A mock
exchange rate canister answers the total value locked requests of the backend
with the same ICP price. The ICP ledger funds the test account that the
"Get ICP" menu uses.

## Testnet

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/tests/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export PLAYWRIGHT_BASE_URL="$(../scripts/dfx-canister-url --network local nns-da
npm run test-e2e
```

Some tests are skipped, because they need an SNS project, ckBTC or a test
token, which the local network does not have.
Some tests are skipped, because they need an SNS project or ckBTC, which the
local network does not have.

# Debugging

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ const isSigned = (request: IcRequest): boolean =>
// This test covers finding 22: an import-token deep link must not send the user
// principal to the canisters that the URL names. The user chose neither
// canister ID, and the modal calls both of them with no click.
// TODO: Run this test again. It needs the ckred test token, which the local icp-cli
// network does not have.
test.skip("Import token deep link validates the URL canisters anonymously", async ({
test("Import token deep link validates the URL canisters anonymously", async ({
page,
context,
}) => {
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/tests/e2e/import-token.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import { expect, test } from "@playwright/test";

const TEST_TOKEN_NAME = "ckRED";

// TODO: Run this test again. It needs the ckred test token, which the local icp-cli
// network does not have.
test.skip("Test imported tokens", async ({ page, context }) => {
test("Test imported tokens", async ({ page, context }) => {
const testLedgerCanisterId = await localCanisterId("ckred_ledger");
const testIndexCanisterId = await localCanisterId("ckred_index");

Expand Down
24 changes: 23 additions & 1 deletion icp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,32 @@ canisters:
configuration:
version: "0.32.0"
dir: mocks/icp-swap
# A test ICRC token for the import token tests, see mocks/ckred. The wasms
# come from the IC commit in config.json, IC_COMMIT_FOR_SNS_AGGREGATOR.
- name: ckred_ledger
build:
steps:
- type: pre-built
url: https://download.dfinity.systems/ic/f54e1f82c3cf8ae0b4be6b0fc3a940cdcd70dd97/canisters/ic-icrc1-ledger-u256.wasm.gz
sha256: 7905265687205b49eb625a242cf7dbac0ff7dc0f0b42d3d4c26b40bc4d566b1c
- name: ckred_index
build:
steps:
- type: pre-built
url: https://download.dfinity.systems/ic/f54e1f82c3cf8ae0b4be6b0fc3a940cdcd70dd97/canisters/ic-icrc1-index-ng-u256.wasm.gz
sha256: 8a0e1189339009dbd5c70fc4310ed4f2a07bac9730aa25f036724e1611fecd5b
# A mock of the exchange rate canister, with the mainnet canister ID. The
# nns-dapp reads the ICP price from it for the total value locked.
- name: xrc
build:
steps:
- type: pre-built
url: https://github.com/dfinity/exchange-rate-canister/releases/download/2024.09.05/xrc_mock.wasm.gz
sha256: 520744bf7f60665722f597c172abebf53be300d66ed0cb87744663567949a1b1
environments:
- name: local
network: local
canisters: [nns-dapp, sns_aggregator, icp-swap]
canisters: [nns-dapp, sns_aggregator, icp-swap, ckred_ledger, ckred_index, xrc]
# Mainnet, for read-only queries. The canister IDs are in .icp/data/mappings/ic.ids.json.
- name: ic
network: ic
Expand Down
5 changes: 5 additions & 0 deletions mocks/ckred/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ckRED test token

`scripts/icp-network-start` deploys an ICRC-1 ledger and index for the ckRED test token on the local network. The import token end-to-end tests import it. The ledger has 7 decimals and a fee of 4000 units, like the snsdemo environment had.

The test account of the "Get tokens" menu, see `frontend/src/lib/api/dev.api.ts`, holds the initial supply. The anonymous principal is the minting account.
26 changes: 26 additions & 0 deletions mocks/ckred/ckRED-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 128 additions & 0 deletions scripts/icp-canister-create-with-id
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
set -euo pipefail
SOURCE_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
PATH="$SOURCE_DIR:$PATH"

print_help() {
cat <<-EOF

Creates a canister with a given ID on a local icp-cli network.

icp canister create cannot choose the ID. This script calls the
provisional_create_canister_with_cycles method of the management canister
through the PocketIC REST API, which accepts any canister ID in the range
of a subnet of the network. The current icp identity becomes the controller.

The network must run in the background, so that
.icp/cache/networks/<network>/descriptor.json exists.
EOF
}

CYCLES=100_000_000_000_000
MAX_ATTEMPTS=120
RETRY_DELAY_SECONDS=0.5

# Source the clap.bash file ---------------------------------------------------
source "$SOURCE_DIR/clap.bash"
# Define options
clap.define short=n long=network desc="The icp network" variable=NETWORK default="local"
clap.define short=c long=canister desc="The canister ID to create" variable=CANISTER_ID
clap.define short=p long=principal desc="The controller of the new canister" variable=CONTROLLER default="$(icp identity principal)"
# Source the output file ----------------------------------------------------------
source "$(clap.build)"

test -n "${CANISTER_ID:-}" || {
echo "ERROR: Please specify the canister ID with --canister."
exit 1
} >&2

if icp canister status "$CANISTER_ID" --network "$NETWORK" >/dev/null 2>&1; then
echo "Canister $CANISTER_ID exists already."
exit 0
fi

DESCRIPTOR="$SOURCE_DIR/../.icp/cache/networks/$NETWORK/descriptor.json"
test -f "$DESCRIPTOR" || {
echo "ERROR: The '$NETWORK' network is not running in the background."
echo " Expected the network descriptor at: $DESCRIPTOR"
exit 1
} >&2
POCKETIC_URL="http://127.0.0.1:$(jq -r '.["pocketic-config-port"]' "$DESCRIPTOR")"
INSTANCE_URL="$POCKETIC_URL/instances/$(jq -r '.["pocketic-instance-id"]' "$DESCRIPTOR")"

# Converts a textual principal to the base64 of its raw bytes, as the PocketIC REST API expects.
principal_to_base64() {
convert-id --input text --output hex "$1" | xxd -r -p | base64 | tr -d '\n'
}

# Polls an operation that PocketIC answered with 202 and a handle.
poll_operation() {
local state_label op_id url response
state_label="$(jq -r .state_label <<<"$1")"
op_id="$(jq -r .op_id <<<"$1")"
url="$POCKETIC_URL/read_graph/$state_label/$op_id"
for ((attempt = 0; attempt < MAX_ATTEMPTS; attempt++)); do
response="$(curl -s -w '\n%{http_code}' "$url")"
if [[ "$(tail -n1 <<<"$response")" != "404" ]]; then
sed '$d' <<<"$response"
return
fi
sleep "$RETRY_DELAY_SECONDS"
done
echo "ERROR: Timed out on operation $op_id." >&2
exit 1
}

# Posts to the PocketIC instance. The network runs in live mode, so PocketIC
# answers 409 while it advances time. Retry until it is free.
post() {
local path body response code
path="$1"
body="$2"
for ((attempt = 0; attempt < MAX_ATTEMPTS; attempt++)); do
response="$(curl -s -w '\n%{http_code}' -X POST -H 'content-type: application/json' "$INSTANCE_URL/update/$path" -d "$body")"
code="$(tail -n1 <<<"$response")"
case "$code" in
202) poll_operation "$(sed '$d' <<<"$response")" && return ;;
409) sleep "$RETRY_DELAY_SECONDS" ;;
*)
sed '$d' <<<"$response"
return
;;
esac
done
echo "ERROR: Timed out on $path." >&2
exit 1
}

payload="$(
didc encode "(record {
amount = opt ($CYCLES : nat);
settings = null;
specified_id = opt principal \"$CANISTER_ID\";
})" | xxd -r -p | base64 | tr -d '\n'
)"

submitted="$(post submit_ingress_message "$(jq -n \
--arg sender "$(principal_to_base64 "$CONTROLLER")" \
--arg effective "$(principal_to_base64 "$CANISTER_ID")" \
--arg payload "$payload" \
'{
sender: $sender,
canister_id: "",
effective_principal: { CanisterId: $effective },
method: "provisional_create_canister_with_cycles",
payload: $payload
}')")"
jq -e .Ok <<<"$submitted" >/dev/null || {
echo "ERROR: Failed to submit the create call: $submitted"
exit 1
} >&2

result="$(post await_ingress_message "$(jq .Ok <<<"$submitted")")"
jq -e 'has("Ok")' <<<"$result" >/dev/null || {
echo "ERROR: The create call failed: $result"
exit 1
} >&2

echo "Created canister $CANISTER_ID with controller $CONTROLLER."
83 changes: 72 additions & 11 deletions scripts/icp-network-start
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ print_help() {
- adds the current icp identity as a controller of both canisters,
- stakes a large NNS neuron, so that test neurons cannot decide proposals,
- deploys the mock ICP Swap price feed,
- deploys the ckRED test token,
- deploys a mock exchange rate canister with the mainnet canister ID,
- installs the given wasms, if any.

If the network runs already, the script keeps it and only does the steps
Expand Down Expand Up @@ -131,21 +133,80 @@ stake_large_neuron() {
}
stake_large_neuron

# Deploys the mock ICP Swap price feed, an asset canister that serves mocks/icp-swap/tickers.
# Creates a canister of icp.yaml on an application subnet, if it does not exist.
# Returns 1 if the canister exists already.
#
# icp-cli creates a new canister on the subnet of the existing canisters, which
# is the NNS subnet here. The NNS subnet refuses that, so the canister is
# created on an application subnet first.
deploy_icp_swap() {
local cmc subnet
if ! jq -e '.["icp-swap"]' ".icp/cache/mappings/$ENVIRONMENT.ids.json" >/dev/null 2>&1; then
cmc="$(canister-id --network "$NETWORK" nns-cycles-minting)"
subnet="$(icp canister call "$cmc" get_default_subnets '()' --network "$NETWORK" --query | idl2json | jq -r '.[0]')"
icp canister create icp-swap --environment "$ENVIRONMENT" --subnet "$subnet"
# created on an application subnet explicitly.
create_on_application_subnet() {
local canister cmc subnet
canister="$1"
if jq -e --arg c "$canister" '.[$c]' ".icp/cache/mappings/$ENVIRONMENT.ids.json" >/dev/null 2>&1; then
return 1
fi
icp deploy icp-swap --environment "$ENVIRONMENT" --yes
cmc="$(canister-id --network "$NETWORK" nns-cycles-minting)"
subnet="$(icp canister call "$cmc" get_default_subnets '()' --network "$NETWORK" --query | idl2json | jq -r '.[0]')"
icp canister create "$canister" --environment "$ENVIRONMENT" --subnet "$subnet"
}

# Whether a canister of the environment has code. A failed status request stops
# the script, also when the caller runs the function in an `||` chain.
is_installed() {
local status module_hash
status="$(icp canister status "$1" --environment "$ENVIRONMENT" --json)" || {
echo "ERROR: Could not get the status of the $1 canister." >&2
exit 1
}
module_hash="$(jq -r .module_hash <<<"$status")"
[[ "$module_hash" != "null" ]]
}

# The mock ICP Swap price feed, an asset canister that serves mocks/icp-swap/tickers.
create_on_application_subnet icp-swap || true
icp deploy icp-swap --environment "$ENVIRONMENT" --yes

# The ckRED test token, see mocks/ckred/README.md. The test account of the
# "Get tokens" menu holds the initial supply. The anonymous principal mints.
# An installed ledger keeps its state; an upgrade would need Upgrade arguments.
deploy_ckred() {
local logo ledger_id
logo="$(base64 <"mocks/ckred/ckRED-logo.svg" | tr -d '\n')"
create_on_application_subnet ckred_ledger || true
is_installed ckred_ledger || icp deploy ckred_ledger --environment "$ENVIRONMENT" --yes --args "(variant { Init = record {
token_symbol = \"ckRED\";
token_name = \"ckRED\";
minting_account = record { owner = principal \"2vxsx-fae\" };
transfer_fee = 4_000 : nat;
max_memo_length = opt (64 : nat16);
decimals = opt (7 : nat8);
metadata = vec { record { \"icrc1:logo\"; variant { Text = \"data:image/svg+xml;base64,$logo\" } } };
initial_balances = vec { record { record { owner = principal \"jg6qm-uw64t-m6ppo-oluwn-ogr5j-dc5pm-lgy2p-eh6px-hebcd-5v73i-nqe\" }; 100_000_000_000_000 : nat } };
archive_options = record {
num_blocks_to_archive = 10_000 : nat64;
trigger_threshold = 20_000 : nat64;
controller_id = principal \"2vxsx-fae\";
cycles_for_archive_creation = opt (4_000_000_000_000 : nat64);
};
feature_flags = opt record { icrc2 = true };
} })"
ledger_id="$(canister-id --network "$NETWORK" ckred_ledger)"
create_on_application_subnet ckred_index || true
is_installed ckred_index || icp deploy ckred_index --environment "$ENVIRONMENT" --yes --args "(opt variant { Init = record { ledger_id = principal \"$ledger_id\" } })"
}
deploy_ckred

# Deploys the mock exchange rate canister with the mainnet canister ID, which
# the nns-dapp backend calls. The ICP price is 9 USD, like the price feed mock.
deploy_xrc() {
local xrc_id="uf6dk-hyaaa-aaaaq-qaaaq-cai"
icp-canister-create-with-id --network "$NETWORK" --canister "$xrc_id"
icp canister link xrc "$xrc_id" --environment "$ENVIRONMENT" --force
# The mock starts with 9 USD per ICP, with the default assets and metadata.
# An installed mock keeps the rate that a test set with set_exchange_rate.
is_installed xrc || icp deploy xrc --environment "$ENVIRONMENT" --yes --args '(record { response = variant { ExchangeRate = record { base_asset = null; quote_asset = null; metadata = null; rate = 900_000_000 : nat64 } } })'
}
deploy_icp_swap
deploy_xrc
Comment thread
yhabib marked this conversation as resolved.
Resolved

if test -n "${NNS_DAPP_WASM:-}"; then
# The default arguments hold the URLs of the mocks, so they are created after the mocks.
Expand All @@ -160,6 +221,6 @@ fi

echo
echo "The '$NETWORK' network is ready:"
for canister in nns-dapp sns_aggregator internet_identity icp-swap; do
for canister in nns-dapp sns_aggregator internet_identity icp-swap ckred_ledger ckred_index xrc; do
printf " %-18s %s\n" "$canister" "$(dfx-canister-url --network "$NETWORK" "$canister")"
done
Loading
Loading