#![warn(
elided_lifetimes_in_paths,
missing_debug_implementations,
missing_docs,
unsafe_op_in_unsafe_fn,
clippy::undocumented_unsafe_blocks,
clippy::missing_safety_doc
)]
use std::convert::TryFrom;
use std::fmt::{self, Display, Formatter};
use std::ops::{Add, AddAssign, Sub, SubAssign};
use candid::{CandidType, Principal, types::reference::Func};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use sha2::Digest;
use ic_cdk::call::{Call, CallResult};
pub const DEFAULT_SUBACCOUNT: Subaccount = Subaccount([0; 32]);
pub const DEFAULT_FEE: Tokens = Tokens { e8s: 10_000 };
pub const MAINNET_LEDGER_CANISTER_ID: Principal =
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01]);
pub const MAINNET_GOVERNANCE_CANISTER_ID: Principal =
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01]);
pub const MAINNET_CYCLES_MINTING_CANISTER_ID: Principal =
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01]);
#[derive(
CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
)]
pub struct Timestamp {
pub timestamp_nanos: u64,
}
#[derive(
CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
)]
pub struct Tokens {
e8s: u64,
}
impl Tokens {
pub const MAX: Self = Tokens { e8s: u64::MAX };
pub const ZERO: Self = Tokens { e8s: 0 };
pub const SUBDIVIDABLE_BY: u64 = 100_000_000;
pub const fn from_e8s(e8s: u64) -> Self {
Self { e8s }
}
pub const fn e8s(&self) -> u64 {
self.e8s
}
}
impl Add for Tokens {
type Output = Self;
fn add(self, other: Self) -> Self {
let e8s = self.e8s.checked_add(other.e8s).unwrap_or_else(|| {
panic!(
"Add Tokens {} + {} failed because the underlying u64 overflowed",
self.e8s, other.e8s
)
});
Self { e8s }
}
}
impl AddAssign for Tokens {
fn add_assign(&mut self, other: Self) {
*self = *self + other;
}
}
impl Sub for Tokens {
type Output = Self;
fn sub(self, other: Self) -> Self {
let e8s = self.e8s.checked_sub(other.e8s).unwrap_or_else(|| {
panic!(
"Subtracting Tokens {} - {} failed because the underlying u64 underflowed",
self.e8s, other.e8s
)
});
Self { e8s }
}
}
impl SubAssign for Tokens {
fn sub_assign(&mut self, other: Self) {
*self = *self - other;
}
}
impl Display for Tokens {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{}.{:08}",
self.e8s / Tokens::SUBDIVIDABLE_BY,
self.e8s % Tokens::SUBDIVIDABLE_BY
)
}
}
#[derive(
CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
)]
pub struct Subaccount(pub [u8; 32]);
#[allow(clippy::range_plus_one)]
impl From<Principal> for Subaccount {
fn from(principal: Principal) -> Self {
let mut subaccount = [0; 32];
let principal = principal.as_slice();
subaccount[0] = principal.len().try_into().unwrap();
subaccount[1..1 + principal.len()].copy_from_slice(principal);
Subaccount(subaccount)
}
}
#[derive(
CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
)]
pub struct AccountIdentifier([u8; 32]);
impl AccountIdentifier {
pub fn new(owner: &Principal, subaccount: &Subaccount) -> Self {
let mut hasher = sha2::Sha224::new();
hasher.update(b"\x0Aaccount-id");
hasher.update(owner.as_slice());
hasher.update(&subaccount.0[..]);
let hash: [u8; 28] = hasher.finalize().into();
let mut hasher = crc32fast::Hasher::new();
hasher.update(&hash);
let crc32_bytes = hasher.finalize().to_be_bytes();
let mut result = [0u8; 32];
result[0..4].copy_from_slice(&crc32_bytes[..]);
result[4..32].copy_from_slice(hash.as_ref());
Self(result)
}
pub fn from_hex(hex_str: &str) -> Result<AccountIdentifier, String> {
let hex: Vec<u8> = hex::decode(hex_str).map_err(|e| e.to_string())?;
Self::from_slice(&hex[..]).map_err(|err| match err {
AccountIdParseError::InvalidLength(_) => format!(
"{} has a length of {} but we expected a length of 64 or 56",
hex_str,
hex_str.len()
),
AccountIdParseError::InvalidChecksum(err) => err.to_string(),
})
}
pub fn from_slice(v: &[u8]) -> Result<AccountIdentifier, AccountIdParseError> {
match v.try_into() {
Ok(h) => {
check_sum(h).map_err(AccountIdParseError::InvalidChecksum)
}
Err(_) => {
match <&[u8] as TryInto<[u8; 28]>>::try_into(v) {
Ok(hash) => AccountIdentifier::try_from(hash)
.map_err(|_| AccountIdParseError::InvalidLength(v.to_vec())),
Err(_) => Err(AccountIdParseError::InvalidLength(v.to_vec())),
}
}
}
}
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn generate_checksum(&self) -> [u8; 4] {
let mut hasher = crc32fast::Hasher::new();
hasher.update(&self.0[4..]);
hasher.finalize().to_be_bytes()
}
}
fn check_sum(hex: [u8; 32]) -> Result<AccountIdentifier, ChecksumError> {
let found_checksum = &hex[0..4];
let mut hasher = crc32fast::Hasher::new();
hasher.update(&hex[4..]);
let expected_checksum = hasher.finalize().to_be_bytes();
if expected_checksum == found_checksum {
Ok(AccountIdentifier(hex))
} else {
Err(ChecksumError {
input: hex,
expected_checksum,
found_checksum: found_checksum.try_into().unwrap(),
})
}
}
impl TryFrom<[u8; 32]> for AccountIdentifier {
type Error = String;
fn try_from(bytes: [u8; 32]) -> Result<Self, Self::Error> {
let hash = &bytes[4..];
let mut hasher = crc32fast::Hasher::new();
hasher.update(hash);
let crc32_bytes = hasher.finalize().to_be_bytes();
if bytes[0..4] == crc32_bytes[0..4] {
Ok(Self(bytes))
} else {
Err("CRC-32 checksum failed to verify".to_string())
}
}
}
impl TryFrom<[u8; 28]> for AccountIdentifier {
type Error = String;
fn try_from(bytes: [u8; 28]) -> Result<Self, Self::Error> {
let mut hasher = crc32fast::Hasher::new();
hasher.update(bytes.as_slice());
let crc32_bytes = hasher.finalize().to_be_bytes();
let mut aid_bytes = [0u8; 32];
aid_bytes[..4].copy_from_slice(&crc32_bytes[..4]);
aid_bytes[4..].copy_from_slice(&bytes[..]);
Ok(Self(aid_bytes))
}
}
impl AsRef<[u8]> for AccountIdentifier {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Display for AccountIdentifier {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex::encode(self.as_ref()))
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ChecksumError {
input: [u8; 32],
expected_checksum: [u8; 4],
found_checksum: [u8; 4],
}
impl Display for ChecksumError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Checksum failed for {}, expected check bytes {} but found {}",
hex::encode(&self.input[..]),
hex::encode(self.expected_checksum),
hex::encode(self.found_checksum),
)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum AccountIdParseError {
InvalidChecksum(ChecksumError),
InvalidLength(Vec<u8>),
}
impl Display for AccountIdParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidChecksum(err) => write!(f, "{err}"),
Self::InvalidLength(input) => write!(
f,
"Received an invalid AccountIdentifier with length {} bytes instead of the expected 28 or 32.",
input.len()
),
}
}
}
#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
pub struct AccountBalanceArgs {
pub account: AccountIdentifier,
}
#[derive(
CandidType, Serialize, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord,
)]
pub struct Memo(pub u64);
#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
pub struct TransferArgs {
pub memo: Memo,
pub amount: Tokens,
pub fee: Tokens,
pub from_subaccount: Option<Subaccount>,
pub to: AccountIdentifier,
pub created_at_time: Option<Timestamp>,
}
pub type BlockIndex = u64;
pub type TransferResult = Result<BlockIndex, TransferError>;
#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum TransferError {
BadFee {
expected_fee: Tokens,
},
InsufficientFunds {
balance: Tokens,
},
TxTooOld {
allowed_window_nanos: u64,
},
TxCreatedInFuture,
TxDuplicate {
duplicate_of: BlockIndex,
},
}
impl Display for TransferError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::BadFee { expected_fee } => {
write!(f, "transaction fee should be {expected_fee}")
}
Self::InsufficientFunds { balance } => {
write!(
f,
"the debit account doesn't have enough funds to complete the transaction, current balance: {balance}",
)
}
Self::TxTooOld {
allowed_window_nanos,
} => write!(
f,
"transaction is older than {} seconds",
allowed_window_nanos / 1_000_000_000
),
Self::TxCreatedInFuture => write!(f, "transaction's created_at_time is in future"),
Self::TxDuplicate { duplicate_of } => write!(
f,
"transaction is a duplicate of another transaction in block {duplicate_of}"
),
}
}
}
#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum Operation {
Mint {
to: AccountIdentifier,
amount: Tokens,
},
Burn {
from: AccountIdentifier,
amount: Tokens,
},
Transfer {
from: AccountIdentifier,
to: AccountIdentifier,
amount: Tokens,
fee: Tokens,
},
Approve {
from: AccountIdentifier,
spender: AccountIdentifier,
expires_at: Option<Timestamp>,
fee: Tokens,
},
TransferFrom {
from: AccountIdentifier,
to: AccountIdentifier,
spender: AccountIdentifier,
amount: Tokens,
fee: Tokens,
},
}
#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Transaction {
pub memo: Memo,
pub operation: Option<Operation>,
pub created_at_time: Timestamp,
pub icrc1_memo: Option<ByteBuf>,
}
#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Block {
pub parent_hash: Option<[u8; 32]>,
pub transaction: Transaction,
pub timestamp: Timestamp,
}
#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct GetBlocksArgs {
pub start: BlockIndex,
pub length: u64,
}
#[derive(CandidType, Deserialize, Clone, Debug)]
pub struct QueryBlocksResponse {
pub chain_length: u64,
pub certificate: Option<ByteBuf>,
pub blocks: Vec<Block>,
pub first_block_index: BlockIndex,
pub archived_blocks: Vec<ArchivedBlockRange>,
}
#[derive(CandidType, Deserialize, Clone, Debug)]
pub struct ArchivedBlockRange {
pub start: BlockIndex,
pub length: u64,
pub callback: QueryArchiveFn,
}
#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct BlockRange {
pub blocks: Vec<Block>,
}
pub type GetBlocksResult = Result<BlockRange, GetBlocksError>;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, CandidType)]
pub enum GetBlocksError {
BadFirstBlockIndex {
requested_index: BlockIndex,
first_valid_index: BlockIndex,
},
Other {
error_code: u64,
error_message: String,
},
}
impl Display for GetBlocksError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::BadFirstBlockIndex {
requested_index,
first_valid_index,
} => write!(
f,
"invalid first block index: requested block = {requested_index}, first valid block = {first_valid_index}"
),
Self::Other {
error_code,
error_message,
} => write!(
f,
"failed to query blocks (error code {error_code}): {error_message}"
),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(transparent)]
pub struct QueryArchiveFn(Func);
impl From<Func> for QueryArchiveFn {
fn from(func: Func) -> Self {
Self(func)
}
}
impl From<QueryArchiveFn> for Func {
fn from(query_func: QueryArchiveFn) -> Self {
query_func.0
}
}
impl CandidType for QueryArchiveFn {
fn _ty() -> candid::types::Type {
candid::func!((GetBlocksArgs) -> (GetBlocksResult) query)
}
fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
where
S: candid::types::Serializer,
{
Func::from(self.clone()).idl_serialize(serializer)
}
}
pub async fn account_balance(
ledger_canister_id: Principal,
args: &AccountBalanceArgs,
) -> CallResult<Tokens> {
Ok(Call::bounded_wait(ledger_canister_id, "account_balance")
.with_arg(args)
.await?
.candid()?)
}
pub async fn transfer(
ledger_canister_id: Principal,
args: &TransferArgs,
) -> CallResult<TransferResult> {
Ok(Call::bounded_wait(ledger_canister_id, "transfer")
.with_arg(args)
.await?
.candid()?)
}
#[derive(Serialize, Deserialize, CandidType, Clone, Hash, Debug, PartialEq, Eq)]
pub struct Symbol {
pub symbol: String,
}
pub async fn token_symbol(ledger_canister_id: Principal) -> CallResult<Symbol> {
Ok(Call::bounded_wait(ledger_canister_id, "token_symbol")
.await?
.candid()?)
}
pub async fn query_blocks(
ledger_canister_id: Principal,
args: &GetBlocksArgs,
) -> CallResult<QueryBlocksResponse> {
Ok(Call::bounded_wait(ledger_canister_id, "query_blocks")
.with_arg(args)
.await?
.candid()?)
}
pub async fn query_archived_blocks(
func: &QueryArchiveFn,
args: &GetBlocksArgs,
) -> CallResult<GetBlocksResult> {
Ok(Call::bounded_wait(func.0.principal, &func.0.method)
.with_arg(args)
.await?
.candid()?)
}
#[cfg(test)]
mod tests {
use std::string::ToString;
use super::*;
#[test]
fn test_account_id() {
assert_eq!(
"bdc4ee05d42cd0669786899f256c8fd7217fa71177bd1fa7b9534f568680a938".to_string(),
AccountIdentifier::new(
&Principal::from_text(
"iooej-vlrze-c5tme-tn7qt-vqe7z-7bsj5-ebxlc-hlzgs-lueo3-3yast-pae"
)
.unwrap(),
&DEFAULT_SUBACCOUNT,
)
.to_string()
);
}
#[test]
fn test_account_id_try_from() {
let mut bytes: [u8; 32] = [0; 32];
bytes.copy_from_slice(
&hex::decode("bdc4ee05d42cd0669786899f256c8fd7217fa71177bd1fa7b9534f568680a938")
.unwrap(),
);
assert!(AccountIdentifier::try_from(bytes).is_ok());
bytes[0] = 0;
assert!(AccountIdentifier::try_from(bytes).is_err());
}
#[test]
fn test_ledger_canister_id() {
assert_eq!(
MAINNET_LEDGER_CANISTER_ID,
Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap()
);
}
#[test]
fn test_governance_canister_id() {
assert_eq!(
MAINNET_GOVERNANCE_CANISTER_ID,
Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()
);
}
#[test]
fn test_cycles_minting_canister_id() {
assert_eq!(
MAINNET_CYCLES_MINTING_CANISTER_ID,
Principal::from_text("rkp4c-7iaaa-aaaaa-aaaca-cai").unwrap()
);
}
#[test]
fn principal_to_subaccount() {
let principal = Principal::from_text("4bkt6-4aaaa-aaaaf-aaaiq-cai").unwrap();
let subaccount = Subaccount::from(principal);
assert_eq!(
AccountIdentifier::new(&MAINNET_CYCLES_MINTING_CANISTER_ID, &subaccount).to_string(),
"d8646d1cbe44002026fa3e0d86d51a560b1c31d669bc8b7f66421c1b2feaa59f"
)
}
#[test]
fn check_hex_round_trip() {
let bytes: [u8; 32] = [
237, 196, 46, 168, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7,
];
let ai = AccountIdentifier::from_slice(bytes.as_ref())
.expect("Failed to create account identifier");
let res = ai.to_hex();
assert_eq!(
AccountIdentifier::from_hex(&res),
Ok(ai),
"The account identifier doesn't change after going back and forth between a string"
)
}
#[test]
fn check_bytes_round_trip() {
let bytes: [u8; 32] = [
237, 196, 46, 168, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7,
];
assert_eq!(
AccountIdentifier::from_slice(&bytes)
.expect("Failed to parse bytes as principal")
.as_bytes(),
&bytes,
"The account identifier doesn't change after going back and forth between a string"
)
}
#[test]
fn test_account_id_from_slice() {
let length_27 = b"123456789_123456789_1234567".to_vec();
assert_eq!(
AccountIdentifier::from_slice(&length_27),
Err(AccountIdParseError::InvalidLength(length_27))
);
let length_28 = b"123456789_123456789_12345678".to_vec();
assert_eq!(
AccountIdentifier::from_slice(&length_28),
Ok(AccountIdentifier::try_from(
<&[u8] as TryInto<[u8; 28]>>::try_into(&length_28).unwrap()
)
.unwrap())
);
let length_29 = b"123456789_123456789_123456789".to_vec();
assert_eq!(
AccountIdentifier::from_slice(&length_29),
Err(AccountIdParseError::InvalidLength(length_29))
);
let length_32 = [0; 32].to_vec();
assert_eq!(
AccountIdentifier::from_slice(&length_32),
Err(AccountIdParseError::InvalidChecksum(ChecksumError {
input: length_32.try_into().unwrap(),
expected_checksum: [128, 112, 119, 233],
found_checksum: [0, 0, 0, 0],
}))
);
let length_32 = [
128, 112, 119, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
]
.to_vec();
assert_eq!(
AccountIdentifier::from_slice(&length_32),
Ok(AccountIdentifier::try_from(
<&[u8] as TryInto<[u8; 28]>>::try_into(&[0u8; 28]).unwrap()
)
.unwrap())
);
}
#[test]
fn test_account_id_from_hex() {
let length_56 = "00000000000000000000000000000000000000000000000000000000";
let aid_bytes = [
128, 112, 119, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
assert_eq!(
AccountIdentifier::from_hex(length_56),
Ok(AccountIdentifier(aid_bytes))
);
let length_57 = "000000000000000000000000000000000000000000000000000000000";
assert!(AccountIdentifier::from_hex(length_57).is_err());
let length_58 = "0000000000000000000000000000000000000000000000000000000000";
assert_eq!(
AccountIdentifier::from_hex(length_58),
Err("0000000000000000000000000000000000000000000000000000000000 has a length of 58 but we expected a length of 64 or 56".to_string())
);
let length_64 = "0000000000000000000000000000000000000000000000000000000000000000";
assert!(
AccountIdentifier::from_hex(length_64)
.unwrap_err()
.contains("Checksum failed")
);
let length_64 = "807077e900000000000000000000000000000000000000000000000000000000";
assert_eq!(
AccountIdentifier::from_hex(length_64),
Ok(AccountIdentifier(aid_bytes))
);
}
}