Skip to content

Commit 2a3fd0b

Browse files
authored
feat: Anonymous refunds in inter-canister-calls (#1289)
With anonymous refunds, `deposit_cycles` also works well with bounded-wait calls, and is easier to explain than threshold signatures.
1 parent 5bbb5d2 commit 2a3fd0b

File tree

3 files changed

+31
-54
lines changed

3 files changed

+31
-54
lines changed

rust/inter-canister-calls/Makefile

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ all: test
55
.SILENT: deploy
66
build:
77
dfx deploy
8-
8+
SHELL := /bin/bash
9+
.SHELLFLAGS := -euo pipefail -c
910
.PHONY: test
1011
.SILENT: test
1112
test: build
@@ -18,7 +19,17 @@ test: build
1819
dfx canister call caller call_get "( principal \"`dfx canister id counter`\" )" | grep '(variant { Ok = 8 : nat })' && echo 'PASS'
1920
dfx canister call caller stubborn_set "( principal \"`dfx canister id counter`\", 42 : nat )" | grep '(variant { Ok })' && echo 'PASS'
2021
dfx canister call caller call_get "( principal \"`dfx canister id counter`\" )" | grep '(variant { Ok = 42 : nat })' && echo 'PASS'
21-
dfx canister call caller sign_message '("Some text to be signed")' | grep "Ok = \"" && echo PASS
22+
# We'll test that the counter's balance increases when calling send_cycles to it.
23+
# Storing the original balance in a file is the easiest with make.
24+
echo $$(dfx canister status counter | awk '/Balance/ { gsub(/_/, "", $$2); print $$2 }') > .counter_before
25+
# Send 10% of the caller's available cycles
26+
CYCLES_TO_SEND=$$(dfx canister status caller | awk '/Balance/ { gsub(/_/, "", $$2); printf("%d", int($$2/10)) }'); \
27+
echo "Sending $$CYCLES_TO_SEND cycles to counter canister"; \
28+
dfx canister call caller send_cycles "( principal \"`dfx canister id counter`\", $$CYCLES_TO_SEND: nat64 )" | grep '(variant { Ok })' && echo PASS
29+
# Check that the counter's balance increased
30+
AFTER=$$(dfx canister status counter | awk '/Balance/ { gsub(/_/, "", $$2); print $$2 }'); \
31+
BEFORE=$$(cat .counter_before); \
32+
test $$AFTER -gt $$BEFORE && echo "PASS (cycles increased: $$BEFORE -> $$AFTER)"
2233

2334
.PHONY: clean
2435
.SILENT: clean

rust/inter-canister-calls/src/caller/caller.did

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ type GetResult = variant {
1313
"Err" : text;
1414
};
1515

16-
type SignMessageResult = variant {
17-
"Ok" : text;
16+
type SendCyclesResult = variant {
17+
"Ok" : null;
1818
"Err" : text;
1919
};
2020

@@ -24,5 +24,5 @@ service : {
2424
"call_get": (counter: principal) -> (GetResult);
2525
"call_increment": (counter: principal) -> (IncrementResult);
2626
"stubborn_set": (counter: principal, new_value: nat) -> (StubbornSetResult);
27-
"sign_message": (text) -> (SignMessageResult);
27+
"send_cycles": (target: principal, amount: nat64) -> (SendCyclesResult);
2828
}

rust/inter-canister-calls/src/caller/src/lib.rs

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
// Some of the imports will only be used in later examples; we list them here for simplicity
22
use candid::{Nat, Principal};
33
use ic_cdk::api::time;
4-
use ic_cdk::call::{Call, CallErrorExt, RejectCode};
5-
use ic_cdk::management_canister::{EcdsaCurve, EcdsaKeyId, SignWithEcdsaArgs, SignWithEcdsaResult, cost_sign_with_ecdsa};
4+
use ic_cdk::call::{Call, CallErrorExt};
5+
use ic_cdk::management_canister::{DepositCyclesArgs, CanisterId};
66
use ic_cdk_macros::update;
7-
use sha2::{Digest, Sha256};
87

98
// When calling other canisters:
10-
// 1. The simplest is to mark your function as `update`. Then you can always call any public
11-
// endpoint on any other canister.
12-
// 2. Mark the function as `async`. Then you can use the `Call` API to call other canisters.
9+
//
10+
// 1. The simplest is to mark your function as `update`. Then you can always call any public
11+
// endpoint on any other canister.
12+
// 2. Mark the function as `async`. Then you can use the `Call` API to call other canisters.
13+
//
1314
// This particular example requires the caller to provide the principal (i.e., ID) of the counter canister.
1415
#[update]
1516
pub async fn call_get_and_set(counter: Principal, new_value: Nat) -> Nat {
@@ -136,52 +137,17 @@ pub async fn stubborn_set(counter: Principal, new_value: Nat) -> Result<(), Stri
136137
}
137138

138139
#[update]
139-
pub async fn sign_message(message: String) -> Result<String, String> {
140-
let message_hash = Sha256::digest(&message).to_vec();
141-
142-
let request = SignWithEcdsaArgs {
143-
message_hash,
144-
// This example does not use the fancier signing features
145-
derivation_path: vec![],
146-
key_id: EcdsaKeyId {
147-
curve: EcdsaCurve::Secp256k1,
148-
// This is the key name used for local testing; different
149-
// key names are needed for the mainnet
150-
name: "dfx_test_key".to_string(),
151-
},
140+
pub async fn send_cycles(target: CanisterId, amount: u64) -> Result<(), String> {
141+
let request = DepositCyclesArgs {
142+
canister_id: target,
152143
};
153144

154-
let cycles_cost = cost_sign_with_ecdsa(&request).map_err(|e| {
155-
format!(
156-
"Failed to compute cycles cost for signing with ECDSA: {:?}",
157-
e
158-
)
159-
})?;
160-
161-
// Use bounded-wait calls in this example, since the amount attached is
162-
// fairly low, and losing the attached cycles isn't catastrophic.
163-
match Call::bounded_wait(Principal::management_canister(), "sign_with_ecdsa")
145+
match Call::bounded_wait(Principal::management_canister(), "deposit_cycles")
164146
.with_arg(&request)
165-
// Signing with a test key requires 30 billion cycles
166-
.with_cycles(cycles_cost)
147+
.with_cycles(amount as u128)
167148
.await
168149
{
169-
Ok(resp) => match resp.candid::<SignWithEcdsaResult>() {
170-
Ok(signature) => Ok(hex::encode(signature.signature)),
171-
Err(e) => Err(format!("Error decoding response: {:?}", e)),
172-
},
173-
// A SysUnknown reject code only occurs due to a bounded-wait call timing out.
174-
// It means that no cycles will be refunded, even
175-
// if the call didn't make it to the callee. Here, this is fine since
176-
// only a small amount is used.
177-
Err(ic_cdk::call::CallFailed::CallRejected(e))
178-
if e.reject_code() == Ok(RejectCode::SysUnknown) =>
179-
{
180-
Err(format!(
181-
"Got a SysUnknown error while signing message: {:?}; cycles are not refunded",
182-
e
183-
))
184-
}
185-
Err(e) => Err(format!("Error signing message: {:?}", e)),
150+
Ok(_) => Ok(()),
151+
Err(e) => Err(format!("Error attaching {} cycles: {:?}", amount, e)),
186152
}
187-
}
153+
}

0 commit comments

Comments
 (0)