Skip to content

Commit ba7e922

Browse files
authored
feat: facade pull ICP, ckBTC, ckETH ledger canisters (#3978)
1 parent 3965ff9 commit ba7e922

File tree

5 files changed

+384
-2
lines changed

5 files changed

+384
-2
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
# UNRELEASED
44

5+
### feat: facade pull ICP, ckBTC, ckETH ledger canisters
6+
7+
The ledger canisters can be pulled even though they are not really "pullable".
8+
The metadata like wasm_url and init_guide are hardcoded inside `dfx deps pull` logic.
9+
10+
- ICP ledger: `ryjl3-tyaaa-aaaaa-aaaba-cai`
11+
- ckBTC ledger: `mxzaz-hqaaa-aaaar-qaada-cai`
12+
- ckETH ledger: `ss2fx-dyaaa-aaaar-qacoq-cai`
13+
514
### chore: update agent version in frontend templates, and include `resolve.dedupe` in Vite config
615

716
### chore: improve error message when trying to use the local replica when it is not running

e2e/tests-dfx/deps.bash

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,3 +673,172 @@ Installing canister: $CANISTER_ID_C (dep_c)"
673673
# this command will fail if the pulled.json is not correct
674674
assert_command dfx deps init
675675
}
676+
677+
@test "dfx deps can facade pull ICP ledger" {
678+
use_test_specific_cache_root # dfx deps pull will download files to cache
679+
680+
dfx_new
681+
jq '.canisters.e2e_project_backend.dependencies=["icp_ledger"]' dfx.json | sponge dfx.json
682+
jq '.canisters.icp_ledger.type="pull"' dfx.json | sponge dfx.json
683+
jq '.canisters.icp_ledger.id="ryjl3-tyaaa-aaaaa-aaaba-cai"' dfx.json | sponge dfx.json
684+
685+
dfx_start
686+
assert_command dfx deps pull --network local
687+
assert_contains "Using facade dependencies for canister ryjl3-tyaaa-aaaaa-aaaba-cai."
688+
689+
dfx identity new --storage-mode plaintext minter
690+
assert_command_fail dfx deps init icp_ledger
691+
assert_contains "1. Create a 'minter' identity: dfx identity new minter
692+
2. Run the following multi-line command:"
693+
694+
assert_command dfx deps init ryjl3-tyaaa-aaaaa-aaaba-cai --argument "(variant {
695+
Init = record {
696+
minting_account = \"$(dfx --identity minter ledger account-id)\";
697+
initial_values = vec {};
698+
send_whitelist = vec {};
699+
transfer_fee = opt record { e8s = 10_000 : nat64; };
700+
token_symbol = opt \"LICP\";
701+
token_name = opt \"Local ICP\";
702+
}
703+
})"
704+
705+
assert_command dfx deps deploy
706+
707+
# Can mint tokens (transfer from minting_account)
708+
assert_command dfx --identity minter canister call icp_ledger icrc1_transfer "(
709+
record {
710+
to = record {
711+
owner = principal \"$(dfx --identity default identity get-principal)\";
712+
};
713+
amount = 1_000_000 : nat;
714+
},
715+
)"
716+
717+
assert_command dfx canister call icp_ledger icrc1_balance_of "(
718+
record {
719+
owner = principal \"$(dfx --identity default identity get-principal)\";
720+
},
721+
)"
722+
assert_eq "(1_000_000 : nat)"
723+
}
724+
725+
@test "dfx deps can facade pull ckBTC ledger" {
726+
[[ "$USE_POCKETIC" ]] && skip "skipped for pocketic which doesn't have ckBTC subnet"
727+
728+
use_test_specific_cache_root # dfx deps pull will download files to cache
729+
730+
dfx_new
731+
jq '.canisters.e2e_project_backend.dependencies=["ckbtc_ledger"]' dfx.json | sponge dfx.json
732+
jq '.canisters.ckbtc_ledger.type="pull"' dfx.json | sponge dfx.json
733+
jq '.canisters.ckbtc_ledger.id="mxzaz-hqaaa-aaaar-qaada-cai"' dfx.json | sponge dfx.json
734+
735+
dfx_start
736+
assert_command dfx deps pull --network local
737+
assert_contains "Using facade dependencies for canister mxzaz-hqaaa-aaaar-qaada-cai."
738+
739+
dfx identity new --storage-mode plaintext minter
740+
assert_command_fail dfx deps init ckbtc_ledger
741+
assert_contains "1. Create a 'minter' identity: dfx identity new minter
742+
2. Run the following multi-line command:"
743+
744+
assert_command dfx deps init mxzaz-hqaaa-aaaar-qaada-cai --argument "(variant {
745+
Init = record {
746+
minting_account = record { owner = principal \"$(dfx --identity minter identity get-principal)\"; };
747+
transfer_fee = 10;
748+
token_symbol = \"ckBTC\";
749+
token_name = \"ckBTC\";
750+
metadata = vec {};
751+
initial_balances = vec {};
752+
max_memo_length = opt 80;
753+
archive_options = record {
754+
num_blocks_to_archive = 1000;
755+
trigger_threshold = 2000;
756+
max_message_size_bytes = null;
757+
cycles_for_archive_creation = opt 100_000_000_000_000;
758+
node_max_memory_size_bytes = opt 3_221_225_472;
759+
controller_id = principal \"2vxsx-fae\"
760+
}
761+
}
762+
})"
763+
764+
assert_command dfx deps deploy
765+
766+
# Can mint tokens (transfer from minting_account)
767+
assert_command dfx --identity minter canister call ckbtc_ledger icrc1_transfer "(
768+
record {
769+
to = record {
770+
owner = principal \"$(dfx --identity default identity get-principal)\";
771+
};
772+
amount = 1_000_000 : nat;
773+
},
774+
)"
775+
776+
assert_command dfx canister call ckbtc_ledger icrc1_balance_of "(
777+
record {
778+
owner = principal \"$(dfx --identity default identity get-principal)\";
779+
},
780+
)"
781+
assert_eq "(1_000_000 : nat)"
782+
}
783+
784+
785+
@test "dfx deps can facade pull ckETH ledger" {
786+
[[ "$USE_POCKETIC" ]] && skip "skipped for pocketic which doesn't have ckETH subnet"
787+
788+
use_test_specific_cache_root # dfx deps pull will download files to cache
789+
790+
dfx_new
791+
jq '.canisters.e2e_project_backend.dependencies=["cketh_ledger"]' dfx.json | sponge dfx.json
792+
jq '.canisters.cketh_ledger.type="pull"' dfx.json | sponge dfx.json
793+
jq '.canisters.cketh_ledger.id="ss2fx-dyaaa-aaaar-qacoq-cai"' dfx.json | sponge dfx.json
794+
795+
dfx_start
796+
assert_command dfx deps pull --network local
797+
assert_contains "Using facade dependencies for canister ss2fx-dyaaa-aaaar-qacoq-cai."
798+
799+
dfx identity new --storage-mode plaintext minter
800+
assert_command_fail dfx deps init cketh_ledger
801+
assert_contains "1. Create a 'minter' identity: dfx identity new minter
802+
2. Run the following multi-line command:"
803+
804+
assert_command dfx deps init ss2fx-dyaaa-aaaar-qacoq-cai --argument "(variant {
805+
Init = record {
806+
minting_account = record { owner = principal \"$(dfx --identity minter identity get-principal)\"; };
807+
decimals = opt 18;
808+
max_memo_length = opt 80;
809+
transfer_fee = 2_000_000_000_000;
810+
token_symbol = \"ckETH\";
811+
token_name = \"ckETH\";
812+
feature_flags = opt record { icrc2 = true };
813+
metadata = vec {};
814+
initial_balances = vec {};
815+
archive_options = record {
816+
num_blocks_to_archive = 1000;
817+
trigger_threshold = 2000;
818+
max_message_size_bytes = null;
819+
cycles_for_archive_creation = opt 100_000_000_000_000;
820+
node_max_memory_size_bytes = opt 3_221_225_472;
821+
controller_id = principal \"2vxsx-fae\"
822+
}
823+
}
824+
})"
825+
826+
assert_command dfx deps deploy
827+
828+
# Can mint tokens (transfer from minting_account)
829+
assert_command dfx --identity minter canister call cketh_ledger icrc1_transfer "(
830+
record {
831+
to = record {
832+
owner = principal \"$(dfx --identity default identity get-principal)\";
833+
};
834+
amount = 1_000_000 : nat;
835+
},
836+
)"
837+
838+
assert_command dfx canister call cketh_ledger icrc1_balance_of "(
839+
record {
840+
owner = principal \"$(dfx --identity default identity get-principal)\";
841+
},
842+
)"
843+
assert_eq "(1_000_000 : nat)"
844+
}

