Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ICRC-2 #24

Merged
merged 3 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions common/src/account_transfer_approvals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use crate::{AHashMap, IcrcCompatibleAccount, TokenAmountE9s, LABEL_DC_TOKEN_APPROVAL};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use borsh::{BorshDeserialize, BorshSerialize};
use icrc_ledger_types::{icrc1::account::Account, icrc2::allowance::Allowance};
use ledger_map::{LedgerError, LedgerMap};
use sha2::Digest;

thread_local! {
static APPROVALS: std::cell::RefCell<AHashMap<(Account, Account), Allowance>> = std::cell::RefCell::new(AHashMap::default());
}

pub fn approval_update(account: Account, spender: Account, allowance: Allowance) {
APPROVALS.with(|approvals| {
let mut approvals = approvals.borrow_mut();
if allowance.allowance > 0u32 {
approvals.insert((account, spender), allowance);
} else {
approvals.remove(&(account, spender));
}
})
}

pub fn approval_get(account: Account, spender: Account) -> Option<Allowance> {
APPROVALS.with(|approvals| {
let approvals = approvals.borrow();
approvals.get(&(account, spender)).cloned()
})
}

#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct FundsTransferApprovalV1 {
approver: IcrcCompatibleAccount,
spender: IcrcCompatibleAccount,
allowance: TokenAmountE9s,
expires_at: Option<u64>,
fee: TokenAmountE9s,
memo: Vec<u8>,
}

impl FundsTransferApprovalV1 {
pub fn new(
approver: IcrcCompatibleAccount,
spender: IcrcCompatibleAccount,
allowance: TokenAmountE9s,
expires_at: Option<u64>,
fee: TokenAmountE9s,
memo: Vec<u8>,
) -> Self {
Self {
approver,
spender,
allowance,
expires_at,
fee,
memo,
}
}

pub fn to_tx_id(&self) -> [u8; 32] {
let mut hasher = sha2::Sha256::new();
hasher.update(borsh::to_vec(self).unwrap());
let result = hasher.finalize();
let mut tx_id = [0u8; 32];
tx_id.copy_from_slice(&result[..32]);
tx_id
}

pub fn spender(&self) -> &IcrcCompatibleAccount {
&self.spender
}

pub fn approver(&self) -> &IcrcCompatibleAccount {
&self.approver
}

pub fn allowance(&self) -> Allowance {
Allowance {
allowance: self.allowance.into(),
expires_at: self.expires_at,
}
}
}

impl std::fmt::Display for FundsTransferApprovalV1 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"FundsTransferApprovalV1 {{ approver={} spender={} allowance={} expires_at={:?} fee={} memo={} }}",
self.spender(),
self.approver(),
self.allowance,
self.expires_at,
self.fee,
match String::try_from_slice(&self.memo) {
Ok(memo) => memo,
Err(_) => BASE64.encode(&self.memo),
},
)
}
}

#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)]
pub enum FundsTransferApproval {
V1(FundsTransferApprovalV1),
}

impl FundsTransferApproval {
pub fn new(
approver: IcrcCompatibleAccount,
spender: IcrcCompatibleAccount,
allowance: TokenAmountE9s,
expires_at: Option<u64>,
fee: TokenAmountE9s,
memo: Vec<u8>,
) -> Self {
Self::V1(FundsTransferApprovalV1 {
approver,
spender,
allowance,
expires_at,
fee,
memo,
})
}

pub fn to_tx_id(&self) -> [u8; 32] {
match self {
FundsTransferApproval::V1(v1) => v1.to_tx_id(),
}
}

pub fn deserialize(bytes: &[u8]) -> Result<Self, std::io::Error> {
borsh::from_slice(bytes)
}

pub fn add_to_ledger(&self, ledger: &mut LedgerMap) -> Result<(), LedgerError> {
ledger.upsert(
LABEL_DC_TOKEN_APPROVAL,
self.to_tx_id(),
borsh::to_vec(self).unwrap(),
)
}

pub fn approver(&self) -> &IcrcCompatibleAccount {
match self {
FundsTransferApproval::V1(v1) => v1.approver(),
}
}

pub fn spender(&self) -> &IcrcCompatibleAccount {
match self {
FundsTransferApproval::V1(v1) => v1.spender(),
}
}

pub fn allowance(&self) -> Allowance {
match self {
FundsTransferApproval::V1(v1) => v1.allowance(),
}
}
}

