use crate::hex::hex_bytes;
use candid::{CandidType, Deserialize, Encode, Int, Nat, Principal, types::reference::Func};
use ic_agent::Agent;
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
const ICRC_LOGO_METADATA_KEY: &str = "icrc1:logo";
pub trait IcrcLedgerError: Sized {
fn agent_build(endpoint: &str, reason: String) -> Self;
fn invalid_principal(field: &'static str, reason: String) -> Self;
fn candid_encode(message: &'static str, reason: String) -> Self;
fn agent_call(method: &'static str, reason: String) -> Self;
fn candid_decode(message: &'static str, reason: String) -> Self;
}
#[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: String,
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)]
pub struct IcrcAllowanceArgs {
pub account: IcrcAccount,
pub spender: IcrcAccount,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
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)]
pub(in crate::icrc) enum Icrc3Value {
Blob(Vec<u8>),
Text(String),
Nat(Nat),
Int(Int),
Array(Vec<Self>),
Map(BTreeMap<String, Self>),
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3GetBlocksRequest {
pub(in crate::icrc) start: Nat,
pub(in crate::icrc) length: Nat,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3BlockWithId {
pub(in crate::icrc) id: Nat,
pub(in crate::icrc) block: Icrc3Value,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3ArchiveCallback(pub(in crate::icrc) Func);
impl CandidType for Icrc3ArchiveCallback {
fn _ty() -> candid::types::Type {
candid::func!((Vec<Icrc3GetBlocksRequest>) -> (Icrc3GetBlocksResult) query)
}
fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
where
S: candid::types::Serializer,
{
self.0.idl_serialize(serializer)
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3ArchivedBlocks {
pub(in crate::icrc) args: Vec<Icrc3GetBlocksRequest>,
pub(in crate::icrc) callback: Icrc3ArchiveCallback,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3GetBlocksResult {
pub(in crate::icrc) log_length: Nat,
pub(in crate::icrc) blocks: Vec<Icrc3BlockWithId>,
pub(in crate::icrc) archived_blocks: Vec<Icrc3ArchivedBlocks>,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3GetArchivesArgs {
pub(in crate::icrc) from: Option<Principal>,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3ArchiveInfo {
pub(in crate::icrc) canister_id: Principal,
pub(in crate::icrc) start: Nat,
pub(in crate::icrc) end: Nat,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3SupportedBlockType {
pub(in crate::icrc) block_type: String,
pub(in crate::icrc) url: String,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub(in crate::icrc) struct Icrc3DataCertificate {
pub(in crate::icrc) certificate: Vec<u8>,
pub(in crate::icrc) hash_tree: Vec<u8>,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
struct IcrcSupportedStandard {
name: String,
url: String,
}
pub fn ic_agent<E>(endpoint: &str) -> Result<Agent, E>
where
E: IcrcLedgerError,
{
Agent::builder()
.with_url(endpoint)
.build()
.map_err(|err| E::agent_build(endpoint, err.to_string()))
}
pub fn principal_from_text<E>(value: &str, field: &'static str) -> Result<Principal, E>
where
E: IcrcLedgerError,
{
Principal::from_text(value).map_err(|err| E::invalid_principal(field, err.to_string()))
}
pub async fn fetch_icrc1_token_metadata<E>(
agent: &Agent,
ledger_canister: &Principal,
) -> Result<IcrcLedgerTokenMetadata, E>
where
E: IcrcLedgerError,
{
let token_name = query_ledger(agent, ledger_canister, "icrc1_name").await?;
let token_symbol = query_ledger(agent, ledger_canister, "icrc1_symbol").await?;
let decimals = query_ledger(agent, ledger_canister, "icrc1_decimals").await?;
let transfer_fee: Nat = query_ledger(agent, ledger_canister, "icrc1_fee").await?;
let total_supply: Nat = query_ledger(agent, ledger_canister, "icrc1_total_supply").await?;
let minting_account: Option<IcrcAccount> =
query_ledger(agent, ledger_canister, "icrc1_minting_account").await?;
let supported_standards = fetch_icrc_supported_standards(agent, ledger_canister).await?;
let metadata: Vec<(String, IcrcMetadataValue)> =
query_ledger(agent, ledger_canister, "icrc1_metadata").await?;
Ok(IcrcLedgerTokenMetadata {
token_name,
token_symbol,
decimals,
transfer_fee: transfer_fee.to_string(),
total_supply: total_supply.to_string(),
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: IcrcLedgerError,
{
let supported_standards: Vec<IcrcSupportedStandard> =
query_ledger(agent, ledger_canister, "icrc1_supported_standards").await?;
Ok(supported_standards
.into_iter()
.map(|standard| IcrcLedgerStandardRow {
name: standard.name,
url: standard.url,
})
.collect())
}
pub async fn query_ledger<T, E>(
agent: &Agent,
ledger_canister: &Principal,
method: &'static str,
) -> Result<T, E>
where
E: IcrcLedgerError,
T: for<'de> Deserialize<'de> + CandidType,
{
let arg = Encode!().map_err(|err| E::candid_encode(method, err.to_string()))?;
query_encoded(agent, ledger_canister, method, arg).await
}
pub async fn query_ledger_arg<Arg, Response, E>(
agent: &Agent,
ledger_canister: &Principal,
method: &'static str,
arg: &Arg,
) -> Result<Response, E>
where
Arg: CandidType + Sync,
E: IcrcLedgerError,
Response: for<'de> Deserialize<'de> + CandidType,
{
let arg = candid::encode_one(arg).map_err(|err| E::candid_encode(method, err.to_string()))?;
query_encoded(agent, ledger_canister, method, arg).await
}
pub fn metadata_row(key: String, value: IcrcMetadataValue) -> IcrcLedgerMetadataRow {
if key == ICRC_LOGO_METADATA_KEY {
return IcrcLedgerMetadataRow {
key,
value_type: "bool".to_string(),
value: JsonValue::Bool(metadata_value_is_present(&value)),
};
}
let (value_type, value) = match value {
IcrcMetadataValue::Nat(value) => ("nat", value.to_string()),
IcrcMetadataValue::Int(value) => ("int", value.to_string()),
IcrcMetadataValue::Text(value) => ("text", value),
IcrcMetadataValue::Blob(value) => ("blob", hex_bytes(&value)),
};
IcrcLedgerMetadataRow {
key,
value_type: value_type.to_string(),
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}"),
}
}
async fn query_encoded<T, E>(
agent: &Agent,
ledger_canister: &Principal,
method: &'static str,
arg: Vec<u8>,
) -> Result<T, E>
where
E: IcrcLedgerError,
T: for<'de> Deserialize<'de> + CandidType,
{
let bytes = agent
.query(ledger_canister, method)
.with_arg(arg)
.call()
.await
.map_err(|err| E::agent_call(method, err.to_string()))?;
candid::decode_one(&bytes).map_err(|err| E::candid_decode(method, err.to_string()))
}
fn metadata_value_is_present(value: &IcrcMetadataValue) -> bool {
match value {
IcrcMetadataValue::Text(value) => !value.trim().is_empty(),
IcrcMetadataValue::Blob(value) => !value.is_empty(),
IcrcMetadataValue::Nat(_) | IcrcMetadataValue::Int(_) => true,
}
}