ic-platform 0.31.1

Controlled Candid and IC runtime dependency boundary
Documentation
//! Standalone Platform-only consumer of the explicit external ledger surface.
use crate::icrc_ledger_types::{
    Account, Allowance, AllowanceArgs, ArchivedBlocks, BlockWithId, GetArchivesArgs,
    GetBlocksRequest, GetBlocksResult, ICRC3ArchiveInfo, ICRC3DataCertificate, ICRC3Value,
    MetadataValue, QueryArchiveFn, SupportedBlockType, TransferError, Value,
};
use crate::principal::Principal;
use crate::tests::reference as support;

use support::decode_hex as wire_hex;

fn roundtrip_value<
    T: crate::candid_codec::Encode
        + for<'a> crate::candid_codec::decoder::Decode<'a>
        + std::fmt::Debug,
>(
    value: &T,
) -> Vec<u8> {
    {
        let bytes = crate::tests::reference::oracle_bytes(value);
        let decoded: T =
            crate::candid_codec::decoder::decode_one(&bytes, crate::tests::reference::limits())
                .unwrap();
        assert_eq!(crate::tests::reference::oracle_bytes(&decoded), bytes);
        bytes
    }
}

/// Exercise selected DTOs, account semantics, upstream hashes, archive callbacks and bytes.
pub fn roundtrip() {
    let standard = crate::icrc_ledger_types::icrc10::SupportedStandard {
        name: "ICRC-10".into(),
        url: "https://example.test/standard".into(),
    };
    roundtrip_value(&vec![standard]);
    let owner = Principal::anonymous();
    let account = Account {
        owner,
        subaccount: None,
    };
    assert_eq!(
        roundtrip_value(&account),
        wire_hex(include_str!("../../../tests/fixtures/icrc-account.hex"))
    );
    let zero_account = Account {
        owner,
        subaccount: Some([0; 32]),
    };
    assert_eq!(account, zero_account);
    assert_ne!(roundtrip_value(&account), roundtrip_value(&zero_account));
    assert_eq!(account.effective_subaccount(), &[0; 32]);
    let named = Account {
        owner,
        subaccount: Some([7; 32]),
    };
    assert_eq!(named.to_string().parse::<Account>().unwrap(), named);
    let big: crate::candid_codec::Nat = "340282366920938463463374607431768211456".parse().unwrap();
    for value in [
        MetadataValue::Nat(big.clone()),
        MetadataValue::Int((-65_i64).into()),
        MetadataValue::Text("token".into()),
        MetadataValue::Blob(vec![0, 255].into()),
    ] {
        roundtrip_value(&value);
    }
    roundtrip_value(&AllowanceArgs {
        account,
        spender: named,
    });
    roundtrip_value(&Allowance {
        allowance: big.clone(),
        expires_at: Some(123),
    });
    roundtrip_value(&TransferError::Duplicate { duplicate_of: big });
    let legacy = Value::Map(
        [
            ("amount".into(), Value::Nat64(42)),
            ("memo".into(), Value::Blob(vec![0, 128, 255].into())),
            ("signed".into(), Value::Int((-65_i64).into())),
        ]
        .into(),
    );
    let modern = ICRC3Value::from(legacy.clone());
    let hash = wire_hex(include_str!("../../../tests/fixtures/icrc-value-hash.hex"));
    assert_eq!(legacy.clone().hash().as_slice(), hash);
    assert_eq!(modern.clone().hash().as_slice(), hash);
    assert_ne!(roundtrip_value(&legacy), roundtrip_value(&modern));
    let request = GetBlocksRequest {
        start: 1_u8.into(),
        length: 1_u8.into(),
    };
    roundtrip_value(&request);
    assert_eq!(request.as_start_and_length().unwrap(), (1, 1));
    let callback: QueryArchiveFn<Vec<GetBlocksRequest>, GetBlocksResult> =
        QueryArchiveFn::new(owner, "icrc3_get_blocks");
    roundtrip_value(&callback);
    let result = GetBlocksResult {
        log_length: 2_u8.into(),
        blocks: vec![BlockWithId {
            id: 0_u8.into(),
            block: modern,
        }],
        archived_blocks: vec![ArchivedBlocks {
            args: vec![request],
            callback,
        }],
    };
    assert_eq!(
        roundtrip_value(&result),
        wire_hex(include_str!("../../../tests/fixtures/icrc-blocks.hex"))
    );
    roundtrip_value(&GetArchivesArgs { from: Some(owner) });
    roundtrip_value(&ICRC3ArchiveInfo {
        canister_id: owner,
        start: 1_u8.into(),
        end: 2_u8.into(),
    });
    roundtrip_value(&ICRC3DataCertificate {
        certificate: vec![0, 255].into(),
        hash_tree: vec![1, 128].into(),
    });
    roundtrip_value(&SupportedBlockType {
        block_type: "1xfer".into(),
        url: "https://example.invalid/spec".into(),
    });
}