impl std::fmt::Display for FundsTransferApproval {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FundsTransferApproval::V1(v1) => write!(f, "{}", v1),
}
}
}

#[allow(clippy::too_many_arguments)]
pub fn ledger_funds_transfer_approve(
ledger: &mut LedgerMap,
approver: &IcrcCompatibleAccount,
spender: &IcrcCompatibleAccount,
allowance: TokenAmountE9s,
expires_at: Option<u64>,
fee: TokenAmountE9s,
memo: Vec<u8>,
) -> Result<(), LedgerError> {
let approval = FundsTransferApproval::new(
approver.clone(),
spender.clone(),
allowance,
expires_at,
fee,
memo,
);
approval.add_to_ledger(ledger)
}
13 changes: 6 additions & 7 deletions common/src/account_transfers.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
use crate::{
account_balance_add, account_balance_get, account_balance_sub, amount_as_string,
get_pubkey_from_principal, get_timestamp_ns, ledger_add_reputation_change,
slice_to_32_bytes_array, DccIdentity, TokenAmountE9s, TransferError, LABEL_DC_TOKEN_TRANSFER,
MINTING_ACCOUNT, MINTING_ACCOUNT_PRINCIPAL,
};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use borsh::{BorshDeserialize, BorshSerialize};
Expand All @@ -13,13 +19,6 @@ use ledger_map::{info, LedgerMap};
use sha2::{Digest, Sha256};
use std::cell::RefCell;

use crate::{
account_balance_add, account_balance_get, account_balance_sub, amount_as_string,
get_pubkey_from_principal, get_timestamp_ns, ledger_add_reputation_change,
slice_to_32_bytes_array, DccIdentity, TokenAmountE9s, TransferError, LABEL_DC_TOKEN_TRANSFER,
MINTING_ACCOUNT, MINTING_ACCOUNT_PRINCIPAL,
};

