From 487eaeb26330435a303915eaa85f5cd704c443a8 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 24 Jul 2024 12:17:15 +0100 Subject: [PATCH] Simplify adding tokens and add compatibility with `burn_fee` (#6102) --- backend/canisters/registry/CHANGELOG.md | 4 + .../impl/src/jobs/check_for_token_updates.rs | 46 ++++--- backend/canisters/registry/impl/src/lib.rs | 1 + .../registry/impl/src/metadata_helper.rs | 74 +++++++++++ .../registry/impl/src/updates/add_token.rs | 118 +++++++----------- 5 files changed, 146 insertions(+), 97 deletions(-) create mode 100644 backend/canisters/registry/impl/src/metadata_helper.rs diff --git a/backend/canisters/registry/CHANGELOG.md b/backend/canisters/registry/CHANGELOG.md index 228c909059..b3ff4dd3cc 100644 --- a/backend/canisters/registry/CHANGELOG.md +++ b/backend/canisters/registry/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Changed + +- Simplify adding tokens and add compatibility with `burn_fee` ([#6102](https://github.com/open-chat-labs/open-chat/pull/6102)) + ## [[2.0.1193](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1193-registry)] - 2024-06-06 ### Fixed diff --git a/backend/canisters/registry/impl/src/jobs/check_for_token_updates.rs b/backend/canisters/registry/impl/src/jobs/check_for_token_updates.rs index d2c0474836..7f707dd5b5 100644 --- a/backend/canisters/registry/impl/src/jobs/check_for_token_updates.rs +++ b/backend/canisters/registry/impl/src/jobs/check_for_token_updates.rs @@ -1,7 +1,8 @@ +use crate::metadata_helper::MetadataHelper; use crate::{mutate_state, read_state}; use ic_cdk::api::call::RejectionCode; -use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; use std::time::Duration; +use tracing::error; use types::CanisterId; use utils::canister_timers::run_now_then_interval; use utils::time::HOUR_IN_MS; @@ -22,34 +23,29 @@ async fn run_async() { async fn check_for_token_updates(ledger_canister_id: CanisterId) -> Result<(), (RejectionCode, String)> { let metadata = icrc_ledger_canister_c2c_client::icrc1_metadata(ledger_canister_id).await?; + let metadata_helper = match MetadataHelper::try_parse(metadata) { + Ok(h) => h, + Err(reason) => { + let error = format!("Token metadata is incomplete: {reason}"); + error!(%ledger_canister_id, error); + return Err((RejectionCode::Unknown, error)); + } + }; mutate_state(|state| { if let Some(token) = state.data.tokens.get(ledger_canister_id).cloned() { let mut args = registry_canister::update_token::Args::new(ledger_canister_id); - for (name, value) in metadata { - match name.as_str() { - "icrc1:logo" => { - if let MetadataValue::Text(logo) = value { - if logo != token.logo { - args.logo = Some(logo); - } - } - } - "icrc1:name" => { - if let MetadataValue::Text(name) = value { - if name != token.name { - args.name = Some(name); - } - } - } - "icrc1:symbol" => { - if let MetadataValue::Text(symbol) = value { - if symbol != token.symbol { - args.symbol = Some(symbol); - } - } - } - _ => {} + if *metadata_helper.name() != token.name { + args.name = Some(metadata_helper.name().to_string()); + } + + if *metadata_helper.symbol() != token.symbol { + args.symbol = Some(metadata_helper.symbol().to_string()); + } + + if let Some(logo) = metadata_helper.logo().cloned() { + if logo != token.logo { + args.logo = Some(logo); } } diff --git a/backend/canisters/registry/impl/src/lib.rs b/backend/canisters/registry/impl/src/lib.rs index 0a9d18a5ed..6e186b4623 100644 --- a/backend/canisters/registry/impl/src/lib.rs +++ b/backend/canisters/registry/impl/src/lib.rs @@ -14,6 +14,7 @@ mod guards; mod jobs; mod lifecycle; mod memory; +mod metadata_helper; mod model; mod queries; mod updates; diff --git a/backend/canisters/registry/impl/src/metadata_helper.rs b/backend/canisters/registry/impl/src/metadata_helper.rs new file mode 100644 index 0000000000..0cf760212d --- /dev/null +++ b/backend/canisters/registry/impl/src/metadata_helper.rs @@ -0,0 +1,74 @@ +use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; + +pub struct MetadataHelper { + name: String, + symbol: String, + decimals: u8, + fee: u128, + logo: Option, + is_icrc1_compatible: bool, +} + +impl MetadataHelper { + pub fn try_parse(metadata: Vec<(String, MetadataValue)>) -> Result { + let mut name = None; + let mut symbol = None; + let mut decimals = None; + let mut fee = None; + let mut burn_fee = None; + let mut logo = None; + let mut is_icrc1_compatible = true; + + for (key, value) in metadata { + match (key.as_str(), value) { + ("icrc1:name", MetadataValue::Text(s)) => name = Some(s), + ("icrc1:symbol", MetadataValue::Text(s)) => symbol = Some(s), + ("icrc1:decimals", MetadataValue::Nat(n)) => decimals = u8::try_from(n.0).ok(), + ("icrc1:fee", MetadataValue::Nat(n)) => fee = u128::try_from(n.0).ok(), + ("icrc1:burn_fee", MetadataValue::Nat(n)) => burn_fee = u128::try_from(n.0).ok(), + ("icrc1:logo", MetadataValue::Text(s)) => logo = Some(s), + ("icrc1:transfer_fee_rate" | "icrc1:burn_fee_rate", _) => is_icrc1_compatible = false, + _ => {} + } + } + + match (name, symbol, decimals, fee) { + (Some(n), Some(s), Some(d), Some(f)) => Ok(MetadataHelper { + name: n, + symbol: s, + decimals: d, + fee: f + burn_fee.unwrap_or_default(), + logo, + is_icrc1_compatible, + }), + (None, ..) => Err("Name not found".to_string()), + (_, None, ..) => Err("Symbol not found".to_string()), + (.., None, _) => Err("Decimals not found".to_string()), + (.., None) => Err("Fee not found".to_string()), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn symbol(&self) -> &str { + &self.symbol + } + + pub fn decimals(&self) -> u8 { + self.decimals + } + + pub fn fee(&self) -> u128 { + self.fee + } + + pub fn logo(&self) -> Option<&String> { + self.logo.as_ref() + } + + pub fn is_icrc1_compatible(&self) -> bool { + self.is_icrc1_compatible + } +} diff --git a/backend/canisters/registry/impl/src/updates/add_token.rs b/backend/canisters/registry/impl/src/updates/add_token.rs index 351ddf9152..e8cab1b76a 100644 --- a/backend/canisters/registry/impl/src/updates/add_token.rs +++ b/backend/canisters/registry/impl/src/updates/add_token.rs @@ -1,10 +1,8 @@ use crate::guards::caller_is_governance_principal; +use crate::metadata_helper::MetadataHelper; use crate::mutate_state; use canister_api_macros::proposal; use canister_tracing_macros::trace; -use futures::try_join; -use ic_cdk::api::call::RejectionCode; -use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; use registry_canister::add_token::{Response::*, *}; use registry_canister::NervousSystemDetails; use tracing::{error, info}; @@ -49,7 +47,12 @@ async fn add_token_impl( Err(error) => return InternalError(format!("{error:?}")), }; - if !check_icrc1_compatibility(&metadata) { + let metadata_helper = match MetadataHelper::try_parse(metadata) { + Ok(h) => h, + Err(reason) => return InvalidRequest(format!("Token metadata is incomplete: {reason}")), + }; + + if !metadata_helper.is_icrc1_compatible() { return InvalidRequest("Token is not compatible with the ICRC1 standard".to_string()); } @@ -65,46 +68,46 @@ async fn add_token_impl( } }; - match try_join!( - icrc_ledger_canister_c2c_client::icrc1_name(ledger_canister_id), - icrc_ledger_canister_c2c_client::icrc1_symbol(ledger_canister_id), - icrc_ledger_canister_c2c_client::icrc1_decimals(ledger_canister_id), - icrc_ledger_canister_c2c_client::icrc1_fee(ledger_canister_id), - icrc_ledger_canister_c2c_client::icrc1_supported_standards(ledger_canister_id), - get_logo(logo, metadata, nervous_system.as_ref().map(|ns| ns.logo.clone())), - ) { - Ok((.., logo)) if logo.is_none() => { - let error = "Failed to find logo for token"; - error!(%ledger_canister_id, error); - InternalError(error.to_string()) - } - Ok((name, symbol, decimals, fee, standards, logo)) => mutate_state(|state| { - let now = state.env.now(); - let standards = standards.into_iter().map(|r| r.name).collect(); + if let Some(logo) = metadata_helper + .logo() + .cloned() + .or(logo) + .or(nervous_system.as_ref().map(|ns| ns.logo.clone())) + { + match icrc_ledger_canister_c2c_client::icrc1_supported_standards(ledger_canister_id).await { + Ok(standards) => mutate_state(|state| { + let now = state.env.now(); + let standards = standards.into_iter().map(|r| r.name).collect(); - if let Some(ns) = nervous_system { - state.data.nervous_systems.add(ns, now); - } - if state.data.tokens.add( - ledger_canister_id, - name.clone(), - symbol, - decimals, - fee.0.try_into().unwrap(), - logo.unwrap(), - info_url, - how_to_buy_url, - transaction_url_format, - standards, - now, - ) { - info!(name, %ledger_canister_id, "Token added"); - Success - } else { - AlreadyAdded - } - }), - Err(error) => InternalError(format!("{error:?}")), + if let Some(ns) = nervous_system { + state.data.nervous_systems.add(ns, now); + } + let name = metadata_helper.name().to_string(); + if state.data.tokens.add( + ledger_canister_id, + name.clone(), + metadata_helper.symbol().to_string(), + metadata_helper.decimals(), + metadata_helper.fee(), + logo, + info_url, + how_to_buy_url, + transaction_url_format, + standards, + now, + ) { + info!(name, %ledger_canister_id, "Token added"); + Success + } else { + AlreadyAdded + } + }), + Err(error) => InternalError(format!("{error:?}")), + } + } else { + let error = "Failed to find logo for token"; + error!(%ledger_canister_id, error); + InternalError(error.to_string()) } } @@ -157,32 +160,3 @@ fn extract_urls( transaction_url_format, }) } - -async fn get_logo( - logo: Option, - metadata: Vec<(String, MetadataValue)>, - governance_logo: Option, -) -> Result, (RejectionCode, String)> { - let metadata_logo = metadata.into_iter().find(|(k, _)| k == "icrc1:logo").and_then(|(_, v)| { - if let MetadataValue::Text(t) = v { - Some(t) - } else { - None - } - }); - - Ok(metadata_logo.or(logo).or(governance_logo)) -} - -fn check_icrc1_compatibility(metadata: &[(String, MetadataValue)]) -> bool { - for (k, v) in metadata { - if k == "icrc1:transfer_fee_rate" || k == "icrc1:burn_fee" || k == "icrc1:burn_fee_rate" { - match v { - MetadataValue::Nat(x) if *x > 0u128 => return false, - MetadataValue::Int(x) if *x > 0i128 => return false, - _ => {} - } - } - } - true -}