#[test]
fn ledger_dtos_preserve_wire_hashes_and_callback_types() {
    roundtrip();
}

#[test]
fn owned_accounts_preserve_external_semantics_and_wire() {
    use crate::account::{Account as OwnedAccount, ExternalAccount};
    for len in 0..=29 {
        for seed in 0..=255u8 {
            let owner = Principal::from_slice(&vec![seed; len]);
            for subaccount in [None, Some([0; 32]), Some([seed; 32])] {
                let external = ExternalAccount { owner, subaccount };
                let owned: OwnedAccount = external.into();
                assert_eq!(owned.to_string(), external.to_string());
                assert_eq!(owned.to_string().parse::<OwnedAccount>().unwrap(), owned);
                let restored: ExternalAccount = owned.into();
                assert_eq!(restored.subaccount, external.subaccount);
                assert_eq!(
                    crate::tests::reference::oracle_bytes(&restored),
                    crate::tests::reference::oracle_bytes(&external)
                );
            }
        }
    }
}

#[test]
fn external_account_display_keeps_the_frozen_nondefault_text() {
    const TEXT: &str = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.1";
    let owner = TEXT.split_once("-6cc627i").unwrap().0.parse().unwrap();
    let mut subaccount = [0; 32];
    subaccount[31] = 1;
    let account = Account {
        owner,
        subaccount: Some(subaccount),
    };
    assert_eq!(account.to_string(), TEXT);
    assert_eq!(TEXT.parse::<Account>().unwrap(), account);
    for subaccount in [None, Some([0; 32])] {
        assert_eq!(Account { owner, subaccount }.to_string(), owner.to_string());
    }
}

#[test]
fn owned_account_parser_and_order_match_external_owner() {
    use crate::account::{Account as OwnedAccount, ExternalAccount};
    use std::hash::{Hash, Hasher};
    let mut accounts = Vec::new();
    // Every possible trimmed hex length, odd/even nibble offsets and mixed bytes.
    for digits in 1..=64 {
        let mut subaccount = [0; 32];
        for (index, byte) in subaccount.iter_mut().enumerate().skip((64 - digits) / 2) {
            *byte = (index as u8).wrapping_mul(37).wrapping_add(19);
        }
        subaccount[(64 - digits) / 2] = if digits % 2 == 0 { 0xa1 } else { 1 };
        let external = ExternalAccount {
            owner: Principal::from_slice(&[digits as u8; 29]),
            subaccount: Some(subaccount),
        };
        let text = external.to_string();
        assert_eq!(text.split_once('.').unwrap().1.len(), digits);
        assert_eq!(text.len(), 72 + digits);
        let owned: OwnedAccount = text.parse().unwrap();
        assert_eq!(owned.owner.as_slice(), external.owner.as_slice());
        assert_eq!(owned.subaccount, external.subaccount);
        assert_eq!(owned.to_string(), text);
        let mut original_hash = std::collections::hash_map::DefaultHasher::new();
        let mut owned_hash = std::collections::hash_map::DefaultHasher::new();
        external.hash(&mut original_hash);
        owned.hash(&mut owned_hash);
        assert_eq!(original_hash.finish(), owned_hash.finish());
        accounts.push((external, owned));
        // Admission parity, not error spelling: mutate every byte including separators.
        for index in 0..text.len() {
            for replacement in [b'0', b'1', b'a', b'F', b'-', b'.', b' ', 0] {
                let mut bytes = text.as_bytes().to_vec();
                bytes[index] = replacement;
                let mutated = String::from_utf8(bytes).unwrap();
                let original = mutated.parse::<ExternalAccount>();
                let candidate = mutated.parse::<OwnedAccount>();
                assert_eq!(original.is_ok(), candidate.is_ok(), "{mutated:?}");
                if let (Ok(original), Ok(candidate)) = (original, candidate) {
                    assert_eq!(original.owner.as_slice(), candidate.owner.as_slice());
                    assert_eq!(original.subaccount, candidate.subaccount);
                }
            }
        }
        for suffix in ["é", "😀", ".", "\n"] {
            assert!(format!("{text}{suffix}").parse::<OwnedAccount>().is_err());
        }
    }
    for (external, owned) in &accounts {
        for (other_external, other_owned) in &accounts {
            assert_eq!(external.cmp(other_external), owned.cmp(other_owned));
        }
    }
}