thread_local! {
static FEES_SINK_ACCOUNTS: RefCell<Option<Vec<IcrcCompatibleAccount>>> = const { RefCell::new(None) };
}
Expand Down
19 changes: 16 additions & 3 deletions common/src/ledger_refresh.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::account_transfer_approvals::{approval_update, FundsTransferApproval};
use crate::account_transfers::FundsTransfer;
use crate::cache_transactions::RecentCache;
use crate::{
Expand All @@ -6,9 +7,9 @@ use crate::{
reputations_apply_changes, reputations_clear, set_num_providers, set_num_users,
set_offering_num_per_provider, AHashMap, ContractSignRequest, ContractSignRequestPayload,
ReputationAge, ReputationChange, UpdateOfferingPayload, CACHE_TXS_NUM_COMMITTED,
LABEL_CONTRACT_SIGN_REPLY, LABEL_CONTRACT_SIGN_REQUEST, LABEL_DC_TOKEN_TRANSFER,
LABEL_NP_OFFERING, LABEL_NP_REGISTER, LABEL_REPUTATION_AGE, LABEL_REPUTATION_CHANGE,
LABEL_USER_REGISTER, PRINCIPAL_MAP,
LABEL_CONTRACT_SIGN_REPLY, LABEL_CONTRACT_SIGN_REQUEST, LABEL_DC_TOKEN_APPROVAL,
LABEL_DC_TOKEN_TRANSFER, LABEL_NP_OFFERING, LABEL_NP_REGISTER, LABEL_REPUTATION_AGE,
LABEL_REPUTATION_CHANGE, LABEL_USER_REGISTER, PRINCIPAL_MAP,
};
use borsh::BorshDeserialize;
use candid::Principal;
Expand Down Expand Up @@ -75,6 +76,18 @@ pub fn refresh_caches_from_ledger(ledger: &LedgerMap) -> anyhow::Result<()> {
RecentCache::add_entry(num_txs, transfer.into());
num_txs += 1;
}
LABEL_DC_TOKEN_APPROVAL => {
let approval =
FundsTransferApproval::deserialize(entry.value()).map_err(|e| {
error!("Failed to deserialize approval {:?} ==> {:?}", entry, e);
e
})?;
approval_update(
approval.approver().into(),
approval.spender().into(),
approval.allowance(),
);
}
LABEL_NP_REGISTER | LABEL_USER_REGISTER => {
if let Ok(dcc_identity) =
dcc_identity::DccIdentity::new_verifying_from_bytes(entry.key())
Expand Down
3 changes: 3 additions & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#[cfg(target_arch = "wasm32")]
#[allow(unused_imports)]
use ic_cdk::println;
pub mod account_transfer_approvals;
pub mod account_transfers;
pub mod account_transfers_errors;
pub mod cache_balances;
Expand All @@ -17,6 +18,7 @@ pub mod profiles;
pub mod registration;
pub mod rewards;

pub use account_transfer_approvals::*;
pub use account_transfers::*;
pub use account_transfers_errors::TransferError;
pub use cache_balances::*;
Expand Down Expand Up @@ -84,6 +86,7 @@ pub const ED25519_SIGNATURE_LENGTH: usize = 64;
pub const ED25519_SIGN_CONTEXT: &[u8] = b"decent-cloud";
pub const FETCH_SIZE_BYTES_DEFAULT: u64 = 1024 * 1024;
pub const KEY_LAST_REWARD_DISTRIBUTION_TS: &[u8] = b"LastRewardNs";
pub const LABEL_DC_TOKEN_APPROVAL: &str = "DCTokenApproval";
pub const LABEL_DC_TOKEN_TRANSFER: &str = "DCTokenTransfer";
pub const LABEL_NP_CHECK_IN: &str = "NPCheckIn";
pub const LABEL_NP_OFFERING: &str = "NPOffering";
Expand Down
62 changes: 62 additions & 0 deletions ic-canister/decent_cloud.did
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,63 @@ type TransferError = variant {
GenericError : record { error_code : nat; message : text };
};

type ApproveArgs = record {
from_subaccount : opt blob;
spender : Account;
amount : nat;
expected_allowance : opt nat;
expires_at : opt nat64;
fee : opt nat;
memo : opt blob;
created_at_time : opt nat64;
};

type ApproveError = variant {
BadFee : record { expected_fee : nat };
InsufficientFunds : record { balance : nat };
AllowanceChanged : record { current_allowance : nat };
Expired : record { ledger_time : Timestamp };
TooOld;
CreatedInFuture : record { ledger_time : Timestamp };
Duplicate : record { duplicate_of : nat };
TemporarilyUnavailable;
GenericError : record { error_code : nat; message : text };
};

type TransferFromError = variant {
BadFee : record { expected_fee : nat };
BadBurn : record { min_burn_amount : nat };
// The [from] account does not hold enough funds for the transfer.
InsufficientFunds : record { balance : nat };
// The caller exceeded its allowance.
InsufficientAllowance : record { allowance : nat };
TooOld;
CreatedInFuture: record { ledger_time : nat64 };
Duplicate : record { duplicate_of : nat };
TemporarilyUnavailable;
GenericError : record { error_code : nat; message : text };
};

type TransferFromArgs = record {
spender_subaccount : opt blob;
from : Account;
to : Account;
amount : nat;
fee : opt nat;
memo : opt blob;
created_at_time : opt nat64;
};

type AllowanceArgs = record {
account : Account;
spender : Account;
};

type Allowance = record {
allowance : nat;
expires_at : opt Timestamp;
};

type MetadataValue = variant {
Nat : nat;
Int : int;
Expand Down Expand Up @@ -355,6 +412,11 @@ type OfferingEntry = record {
};

service : {
// ICRC-2 endpoints
icrc2_approve : (ApproveArgs) -> (variant { Ok : nat; Err : ApproveError });
icrc2_transfer_from : (TransferFromArgs) -> (variant { Ok : nat; Err : TransferFromError });
icrc2_allowance : (AllowanceArgs) -> (Allowance) query;

// Node Provider (NP) management operations
// crypto_sig can be used to cryptographically verify the authenticity of the message
node_provider_register: (pubkey_bytes: vec nat8, crypto_sig: vec nat8) -> (ResultString);
Expand Down
4 changes: 4 additions & 0 deletions ic-canister/src/canister_backend/icrc1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ pub fn _icrc1_supported_standards() -> Vec<Icrc1StandardRecord> {
name: "ICRC-1".to_string(),
url: "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1".to_string(),
},
Icrc1StandardRecord {
name: "ICRC-2".to_string(),
url: "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-2".to_string(),
},
// Icrc1StandardRecord {
// name: "ICRC-3".to_string(),
// url: "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-3".to_string(),
Expand Down
Loading
Loading