Skip to content

Commit

Permalink
feat: SDK-1162 dfx cycles transfer (to account) (#3418)
Browse files Browse the repository at this point in the history
Adds `dfx cycles transfer --to-owner <principal>` to transfer from one account to another account.  Transfer from account to canister will follow.

Fixes https://dfinity.atlassian.net/browse/SDK-1162
  • Loading branch information
ericswanson-dfinity authored Oct 13, 2023
1 parent 2382c08 commit 66197c7
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 8 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

# UNRELEASED

### feat: added `cycles balance` command
### feat: added `dfx cycles` command

This won't work on mainnet yet, but it can work locally after installing the cycles ledger.
This won't work on mainnet yet, but can work locally after installing the cycles ledger.

Added the following subcommands:
- `dfx cycles balance`
- `dfx cycles transfer --to-owner <principal>` (transfer from one account to another account)

## Dependencies

Expand Down
48 changes: 47 additions & 1 deletion docs/cli-reference/dfx-cycles.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ The following subcommands are available:

| Command | Description |
|---------------------------------------|--------------------------------------------------------------------------------------|
| [`balance`](#dfx-ledger-balance) | Prints the account balance of the user. |
| [`balance`](#dfx-cycles-balance) | Prints the account balance of the user. |
| [`transfer`](#dfx-cycles-transfer) | Send cycles to another account. |
| `help` | Displays usage information message for a specified subcommand. |

To view usage information for a specific subcommand, specify the subcommand and the `--help` flag. For example, to see usage information for `dfx cycles balance`, you can run the following command:
Expand Down Expand Up @@ -65,3 +66,48 @@ You can use the `dfx cycles balance` command to check the balance of another pri
dfx cycles balance --owner raxcz-bidhr-evrzj-qyivt-nht5a-eltcc-24qfc-o6cvi-hfw7j-dcecz-kae --network ic
```

## dfx cycles transfer

Use the `dfx cycles transfer` command to transfer cycles from your account to another account.

### Basic usage

``` bash
dfx cycles transfer [options] <amount>
```

### Arguments

You must specify the following argument for the `dfx cycles transfer` command.

| Argument | Description |
|--------------|-----------------------------------|
| `<amount>` | The number of cycles to transfer. |

### Options

You can specify the following options for the `dfx cycles transfer` command.

| Option | Description |
|----------------------------------|--------------------------------------------------------------------|
| `--to-owner <principal>` | The principal of the account to which you want to transfer cycles. |
| `--to-subaccount <subaccount>` | The subaccount to which you want to transfer cycles. |
| `--from-subaccount <subaccount>` | The subaccount from which you want to transfer cycles. |
| `--fee <fee>` | Specifies a transaction fee. |
| `--memo <memo>` | Specifies a numeric memo for this transaction. |
| `--created-at-time <timestamp>` | Specify the timestamp-nanoseconds for the `created_at_time` field on the transfer request. Useful for controlling transaction-de-duplication. https://internetcomputer.org/docs/current/developer-docs/integrations/icrc-1/#transaction-deduplication- |

### Examples

Transfer 1 billion cycles to another account:

``` bash
dfx cycles transfer 1000000000 --to-owner raxcz-bidhr-evrzj-qyivt-nht5a-eltcc-24qfc-o6cvi-hfw7j-dcecz-kae --network ic
```

Transfer from a subaccount:

``` bash
dfx cycles transfer 1000000000 --from-subaccount 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --to-owner raxcz-bidhr-evrzj-qyivt-nht5a-eltcc-24qfc-o6cvi-hfw7j-dcecz-kae --network ic
```

149 changes: 145 additions & 4 deletions e2e/tests-dfx/cycles-ledger.bash
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ teardown() {
standard_teardown
}

@test "cycles ledger balance" {
current_time_nanoseconds() {
echo "$(date +%s)"000000000
}

@test "balance" {
ALICE=$(dfx identity get-principal --identity alice)
ALICE_SUBACCT1="000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
ALICE_SUBACCT1_CANDID="\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f"
Expand Down Expand Up @@ -99,7 +103,144 @@ teardown() {
assert_eq "2.900 TC (trillion cycles)."
}

@test "cycles ledger howto" {
@test "transfer" {
ALICE=$(dfx identity get-principal --identity alice)
ALICE_SUBACCT1="000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
ALICE_SUBACCT1_CANDID="\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f"
ALICE_SUBACCT2="9C9B9A030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
ALICE_SUBACCT2_CANDID="\9C\9B\9A\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f"
BOB=$(dfx identity get-principal --identity bob)
BOB_SUBACCT1="7C7B7A030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"

assert_command dfx deploy cycles-ledger
assert_command dfx deploy cycles-depositor --argument "(record {ledger_id = principal \"$(dfx canister id cycles-ledger)\"})" --with-cycles 10000000000000

assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\";};cycles = 3_000_000_000_000;})" --identity cycle-giver
assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\"; subaccount = opt blob \"$ALICE_SUBACCT1_CANDID\"};cycles = 2_000_000_000_000;})" --identity cycle-giver
assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\"; subaccount = opt blob \"$ALICE_SUBACCT2_CANDID\"};cycles = 1_000_000_000_000;})" --identity cycle-giver

# account to account
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice
assert_eq "3000000000000 cycles."
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob
assert_eq "0 cycles."

assert_command dfx cycles transfer 100000 --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --to-owner "$BOB"
assert_eq "Transfer sent at block index 3"

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice
assert_eq "2999899900000 cycles."

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob
assert_eq "100000 cycles."

# account to subaccount
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice
assert_eq "2999899900000 cycles."
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob --subaccount "$BOB_SUBACCT1"
assert_eq "0 cycles."

assert_command dfx cycles transfer 100000 --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --to-owner "$BOB" --to-subaccount "$BOB_SUBACCT1"
assert_eq "Transfer sent at block index 4"

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice
assert_eq "2999799800000 cycles."

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob --subaccount "$BOB_SUBACCT1"
assert_eq "100000 cycles."


# subaccount to account
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice --subaccount "$ALICE_SUBACCT2"
assert_eq "1000000000000 cycles."
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob
assert_eq "100000 cycles."

# assert_command dfx canister call cycles-ledger icrc1_transfer "(record {to = record{owner = principal \"$BOB\"}; amount = 100_000;})" --identity alice
assert_command dfx cycles transfer 700000 --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --to-owner "$BOB" --from-subaccount "$ALICE_SUBACCT2"

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice --subaccount "$ALICE_SUBACCT2"
assert_eq "999899300000 cycles."

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob
assert_eq "800000 cycles."


# subaccount to subaccount
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice --subaccount "$ALICE_SUBACCT2"
assert_eq "999899300000 cycles."
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob --subaccount "$BOB_SUBACCT1"
assert_eq "100000 cycles."

assert_command dfx cycles transfer 400000 --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --to-owner "$BOB" --to-subaccount "$BOB_SUBACCT1" --from-subaccount "$ALICE_SUBACCT2"
assert_eq "Transfer sent at block index 6"

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice --subaccount "$ALICE_SUBACCT2"
assert_eq "999798900000 cycles."

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob --subaccount "$BOB_SUBACCT1"
assert_eq "500000 cycles."
}

@test "transfer deduplication" {
ALICE=$(dfx identity get-principal --identity alice)
BOB=$(dfx identity get-principal --identity bob)

assert_command dfx deploy cycles-ledger
assert_command dfx deploy cycles-depositor --argument "(record {ledger_id = principal \"$(dfx canister id cycles-ledger)\"})" --with-cycles 10000000000000

assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\";};cycles = 3_000_000_000_000;})" --identity cycle-giver

assert_command dfx cycles balance --precise --identity alice --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "3000000000000 cycles."

assert_command dfx cycles balance --precise --identity bob --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "0 cycles."

t=$(current_time_nanoseconds)

assert_command dfx cycles transfer 100000 --created-at-time "$t" --memo 1 --identity alice --to-owner "$BOB" --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "Transfer sent at block index 1"

assert_command dfx cycles balance --precise --identity alice --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "2999899900000 cycles."

assert_command dfx cycles balance --precise --identity bob --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "100000 cycles."

# same memo and created-at-time: dupe
assert_command dfx cycles transfer 100000 --created-at-time "$t" --memo 1 --identity alice --to-owner "$BOB" --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_contains "transaction is a duplicate of another transaction in block 1"
assert_contains "Transfer sent at block index 1"

assert_command dfx cycles balance --precise --identity alice --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "2999899900000 cycles."

assert_command dfx cycles balance --precise --identity bob --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "100000 cycles."

# different memo and same created-at-time same: not dupe
assert_command dfx cycles transfer 100000 --created-at-time "$t" --memo 2 --identity alice --to-owner "$BOB" --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_contains "Transfer sent at block index 2"

assert_command dfx cycles balance --precise --identity alice --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "2999799800000 cycles."

assert_command dfx cycles balance --precise --identity bob --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "200000 cycles."

# same memo and different created-at-time same: not dupe
assert_command dfx cycles transfer 100000 --created-at-time $((t+1)) --memo 1 --identity alice --to-owner "$BOB" --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_contains "Transfer sent at block index 3"

assert_command dfx cycles balance --precise --identity alice --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "2999699700000 cycles."

assert_command dfx cycles balance --precise --identity bob --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)"
assert_eq "300000 cycles."
}

@test "howto" {
# This is the equivalent of https://www.notion.so/dfinityorg/How-to-install-and-test-the-cycles-ledger-521c9f3c410f4a438514a03e35464299
ALICE=$(dfx identity get-principal --identity alice)
BOB=$(dfx identity get-principal --identity bob)
Expand Down Expand Up @@ -131,8 +272,8 @@ teardown() {
assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --precise
assert_eq "500000000 cycles."

assert_command dfx canister call cycles-ledger icrc1_transfer "(record {to = record{owner = principal \"$BOB\"}; amount = 100_000;})" --identity alice
assert_eq "(variant { Ok = 1 : nat })"
assert_command dfx cycles transfer 100000 --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --to-owner "$BOB"
assert_eq "Transfer sent at block index 1"

assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --precise
assert_eq "399900000 cycles."
Expand Down
3 changes: 3 additions & 0 deletions src/dfx/src/commands/cycles/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use clap::Parser;
use tokio::runtime::Runtime;

mod balance;
mod transfer;

/// Helper commands to manage the user's cycles.
#[derive(Parser)]
Expand All @@ -21,6 +22,7 @@ pub struct CyclesOpts {
#[derive(Parser)]
enum SubCommand {
Balance(balance::CyclesBalanceOpts),
Transfer(transfer::TransferOpts),
}

pub fn exec(env: &dyn Environment, opts: CyclesOpts) -> DfxResult {
Expand All @@ -29,6 +31,7 @@ pub fn exec(env: &dyn Environment, opts: CyclesOpts) -> DfxResult {
runtime.block_on(async {
match opts.subcmd {
SubCommand::Balance(v) => balance::exec(&agent_env, v).await,
SubCommand::Transfer(v) => transfer::exec(&agent_env, v).await,
}
})
}
96 changes: 96 additions & 0 deletions src/dfx/src/commands/cycles/transfer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
use crate::lib::nns_types::account_identifier::Subaccount;
use crate::lib::operations::cycles_ledger;
use crate::lib::root_key::fetch_root_key_if_needed;
use crate::util::clap::parsers::cycle_amount_parser;
use anyhow::Context;
use candid::Principal;
use clap::{ArgGroup, Parser};
use std::time::{SystemTime, UNIX_EPOCH};

/// Transfer cycles to another principal.
#[derive(Parser)]
#[clap(
group(ArgGroup::new("target").multiple(false).required(true)),
)]
pub struct TransferOpts {
/// The amount of cycles to send.
#[arg(value_parser = cycle_amount_parser)]
amount: u128,

/// Transfer cycles from this subaccount.
#[arg(long)]
from_subaccount: Option<Subaccount>,

/// Transfer cycles to this principal.
#[arg(long, group = "target")]
to_owner: Option<Principal>,

/// Transfer cycles to this subaccount.
#[arg(long, requires("to_owner"))]
to_subaccount: Option<Subaccount>,

/// Transaction timestamp, in nanoseconds, for use in controlling transaction-deduplication, default is system-time.
/// https://internetcomputer.org/docs/current/developer-docs/integrations/icrc-1/#transaction-deduplication-
#[arg(long)]
created_at_time: Option<u64>,

/// Transfer fee.
#[arg(long, value_parser = cycle_amount_parser)]
fee: Option<u128>,

/// Memo.
#[arg(long)]
memo: Option<u64>,

/// Canister ID of the cycles ledger canister.
/// If not specified, the default cycles ledger canister ID will be used.
// todo: remove this. See https://dfinity.atlassian.net/browse/SDK-1262
#[arg(long)]
cycles_ledger_canister_id: Principal,
}

pub async fn exec(env: &dyn Environment, opts: TransferOpts) -> DfxResult {
let agent = env.get_agent();

let amount = opts.amount;

fetch_root_key_if_needed(env).await?;

let created_at_time = opts.created_at_time.unwrap_or(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64,
);

let block_index = if let Some(to_owner) = opts.to_owner {
let from_subaccount = opts.from_subaccount.map(|x| x.0);
let to_subaccount = opts.to_subaccount.map(|x| x.0);
cycles_ledger::transfer(
agent,
amount,
from_subaccount,
to_owner,
to_subaccount,
created_at_time,
opts.fee,
opts.memo,
opts.cycles_ledger_canister_id,
)
.await
.with_context(|| {
format!(
"If you retry this operation, use --created-at-time {}",
created_at_time
)
})?
} else {
unreachable!();
};

println!("Transfer sent at block index {block_index}");

Ok(())
}
Loading

0 comments on commit 66197c7

Please sign in to comment.