/// Construct transfers and consent messages without a direct Candid/ledger dependency.
pub fn transfers_and_consent_contracts() {
    use crate::icrc_ledger_types::{Memo, TransferArg, icrc21};
    let request = TransferArg {
        from_subaccount: Some([7; 32]),
        to: Account {
            owner: Principal::anonymous(),
            subaccount: None,
        },
        fee: Some(10u64.into()),
        created_at_time: Some(42),
        memo: Some(Memo::from(99u64)),
        amount: 100u64.into(),
    };
    roundtrip_value(&request);
    let metadata = icrc21::ConsentMessageMetadata {
        language: "en".into(),
        utc_offset_minutes: Some(-60),
    };
    for device_spec in [
        None,
        Some(icrc21::DisplayMessageType::GenericDisplay),
        Some(icrc21::DisplayMessageType::FieldsDisplay),
    ] {
        roundtrip_value(&icrc21::ConsentMessageRequest {
            method: "icrc1_transfer".into(),
            arg: vec![1, 2, 3],
            user_preferences: icrc21::ConsentMessageSpec {
                metadata: metadata.clone(),
                device_spec,
            },
        });
    }
    for consent_message in [
        icrc21::ConsentMessage::GenericDisplayMessage("transfer".into()),
        icrc21::ConsentMessage::FieldsDisplayMessage(icrc21::FieldsDisplay {
            intent: "transfer".into(),
            fields: vec![
                (
                    "amount".into(),
                    icrc21::Value::TokenAmount {
                        decimals: 8,
                        amount: 100,
                        symbol: "ICP".into(),
                    },
                ),
                (
                    "time".into(),
                    icrc21::Value::TimestampSeconds { amount: 42 },
                ),
                (
                    "duration".into(),
                    icrc21::Value::DurationSeconds { amount: 60 },
                ),
                (
                    "description".into(),
                    icrc21::Value::Text {
                        content: "external consent".into(),
                    },
                ),
            ],
        }),
    ] {
        roundtrip_value(&icrc21::ConsentInfo {
            consent_message,
            metadata: metadata.clone(),
        });
    }
    for error in [
        icrc21::Icrc21Error::UnsupportedCanisterCall(icrc21::ErrorInfo {
            description: "unsupported".into(),
        }),
        icrc21::Icrc21Error::ConsentMessageUnavailable(icrc21::ErrorInfo {
            description: "unavailable".into(),
        }),
        icrc21::Icrc21Error::InsufficientPayment(icrc21::ErrorInfo {
            description: "insufficient".into(),
        }),
        icrc21::Icrc21Error::GenericError {
            error_code: 42u64.into(),
            description: "error".into(),
        },
    ] {
        roundtrip_value(&error);
    }
}

#[test]
fn external_transfer_and_consent_shapes_roundtrip() {
    transfers_and_consent_contracts();
}

#[test]
fn selected_icrc_types_match_frozen_registry_schemas() {
    use crate::icrc_ledger_types::{self as ledger, icrc21};
    // Generated independently by icrc-ledger-types =0.2.0 and Candid =0.10.39.
    // This includes recursive values, defaulted fields and query callbacks.
    let env = support::frozen_schema(include_str!(
        "../../../tests/fixtures/icrc-reference-schema.json"
    ));
    use support::assert_named_schema as equal;
    macro_rules! check {
        ($($name:ident),+ $(,)?) => { $(equal::<ledger::$name>(&env, concat!("Expected", stringify!($name)));)+ };
    }
    check!(
        Account,
        MetadataValue,
        Value,
        ICRC3Value,
        TransferArg,
        Memo,
        TransferError,
        AllowanceArgs,
        Allowance,
        GetArchivesArgs,
        ICRC3ArchiveInfo,
        ArchivedBlocks,
        BlockWithId,
        GetBlocksRequest,
        GetBlocksResult,
        ICRC3DataCertificate,
        SupportedBlockType
    );
    equal::<ledger::QueryArchiveFn<Vec<ledger::GetBlocksRequest>, ledger::GetBlocksResult>>(
        &env,
        "ExpectedQueryArchiveFn",
    );
    macro_rules! consent {
        ($($name:ident),+ $(,)?) => { $(equal::<icrc21::$name>(&env, concat!("Expected", stringify!($name)));)+ };
    }
    consent!(
        ConsentMessageMetadata,
        ConsentMessageRequest,
        ConsentMessageSpec,
        DisplayMessageType,
        ConsentInfo,
        ConsentMessage,
        FieldsDisplay,
        ErrorInfo,
        Icrc21Error
    );
    equal::<icrc21::Value>(&env, "ExpectedConsentValue");
}