OK. thank you guys.
I modified a dfinity example ‘token_transfer_from’ slightly:
Into:
#[ic_cdk::update]
async fn transfer(args: TransferArgs) -> Result<BlockIndex, String> {
ic_cdk::println!(
"Transferring {} tokens to account {} \n from account {} after {} seconds",
&args.amount,
&args.to_account,
&args.from_account,
&args.delay_in_seconds
);
let secs = Duration::from_secs(args.delay_in_seconds);
let amount = args.amount.clone();
let to_account = args.to_account.clone();
let from_account = args.from_account.clone();
ic_cdk::println!("Timer canister: Starting a new timer with {secs:?} interval...");
// Schedule a new periodic task to increment the counter.
let _timer_id = ic_cdk_timers::set_timer(secs, move || {
ic_cdk::println!("Timer canister: in set_timer closure");
// To drive an async function to completion inside the timer handler,
// use `ic_cdk::spawn()`, for example:
// ic_cdk_timers::set_timer_interval(interval, || ic_cdk::spawn(async_function()));
let transfer_from_args = TransferFromArgs {
// the account we want to transfer tokens from (in this case we assume the caller approved the canister to spend funds on their behalf)
from: Account::from(from_account.clone()),
// can be used to distinguish between transactions
memo: None,
// the amount we want to transfer
amount: amount.clone(),
// the subaccount we want to spend the tokens from (in this case we assume the default subaccount has been approved)
spender_subaccount: None,
// if not specified, the default fee for the canister is used
fee: None,
// the account we want to transfer tokens to
to: to_account.clone(),
// a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time
created_at_time: None,
};
ic_cdk::spawn(async {
// Your async code here
ic_cdk::println!("Timer canister: in spawned async block");
// 1. Asynchronously call another canister function using `ic_cdk::call`.
let result =ic_cdk::call::<(TransferFromArgs,), (Result<BlockIndex, TransferFromError>,)>(
// 2. Convert a textual representation of a Principal into an actual `Principal` object. The principal is the one we specified in `dfx.json`.
// `expect` will panic if the conversion fails, ensuring the code does not proceed with an invalid principal.
Principal::from_text(LEDGER_CANISTER_ID)
.expect("Could not decode the principal."),
// 3. Specify the method name on the target canister to be called, in this case, "icrc1_transfer".
"icrc2_transfer_from",
// 4. Provide the arguments for the call in a tuple, here `transfer_args` is encapsulated as a single-element tuple.
(transfer_from_args,),
)
.await // 5. Await the completion of the asynchronous call, pausing the execution until the future is resolved.
// 6. Apply `map_err` to transform any network or system errors encountered during the call into a more readable string format.
// The `?` operator is then used to propagate errors: if the result is an `Err`, it returns from the function with that error,
// otherwise, it unwraps the `Ok` value, allowing the chain to continue.
.map_err(|e| format!("failed to call ledger: {:?}", e))
.and_then(|response| response.0.map_err(|e| format!("ledger transfer error {:?}", e)));
// 7. Access the first element of the tuple, which is the `Result<BlockIndex, TransferError>`, for further processing.
// 8. Use `map_err` again to transform any specific ledger transfer errors into a readable string format, facilitating error handling and debugging.
ic_cdk::println!("Timer canister: in spawned async block after call");
match result {
Ok(block_index) => {
ic_cdk::println!("Transfer successful. Block index: {}", block_index);
}
Err(e) => {
ic_cdk::println!("Transfer failed: {:?}", e);
}
}
});
});
ic_cdk::println!("Timer canister: returning from transfer");
Ok(BlockIndex::from(0 as u32))
}
I have a shell script that transfers ICP from Alice identity to Bob identity
It works both in the local mode and in a playground mode , the latter operating with real ICP tokens.
#!/usr/bin/env bash
# If you run the script without any arguments, `MODE` will be set to `local`.
# can be run with `local` or `nonlocal` as a script argument
MODE=${1:-local}
# in nonllocal mode feed Alice with some ICP first:
# dfx ledger --ic transfer --identity Matiki --amount 0.01 --memo 9 $(dfx ledger account-id --identity Alice)
if [ "$MODE" = "local" ]; then
echo "Building in local mode"
else
echo "Building in non-local mode"
fi
dfx killall
set -e
echo "===========SETUP========="
dfx start --background --clean
sleep 5
dfx identity list
if [ "$MODE" == "local" ]; then
export LEDGER_CANISTER_ID=mxzaz-hqaaa-aaaar-qaada-cai
export LEDGER=icrc1_ledger_canister
export NETWORK=
export PLAYGROUND=
else
export LEDGER_CANISTER_ID=ryjl3-tyaaa-aaaaa-aaaba-cai
export LEDGER=$LEDGER_CANISTER_ID
export NETWORK=--ic
export PLAYGROUND=--playground
fi
function balance() {
local identity=$1
# --identity Matiki is only to silence a warning about the identity 'default'
dfx canister $NETWORK call --identity Matiki $LEDGER icrc1_balance_of "(record {
owner = principal \"$(dfx identity --identity $identity get-principal)\";
})"
}
function balance_of_canister() {
local canister_name=$1
dfx canister $NETWORK call $LEDGER icrc1_balance_of "(record {
owner = principal \"$(dfx canister id $canister_name)\";
})"
}
PUSH_RED="\e[31m"
PUSH_GREEN="\e[32m"
PUSH_YELLOW="\e[33m"
POP="\e[0m"
if [ "$MODE" == "local" ]; then
dfx deploy $LEDGER --argument "(variant {
Init = record {
token_symbol = \"ICRC1\";
token_name = \"L-ICRC1\";
minting_account = record {
owner = principal \"$(dfx identity --identity anonymous get-principal)\"
};
transfer_fee = 10_000;
metadata = vec {};
initial_balances = vec {
record {
record {
owner = principal \"$(dfx identity --identity Alice get-principal)\";
};
10_000_000_000;
};
};
archive_options = record {
num_blocks_to_archive = 1000;
trigger_threshold = 2000;
controller_id = principal \"$(dfx identity --identity anonymous get-principal)\";
};
feature_flags = opt record {
icrc2 = true;
};
}
})"
fi
balance Matiki
MatikiBalanceBefore=$(balance Matiki)
balance Alice
balance Bob
BobBalanceBefore=$(balance Bob)
echo "===========SETUP DONE========="
balance Alice
dfx deploy $PLAYGROUND token_transfer_from_backend
export BACKEND_CANISTER_ID=$(dfx canister $PLAYGROUND id token_transfer_from_backend)
echo -e "${PUSH_YELLOW}BACKEND_CANISTER_ID: $BACKEND_CANISTER_ID$POP"
echo "===========APPROVE========="
# approve the token_transfer_from_backend canister to spend 300 + transfer fee tokens
dfx canister --verbose $NETWORK call --identity Alice $LEDGER icrc2_approve "(
record {
spender= record {
owner = principal \"$BACKEND_CANISTER_ID\";
};
amount = 10_300: nat;
}
)"
echo =========ALLOWANCE is what is approved
dfx canister $NETWORK call --identity Alice $LEDGER icrc2_allowance "(
record {
account = record {
owner = principal \"$(dfx identity --identity Alice get-principal)\";
};
spender = record {
owner = principal \"$BACKEND_CANISTER_ID\";
};
})"
balance Alice
echo "===========TRANSFER========="
dfx canister $NETWORK call $BACKEND_CANISTER_ID transfer "(record {
amount = 300;
to_account = record {
owner = principal \"$(dfx identity $NETWORK --identity Bob get-principal)\";
};
from_account = record {
owner = principal \"$(dfx identity $NETWORK --identity Alice get-principal)\";
};
delay_in_seconds = 20;
})"
balance Alice
balance Bob
dfx canister $NETWORK call $LEDGER icrc1_balance_of "(record {
owner = principal \"$(dfx canister $PLAYGROUND id token_transfer_from_backend)\";
})"
function check_bob_balance_increase {
local BobBalanceAfter=$(balance Bob)
# BoB balances are in the form (3_300 : nat), so we need to parse them before subtraction
BobBalanceBefore=$(echo $BobBalanceBefore | tr -d '_' | sed 's/.*(\([0-9]*\).*/\1/')
BobBalanceAfter=$(echo $BobBalanceAfter | tr -d '_' | sed 's/.*(\([0-9]*\).*/\1/')
if [ $((BobBalanceAfter - BobBalanceBefore)) -ne 300 ]; then
echo -e "${PUSH_RED}Error: Bob's balance hasn't increased by 300$POP"
else
echo -e "${PUSH_GREEN}OK: Bob's balance has increased by 300$POP"
fi
}
check_bob_balance_increase
sleep 20
balance Alice
balance Bob
balance Matiki
MatikiBalanceAfter=$(balance Matiki)
if [ "$MatikiBalanceBefore" == "$MatikiBalanceAfter" ]; then
echo "Matiki balance hasn't changed."
else
echo "${PUSH_RED}Error: Matiki balance has changed !!!$POP"
fi
check_bob_balance_increase
echo "DONE"
IN either case the output looks like this:
So far so good.
Now I’m trying to implement the approval in a frontend canister
async login() {
const authClient = await AuthClient.create();
await authClient.login({
//identityProvider: import.meta.env.VITE_APP_IDENTITY_PROVIDER,
identityProvider: "https://identity.ic0.app/#authorize",
//identityProvider: "http://be2us-64aaa-aaaaa-qaabq-cai.localhost:4943/",
onSuccess: async () => {
const identity = authClient.getIdentity();
console.log('authClient.getIdentity():', identity);
const principal = identity.getPrincipal();
this.principal = principal;
this.identity = identity;
}
})
},
async transfer() {
console.log('mtlk Matiki here 0');
console.log('identity:', this.identity);
console.log('identity principal:', this.identity?.getPrincipal());
console.log('identity principal toText:', this.identity?.getPrincipal().toText());
console.log('mtlk Matiki here 2');
if (!this.identity) {
throw new Error("mtlk No identity")
}
const identity = this.identity;
const AUTH_PROVIDER_URL = 'https://icp-api.io';
console.log('identity principal toText:', identity.getPrincipal().toText());
console.log('mtlk Matiki here 2f');
console.log('identity:', identity);
console.log('host:', AUTH_PROVIDER_URL);
console.log('dfx netowrk:', process.env.DFX_NETWORK);
console.log('fetchRootKey:', process.env.DFX_NETWORK === "local");
console.log('canisterId:', canisterId);
const agent = await createAgent({
identity: identity,
host: AUTH_PROVIDER_URL,
fetchRootKey: process.env.DFX_NETWORK === "local",
});
console.log('agent:', agent);
console.log('mtlk Matiki here 3');
console.log('icrc1_ledger_canister_id :', LEDGER_CANISTER_ID);
// Create an instance of the icrc1_ledger_canister
const icrc1_ledger_canister = Actor.createActor(icrc1_ledger_canister_idl, { agent, canisterId: LEDGER_CANISTER_ID });
console.log('mtlk Matiki here 4');
console.log('icrc1_ledger_canister:', icrc1_ledger_canister);
// Now you can call functions on the icrc1_ledger_canister instance. For example:
console.log('mtlk Matiki here 5');
const name = await icrc1_ledger_canister.icrc1_name();
console.log('icrc1_ledger_canister.icrc1_name():', name);
// //'icrc1_balance_of' : IDL.Func([Account], [Tokens], ['query']),
// balance = await this.actor.icrc1_balance_of({
// owner: Principal.fromText(address),
const identity_balance = await icrc1_ledger_canister.icrc1_balance_of({
owner: Principal.fromText('zgcr3-w3e7h-6okfu-e5dke-k66xm-kadri-lvnuw-kipko-mysl7-r6p53-xae'), subaccount: [],
});
console.log('identity_balance:', identity_balance);
const metadata = await icrc1_ledger_canister.icrc1_metadata();
console.log('metadata:', metadata);
const approveResult = await icrc1_ledger_canister.icrc2_approve({
fee: [],
memo: [],
from_subaccount: [],
created_at_time: [],
amount: BigInt(3),
expected_allowance: [],
expires_at: [],
spender: {
owner: Principal.fromText('dxzul-dqaaa-aaaan-qmvuq-cai'),
subaccount: []
}
});
console.log('approveResult:', approveResult);
console.log('mtlk Matiki here 5f');
...
I’m logging in with Internet Identity
‘zgcr3-w3e7h-6okfu-e5dke-k66xm-kadri-lvnuw-kipko-mysl7-r6p53-xae’ is my principal in nns-app
But I get another principal here and my approve is not successful
App.vue:82 authClient.getIdentity(): Pu {_inner: Ms, _delegation: ks}
App.vue:87 Zalogowano Proxy(Yi) {_arr: Uint8Array(29), _isPrincipal: true}
App.vue:116 User data Proxy(Object) {nickname: 'qaqa', avatar_url: Array(0)}
App.vue:125 mtlk Matiki here 0
App.vue:128 identity: Proxy(Pu) {_inner: Ms, _delegation: ks, _principal: Yi}
App.vue:129 identity principal: Proxy(Yi) {_arr: Uint8Array(29), _isPrincipal: true}
App.vue:130 identity principal toText: 22hlo-vukku-nwwrv-hndxd-p6vwz-thrwl-udox2-gchyj-kbtpm-7watc-jqe
App.vue:132 mtlk Matiki here 2
App.vue:142 identity principal toText: 22hlo-vukku-nwwrv-hndxd-p6vwz-thrwl-udox2-gchyj-kbtpm-7watc-jqe
App.vue:144 mtlk Matiki here 2f
App.vue:145 identity: Proxy(Pu) {_inner: Ms, _delegation: ks, _principal: Yi}
App.vue:146 host: https://icp-api.io
App.vue:147 dfx netowrk: ic
App.vue:148 fetchRootKey: false
App.vue:150 canisterId: dxzul-dqaaa-aaaan-qmvuq-cai
App.vue:158 agent: yi {rootKey: ArrayBuffer(133), _timeDiffMsecs: 0, _rootKeyFetched: false, _isAgent: true, log: cm, …}
App.vue:159 mtlk Matiki here 3
App.vue:162 icrc1_ledger_canister_id : ryjl3-tyaaa-aaaaa-aaaba-cai
App.vue:167 mtlk Matiki here 4
App.vue:168 icrc1_ledger_canister: i {get_blocks: ƒ, get_data_certificate: ƒ, get_transactions: ƒ, icrc1_balance_of: ƒ, icrc1_decimals: ƒ, …}
App.vue:172 mtlk Matiki here 5
App.vue:176 icrc1_ledger_canister.icrc1_name(): Internet Computer
App.vue:186 identity_balance: 600250000n
App.vue:189 metadata: (4) [Array(2), Array(2), Array(2), Array(2)]0: (2) ['icrc1:decimals', {…}]1: (2) ['icrc1:name', {…}]2: (2) ['icrc1:symbol', {…}]3: (2) ['icrc1:fee', {…}]length: 4[[Prototype]]: Array(0)
App.vue:226 approveResult: {Err: {…}}Err: {InsufficientFunds: {…}}InsufficientFunds: {balance: 0n}[[Prototype]]: Object[[Prototype]]: Object
App.vue:228 mtlk Matiki here 5f
Matiki