use crate::{
candid_call::{CandidCallError, query_no_args},
hex::hex_bytes,
icrc::IcrcMetadataValueKind,
};
use candid::{CandidType, Deserialize, Int, Nat, Principal};
use ic_agent::Agent;
use serde_json::Value as JsonValue;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IcrcLedgerTokenMetadata {
pub token_name: String,
pub token_symbol: String,
pub decimals: u8,
pub transfer_fee: String,
pub total_supply: String,
pub minting_account_owner: Option<String>,
pub minting_account_subaccount_hex: Option<String>,
pub supported_standards: Vec<IcrcLedgerStandardRow>,
pub metadata: Vec<IcrcLedgerMetadataRow>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IcrcLedgerStandardRow {
pub name: String,
pub url: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IcrcLedgerMetadataRow {
pub key: String,
pub value_type: IcrcMetadataValueKind,
pub value: JsonValue,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct IcrcAccount {
pub owner: Principal,
pub subaccount: Option<Vec<u8>>,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum IcrcMetadataValue {
Nat(Nat),
Int(Int),
Text(String),
Blob(Vec<u8>),
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
#[cfg(feature = "icrc-host")]
pub struct IcrcAllowanceArgs {
pub account: IcrcAccount,
pub spender: IcrcAccount,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
#[cfg(feature = "icrc-host")]
pub struct IcrcAllowance {
pub allowance: Nat,
pub expires_at: Option<u64>,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum GetIndexPrincipalResult {
Ok(Principal),
Err(GetIndexPrincipalError),
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum GetIndexPrincipalError {
IndexPrincipalNotSet,
GenericError {
error_code: Nat,
description: String,
},
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
struct IcrcSupportedStandard {
name: String,
url: String,
}
#[cfg(feature = "icrc-host")]
pub fn principal_from_text(
value: &str,
field: &'static str,
) -> Result<Principal, crate::icrc::IcrcError> {
Principal::from_text(value).map_err(|err| crate::icrc::IcrcError::InvalidPrincipal {
field,
reason: err.to_string(),
})
}
pub async fn fetch_icrc1_token_metadata<E>(
agent: &Agent,
ledger_canister: &Principal,
) -> Result<IcrcLedgerTokenMetadata, E>
where
E: CandidCallError,
{
let (
token_name,
token_symbol,
decimals,
transfer_fee,
total_supply,
minting_account,
supported_standards,
metadata,
) = futures::try_join!(
query_no_args::<String, E>(agent, ledger_canister, "icrc1_name"),
query_no_args::<String, E>(agent, ledger_canister, "icrc1_symbol"),
query_no_args::<u8, E>(agent, ledger_canister, "icrc1_decimals"),
query_no_args::<Nat, E>(agent, ledger_canister, "icrc1_fee"),
query_no_args::<Nat, E>(agent, ledger_canister, "icrc1_total_supply"),
query_no_args::<Option<IcrcAccount>, E>(agent, ledger_canister, "icrc1_minting_account"),
fetch_icrc_supported_standards::<E>(agent, ledger_canister),
query_no_args::<Vec<(String, IcrcMetadataValue)>, E>(
agent,
ledger_canister,
"icrc1_metadata"
),
)?;
Ok(IcrcLedgerTokenMetadata {
token_name,
token_symbol,
decimals,
transfer_fee: nat_text(&transfer_fee),
total_supply: nat_text(&total_supply),
minting_account_owner: minting_account
.as_ref()
.map(|account| account.owner.to_text()),
minting_account_subaccount_hex: minting_account
.as_ref()
.and_then(|account| account.subaccount.as_deref())
.map(hex_bytes),
supported_standards,
metadata: metadata
.into_iter()
.map(|(key, value)| metadata_row(key, value))
.collect(),
})
}
pub async fn fetch_icrc_supported_standards<E>(
agent: &Agent,
ledger_canister: &Principal,
) -> Result<Vec<IcrcLedgerStandardRow>, E>
where
E: CandidCallError,
{
let supported_standards: Vec<IcrcSupportedStandard> =
query_no_args(agent, ledger_canister, "icrc1_supported_standards").await?;
Ok(supported_standards
.into_iter()
.map(|standard| IcrcLedgerStandardRow {
name: standard.name,
url: standard.url,
})
.collect())
}
pub fn nat_text(value: &Nat) -> String {
value.0.to_str_radix(10)
}
pub fn metadata_row(key: String, value: IcrcMetadataValue) -> IcrcLedgerMetadataRow {
let (value_type, value) = match value {
IcrcMetadataValue::Nat(value) => (IcrcMetadataValueKind::Nat, nat_text(&value)),
IcrcMetadataValue::Int(value) => (IcrcMetadataValueKind::Int, value.0.to_str_radix(10)),
IcrcMetadataValue::Text(value) => (IcrcMetadataValueKind::Text, value),
IcrcMetadataValue::Blob(value) => (IcrcMetadataValueKind::Blob, hex_bytes(&value)),
};
IcrcLedgerMetadataRow {
key,
value_type,
value: JsonValue::String(value),
}
}
#[must_use]
pub fn index_principal_error_text(error: GetIndexPrincipalError) -> String {
match error {
GetIndexPrincipalError::IndexPrincipalNotSet => "index principal not set".to_string(),
GetIndexPrincipalError::GenericError {
error_code,
description,
} => format!("generic error {error_code}: {description}"),
}
}
#[cfg(all(test, feature = "icrc-host"))]
mod tests {
use super::*;
#[test]
fn metadata_conversion_preserves_native_kind_and_value() {
for (raw, kind, value) in [
(
IcrcMetadataValue::Nat(Nat::from(7_u8)),
IcrcMetadataValueKind::Nat,
"7",
),
(
IcrcMetadataValue::Int(Int::from(-2_i8)),
IcrcMetadataValueKind::Int,
"-2",
),
(
IcrcMetadataValue::Nat("18446744073709551616".parse().unwrap()),
IcrcMetadataValueKind::Nat,
"18446744073709551616",
),
(
IcrcMetadataValue::Int("-18446744073709551616".parse().unwrap()),
IcrcMetadataValueKind::Int,
"-18446744073709551616",
),
(
IcrcMetadataValue::Text("token".to_string()),
IcrcMetadataValueKind::Text,
"token",
),
(
IcrcMetadataValue::Blob(vec![0x01, 0xab]),
IcrcMetadataValueKind::Blob,
"01ab",
),
] {
let row = metadata_row("key".to_string(), raw);
assert_eq!(row.key, "key");
assert_eq!(row.value_type, kind);
assert_eq!(row.value, JsonValue::String(value.to_string()));
}
}
}