diff --git a/cycles-ledger/src/endpoints.rs b/cycles-ledger/src/endpoints.rs index d284532..d6e9de9 100644 --- a/cycles-ledger/src/endpoints.rs +++ b/cycles-ledger/src/endpoints.rs @@ -11,6 +11,8 @@ pub type NumCycles = Nat; pub struct DepositArg { pub to: Account, pub memo: Option, + #[serde(default)] + pub created_at_time: Option, } #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] diff --git a/cycles-ledger/src/main.rs b/cycles-ledger/src/main.rs index 46b35ec..af98432 100644 --- a/cycles-ledger/src/main.rs +++ b/cycles-ledger/src/main.rs @@ -113,8 +113,13 @@ fn deposit(arg: endpoints::DepositArg) -> endpoints::DepositResult { ic_cdk::trap("deposit amount is insufficient"); } let memo = validate_memo(arg.memo); - let (txid, balance, _phash) = - storage::record_deposit(&arg.to, amount, memo, ic_cdk::api::time()); + let (txid, balance, _phash) = storage::record_deposit( + &arg.to, + amount, + memo, + ic_cdk::api::time(), + arg.created_at_time, + ); // TODO(FI-766): set the certified variable. @@ -178,6 +183,7 @@ fn icrc1_transfer(args: TransferArg) -> Result { amount, fee: config::FEE, memo, + created_at_time: args.created_at_time }; deduplicate(args.created_at_time, operation.hash(), now)?; @@ -274,6 +280,7 @@ async fn send(args: endpoints::SendArg) -> Result { amount, fee: config::FEE, memo: memo.clone(), + created_at_time: args.created_at_time }; let now = ic_cdk::api::time(); deduplicate(args.created_at_time, operation.hash(), now) @@ -302,7 +309,8 @@ async fn send(args: endpoints::SendArg) -> Result { }, )) } else { - let (send, _send_hash) = storage::send(&from, amount, memo, now); + let now = ic_cdk::api::time(); + let (send, _send_hash) = storage::send(&from, amount, memo, now, args.created_at_time); Ok(send) } } diff --git a/cycles-ledger/src/storage.rs b/cycles-ledger/src/storage.rs index 92fe39c..b5fc8f1 100644 --- a/cycles-ledger/src/storage.rs +++ b/cycles-ledger/src/storage.rs @@ -43,6 +43,10 @@ pub enum Operation { fee: u128, #[serde(skip_serializing_if = "Option::is_none")] memo: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "ts")] + created_at_time: Option, }, Transfer { #[serde(with = "compact_account")] @@ -54,6 +58,10 @@ pub enum Operation { fee: u128, #[serde(skip_serializing_if = "Option::is_none")] memo: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "ts")] + created_at_time: Option, }, Burn { #[serde(with = "compact_account")] @@ -63,6 +71,10 @@ pub enum Operation { fee: u128, #[serde(skip_serializing_if = "Option::is_none")] memo: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "ts")] + created_at_time: Option, }, } @@ -221,6 +233,7 @@ pub fn record_deposit( amount: u128, memo: Option, now: u64, + created_at_time: Option, ) -> (u64, u128, Hash) { assert!(amount >= crate::config::FEE); @@ -236,6 +249,7 @@ pub fn record_deposit( amount, memo, fee: crate::config::FEE, + created_at_time, }, timestamp: now, phash, @@ -293,6 +307,7 @@ pub fn transfer(operation: Operation, now: u64) -> (u64, Hash) { amount, fee, memo: _, + created_at_time:_ } => { let from_key = to_account_key(&from); let to_key = to_account_key(&to); @@ -349,6 +364,7 @@ pub fn penalize(from: &Account, now: u64) -> (BlockIndex, Hash) { amount: 0, memo: None, fee: crate::config::FEE, + created_at_time: None, }, timestamp: now, phash, @@ -357,7 +373,13 @@ pub fn penalize(from: &Account, now: u64) -> (BlockIndex, Hash) { }) } -pub fn send(from: &Account, amount: u128, memo: Option, now: u64) -> (BlockIndex, Hash) { +pub fn send( + from: &Account, + amount: u128, + memo: Option, + now: u64, + created_at_time: Option, +) -> (BlockIndex, Hash) { let from_key = to_account_key(from); mutate_state(|s| { @@ -379,6 +401,7 @@ pub fn send(from: &Account, amount: u128, memo: Option, now: u64) -> (Bloc amount, memo, fee: crate::config::FEE, + created_at_time, }, timestamp: now, phash, @@ -411,6 +434,7 @@ mod tests { amount: u128::MAX, fee: 10_000, memo: Some(Memo::default()), + created_at_time: None, }, timestamp: 1691065957, phash: None, diff --git a/cycles-ledger/tests/tests.rs b/cycles-ledger/tests/tests.rs index cf985da..a18a617 100644 --- a/cycles-ledger/tests/tests.rs +++ b/cycles-ledger/tests/tests.rs @@ -229,10 +229,10 @@ fn test_send_flow() { // send cycles from subaccount with created_at_time set let now = env - .time() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_nanos() as u64; + .time() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; let send_receiver_balance = env.cycle_balance(send_receiver); let send_amount = 300_000_000_u128; let _send_idx = send( @@ -486,7 +486,7 @@ fn test_transfer() { let fee = fee(env, ledger_id); let transfer_amount = Nat::from(100_000); - let idx = transfer( + transfer( env, ledger_id, user1, @@ -550,7 +550,7 @@ fn test_transfer() { ); // Should not be able commit a transaction that was created in the future - let now = env + let mut now = env .time() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() @@ -573,9 +573,66 @@ fn test_transfer() { .unwrap_err() ); - // When making the same transaction again + // Should be able to make a transfer when created time is valid + let tx: Nat= transfer( + env, + ledger_id, + user1, + TransferArg { + from_subaccount: None, + to: user2, + fee: None, + created_at_time: Some(now), + memo: None, + amount: transfer_amount.clone(), + }, + ) + .unwrap(); + + // Should not be able send the same transfer twice if created_at_time is set assert_eq!( - TransferError::Duplicate { duplicate_of: idx }, + TransferError::Duplicate { duplicate_of: tx}, + transfer( + env, + ledger_id, + user1, + TransferArg { + from_subaccount: None, + to: user2, + fee: None, + created_at_time: Some(now), + memo: None, + amount: transfer_amount.clone(), + }, + ) + .unwrap_err() + ); + + // Setting a different memo field should result in no deduplication + transfer( + env, + ledger_id, + user1, + TransferArg { + from_subaccount: None, + to: user2, + fee: None, + created_at_time: Some(now), + memo: Some(Memo(ByteBuf::from(b"1234".to_vec()))), + amount: transfer_amount.clone(), + }, + ) + .unwrap(); + + // Advance time so that the deduplication window is over + env.advance_time(config::TRANSACTION_WINDOW*2); + now = env + .time() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + + // Now the transfer which was deduplicated previously should be ok transfer( env, ledger_id, @@ -589,5 +646,6 @@ fn test_transfer() { amount: transfer_amount.clone(), }, ) - .unwrap_err()); + .unwrap(); + } diff --git a/depositor/src/main.rs b/depositor/src/main.rs index 366c008..081b29c 100644 --- a/depositor/src/main.rs +++ b/depositor/src/main.rs @@ -35,6 +35,7 @@ async fn deposit(arg: DepositArg) -> DepositResult { let arg = cycles_ledger::endpoints::DepositArg { to: arg.to, memo: arg.memo, + created_at_time: Some(ic_cdk::api::time()), }; let (result,): (DepositResult,) = call_with_payment128(ledger_id, "deposit", (arg,), cycles) .await