-
Notifications
You must be signed in to change notification settings - Fork 395
feat(ckdoge-minter): support ICRC-21 consent messages #10140
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
Merged
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
3ce248b
feat(ckdoge-minter): support ICRC-21 consent messages
ce7704d
fix(ckdoge-minter): import candid::Encode in ICRC-21 smoke test
0afc38b
chore(ckdoge-minter): reorder ICRC-21 endpoints in .did
9e7c605
Merge branch 'master' into claude/inspiring-neumann-b9d915
Dfinity-Bjoern File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| //! Implementation of the [ICRC-21](https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-21/ICRC-21.md) | ||
| //! Canister Call Consent Message standard for the ckDOGE minter. | ||
|
|
||
| use crate::address::DogecoinAddress; | ||
| use crate::candid_api::RetrieveDogeWithApprovalArgs; | ||
| use crate::lifecycle::init::Network; | ||
| use candid::{CandidType, Decode, Deserialize}; | ||
| use ic_ckbtc_minter::state::read_state; | ||
| use icrc_ledger_types::icrc21::errors::{ErrorInfo, Icrc21Error}; | ||
| use icrc_ledger_types::icrc21::lib::MAX_CONSENT_MESSAGE_ARG_SIZE_BYTES; | ||
| use icrc_ledger_types::icrc21::requests::{ | ||
| ConsentMessageMetadata, ConsentMessageRequest, DisplayMessageType, | ||
| }; | ||
| use icrc_ledger_types::icrc21::responses::{ConsentInfo, ConsentMessage, FieldsDisplay, Value}; | ||
|
|
||
| /// The number of decimals used to display token amounts. | ||
| /// Both ckDOGE and DOGE use 8 decimals (1 DOGE = 10^8 koinus). | ||
| pub(super) const DECIMALS: u8 = 8; | ||
|
|
||
| /// Token symbols used in consent messages. They depend on the configured | ||
| /// Dogecoin network so that test deployments use the test-token names. | ||
| #[derive(Copy, Clone, Debug, Eq, PartialEq)] | ||
| pub(super) struct TokenSymbols { | ||
| /// The ledger token, e.g. "ckDOGE" on mainnet, "ckTESTDOGE" otherwise. | ||
| pub(super) ckdoge: &'static str, | ||
| /// The native Dogecoin token, e.g. "DOGE" on mainnet, "TESTDOGE" otherwise. | ||
| pub(super) doge: &'static str, | ||
| } | ||
|
|
||
| impl TokenSymbols { | ||
| pub(super) fn for_network(network: Network) -> Self { | ||
| match network { | ||
| Network::Mainnet => Self { | ||
| ckdoge: "ckDOGE", | ||
| doge: "DOGE", | ||
| }, | ||
| Network::Regtest => Self { | ||
| ckdoge: "ckTESTDOGE", | ||
| doge: "TESTDOGE", | ||
| }, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// An entry of the ICRC-10 supported standards list. | ||
| #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] | ||
| pub struct StandardRecord { | ||
| pub name: String, | ||
| pub url: String, | ||
| } | ||
|
|
||
| pub fn icrc10_supported_standards() -> Vec<StandardRecord> { | ||
| vec![ | ||
| StandardRecord { | ||
| name: "ICRC-10".to_string(), | ||
| url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-10/ICRC-10.md".to_string(), | ||
| }, | ||
| StandardRecord { | ||
| name: "ICRC-21".to_string(), | ||
| url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-21/ICRC-21.md".to_string(), | ||
| }, | ||
| ] | ||
| } | ||
|
|
||
| pub fn icrc21_canister_call_consent_message( | ||
| consent_msg_request: ConsentMessageRequest, | ||
| ) -> Result<ConsentInfo, Icrc21Error> { | ||
| let network = | ||
| read_state(|s| Network::try_from(s.btc_network).unwrap_or_else(|err| ic_cdk::trap(err))); | ||
| build_consent_info(consent_msg_request, network) | ||
| } | ||
|
|
||
| pub(super) fn build_consent_info( | ||
| consent_msg_request: ConsentMessageRequest, | ||
| network: Network, | ||
| ) -> Result<ConsentInfo, Icrc21Error> { | ||
| if consent_msg_request.arg.len() > MAX_CONSENT_MESSAGE_ARG_SIZE_BYTES as usize { | ||
| return Err(Icrc21Error::UnsupportedCanisterCall(ErrorInfo { | ||
| description: format!( | ||
| "The argument size is too large. The maximum allowed size is \ | ||
| {MAX_CONSENT_MESSAGE_ARG_SIZE_BYTES} bytes." | ||
| ), | ||
| })); | ||
| } | ||
|
|
||
| let display_type = consent_msg_request | ||
| .user_preferences | ||
| .device_spec | ||
| .clone() | ||
| .unwrap_or(DisplayMessageType::GenericDisplay); | ||
|
|
||
| let symbols = TokenSymbols::for_network(network); | ||
|
|
||
| let consent_message = match consent_msg_request.method.as_str() { | ||
| "retrieve_doge_with_approval" => { | ||
| let args = Decode!( | ||
| consent_msg_request.arg.as_slice(), | ||
| RetrieveDogeWithApprovalArgs | ||
| ) | ||
| .map_err(|e| { | ||
| Icrc21Error::UnsupportedCanisterCall(ErrorInfo { | ||
| description: format!("Failed to decode RetrieveDogeWithApprovalArgs: {e}"), | ||
| }) | ||
| })?; | ||
| validate_address(&args.address, network)?; | ||
| build_retrieve_doge_with_approval_message(&args, &display_type, symbols) | ||
| } | ||
| method => { | ||
| return Err(Icrc21Error::UnsupportedCanisterCall(ErrorInfo { | ||
| description: format!( | ||
| "The method '{method}' is not supported by the ckDOGE minter ICRC-21 endpoint." | ||
| ), | ||
| })); | ||
| } | ||
| }; | ||
|
|
||
| // Respond in English regardless of what the client requested for now. | ||
| let metadata = ConsentMessageMetadata { | ||
| language: "en".to_string(), | ||
| utc_offset_minutes: consent_msg_request | ||
| .user_preferences | ||
| .metadata | ||
| .utc_offset_minutes, | ||
| }; | ||
|
|
||
| Ok(ConsentInfo { | ||
| metadata, | ||
| consent_message, | ||
| }) | ||
| } | ||
|
|
||
| fn build_retrieve_doge_with_approval_message( | ||
| args: &RetrieveDogeWithApprovalArgs, | ||
| display_type: &DisplayMessageType, | ||
| symbols: TokenSymbols, | ||
| ) -> ConsentMessage { | ||
| let TokenSymbols { ckdoge, doge } = symbols; | ||
| let amount = format_amount(args.amount, DECIMALS); | ||
| match display_type { | ||
| DisplayMessageType::GenericDisplay => { | ||
| let mut message = format!( | ||
| "# Convert {ckdoge} to {doge}\n\n\ | ||
| Authorize the {ckdoge} minter to burn {ckdoge} from your account and \ | ||
| send the equivalent amount in {doge} (minus network and minter fees) to \ | ||
| the Dogecoin address below.\n\n\ | ||
| **Amount to convert:** `{amount} {ckdoge}`\n\n\ | ||
| **Dogecoin destination address:**\n`{address}`", | ||
| address = args.address, | ||
| ); | ||
| if let Some(subaccount) = args.from_subaccount { | ||
| message.push_str(&format!( | ||
| "\n\n**{ckdoge} source subaccount:**\n`{}`", | ||
| hex::encode(subaccount) | ||
| )); | ||
| } | ||
| ConsentMessage::GenericDisplayMessage(message) | ||
| } | ||
| DisplayMessageType::FieldsDisplay => { | ||
| // Long values (Dogecoin addresses, subaccount hex) are sent as a | ||
| // single `Value::Text` per the ICRC-21 spec — wallets are | ||
| // responsible for paginating them across screens. See e.g. the | ||
| // Ledger ICP app, which calls `handle_ui_message` to chunk the | ||
| // value into device-sized pages. | ||
| let mut fields = vec![ | ||
| ( | ||
| "Amount".to_string(), | ||
| Value::TokenAmount { | ||
| decimals: DECIMALS, | ||
| amount: args.amount, | ||
| symbol: ckdoge.to_string(), | ||
| }, | ||
| ), | ||
| ( | ||
| format!("{doge} address"), | ||
| Value::Text { | ||
| content: args.address.clone(), | ||
| }, | ||
| ), | ||
| ]; | ||
| if let Some(subaccount) = args.from_subaccount { | ||
| fields.push(( | ||
| "From subaccount".to_string(), | ||
| Value::Text { | ||
| content: hex::encode(subaccount), | ||
| }, | ||
| )); | ||
| } | ||
| ConsentMessage::FieldsDisplayMessage(FieldsDisplay { | ||
| intent: format!("{ckdoge} to {doge}"), | ||
| fields, | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Verifies that `address` parses as a valid Dogecoin address on the configured | ||
| /// network before it gets interpolated into a consent message. This both | ||
| /// guarantees the user is shown a meaningful (parseable) destination and rules | ||
| /// out Markdown-injection vectors in the GenericDisplay output (e.g. an | ||
| /// "address" that contains newlines or backticks crafted to fake additional | ||
| /// fields). Uses the same parser as `retrieve_doge_with_approval`, so any | ||
| /// address the consent endpoint accepts is also accepted by the actual call. | ||
| fn validate_address(address: &str, network: Network) -> Result<(), Icrc21Error> { | ||
| DogecoinAddress::parse(address, &network).map_err(|e| { | ||
| Icrc21Error::UnsupportedCanisterCall(ErrorInfo { | ||
| description: format!("Invalid Dogecoin destination address: {e}"), | ||
| }) | ||
| })?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub(super) fn format_amount(amount: u64, decimals: u8) -> String { | ||
| let divisor = 10_u64.pow(decimals as u32); | ||
| let whole = amount / divisor; | ||
| let frac = amount % divisor; | ||
| if frac == 0 { | ||
| format!("{whole}") | ||
| } else { | ||
| let frac_str = format!("{frac:0width$}", width = decimals as usize); | ||
| let trimmed = frac_str.trim_end_matches('0'); | ||
| format!("{whole}.{trimmed}") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.