src/dfx/src/lib/deps/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub struct PulledJson {
2222
pub canisters: BTreeMap<Principal, PulledCanister>,
2323
}
2424

25-
#[derive(Serialize, Deserialize, Default)]
25+
#[derive(Serialize, Deserialize, Default, Clone)]
2626
pub struct PulledCanister {
2727
/// Name of `type: pull` in dfx.json. Omitted if indirect dependency.
2828
#[serde(skip_serializing_if = "Option::is_none")]

src/dfx/src/lib/deps/pull/facade.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use candid::{pretty::candid::pp_args, Principal};
2+
use candid_parser::utils::{instantiate_candid, CandidSource};
3+
use dfx_core::config::cache::get_cache_root;
4+
use dfx_core::fs::{composite::ensure_parent_dir_exists, read, read_to_string, write};
5+
use sha2::{Digest, Sha256};
6+
use std::collections::HashMap;
7+
8+
use super::{super::PulledCanister, write_to_tempfile_then_rename};
9+
use crate::lib::deps::{
10+
get_pulled_canister_dir, get_pulled_service_candid_path, get_pulled_wasm_path,
11+
};
12+
use crate::lib::error::DfxResult;
13+
use crate::util::{download_file, download_file_to_path};
14+
15+
static IC_REV: &str = "1eeb4d74deb00bd52739cbd6f37ce1dc72e0c76e";
16+
17+
#[derive(Debug)]
18+
struct Facade {
19+
wasm_url: String,
20+
candid_url: String,
21+
dependencies: Vec<Principal>,
22+
init_guide: String,
23+
}
24+
25+
lazy_static::lazy_static! {
26+
static ref ICP_LEDGER: Principal=Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
27+
static ref CKBTC_LEDGER: Principal=Principal::from_text("mxzaz-hqaaa-aaaar-qaada-cai").unwrap();
28+
static ref CKETH_LEDGER: Principal=Principal::from_text("ss2fx-dyaaa-aaaar-qacoq-cai").unwrap();
29+
static ref FACADE: HashMap<Principal, Facade> = {
30+
let mut m = HashMap::new();
31+
// https://internetcomputer.org/docs/current/developer-docs/defi/tokens/ledger/setup/icp_ledger_setup
32+
m.insert(
33+
*ICP_LEDGER,
34+
Facade {
35+
wasm_url: format!("https://download.dfinity.systems/ic/{IC_REV}/canisters/ledger-canister.wasm.gz"),
36+
candid_url: format!("https://raw.githubusercontent.com/dfinity/ic/{IC_REV}/rs/ledger_suite/icp/ledger.did"),
37+
dependencies:vec![],
38+
init_guide: r#"
39+
1. Create a 'minter' identity: dfx identity new minter
40+
2. Run the following multi-line command:
41+
42+
dfx deps init ryjl3-tyaaa-aaaaa-aaaba-cai --argument "(variant {
43+
Init = record {
44+
minting_account = \"$(dfx --identity minter ledger account-id)\";
45+
initial_values = vec {};
46+
send_whitelist = vec {};
47+
transfer_fee = opt record { e8s = 10_000 : nat64; };
48+
token_symbol = opt \"LICP\";
49+
token_name = opt \"Local ICP\";
50+
}
51+
})"
52+
"#.to_string(),
53+
}
54+
);
55+
// https://github.com/dfinity/ic/blob/master/rs/bitcoin/ckbtc/mainnet/README.md#installing-the-ledger-mxzaz-hqaaa-aaaar-qaada-cai
56+
m.insert(
57+
*CKBTC_LEDGER,
58+
Facade {
59+
wasm_url: format!("https://download.dfinity.systems/ic/{IC_REV}/canisters/ic-icrc1-ledger.wasm.gz"),
60+
candid_url: format!("https://raw.githubusercontent.com/dfinity/ic/{IC_REV}/rs/ledger_suite/icrc1/ledger/ledger.did"),
61+
dependencies:vec![],
62+
init_guide: r#"
63+
1. Create a 'minter' identity: dfx identity new minter
64+
2. Run the following multi-line command:
65+
66+
dfx deps init mxzaz-hqaaa-aaaar-qaada-cai --argument "(variant {
67+
Init = record {
68+
minting_account = record { owner = principal \"$(dfx --identity minter identity get-principal)\"; };
69+
transfer_fee = 10;
70+
token_symbol = \"ckBTC\";
71+
token_name = \"ckBTC\";
72+
metadata = vec {};
73+
initial_balances = vec {};
74+
max_memo_length = opt 80;
75+
archive_options = record {
76+
num_blocks_to_archive = 1000;
77+
trigger_threshold = 2000;
78+
max_message_size_bytes = null;
79+
cycles_for_archive_creation = opt 100_000_000_000_000;
80+
node_max_memory_size_bytes = opt 3_221_225_472;
81+
controller_id = principal \"2vxsx-fae\"
82+
}
83+
}
84+
})"
85+
"#.to_string(),
86+
}
87+
);
88+
// https://github.com/dfinity/ic/blob/master/rs/ethereum/cketh/mainnet/README.md#installing-the-ledger
89+
m.insert(
90+
*CKETH_LEDGER,
91+
Facade {
92+
wasm_url: format!("https://download.dfinity.systems/ic/{IC_REV}/canisters/ic-icrc1-ledger-u256.wasm.gz"),
93+
candid_url: format!("https://raw.githubusercontent.com/dfinity/ic/{IC_REV}/rs/ledger_suite/icrc1/ledger/ledger.did"),
94+
dependencies:vec![],
95+
init_guide: r#"
96+
1. Create a 'minter' identity: dfx identity new minter
97+
2. Run the following multi-line command:
98+
99+
dfx deps init ss2fx-dyaaa-aaaar-qacoq-cai --argument "(variant {
100+
Init = record {
101+
minting_account = record { owner = principal \"$(dfx --identity minter identity get-principal)\"; };
102+
decimals = opt 18;
103+
max_memo_length = opt 80;
104+
transfer_fee = 2_000_000_000_000;
105+
token_symbol = \"ckETH\";
106+
token_name = \"ckETH\";
107+
feature_flags = opt record { icrc2 = true };
108+
metadata = vec {};
109+
initial_balances = vec {};
110+
archive_options = record {
111+
num_blocks_to_archive = 1000;
112+
trigger_threshold = 2000;
113+
max_message_size_bytes = null;
114+
cycles_for_archive_creation = opt 100_000_000_000_000;
115+
node_max_memory_size_bytes = opt 3_221_225_472;
116+
controller_id = principal \"2vxsx-fae\"
117+
}
118+
}
119+
})"
120+
"#.to_string(),
121+
}
122+
);
123+
m
124+
};
125+
}
126+
127+
pub(super) fn facade_dependencies(canister_id: &Principal) -> Option<Vec<Principal>> {
128+
FACADE
129+
.get(canister_id)
130+
.map(|facade| facade.dependencies.clone())
131+
}
132+
133+
pub(super) async fn facade_download(canister_id: &Principal) -> DfxResult<Option<PulledCanister>> {
134+
if let Some(facade) = FACADE.get(canister_id) {
135+
let mut pulled_canister = PulledCanister {
136+
dependencies: facade.dependencies.clone(),
137+
init_guide: facade.init_guide.clone(),
138+
gzip: facade.wasm_url.ends_with(".gz"),
139+
..Default::default()
140+
};
141+
let ic_rev_path = get_cache_root()?
142+
.join("pulled")
143+
.join(".facade")
144+
.join(canister_id.to_text());
145+
let wasm_path = get_pulled_wasm_path(canister_id, pulled_canister.gzip)?;
146+
let service_candid_path = get_pulled_service_candid_path(canister_id)?;
147+
let mut cache_hit = false;
148+
if ic_rev_path.exists() && wasm_path.exists() && service_candid_path.exists() {
149+
let ic_rev = read_to_string(&ic_rev_path)?;
150+
if ic_rev == IC_REV {
151+
cache_hit = true;
152+
}
153+
}
154+
if !cache_hit {
155+
// delete files from previous pull
156+
let pulled_canister_dir = get_pulled_canister_dir(canister_id)?;
157+
if pulled_canister_dir.exists() {
158+
dfx_core::fs::remove_dir_all(&pulled_canister_dir)?;
159+
}
160+
dfx_core::fs::create_dir_all(&pulled_canister_dir)?;
161+
// download wasm and candid
162+
let wasm_url = reqwest::Url::parse(&facade.wasm_url)?;
163+
download_file_to_path(&wasm_url, &wasm_path).await?;
164+
let candid_url = reqwest::Url::parse(&facade.candid_url)?;
165+
let candid_bytes = download_file(&candid_url).await?;
166+
let candid_service = String::from_utf8(candid_bytes)?;
167+
write_to_tempfile_then_rename(candid_service.as_bytes(), &service_candid_path)?;
168+
// write ic_rev for cache logic
169+
ensure_parent_dir_exists(&ic_rev_path)?;
170+
write(&ic_rev_path, IC_REV)?;
171+
}
172+
173+
// wasm_hash
174+
let wasm_content = read(&wasm_path)?;
175+
let wasm_hash = Sha256::digest(wasm_content).to_vec();
176+
pulled_canister.wasm_hash = hex::encode(&wasm_hash);
177+
pulled_canister.wasm_hash_download = hex::encode(&wasm_hash);
178+
179+
// candid_args
180+
let candid_service = read_to_string(&service_candid_path)?;
181+
let candid_source = CandidSource::Text(&candid_service);
182+
let (args, _service) = instantiate_candid(candid_source)?;
183+
let candid_args = pp_args(&args).pretty(80).to_string();
184+
pulled_canister.candid_args = candid_args;
185+
186+
Ok(Some(pulled_canister))
187+
} else {
188+
Ok(None)
189+
}
190+
}

0 commit comments

Comments
 (0)