├── assets ├── nft │ ├── nft1.png │ └── nft2.png └── nft_image.jpg ├── wasm ├── icrc7.wasm.gz ├── ic_canister_assets.wasm.gz ├── ic_canister_assets.did └── icrc7.did ├── src ├── icrc7_types │ ├── src │ │ ├── lib.rs │ │ ├── errors.rs │ │ ├── icrc37_types.rs │ │ ├── icrc7_types.rs │ │ └── icrc3_types.rs │ └── Cargo.toml ├── icrc7_archive │ ├── wasm │ │ ├── icrc7_archive.wasm │ │ ├── icrc7_archive.wasm.gz │ │ └── icrc7_archive.did │ ├── src │ │ ├── lib.rs │ │ ├── guards.rs │ │ ├── candid_file_generator.rs │ │ ├── cycles.rs │ │ ├── init_method.rs │ │ ├── update_method.rs │ │ ├── query_method.rs │ │ ├── state.rs │ │ └── types.rs │ ├── Cargo.toml │ └── icrc7_archive.did ├── icrc7_launchpad │ ├── src │ │ ├── lib.rs │ │ ├── candid_file_generator.rs │ │ └── update_method.rs │ ├── Cargo.toml │ └── icrc7_launchpad.did └── icrc7 │ ├── src │ ├── lib.rs │ ├── guards.rs │ ├── cycles.rs │ ├── candid_file_generator.rs │ ├── utils.rs │ ├── icrc3_query_method.rs │ ├── update_method.rs │ ├── memory.rs │ ├── icrc37_update_method.rs │ ├── icrc37_query_method.rs │ ├── archive.rs │ ├── errors.rs │ ├── query_method.rs │ ├── init_method.rs │ ├── icrc37_types.rs │ ├── icrc7_types.rs │ └── icrc3_types.rs │ ├── Cargo.toml │ └── icrc7.did ├── canister_ids.json ├── Cargo.toml ├── scripts ├── wasm.sh ├── icrc7_archive.sh ├── uploader.sh └── icrc7.sh ├── tests ├── Cargo.toml └── src │ └── lib.rs ├── .gitignore ├── dfx.json ├── LICENSE ├── icrc7_launchpad.sh └── README.md /assets/nft/nft1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuminfei/icrc7_launchpad/HEAD/assets/nft/nft1.png -------------------------------------------------------------------------------- /assets/nft/nft2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuminfei/icrc7_launchpad/HEAD/assets/nft/nft2.png -------------------------------------------------------------------------------- /wasm/icrc7.wasm.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuminfei/icrc7_launchpad/HEAD/wasm/icrc7.wasm.gz -------------------------------------------------------------------------------- /assets/nft_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuminfei/icrc7_launchpad/HEAD/assets/nft_image.jpg -------------------------------------------------------------------------------- /src/icrc7_types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod icrc3_types; 2 | pub mod icrc37_types; 3 | pub mod icrc7_types; 4 | pub mod errors; 5 | -------------------------------------------------------------------------------- /wasm/ic_canister_assets.wasm.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuminfei/icrc7_launchpad/HEAD/wasm/ic_canister_assets.wasm.gz -------------------------------------------------------------------------------- /src/icrc7_archive/wasm/icrc7_archive.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuminfei/icrc7_launchpad/HEAD/src/icrc7_archive/wasm/icrc7_archive.wasm -------------------------------------------------------------------------------- /src/icrc7_archive/wasm/icrc7_archive.wasm.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuminfei/icrc7_launchpad/HEAD/src/icrc7_archive/wasm/icrc7_archive.wasm.gz -------------------------------------------------------------------------------- /src/icrc7_launchpad/src/lib.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk_macros::export_candid; 2 | 3 | pub mod update_method; 4 | pub mod candid_file_generator; 5 | 6 | export_candid!(); 7 | -------------------------------------------------------------------------------- /canister_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "icrc7": { 3 | "ic": "niqkt-7qaaa-aaaah-ad6zq-cai" 4 | }, 5 | "ic_canister_assets": { 6 | "ic": "fadih-eiaaa-aaaah-adu7q-cai" 7 | } 8 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "src/icrc7_launchpad", 4 | "src/icrc7", 5 | "src/icrc7_archive", 6 | "src/icrc7_types", 7 | "tests" 8 | ] 9 | resolver = "2" 10 | -------------------------------------------------------------------------------- /src/icrc7_archive/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod state; 2 | pub mod types; 3 | pub mod guards; 4 | pub mod cycles; 5 | pub mod init_method; 6 | pub mod query_method; 7 | pub mod update_method; 8 | pub mod candid_file_generator; -------------------------------------------------------------------------------- /scripts/wasm.sh: -------------------------------------------------------------------------------- 1 | cargo build --release --target wasm32-unknown-unknown --package icrc7 2 | ic-wasm target/wasm32-unknown-unknown/release/icrc7.wasm -o target/wasm32-unknown-unknown/release/icrc7.wasm shrink 3 | gzip -f -c target/wasm32-unknown-unknown/release/icrc7.wasm > wasm/icrc7.wasm.gz -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde = "1.0.159" 10 | sha2 = "0.10.6" 11 | hex = "0.4.3" 12 | percent-encoding = "2.2.0" 13 | regex = "1.6.0" 14 | -------------------------------------------------------------------------------- /scripts/icrc7_archive.sh: -------------------------------------------------------------------------------- 1 | dfx deploy icrc7_archive --argument '(record{ 2 | first_index= 0; 3 | index_type= variant {Stable}; 4 | max_pages= 10; 5 | max_records= 100 6 | })' 7 | 8 | dfx canister call icrc7_archive append_blocks '(vec{ 9 | variant { 10 | Int= 1 11 | }; 12 | variant { 13 | Int= 1 14 | } 15 | })' -------------------------------------------------------------------------------- /src/icrc7_archive/src/guards.rs: -------------------------------------------------------------------------------- 1 | use crate::state::STATE; 2 | use ic_cdk::caller; 3 | 4 | #[inline(always)] 5 | pub fn owner_guard() -> Result<(), String> { 6 | let owner = STATE.with(|s| s.borrow().ledger_id); 7 | 8 | if caller() == owner { 9 | Ok(()) 10 | } else { 11 | Err(String::from("The caller is not the owner of contract")) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Various IDEs and Editors 2 | .vscode/ 3 | .idea/ 4 | **/*~ 5 | 6 | # Mac OSX temporary files 7 | .DS_Store 8 | **/.DS_Store 9 | 10 | # dfx temporary files 11 | .dfx/ 12 | 13 | # generated files 14 | **/declarations/ 15 | 16 | # rust 17 | target/ 18 | 19 | # frontend code 20 | node_modules/ 21 | dist/ 22 | .svelte-kit/ 23 | 24 | # environment variables 25 | .env 26 | -------------------------------------------------------------------------------- /scripts/uploader.sh: -------------------------------------------------------------------------------- 1 | dfx deploy ic_canister_assets 2 | 3 | cargo test --package tests --lib -- --show-output 4 | 5 | dfx canister call ic_canister_assets permission_set_admin '(principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe")' 6 | 7 | dfx canister call ic_canister_assets permission_is_admin '(principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe")' 8 | 9 | -------------------------------------------------------------------------------- /src/icrc7_launchpad/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icrc7_launchpad" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | candid = "0.10.7" 13 | ic-cdk = "0.13.2" 14 | ic-cdk-macros = "0.13.2" 15 | icrc-ledger-types = "0.1.1" 16 | serde = { version = "1.0.188", features = ["derive"] } 17 | icrc7-types = { path = "../icrc7_types" } -------------------------------------------------------------------------------- /src/icrc7/src/lib.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk_macros::export_candid; 2 | 3 | pub mod icrc7_types; 4 | pub mod icrc37_types; 5 | pub mod icrc3_types; 6 | pub mod init_method; 7 | pub mod memory; 8 | pub mod query_method; 9 | pub mod icrc37_query_method; 10 | pub mod icrc3_query_method; 11 | pub mod state; 12 | pub mod update_method; 13 | pub mod icrc37_update_method; 14 | pub mod cycles; 15 | pub mod utils; 16 | pub mod candid_file_generator; 17 | pub mod guards; 18 | pub mod errors; 19 | pub mod archive; 20 | 21 | use icrc7_types::*; 22 | 23 | export_candid!(); 24 | -------------------------------------------------------------------------------- /src/icrc7_archive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icrc7_archive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | candid = "0.10.7" 12 | ciborium = "0.2.1" 13 | ic-cdk = "0.13.2" 14 | ic-cdk-macros = "0.13.2" 15 | ic-stable-structures = "0.6.1" 16 | serde = { version = "1", features = ["derive"]} 17 | serde_derive = "1.0.166" 18 | serde_bytes = "0.11.11" 19 | icrc-ledger-types = "0.1.5" 20 | num-bigint = "0.4" 21 | num-traits = "0.2" -------------------------------------------------------------------------------- /src/icrc7_types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icrc7-types" 3 | version = "0.1.0" 4 | authors = ["Terry.Tu "] 5 | edition = "2021" 6 | description = "ICRC7 basic type definitions" 7 | license = "MIT" 8 | repository = "https://github.com/tuminfei/icrc7_launchpad" 9 | homepage = "https://github.com/tuminfei/icrc7_launchpad" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | icrc-ledger-types = "0.1.5" 15 | candid = "0.10.7" 16 | serde = { version = "1.0.188", features = ["derive"] } 17 | serde_bytes = "0.11" 18 | ic-stable-structures = "0.6.1" 19 | -------------------------------------------------------------------------------- /src/icrc7/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icrc7" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | candid = "0.10.7" 13 | ciborium = "0.2.1" 14 | ic-cdk = "0.13.2" 15 | ic-cdk-macros = "0.13.2" 16 | ic-stable-structures = "0.6.1" 17 | ic-certified-map = "0.4" 18 | icrc-ledger-types = "0.1.5" 19 | serde = { version = "1.0.188", features = ["derive"] } 20 | serde_bytes = "0.11" 21 | serde_cbor = "0.11.2" 22 | crc32fast = "1.3" 23 | sha2 = "0.10.2" 24 | hex = "0.4" 25 | ic-cdk-timers = "0.7.0" 26 | -------------------------------------------------------------------------------- /src/icrc7_launchpad/src/candid_file_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::update_method::Arg; 2 | use candid::{export_service, Principal}; 3 | use ic_cdk_macros::query; 4 | 5 | #[query(name = "__get_candid_interface_tmp_hack")] 6 | fn export_candid() -> String { 7 | export_service!(); 8 | __export_service() 9 | } 10 | 11 | #[cfg(test)] 12 | mod tests { 13 | use super::*; 14 | 15 | #[test] 16 | fn save_candid() { 17 | use std::env; 18 | use std::fs::write; 19 | use std::path::PathBuf; 20 | 21 | let dir = PathBuf::from(env::current_dir().unwrap()); 22 | write(dir.join("icrc7_launchpad.did"), export_candid()).expect("Write failed."); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/icrc7/src/guards.rs: -------------------------------------------------------------------------------- 1 | use crate::state::STATE; 2 | use candid::Principal; 3 | use ic_cdk::caller; 4 | 5 | #[inline(always)] 6 | pub fn owner_guard() -> Result<(), String> { 7 | let owner = STATE 8 | .with(|s| s.borrow().icrc7_minting_authority()) 9 | .ok_or_else(|| String::from("The canister not set owner"))?; 10 | 11 | if caller() == owner.owner { 12 | Ok(()) 13 | } else { 14 | Err(String::from("The caller is not the owner of contract")) 15 | } 16 | } 17 | 18 | #[inline(always)] 19 | pub fn authenticated_guard() -> Result<(), String> { 20 | if ic_cdk::caller() == Principal::anonymous() { 21 | Err("anonymous user is not allowed".to_string()) 22 | } else { 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/icrc7_archive/src/candid_file_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::cycles::WalletReceiveResult; 2 | use crate::types::*; 3 | use candid::export_service; 4 | use candid::Principal; 5 | use ic_cdk_macros::query; 6 | use icrc_ledger_types::icrc3::blocks::GetBlocksRequest; 7 | 8 | #[query(name = "__get_candid_interface_tmp_hack")] 9 | fn export_candid() -> String { 10 | export_service!(); 11 | __export_service() 12 | } 13 | 14 | #[cfg(test)] 15 | mod tests { 16 | use super::*; 17 | 18 | #[test] 19 | fn save_candid() { 20 | use std::env; 21 | use std::fs::write; 22 | use std::path::PathBuf; 23 | 24 | let dir = PathBuf::from(env::current_dir().unwrap()); 25 | write(dir.join("icrc7_archive.did"), export_candid()).expect("Write failed."); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/icrc7/src/cycles.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk_macros::{query, update}; 2 | 3 | #[derive(candid::CandidType, candid::Deserialize, Debug)] 4 | pub struct WalletReceiveResult { 5 | accepted: u64, 6 | } 7 | 8 | #[query(name = "wallet_balance")] 9 | pub fn wallet_balance() -> candid::Nat { 10 | return candid::Nat::from(ic_cdk::api::canister_balance128()); 11 | } 12 | 13 | #[update(name = "wallet_receive")] 14 | pub fn wallet_receive() -> WalletReceiveResult { 15 | let available = ic_cdk::api::call::msg_cycles_available128(); 16 | 17 | if available == 0 { 18 | return WalletReceiveResult { accepted: 0 }; 19 | } 20 | let accepted = ic_cdk::api::call::msg_cycles_accept128(available); 21 | assert!(accepted == available); 22 | WalletReceiveResult { 23 | accepted: accepted as u64, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/icrc7_archive/src/cycles.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk_macros::{query, update}; 2 | 3 | #[derive(candid::CandidType, candid::Deserialize, Debug)] 4 | pub struct WalletReceiveResult { 5 | accepted: u64, 6 | } 7 | 8 | #[query(name = "wallet_balance")] 9 | pub fn wallet_balance() -> candid::Nat { 10 | return candid::Nat::from(ic_cdk::api::canister_balance128()); 11 | } 12 | 13 | #[update(name = "wallet_receive")] 14 | pub fn wallet_receive() -> WalletReceiveResult { 15 | let available = ic_cdk::api::call::msg_cycles_available128(); 16 | 17 | if available == 0 { 18 | return WalletReceiveResult { accepted: 0 }; 19 | } 20 | let accepted = ic_cdk::api::call::msg_cycles_accept128(available); 21 | assert!(accepted == available); 22 | WalletReceiveResult { 23 | accepted: accepted as u64, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "canisters": { 3 | "ic_canister_assets": { 4 | "candid": "wasm/ic_canister_assets.did", 5 | "package": "ic_canister_assets", 6 | "type": "custom", 7 | "wasm": "wasm/ic_canister_assets.wasm.gz" 8 | }, 9 | "icrc7_launchpad": { 10 | "candid": "src/icrc7_launchpad/icrc7_launchpad.did", 11 | "package": "icrc7_launchpad", 12 | "type": "rust" 13 | }, 14 | "icrc7": { 15 | "candid": "src/icrc7/icrc7.did", 16 | "package": "icrc7", 17 | "type": "rust" 18 | }, 19 | "icrc7_archive": { 20 | "candid": "src/icrc7_archive/icrc7_archive.did", 21 | "package": "icrc7_archive", 22 | "type": "rust" 23 | } 24 | }, 25 | "defaults": { 26 | "build": { 27 | "args": "", 28 | "packtool": "" 29 | } 30 | }, 31 | "output_env_file": ".env", 32 | "version": 1 33 | } 34 | -------------------------------------------------------------------------------- /src/icrc7/src/candid_file_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::cycles::WalletReceiveResult; 2 | use crate::icrc37_types::*; 3 | use crate::icrc3_types::*; 4 | use crate::icrc7_types::*; 5 | use candid::export_service; 6 | use candid::{Nat, Principal}; 7 | use ic_cdk_macros::query; 8 | use icrc_ledger_types::{icrc1::account::Account, icrc3::blocks::DataCertificate}; 9 | 10 | #[query(name = "__get_candid_interface_tmp_hack")] 11 | fn export_candid() -> String { 12 | export_service!(); 13 | __export_service() 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use super::*; 19 | 20 | #[test] 21 | fn save_candid() { 22 | use std::env; 23 | use std::fs::write; 24 | use std::path::PathBuf; 25 | 26 | let dir = PathBuf::from(env::current_dir().unwrap()); 27 | write(dir.join("icrc7.did"), export_candid()).expect("Write failed."); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Terry Tu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/icrc7/src/utils.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use icrc_ledger_types::icrc::generic_value::{self, Value}; 3 | use icrc_ledger_types::icrc1::account::{Account, Subaccount, DEFAULT_SUBACCOUNT}; 4 | 5 | pub fn account_transformer(account: Account) -> Account { 6 | if let Some(_) = account.subaccount { 7 | account 8 | } else { 9 | Account { 10 | owner: account.owner, 11 | subaccount: Some(DEFAULT_SUBACCOUNT.clone()), 12 | } 13 | } 14 | } 15 | 16 | pub fn default_account(owner: &Principal) -> Account { 17 | Account { 18 | owner: owner.clone(), 19 | subaccount: Some(DEFAULT_SUBACCOUNT.clone()), 20 | } 21 | } 22 | 23 | pub fn burn_subaccount() -> Subaccount { 24 | let mut bytes = [0; 32]; 25 | let slice = b"BURN SUBACCOUNT"; 26 | bytes[0..15].copy_from_slice(slice); 27 | bytes 28 | } 29 | 30 | pub fn burn_account() -> Account { 31 | Account { 32 | owner: ic_cdk::api::id(), 33 | subaccount: Some(burn_subaccount()), 34 | } 35 | } 36 | 37 | pub fn hash_icrc_value(value: &Value) -> generic_value::Hash { 38 | return value.hash(); 39 | } 40 | -------------------------------------------------------------------------------- /src/icrc7/src/icrc3_query_method.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk_macros::query; 2 | 3 | use crate::icrc3_types::{ 4 | BlockType, GetArchiveArgs, GetArchivesResultItem, GetBlocksArgs, GetBlocksResult, Tip, 5 | }; 6 | use crate::state::STATE; 7 | use icrc_ledger_types::icrc3::blocks::DataCertificate; 8 | 9 | // Returns all the supported block types. 10 | #[query] 11 | pub fn icrc3_supported_block_types() -> Vec { 12 | STATE.with(|s| s.borrow().archive_ledger_info.supported_blocks.clone()) 13 | } 14 | 15 | // Listing all the canisters containing its blocks 16 | #[query] 17 | pub fn icrc3_get_archives(arg: GetArchiveArgs) -> Vec { 18 | STATE.with(|s| s.borrow().icrc3_get_archives(arg)) 19 | } 20 | 21 | // The Ledger MUST certify the last block (tip) recorded 22 | #[query] 23 | pub fn icrc3_get_tip_certificate() -> Option { 24 | STATE.with(|s| s.borrow().icrc3_get_tip_certificate()) 25 | } 26 | 27 | // Get icrc3 blocks information 28 | #[query] 29 | pub fn icrc3_get_blocks(args: GetBlocksArgs) -> GetBlocksResult { 30 | STATE.with(|s| s.borrow().icrc3_get_blocks(args)) 31 | } 32 | 33 | // Returns the latest hash and lastest index along with a witness 34 | #[query] 35 | pub fn get_tip() -> Tip { 36 | STATE.with(|s| s.borrow().icrc3_get_tip()) 37 | } 38 | -------------------------------------------------------------------------------- /src/icrc7/src/update_method.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use ic_cdk_macros::update; 3 | 4 | use crate::{ 5 | guards::owner_guard, state::STATE, BurnArg, BurnResult, MintArg, MintResult, TransferArg, 6 | TransferResult, 7 | }; 8 | use icrc_ledger_types::icrc1::account::Account; 9 | 10 | #[update] 11 | pub fn icrc7_transfer(args: Vec) -> Vec> { 12 | let caller = ic_cdk::caller(); 13 | STATE.with(|s| s.borrow_mut().icrc7_transfer(&caller, args)) 14 | } 15 | 16 | #[update] 17 | pub fn mint(arg: MintArg) -> MintResult { 18 | let caller = ic_cdk::caller(); 19 | if caller == Principal::anonymous() { 20 | return Err(crate::errors::MintError::GenericBatchError { 21 | error_code: 100, 22 | message: "Anonymous Identity".into(), 23 | }); 24 | } 25 | STATE.with(|s| s.borrow_mut().mint(&caller, arg)) 26 | } 27 | 28 | #[update] 29 | pub fn burn(args: Vec) -> Vec> { 30 | let caller = ic_cdk::caller(); 31 | STATE.with(|s| s.borrow_mut().burn(&caller, args)) 32 | } 33 | 34 | #[update(guard = "owner_guard")] 35 | pub fn set_minting_authority(minting_account: Account) -> bool { 36 | STATE.with(|s| s.borrow_mut().minting_authority = Some(minting_account)); 37 | return true; 38 | } 39 | -------------------------------------------------------------------------------- /src/icrc7_archive/src/init_method.rs: -------------------------------------------------------------------------------- 1 | use candid::candid_method; 2 | use ic_cdk_macros::{init, post_upgrade, pre_upgrade}; 3 | use std::mem; 4 | 5 | use ic_cdk::storage; 6 | 7 | use crate::{ 8 | state::{StableState, State, DEFAULT_MAX_TRANSACTIONS_PER_GET_TRANSACTION_RESPONSE, STATE}, 9 | types::ArchiveInitArgs, 10 | }; 11 | 12 | #[init] 13 | #[candid_method(init)] 14 | pub fn init(arg: ArchiveInitArgs) { 15 | let ledger_id = ic_cdk::caller(); 16 | STATE.with(|s| { 17 | let mut s = s.borrow_mut(); 18 | let state = State { 19 | max_pages: arg.max_pages, 20 | max_records: arg.max_records, 21 | block_index_offset: arg.first_index, 22 | block_index: arg.first_index, 23 | max_transactions_per_response: DEFAULT_MAX_TRANSACTIONS_PER_GET_TRANSACTION_RESPONSE, 24 | index_type: arg.index_type, 25 | ledger_id, 26 | }; 27 | *s = state; 28 | }); 29 | } 30 | 31 | #[pre_upgrade] 32 | fn pre_upgrade() { 33 | let state = STATE.with(|state| mem::take(&mut *state.borrow_mut())); 34 | let stable_state = StableState { state }; 35 | storage::stable_save((stable_state,)).unwrap(); 36 | } 37 | #[post_upgrade] 38 | fn post_upgrade() { 39 | let (StableState { state },) = storage::stable_restore().unwrap(); 40 | STATE.with(|state0| *state0.borrow_mut() = state); 41 | } 42 | -------------------------------------------------------------------------------- /src/icrc7/src/memory.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | icrc37_types::{CollectionApprovalInfo, TokenApprovalInfo, UserAccount}, 3 | icrc7_types::Transaction, 4 | state::{Icrc7Token, MEMORY_MANAGER}, 5 | }; 6 | use ic_stable_structures::{ 7 | memory_manager::{MemoryId, VirtualMemory}, 8 | DefaultMemoryImpl, StableBTreeMap, 9 | }; 10 | 11 | // A memory for upgrades, where data from the heap can be serialized/deserialized. 12 | const UPGRADES: MemoryId = MemoryId::new(0); 13 | 14 | pub type Memory = VirtualMemory; 15 | 16 | pub fn get_upgrades_memory() -> Memory { 17 | MEMORY_MANAGER.with(|m| m.borrow().get(UPGRADES)) 18 | } 19 | 20 | pub fn get_token_map_memory() -> StableBTreeMap { 21 | StableBTreeMap::init(MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1)))) 22 | } 23 | 24 | pub fn get_log_memory() -> StableBTreeMap { 25 | StableBTreeMap::init(MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(2)))) 26 | } 27 | 28 | pub fn get_token_approvals_memory() -> StableBTreeMap { 29 | StableBTreeMap::init(MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(3)))) 30 | } 31 | 32 | pub fn get_collection_approvals_memory( 33 | ) -> StableBTreeMap { 34 | StableBTreeMap::init(MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(4)))) 35 | } 36 | -------------------------------------------------------------------------------- /src/icrc7_archive/src/update_method.rs: -------------------------------------------------------------------------------- 1 | use candid::{candid_method, Principal}; 2 | use ic_cdk_macros::update; 3 | 4 | use crate::{ 5 | guards::owner_guard, 6 | state::{with_archive_opts, with_blocks, BLOCK_MAP, STATE}, 7 | types::Block, 8 | }; 9 | 10 | #[update(guard = "owner_guard")] 11 | #[candid_method(update)] 12 | fn append_blocks(new_blocks: Vec) { 13 | let max_records = with_archive_opts(|opts| opts.max_records); 14 | let mut block_index = with_archive_opts(|opts| opts.block_index); 15 | 16 | with_blocks(|blocks| { 17 | let new_blocks_size = new_blocks.len() as u128; 18 | if max_records < (blocks.len() as u128).saturating_add(new_blocks_size) { 19 | ic_cdk::api::trap("no space left"); 20 | } 21 | }); 22 | 23 | for block in new_blocks { 24 | block_index = block_index + 1; 25 | BLOCK_MAP.with(|p| { 26 | let mut block_map = p.borrow_mut(); 27 | 28 | block_map.insert(block_index, block) 29 | }); 30 | } 31 | 32 | STATE.with(|s| { 33 | let mut state = s.borrow_mut(); 34 | state.block_index = block_index; 35 | }) 36 | } 37 | 38 | #[update(guard = "owner_guard")] 39 | pub fn update_owner(owner: Principal) -> bool { 40 | STATE.with(|s| { 41 | let mut state = s.borrow_mut(); 42 | state.ledger_id = owner; 43 | }); 44 | return true; 45 | } 46 | -------------------------------------------------------------------------------- /src/icrc7_launchpad/icrc7_launchpad.did: -------------------------------------------------------------------------------- 1 | type Arg = record { 2 | icrc7_supply_cap : opt nat; 3 | icrc7_description : opt text; 4 | tx_window : opt nat64; 5 | icrc7_max_query_batch_size : opt nat16; 6 | permitted_drift : opt nat64; 7 | archive_init : opt InitArchiveArg; 8 | icrc7_max_take_value : opt nat; 9 | icrc7_max_memo_size : opt nat32; 10 | icrc7_symbol : text; 11 | icrc7_max_update_batch_size : opt nat16; 12 | icrc7_atomic_batch_transfers : opt bool; 13 | approval_init : opt InitApprovalsArg; 14 | icrc7_default_take_value : opt nat; 15 | icrc7_logo : opt text; 16 | icrc7_name : text; 17 | }; 18 | type IndexType = variant { Stable; StableTyped; Managed }; 19 | type InitApprovalsArg = record { 20 | max_approvals : opt nat16; 21 | max_approvals_per_token_or_collection : opt nat16; 22 | settle_to_approvals : opt nat16; 23 | max_revoke_approvals : opt nat16; 24 | collection_approval_requires_token : opt bool; 25 | }; 26 | type InitArchiveArg = record { 27 | maxRecordsToArchive : nat; 28 | archiveIndexType : IndexType; 29 | maxArchivePages : nat; 30 | settleToRecords : nat; 31 | archiveCycles : nat; 32 | maxActiveRecords : nat; 33 | maxRecordsInArchiveInstance : nat; 34 | archiveControllers : opt opt vec principal; 35 | }; 36 | type Result = variant { Ok : principal; Err : text }; 37 | service : { 38 | __get_candid_interface_tmp_hack : () -> (text) query; 39 | mint_collection_canister : (Arg) -> (Result); 40 | } -------------------------------------------------------------------------------- /icrc7_launchpad.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ICRC7_LAUNCHPAD_CANISTER_ID="icrc7_launchpad" 4 | 5 | # Arguments for the `mint_collection_canister` method 6 | ARG=$(cat <) -> Vec> { 15 | let caller = ic_cdk::caller(); 16 | STATE.with(|s| s.borrow_mut().approve(&caller, args)) 17 | } 18 | 19 | #[update(guard = "authenticated_guard")] 20 | pub fn icrc37_approve_collection( 21 | args: Vec, 22 | ) -> Vec> { 23 | let caller = ic_cdk::caller(); 24 | 25 | STATE.with(|s| s.borrow_mut().collection_approve(&caller, args)) 26 | } 27 | 28 | // Revokes the specified approvals for a token given by `token_id` from the set of active approvals. 29 | #[ic_cdk::update(guard = "authenticated_guard")] 30 | pub fn icrc37_revoke_token_approvals( 31 | args: Vec, 32 | ) -> Vec> { 33 | let caller = ic_cdk::caller(); 34 | 35 | STATE.with(|s| s.borrow_mut().revoke_approve(&caller, args)) 36 | } 37 | 38 | // Revokes collection-level approvals from the set of active approvals. 39 | #[ic_cdk::update(guard = "authenticated_guard")] 40 | pub fn icrc37_revoke_collection_approvals( 41 | args: Vec, 42 | ) -> Vec> { 43 | let caller = ic_cdk::caller(); 44 | 45 | STATE.with(|s| s.borrow_mut().revoke_collection_approve(&caller, args)) 46 | } 47 | 48 | // Transfers one or more tokens from the from account to the to account. 49 | // The transfer can be initiated by the holder of the tokens. 50 | #[ic_cdk::update(guard = "authenticated_guard")] 51 | pub fn icrc37_transfer_from(args: Vec) -> Vec> { 52 | let caller = ic_cdk::caller(); 53 | 54 | STATE.with(|s| s.borrow_mut().transfer_from(&caller, args)) 55 | } 56 | -------------------------------------------------------------------------------- /src/icrc7/src/icrc37_query_method.rs: -------------------------------------------------------------------------------- 1 | use candid::Nat; 2 | use ic_cdk_macros::query; 3 | use icrc_ledger_types::icrc1::account::Account; 4 | 5 | use crate::{ 6 | icrc37_types::{CollectionApproval, IsApprovedArg, Metadata, TokenApproval}, 7 | state::STATE, 8 | }; 9 | 10 | // Returns the approval-related metadata of the ledger implementation. 11 | #[query] 12 | pub fn icrc37_metadata() -> Metadata { 13 | STATE.with(|s| s.borrow().icrc37_metadata()) 14 | } 15 | 16 | // Returns the maximum number of approvals this ledger implementation allows to be active per token or per principal for the collection. 17 | #[query] 18 | pub fn icrc37_max_approvals_per_token_or_collection() -> Option { 19 | STATE.with(|s| { 20 | Some(Nat::from( 21 | s.borrow() 22 | .approval_ledger_info 23 | .max_approvals_per_token_or_collection, 24 | )) 25 | }) 26 | } 27 | 28 | // Returns the maximum number of approvals that may be revoked in a single invocation of `icrc37_revoke_token_approvals` or `icrc37_revoke_collection_approvals`. 29 | #[query] 30 | pub fn icrc37_max_revoke_approvals() -> Option { 31 | STATE.with(|s| { 32 | Some(Nat::from( 33 | s.borrow().approval_ledger_info.max_revoke_approvals, 34 | )) 35 | }) 36 | } 37 | 38 | // Returns `true` if an active approval, i.e., a token-level approval or collection-level approval 39 | #[query] 40 | pub fn icrc37_is_approved(args: Vec) -> Vec { 41 | STATE.with(|s| s.borrow().icrc37_is_approved(args)) 42 | } 43 | 44 | // Returns the token-level approvals that exist for the given `token_id`. 45 | #[query] 46 | pub fn icrc37_get_token_approvals( 47 | token_id: u128, 48 | prev: Option, 49 | take: Option, 50 | ) -> Vec { 51 | STATE.with(|s| s.borrow().icrc37_get_token_approvals(token_id, prev, take)) 52 | } 53 | 54 | // Returns the collection-level approvals that exist for the specified `owner`. 55 | #[ic_cdk::query] 56 | pub fn icrc37_get_collection_approvals( 57 | owner: Account, 58 | prev: Option, 59 | take: Option, 60 | ) -> Vec { 61 | STATE.with(|s| { 62 | s.borrow() 63 | .icrc37_get_collection_approvals(owner, prev, take) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /src/icrc7_archive/icrc7_archive.did: -------------------------------------------------------------------------------- 1 | type ArchiveInitArgs = record { 2 | max_records : nat; 3 | index_type : IndexType; 4 | first_index : nat; 5 | max_pages : nat; 6 | }; 7 | type ArchivedTransactionResponse = record { 8 | args : vec TransactionRange; 9 | callback : func (vec QueryBlock) -> (GetTransactionsResult) query; 10 | }; 11 | type Block = variant { 12 | Int : int; 13 | Map : vec record { text; Value }; 14 | Nat : nat; 15 | Nat64 : nat64; 16 | Blob : blob; 17 | Text : text; 18 | Array : vec Value; 19 | }; 20 | type GetBlocksRequest = record { start : nat; length : nat }; 21 | type GetBlocksResult = record { 22 | log_length : nat; 23 | blocks : vec QueryBlock; 24 | archived_blocks : vec ArchivedTransactionResponse; 25 | }; 26 | type GetTransactionsResult = record { 27 | log_length : nat; 28 | blocks : Vec; 29 | archived_blocks : blob; 30 | }; 31 | type IndexType = variant { Stable; StableTyped; Managed }; 32 | type QueryBlock = record { id : nat; block : Block }; 33 | type TransactionRange = record { start : nat; length : nat }; 34 | type Value = variant { 35 | Int : int; 36 | Map : vec record { text; Value }; 37 | Nat : nat; 38 | Nat64 : nat64; 39 | Blob : blob; 40 | Text : text; 41 | Array : vec Value; 42 | }; 43 | type Vec = vec record { 44 | args : vec record { start : nat; length : nat }; 45 | callback : func ( 46 | vec record { 47 | id : nat; 48 | block : variant { 49 | Int : int; 50 | Map : vec record { text; Value }; 51 | Nat : nat; 52 | Nat64 : nat64; 53 | Blob : blob; 54 | Text : text; 55 | Array : vec Value; 56 | }; 57 | }, 58 | ) -> ( 59 | record { log_length : nat; blocks : Vec; archived_blocks : blob }, 60 | ) query; 61 | }; 62 | type WalletReceiveResult = record { accepted : nat64 }; 63 | service : (ArchiveInitArgs) -> { 64 | __get_candid_interface_tmp_hack : () -> (text) query; 65 | append_blocks : (vec Block) -> (); 66 | get_owner : () -> (principal) query; 67 | get_transaction : (nat) -> (opt Block) query; 68 | icrc3_get_blocks : (vec GetBlocksRequest) -> (GetBlocksResult) query; 69 | remaining_capacity : () -> (nat64) query; 70 | update_owner : (principal) -> (bool); 71 | wallet_balance : () -> (nat) query; 72 | wallet_receive : () -> (WalletReceiveResult); 73 | } -------------------------------------------------------------------------------- /src/icrc7_archive/wasm/icrc7_archive.did: -------------------------------------------------------------------------------- 1 | type ArchiveInitArgs = record { 2 | max_records : nat; 3 | index_type : IndexType; 4 | first_index : nat; 5 | max_pages : nat; 6 | }; 7 | type ArchivedTransactionResponse = record { 8 | args : vec TransactionRange; 9 | callback : func (vec QueryBlock) -> (GetTransactionsResult) query; 10 | }; 11 | type Block = variant { 12 | Int : int; 13 | Map : vec record { text; Value }; 14 | Nat : nat; 15 | Nat64 : nat64; 16 | Blob : vec nat8; 17 | Text : text; 18 | Array : vec Value; 19 | }; 20 | type GetBlocksRequest = record { start : nat; length : nat }; 21 | type GetBlocksResult = record { 22 | log_length : nat; 23 | blocks : vec QueryBlock; 24 | archived_blocks : vec ArchivedTransactionResponse; 25 | }; 26 | type GetTransactionsResult = record { 27 | log_length : nat; 28 | blocks : Vec; 29 | archived_blocks : vec nat8; 30 | }; 31 | type IndexType = variant { Stable; StableTyped; Managed }; 32 | type QueryBlock = record { id : nat; block : Block }; 33 | type TransactionRange = record { start : nat; length : nat }; 34 | type Value = variant { 35 | Int : int; 36 | Map : vec record { text; Value }; 37 | Nat : nat; 38 | Nat64 : nat64; 39 | Blob : vec nat8; 40 | Text : text; 41 | Array : vec Value; 42 | }; 43 | type Vec = vec record { 44 | args : vec record { start : nat; length : nat }; 45 | callback : func ( 46 | vec record { 47 | id : nat; 48 | block : variant { 49 | Int : int; 50 | Map : vec record { text; Value }; 51 | Nat : nat; 52 | Nat64 : nat64; 53 | Blob : vec nat8; 54 | Text : text; 55 | Array : vec Value; 56 | }; 57 | }, 58 | ) -> ( 59 | record { log_length : nat; blocks : Vec; archived_blocks : vec nat8 }, 60 | ) query; 61 | }; 62 | type WalletReceiveResult = record { accepted : nat64 }; 63 | service : (ArchiveInitArgs) -> { 64 | __get_candid_interface_tmp_hack : () -> (text) query; 65 | append_blocks : (vec Block) -> (); 66 | get_owner : () -> (principal) query; 67 | get_transaction : (nat) -> (opt Block) query; 68 | icrc3_get_blocks : (vec GetBlocksRequest) -> (GetBlocksResult) query; 69 | remaining_capacity : () -> (nat64) query; 70 | update_owner : (principal) -> (bool); 71 | wallet_balance : () -> (nat) query; 72 | wallet_receive : () -> (WalletReceiveResult); 73 | } -------------------------------------------------------------------------------- /src/icrc7/src/archive.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Deserialize, Encode, Principal}; 2 | use ic_cdk::api::management_canister::{ 3 | main::{create_canister, install_code, CreateCanisterArgument, InstallCodeArgument}, 4 | provisional::CanisterSettings, 5 | }; 6 | use serde::Serialize; 7 | 8 | use crate::icrc3_types::{ArchiveCreateArgs, IndexType}; 9 | 10 | pub const ARCHIVE_WASM: &[u8] = 11 | std::include_bytes!("./../../icrc7_archive/wasm/icrc7_archive.wasm.gz"); 12 | 13 | pub const ARCHIVE_DEFAULT_CYCLES: u128 = 10_000_000_000_000; 14 | 15 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 16 | pub struct ArchiveInitArgs { 17 | pub first_index: u128, 18 | pub index_type: IndexType, 19 | pub max_pages: u128, 20 | pub max_records: u128, 21 | } 22 | 23 | impl ArchiveInitArgs { 24 | fn new(max_pages: u128, max_records: u128, first_index: u128) -> Self { 25 | Self { 26 | index_type: IndexType::Stable, 27 | first_index, 28 | max_pages, 29 | max_records, 30 | } 31 | } 32 | } 33 | 34 | #[allow(unused)] 35 | pub async fn create_archive_canister(arg: ArchiveCreateArgs) -> Result { 36 | let mut archive_controllers = vec![ic_cdk::id()]; 37 | 38 | if let Some(Some(controllers)) = arg.controllers { 39 | if !controllers.is_empty() { 40 | archive_controllers.extend(controllers); 41 | } 42 | } 43 | 44 | let principal = match create_canister( 45 | CreateCanisterArgument { 46 | settings: Some(CanisterSettings { 47 | controllers: Some(archive_controllers), 48 | compute_allocation: None, 49 | memory_allocation: None, 50 | freezing_threshold: None, 51 | reserved_cycles_limit: None, 52 | }), 53 | }, 54 | ARCHIVE_DEFAULT_CYCLES, 55 | ) 56 | .await 57 | { 58 | Err((code, msg)) => return Err(format!("Rejection Code: {:?}, Message: {:?}", code, msg)), 59 | Ok((principal,)) => principal.canister_id, 60 | }; 61 | ic_cdk::println!("new archive canister: {}", principal); 62 | 63 | let init_arg = ArchiveInitArgs::new(arg.max_pages, arg.max_records, arg.first_index); 64 | let init_arg = Encode!(&init_arg).unwrap(); 65 | match install_code(InstallCodeArgument { 66 | mode: ic_cdk::api::management_canister::main::CanisterInstallMode::Install, 67 | canister_id: principal, 68 | wasm_module: ARCHIVE_WASM.to_vec(), 69 | arg: init_arg, 70 | }) 71 | .await 72 | { 73 | Ok(()) => Ok(principal), 74 | Err((code, msg)) => Err(format!("Code: {:?}, Message: {:?}", code, msg)), 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /wasm/ic_canister_assets.did: -------------------------------------------------------------------------------- 1 | type CanisterStatusResponse = record { 2 | status : CanisterStatusType; 3 | memory_size : nat; 4 | cycles : nat; 5 | settings : DefiniteCanisterSettings; 6 | idle_cycles_burned_per_day : nat; 7 | module_hash : opt vec nat8; 8 | }; 9 | type CanisterStatusType = variant { stopped; stopping; running }; 10 | type CustomHttpRequest = record { 11 | url : text; 12 | method : text; 13 | body : vec nat8; 14 | headers : vec record { text; text }; 15 | }; 16 | type CustomHttpResponse = record { 17 | body : vec nat8; 18 | headers : vec record { text; text }; 19 | streaming_strategy : opt StreamingStrategy; 20 | status_code : nat16; 21 | }; 22 | type DefiniteCanisterSettings = record { 23 | freezing_threshold : nat; 24 | controllers : vec principal; 25 | memory_allocation : nat; 26 | compute_allocation : nat; 27 | }; 28 | type MaintainingReason = record { created : nat64; message : text }; 29 | type QueryFile = record { 30 | created : nat64; 31 | modified : nat64; 32 | hash : text; 33 | path : text; 34 | size : nat64; 35 | headers : vec record { text; text }; 36 | }; 37 | type StreamingCallbackHttpResponse = record { 38 | token : opt StreamingCallbackToken; 39 | body : vec nat8; 40 | }; 41 | type StreamingCallbackToken = record { 42 | end : nat64; 43 | path : text; 44 | headers : vec record { text; text }; 45 | start : nat64; 46 | params : text; 47 | }; 48 | type StreamingStrategy = variant { 49 | Callback : record { 50 | token : StreamingCallbackToken; 51 | callback : func (StreamingCallbackToken) -> ( 52 | StreamingCallbackHttpResponse, 53 | ) query; 54 | }; 55 | }; 56 | type UploadingArg = record { 57 | chunk : vec nat8; 58 | path : text; 59 | size : nat64; 60 | headers : vec record { text; text }; 61 | index : nat32; 62 | chunk_size : nat64; 63 | }; 64 | type WalletReceiveResult = record { accepted : nat64 }; 65 | service : { 66 | __get_candid_interface_tmp_hack : () -> (text) query; 67 | canister_status : () -> (CanisterStatusResponse); 68 | delete : (vec text) -> (); 69 | download : (text) -> (vec nat8) query; 70 | download_by : (text, nat64, nat64) -> (vec nat8) query; 71 | files : () -> (vec QueryFile) query; 72 | http_request : (CustomHttpRequest) -> (CustomHttpResponse) query; 73 | maintainable_is_maintaining : () -> (bool) query; 74 | maintainable_set_maintaining : (opt MaintainingReason) -> (); 75 | permission_get_admins : () -> (vec principal) query; 76 | permission_is_admin : (principal) -> (bool) query; 77 | permission_remove_admin : (principal) -> (); 78 | permission_set_admin : (principal) -> (); 79 | upload : (vec UploadingArg) -> (); 80 | wallet_balance : () -> (nat) query; 81 | wallet_receive : () -> (WalletReceiveResult); 82 | whoami : () -> (principal) query; 83 | } -------------------------------------------------------------------------------- /src/icrc7_archive/src/query_method.rs: -------------------------------------------------------------------------------- 1 | use candid::{candid_method, Principal}; 2 | use ic_cdk_macros::query; 3 | 4 | use crate::{ 5 | state::{with_archive_opts, with_blocks, STATE}, 6 | types::{Block, GetBlocksResult, QueryBlock}, 7 | }; 8 | use icrc_ledger_types::icrc3::blocks::GetBlocksRequest; 9 | use num_traits::ToPrimitive; 10 | 11 | #[query] 12 | #[candid_method(query)] 13 | pub fn get_owner() -> Principal { 14 | STATE.with(|s| s.borrow().ledger_id) 15 | } 16 | 17 | #[query] 18 | #[candid_method(query)] 19 | fn remaining_capacity() -> u64 { 20 | let total_block_size = with_blocks(|blocks| blocks.len()); 21 | with_archive_opts(|opts| { 22 | (opts.max_records as u64) 23 | .checked_sub(total_block_size) 24 | .expect("bug: archive capacity underflow") 25 | }) 26 | } 27 | 28 | #[query] 29 | #[candid_method(query)] 30 | fn get_transaction(index: u128) -> Option { 31 | let idx_offset = with_archive_opts(|opts| opts.block_index_offset); 32 | let relative_idx = (idx_offset <= index).then_some(index - idx_offset)?; 33 | 34 | let block = with_blocks(|blocks| blocks.get(&relative_idx))?; 35 | Some(block) 36 | } 37 | 38 | #[query] 39 | #[candid_method(query)] 40 | fn icrc3_get_blocks(reqs: Vec) -> GetBlocksResult { 41 | const MAX_BLOCKS_PER_RESPONSE: u64 = 100; 42 | 43 | let mut blocks = vec![]; 44 | for req in reqs { 45 | let mut id = req.start.0.to_u128().unwrap(); 46 | 47 | let (start, length) = req 48 | .as_start_and_length() 49 | .unwrap_or_else(|msg| ic_cdk::api::trap(&msg)); 50 | let max_length = MAX_BLOCKS_PER_RESPONSE.saturating_sub(blocks.len() as u64); 51 | if max_length == 0 { 52 | break; 53 | } 54 | let length = length.min(max_length); 55 | let block_range = block_range(start, length); 56 | for block in block_range { 57 | blocks.push(QueryBlock { 58 | id: id.clone(), 59 | block: block.value().clone(), 60 | }); 61 | id += 1u128; 62 | } 63 | } 64 | 65 | GetBlocksResult { 66 | // We return the local log length because the archive 67 | // knows only about its local blocks. 68 | log_length: with_blocks(|blocks| blocks.len()) as u128, 69 | blocks, 70 | archived_blocks: vec![], 71 | } 72 | } 73 | 74 | fn block_range(start: u64, length: u64) -> Vec { 75 | let offset = with_archive_opts(|opts| { 76 | let block_index_offset = opts.block_index_offset as u64; 77 | if start < block_index_offset { 78 | ic_cdk::api::trap(&format!( 79 | "requested index {} is less than the minimal index {} this archive serves", 80 | start, block_index_offset 81 | )); 82 | } 83 | start - block_index_offset 84 | }); 85 | 86 | let length = length.min(with_archive_opts(|opts| opts.max_transactions_per_response)); 87 | with_blocks(|blocks| { 88 | let limit = blocks.len().min(offset.saturating_add(length)); 89 | (offset..limit) 90 | .map(|i| blocks.get(&(i as u128)).unwrap()) 91 | .collect() 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /src/icrc7/src/errors.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(CandidType, Deserialize, Clone, Debug)] 5 | pub enum TransferError { 6 | NonExistingTokenId, 7 | InvalidRecipient, 8 | Unauthorized, 9 | TooOld, 10 | CreatedInFuture { ledger_time: u64 }, 11 | Duplicate { duplicate_of: u128 }, 12 | GenericError { error_code: u128, message: String }, 13 | GenericBatchError { error_code: u128, message: String }, 14 | } 15 | 16 | #[derive(CandidType, Clone)] 17 | pub enum BurnError { 18 | Unauthorized, 19 | NonExistingTokenId, 20 | GenericError { error_code: u128, message: String }, 21 | GenericBatchError { error_code: u128, message: String }, 22 | } 23 | 24 | #[derive(CandidType, Clone)] 25 | pub enum MintError { 26 | SupplyCapReached, 27 | Unauthorized, 28 | TokenIdAlreadyExist, 29 | TokenIdMinimumLimit, 30 | GenericError { error_code: u128, message: String }, 31 | GenericBatchError { error_code: u128, message: String }, 32 | } 33 | 34 | #[derive(CandidType, Debug, PartialEq, Deserialize)] 35 | pub enum InsertTransactionError { 36 | SyncPending, 37 | NotSetArchiveCanister, 38 | RemoteError, 39 | Unexpected(String), 40 | CantWrite, 41 | InvalidId, 42 | } 43 | 44 | // ICRC37 Error 45 | 46 | #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] 47 | pub enum ApproveTokenError { 48 | TooOld, 49 | InvalidSpender, 50 | CreatedInFuture { ledger_time: u64 }, 51 | NonExistingTokenId, 52 | Unauthorized, 53 | GenericError { error_code: u128, message: String }, 54 | Duplicate { duplicate_of: u128 }, 55 | GenericBatchError { error_code: u128, message: String }, 56 | } 57 | 58 | #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] 59 | pub enum ApproveCollectionError { 60 | InvalidSpender, 61 | TooOld, 62 | CreatedInFuture { ledger_time: u64 }, 63 | GenericError { error_code: u128, message: String }, 64 | Duplicate { duplicate_of: u128 }, 65 | GenericBatchError { error_code: u128, message: String }, 66 | } 67 | 68 | #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] 69 | pub enum RevokeTokenApprovalError { 70 | TooOld, 71 | CreatedInFuture { ledger_time: u64 }, 72 | NonExistingTokenId, 73 | Unauthorized, 74 | ApprovalDoesNotExist, 75 | GenericError { error_code: u128, message: String }, 76 | Duplicate { duplicate_of: u128 }, 77 | GenericBatchError { error_code: u128, message: String }, 78 | } 79 | 80 | #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] 81 | pub enum RevokeCollectionApprovalError { 82 | TooOld, 83 | CreatedInFuture { ledger_time: u64 }, 84 | Unauthorized, 85 | ApprovalDoesNotExist, 86 | GenericError { error_code: u128, message: String }, 87 | Duplicate { duplicate_of: u128 }, 88 | GenericBatchError { error_code: u128, message: String }, 89 | } 90 | 91 | #[derive(CandidType, Deserialize, Serialize, Clone, Debug)] 92 | pub enum TransferFromError { 93 | NonExistingTokenId, 94 | InvalidRecipient, 95 | Unauthorized, 96 | TooOld, 97 | CreatedInFuture { ledger_time: u64 }, 98 | Duplicate { duplicate_of: u128 }, 99 | GenericError { error_code: u128, message: String }, 100 | GenericBatchError { error_code: u128, message: String }, 101 | } -------------------------------------------------------------------------------- /src/icrc7_types/src/errors.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(CandidType, Deserialize, Clone, Debug)] 5 | pub enum TransferError { 6 | NonExistingTokenId, 7 | InvalidRecipient, 8 | Unauthorized, 9 | TooOld, 10 | CreatedInFuture { ledger_time: u64 }, 11 | Duplicate { duplicate_of: u128 }, 12 | GenericError { error_code: u128, message: String }, 13 | GenericBatchError { error_code: u128, message: String }, 14 | } 15 | 16 | #[derive(CandidType, Clone)] 17 | pub enum BurnError { 18 | Unauthorized, 19 | NonExistingTokenId, 20 | GenericError { error_code: u128, message: String }, 21 | GenericBatchError { error_code: u128, message: String }, 22 | } 23 | 24 | #[derive(CandidType, Clone)] 25 | pub enum MintError { 26 | SupplyCapReached, 27 | Unauthorized, 28 | TokenIdAlreadyExist, 29 | TokenIdMinimumLimit, 30 | GenericError { error_code: u128, message: String }, 31 | GenericBatchError { error_code: u128, message: String }, 32 | } 33 | 34 | #[derive(CandidType, Debug, PartialEq, Deserialize)] 35 | pub enum InsertTransactionError { 36 | SyncPending, 37 | NotSetArchiveCanister, 38 | RemoteError, 39 | Unexpected(String), 40 | CantWrite, 41 | InvalidId, 42 | } 43 | 44 | // ICRC37 Error 45 | 46 | #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] 47 | pub enum ApproveTokenError { 48 | TooOld, 49 | InvalidSpender, 50 | CreatedInFuture { ledger_time: u64 }, 51 | NonExistingTokenId, 52 | Unauthorized, 53 | GenericError { error_code: u128, message: String }, 54 | Duplicate { duplicate_of: u128 }, 55 | GenericBatchError { error_code: u128, message: String }, 56 | } 57 | 58 | #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] 59 | pub enum ApproveCollectionError { 60 | InvalidSpender, 61 | TooOld, 62 | CreatedInFuture { ledger_time: u64 }, 63 | GenericError { error_code: u128, message: String }, 64 | Duplicate { duplicate_of: u128 }, 65 | GenericBatchError { error_code: u128, message: String }, 66 | } 67 | 68 | #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] 69 | pub enum RevokeTokenApprovalError { 70 | TooOld, 71 | CreatedInFuture { ledger_time: u64 }, 72 | NonExistingTokenId, 73 | Unauthorized, 74 | ApprovalDoesNotExist, 75 | GenericError { error_code: u128, message: String }, 76 | Duplicate { duplicate_of: u128 }, 77 | GenericBatchError { error_code: u128, message: String }, 78 | } 79 | 80 | #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] 81 | pub enum RevokeCollectionApprovalError { 82 | TooOld, 83 | CreatedInFuture { ledger_time: u64 }, 84 | Unauthorized, 85 | ApprovalDoesNotExist, 86 | GenericError { error_code: u128, message: String }, 87 | Duplicate { duplicate_of: u128 }, 88 | GenericBatchError { error_code: u128, message: String }, 89 | } 90 | 91 | #[derive(CandidType, Deserialize, Serialize, Clone, Debug)] 92 | pub enum TransferFromError { 93 | NonExistingTokenId, 94 | InvalidRecipient, 95 | Unauthorized, 96 | TooOld, 97 | CreatedInFuture { ledger_time: u64 }, 98 | Duplicate { duplicate_of: u128 }, 99 | GenericError { error_code: u128, message: String }, 100 | GenericBatchError { error_code: u128, message: String }, 101 | } -------------------------------------------------------------------------------- /src/icrc7_archive/src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{Block, IndexType}; 2 | use candid::{CandidType, Decode, Encode, Principal}; 3 | use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; 4 | use ic_stable_structures::{storable::Bound, DefaultMemoryImpl, StableBTreeMap, Storable}; 5 | use serde::Deserialize; 6 | use std::cell::RefCell; 7 | 8 | type Memory = VirtualMemory; 9 | 10 | /// The maximum number of blocks to return in a single get_transactions request. 11 | pub const DEFAULT_MAX_TRANSACTIONS_PER_GET_TRANSACTION_RESPONSE: u64 = 2000; 12 | 13 | // For a type to be used in a `StableBTreeMap`, it needs to implement the `Storable` 14 | // trait, which specifies how the type can be serialized/deserialized. 15 | // 16 | // In this example, we're using candid to serialize/deserialize the struct, but you 17 | // can use anything as long as you're maintaining backward-compatibility. The 18 | // backward-compatibility allows you to change your struct over time (e.g. adding 19 | // new fields). 20 | // 21 | // The `Storable` trait is already implemented for several common types (e.g. u64), 22 | // so you can use those directly without implementing the `Storable` trait for them. 23 | impl Storable for Block { 24 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 25 | Decode!(bytes.as_ref(), Self).unwrap() 26 | } 27 | 28 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 29 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 30 | } 31 | 32 | const BOUND: Bound = Bound::Unbounded; 33 | } 34 | 35 | #[derive(CandidType, Deserialize, Debug)] 36 | pub struct State { 37 | pub max_records: u128, 38 | pub max_pages: u128, 39 | pub max_transactions_per_response: u64, 40 | pub index_type: IndexType, 41 | pub ledger_id: Principal, 42 | pub block_index_offset: u128, 43 | pub block_index: u128, 44 | } 45 | 46 | #[derive(CandidType, Deserialize)] 47 | pub struct StableState { 48 | pub state: State, 49 | } 50 | 51 | impl Default for State { 52 | fn default() -> Self { 53 | State { 54 | max_records: 0, 55 | max_pages: 0, 56 | block_index_offset: 0, 57 | block_index: 0, 58 | max_transactions_per_response: DEFAULT_MAX_TRANSACTIONS_PER_GET_TRANSACTION_RESPONSE, 59 | 60 | ledger_id: Principal::anonymous(), 61 | index_type: IndexType::Stable, 62 | } 63 | } 64 | } 65 | 66 | thread_local! { 67 | // The memory manager is used for simulating multiple memories. Given a `MemoryId` it can 68 | // return a memory that can be used by stable structures. 69 | pub static MEMORY_MANAGER: RefCell> = 70 | RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); 71 | 72 | pub static BLOCK_MAP: RefCell> = RefCell::new( 73 | StableBTreeMap::init( 74 | MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))), 75 | ) 76 | ); 77 | 78 | pub static STATE: RefCell = RefCell::default(); 79 | } 80 | 81 | /// A helper function to access the block list. 82 | pub fn with_blocks(f: impl FnOnce(&StableBTreeMap) -> R) -> R { 83 | BLOCK_MAP.with(|cell| f(&cell.borrow())) 84 | } 85 | 86 | /// A helper function to access the configuration. 87 | pub fn with_archive_opts(f: impl FnOnce(&State) -> R) -> R { 88 | STATE.with(|cell| f(&cell.borrow())) 89 | } 90 | -------------------------------------------------------------------------------- /src/icrc7/src/query_method.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use ic_cdk_macros::query; 3 | use icrc_ledger_types::icrc1::account::Account; 4 | 5 | use crate::{icrc7_types::Transaction, state::STATE, Icrc7TokenMetadata, Standard}; 6 | 7 | #[query] 8 | pub fn icrc7_symbol() -> String { 9 | STATE.with(|s| s.borrow().icrc7_symbol()) 10 | } 11 | 12 | #[query] 13 | pub fn icrc7_name() -> String { 14 | STATE.with(|s| s.borrow().icrc7_name()) 15 | } 16 | 17 | #[query] 18 | pub fn icrc7_description() -> Option { 19 | STATE.with(|s| s.borrow().icrc7_description()) 20 | } 21 | 22 | #[query] 23 | pub fn icrc7_logo() -> Option { 24 | STATE.with(|s| s.borrow().icrc7_logo()) 25 | } 26 | 27 | #[query] 28 | pub fn icrc7_total_supply() -> u128 { 29 | STATE.with(|s| s.borrow().icrc7_total_supply()) 30 | } 31 | 32 | #[query] 33 | pub fn icrc7_supply_cap() -> Option { 34 | STATE.with(|s| s.borrow().icrc7_supply_cap()) 35 | } 36 | 37 | #[query] 38 | pub fn icrc7_max_query_batch_size() -> Option { 39 | STATE.with(|s| s.borrow().icrc7_max_query_batch_size()) 40 | } 41 | 42 | #[query] 43 | pub fn icrc7_max_update_batch_size() -> Option { 44 | STATE.with(|s| s.borrow().icrc7_max_update_batch_size()) 45 | } 46 | 47 | #[query] 48 | pub fn icrc7_default_take_value() -> Option { 49 | STATE.with(|s| s.borrow().icrc7_default_take_value()) 50 | } 51 | 52 | #[query] 53 | pub fn icrc7_max_take_value() -> Option { 54 | STATE.with(|s| s.borrow().icrc7_max_take_value()) 55 | } 56 | 57 | #[query] 58 | pub fn icrc7_max_memo_size() -> Option { 59 | STATE.with(|s| s.borrow().icrc7_max_memo_size()) 60 | } 61 | 62 | #[query] 63 | pub fn icrc7_atomic_batch_transfers() -> Option { 64 | STATE.with(|s| s.borrow().icrc7_atomic_batch_transfers()) 65 | } 66 | 67 | #[query] 68 | pub fn icrc7_owner_of(ids: Vec) -> Vec> { 69 | STATE.with(|s| s.borrow().icrc7_owner_of(&ids)) 70 | } 71 | 72 | #[query] 73 | pub fn icrc7_supported_standards() -> Vec { 74 | vec![ 75 | Standard { 76 | name: "ICRC-7".into(), 77 | url: "https://github.com/dfinity/ICRC/tree/main/ICRCs/ICRC-7".into(), 78 | }, 79 | Standard { 80 | name: "ICRC-10".into(), 81 | url: "https://github.com/dfinity/ICRC/tree/main/ICRCs/ICRC-10".into(), 82 | }, 83 | Standard { 84 | name: "ICRC-37".into(), 85 | url: "https://github.com/dfinity/ICRC/tree/main/ICRCs/ICRC-37".into(), 86 | }, 87 | Standard { 88 | name: "ICRC-3".into(), 89 | url: "https://github.com/dfinity/ICRC/tree/main/ICRCs/ICRC-3".into(), 90 | }, 91 | ] 92 | } 93 | 94 | #[query] 95 | pub fn icrc7_tokens(prev: Option, take: Option) -> Vec { 96 | STATE.with(|s| s.borrow().icrc7_tokens(prev, take)) 97 | } 98 | 99 | #[query] 100 | pub fn icrc7_token_metadata(token_ids: Vec) -> Vec> { 101 | STATE.with(|s| s.borrow().icrc7_token_metadata(&token_ids)) 102 | } 103 | 104 | #[query] 105 | pub fn icrc7_balance_of(accounts: Vec) -> Vec { 106 | STATE.with(|s| s.borrow().icrc7_balance_of(&accounts)) 107 | } 108 | 109 | #[query] 110 | pub fn icrc7_tokens_of(account: Account, prev: Option, take: Option) -> Vec { 111 | STATE.with(|s| s.borrow().icrc7_tokens_of(account, prev, take)) 112 | } 113 | 114 | #[query] 115 | pub fn minting_authority() -> Option { 116 | STATE.with(|s| s.borrow().icrc7_minting_authority()) 117 | } 118 | 119 | #[query] 120 | pub fn txn_logs(page_number: u32, page_size: u32) -> Vec { 121 | STATE.with(|s| s.borrow().icrc7_txn_logs(page_number, page_size)) 122 | } 123 | 124 | #[query] 125 | pub fn archive_log_canister() -> Option { 126 | STATE.with(|s| s.borrow().get_archive_log_canister()) 127 | } 128 | -------------------------------------------------------------------------------- /src/icrc7_archive/src/types.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Principal}; 2 | use icrc_ledger_types::icrc::generic_value::Value; 3 | 4 | use serde_derive::{Deserialize, Serialize}; 5 | use std::marker::PhantomData; 6 | 7 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 8 | pub struct Block(Value); 9 | 10 | impl Block { 11 | pub fn value(&self) -> &Value { 12 | &self.0 13 | } 14 | 15 | pub fn value_mut(&mut self) -> &mut Value { 16 | &mut self.0 17 | } 18 | } 19 | 20 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 21 | pub enum IndexType { 22 | Managed, 23 | Stable, 24 | StableTyped, 25 | } 26 | 27 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 28 | pub struct ArchiveInitArgs { 29 | pub first_index: u128, 30 | pub index_type: IndexType, 31 | pub max_pages: u128, 32 | pub max_records: u128, 33 | } 34 | 35 | #[derive(CandidType, Deserialize, Debug)] 36 | pub struct QueryBlock { 37 | pub id: u128, 38 | pub block: Value, 39 | } 40 | 41 | #[derive(CandidType, Deserialize, Debug)] 42 | pub struct GetBlocksResult { 43 | pub blocks: Vec, 44 | pub log_length: u128, 45 | pub archived_blocks: Vec, 46 | } 47 | 48 | #[derive(CandidType, Deserialize, Debug)] 49 | pub struct GetTransactionsResult { 50 | pub blocks: Vec, 51 | pub log_length: u128, 52 | pub archived_blocks: Vec, 53 | } 54 | 55 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 56 | pub struct TransactionRange { 57 | pub start: u128, 58 | pub length: u128, 59 | } 60 | 61 | #[derive(CandidType, Deserialize, Debug)] 62 | pub struct ArchivedTransactionResponse { 63 | pub args: Vec, 64 | pub callback: QueryTransactionsFn, 65 | } 66 | 67 | pub type QueryTransactionsFn = GetTransactionsFn, GetTransactionsResult>; 68 | 69 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 70 | #[serde(try_from = "candid::types::reference::Func")] 71 | pub struct GetTransactionsFn { 72 | pub canister_id: Principal, 73 | pub method: String, 74 | pub _marker: PhantomData<(Input, Output)>, 75 | } 76 | 77 | impl GetTransactionsFn { 78 | pub fn new(canister_id: Principal, method: impl Into) -> Self { 79 | Self { 80 | canister_id, 81 | method: method.into(), 82 | _marker: PhantomData, 83 | } 84 | } 85 | } 86 | 87 | impl Clone for GetTransactionsFn { 88 | fn clone(&self) -> Self { 89 | Self { 90 | canister_id: self.canister_id, 91 | method: self.method.clone(), 92 | _marker: PhantomData, 93 | } 94 | } 95 | } 96 | 97 | impl From> 98 | for candid::types::reference::Func 99 | { 100 | fn from(get_transactions_fn: GetTransactionsFn) -> Self { 101 | let p: &Principal = &Principal::try_from(get_transactions_fn.canister_id.as_ref()) 102 | .expect("could not deserialize principal"); 103 | Self { 104 | principal: *p, 105 | method: get_transactions_fn.method, 106 | } 107 | } 108 | } 109 | 110 | impl TryFrom 111 | for GetTransactionsFn 112 | { 113 | type Error = String; 114 | fn try_from(func: candid::types::reference::Func) -> Result { 115 | let canister_id = Principal::try_from(func.principal.as_slice()) 116 | .map_err(|e| format!("principal is not a canister id: {}", e))?; 117 | Ok(GetTransactionsFn { 118 | canister_id, 119 | method: func.method, 120 | _marker: PhantomData, 121 | }) 122 | } 123 | } 124 | 125 | impl CandidType for GetTransactionsFn { 126 | fn _ty() -> candid::types::Type { 127 | candid::func!((Input) -> (Output) query) 128 | } 129 | 130 | fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> 131 | where 132 | S: candid::types::Serializer, 133 | { 134 | candid::types::reference::Func::from(self.clone()).idl_serialize(serializer) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/icrc7/src/init_method.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk_macros::{init, post_upgrade, pre_upgrade}; 2 | use ic_stable_structures::{writer::Writer, Memory}; 3 | use icrc_ledger_types::icrc1::account::Account; 4 | 5 | use crate::{ 6 | icrc37_types::LedgerInfo, icrc3_types::ArchiveLedgerInfo, icrc7_types::InitArg, state::STATE, 7 | utils::account_transformer, 8 | }; 9 | 10 | #[init] 11 | pub fn init(arg: InitArg) { 12 | let minting_authority = account_transformer(match arg.minting_account { 13 | None => { 14 | let caller = ic_cdk::caller(); 15 | account_transformer(Account { 16 | owner: caller, 17 | subaccount: None, 18 | }) 19 | } 20 | Some(acc) => account_transformer(acc), 21 | }); 22 | 23 | let mut ledger_info = LedgerInfo::default(); 24 | if let Some(approval_init) = arg.approval_init { 25 | if let Some(max_approvals_per_token_or_collection) = 26 | approval_init.max_approvals_per_token_or_collection 27 | { 28 | ledger_info.max_approvals_per_token_or_collection = 29 | max_approvals_per_token_or_collection; 30 | } 31 | if let Some(max_revoke_approvals) = approval_init.max_revoke_approvals { 32 | ledger_info.max_revoke_approvals = max_revoke_approvals; 33 | } 34 | if let Some(max_approvals) = approval_init.max_approvals { 35 | ledger_info.max_approvals = max_approvals; 36 | } 37 | if let Some(settle_to_approvals) = approval_init.settle_to_approvals { 38 | ledger_info.settle_to_approvals = settle_to_approvals; 39 | } 40 | if let Some(collection_approval_requires_token) = 41 | approval_init.collection_approval_requires_token 42 | { 43 | ledger_info.collection_approval_requires_token = collection_approval_requires_token; 44 | } 45 | } 46 | 47 | let mut archive_ledger_info = ArchiveLedgerInfo::default(); 48 | if let Some(archive_init) = arg.archive_init { 49 | archive_ledger_info = ArchiveLedgerInfo::new(Some(archive_init.to_archive_setting())) 50 | } 51 | 52 | STATE.with(|s| { 53 | let mut s = s.borrow_mut(); 54 | s.minting_authority = Some(minting_authority); 55 | s.icrc7_symbol = arg.icrc7_symbol; 56 | s.icrc7_name = arg.icrc7_name; 57 | s.icrc7_description = arg.icrc7_description; 58 | s.icrc7_logo = arg.icrc7_logo; 59 | s.icrc7_supply_cap = arg.icrc7_supply_cap; 60 | s.icrc7_max_query_batch_size = arg.icrc7_max_query_batch_size; 61 | s.icrc7_max_update_batch_size = arg.icrc7_max_update_batch_size; 62 | s.icrc7_max_take_value = arg.icrc7_max_take_value; 63 | s.icrc7_default_take_value = arg.icrc7_default_take_value; 64 | s.icrc7_max_memo_size = arg.icrc7_max_memo_size; 65 | s.icrc7_atomic_batch_transfers = arg.icrc7_atomic_batch_transfers; 66 | s.tx_window = arg.tx_window; 67 | s.permitted_drift = arg.permitted_drift; 68 | s.approval_ledger_info = ledger_info; 69 | s.archive_ledger_info = archive_ledger_info; 70 | }) 71 | } 72 | 73 | #[pre_upgrade] 74 | fn pre_upgrade() { 75 | // Serialize the state. 76 | // This example is using CBOR, but you can use any data format you like. 77 | let mut state_bytes = vec![]; 78 | STATE 79 | .with(|s| ciborium::ser::into_writer(&*s.borrow(), &mut state_bytes)) 80 | .expect("failed to encode state"); 81 | 82 | // Write the length of the serialized bytes to memory, followed by the 83 | // by the bytes themselves. 84 | let len = state_bytes.len() as u32; 85 | let mut memory = crate::memory::get_upgrades_memory(); 86 | let mut writer = Writer::new(&mut memory, 0); 87 | writer.write(&len.to_le_bytes()).unwrap(); 88 | writer.write(&state_bytes).unwrap(); 89 | } 90 | 91 | // A post-upgrade hook for deserializing the data back into the heap. 92 | #[post_upgrade] 93 | fn post_upgrade() { 94 | let memory = crate::memory::get_upgrades_memory(); 95 | 96 | // Read the length of the state bytes. 97 | let mut state_len_bytes = [0; 4]; 98 | memory.read(0, &mut state_len_bytes); 99 | let state_len = u32::from_le_bytes(state_len_bytes) as usize; 100 | 101 | // Read the bytes 102 | let mut state_bytes = vec![0; state_len]; 103 | memory.read(4, &mut state_bytes); 104 | 105 | // Deserialize and set the state. 106 | let state = ciborium::de::from_reader(&*state_bytes).expect("failed to decode state"); 107 | STATE.with(|s| *s.borrow_mut() = state); 108 | } 109 | -------------------------------------------------------------------------------- /src/icrc7_launchpad/src/update_method.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Encode, Principal}; 2 | use ic_cdk::api::management_canister::{ 3 | main::{create_canister, install_code, CreateCanisterArgument, InstallCodeArgument}, 4 | provisional::CanisterSettings, 5 | }; 6 | use ic_cdk_macros::update; 7 | use icrc7_types::icrc37_types::InitApprovalsArg; 8 | use icrc7_types::icrc3_types::InitArchiveArg; 9 | use icrc_ledger_types::icrc1::account::Account; 10 | use serde::Deserialize; 11 | 12 | pub const ICRC7_WASM: &[u8] = std::include_bytes!("./../../../wasm/icrc7.wasm.gz"); 13 | 14 | #[derive(CandidType, Deserialize)] 15 | pub struct InitArg { 16 | pub minting_account: Option, 17 | pub icrc7_symbol: String, 18 | pub icrc7_name: String, 19 | pub icrc7_description: Option, 20 | pub icrc7_logo: Option, 21 | pub icrc7_supply_cap: Option, 22 | pub icrc7_max_query_batch_size: Option, 23 | pub icrc7_max_update_batch_size: Option, 24 | pub icrc7_max_take_value: Option, 25 | pub icrc7_default_take_value: Option, 26 | pub icrc7_max_memo_size: Option, 27 | pub icrc7_atomic_batch_transfers: Option, 28 | pub tx_window: Option, 29 | pub permitted_drift: Option, 30 | pub approval_init: Option, 31 | pub archive_init: Option, 32 | } 33 | 34 | #[derive(CandidType, Deserialize)] 35 | pub struct Arg { 36 | pub icrc7_symbol: String, 37 | pub icrc7_name: String, 38 | pub icrc7_description: Option, 39 | pub icrc7_logo: Option, 40 | pub icrc7_supply_cap: Option, 41 | pub icrc7_max_query_batch_size: Option, 42 | pub icrc7_max_update_batch_size: Option, 43 | pub icrc7_max_take_value: Option, 44 | pub icrc7_default_take_value: Option, 45 | pub icrc7_max_memo_size: Option, 46 | pub icrc7_atomic_batch_transfers: Option, 47 | pub tx_window: Option, 48 | pub permitted_drift: Option, 49 | pub approval_init: Option, 50 | pub archive_init: Option, 51 | } 52 | 53 | impl From<(Account, Arg)> for InitArg { 54 | fn from((account, arg): (Account, Arg)) -> Self { 55 | Self { 56 | minting_account: Some(account), 57 | icrc7_symbol: arg.icrc7_symbol, 58 | icrc7_name: arg.icrc7_name, 59 | icrc7_description: arg.icrc7_description, 60 | icrc7_logo: arg.icrc7_logo, 61 | icrc7_supply_cap: arg.icrc7_supply_cap, 62 | icrc7_max_query_batch_size: arg.icrc7_max_query_batch_size, 63 | icrc7_max_update_batch_size: arg.icrc7_max_update_batch_size, 64 | icrc7_max_take_value: arg.icrc7_max_take_value, 65 | icrc7_default_take_value: arg.icrc7_default_take_value, 66 | icrc7_max_memo_size: arg.icrc7_max_memo_size, 67 | icrc7_atomic_batch_transfers: arg.icrc7_atomic_batch_transfers, 68 | tx_window: arg.tx_window, 69 | permitted_drift: arg.permitted_drift, 70 | approval_init: arg.approval_init, 71 | archive_init: arg.archive_init, 72 | } 73 | } 74 | } 75 | 76 | #[update] 77 | async fn mint_collection_canister(arg: Arg) -> Result { 78 | let caller = ic_cdk::caller(); 79 | if caller == Principal::anonymous() { 80 | return Err("Anonymous Caller".into()); 81 | } 82 | let account = Account { 83 | owner: caller.clone(), 84 | subaccount: None, 85 | }; 86 | let principal = match create_canister( 87 | CreateCanisterArgument { 88 | settings: Some(CanisterSettings { 89 | controllers: Some(vec![ic_cdk::id(), caller.clone()]), 90 | compute_allocation: None, 91 | memory_allocation: None, 92 | freezing_threshold: None, 93 | reserved_cycles_limit: None, 94 | }), 95 | }, 96 | 10_000_000_000_000, 97 | ) 98 | .await 99 | { 100 | Err((code, msg)) => return Err(format!("Rejection Code: {:?}, Message: {:?}", code, msg)), 101 | Ok((principal,)) => principal.canister_id, 102 | }; 103 | let init_arg = InitArg::from((account, arg)); 104 | let init_arg = Encode!(&init_arg).unwrap(); 105 | match install_code(InstallCodeArgument { 106 | mode: ic_cdk::api::management_canister::main::CanisterInstallMode::Install, 107 | canister_id: principal, 108 | wasm_module: ICRC7_WASM.to_vec(), 109 | arg: init_arg, 110 | }) 111 | .await 112 | { 113 | Ok(()) => Ok(principal), 114 | Err((code, msg)) => Err(format!("Code: {:?}, Message: {:?}", code, msg)), 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /scripts/icrc7.sh: -------------------------------------------------------------------------------- 1 | dfx deploy icrc7 --argument '(record{ 2 | minting_account= opt record { 3 | owner = principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe"; 4 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 5 | }; 6 | icrc7_supply_cap= null; 7 | icrc7_description= opt "ICP Flower Collection"; 8 | tx_window= null; 9 | permitted_drift= null; 10 | icrc7_max_take_value= null; 11 | icrc7_max_memo_size= null; 12 | icrc7_symbol= "ICFL"; 13 | icrc7_max_update_batch_size= opt 5; 14 | icrc7_max_query_batch_size= opt 5; 15 | icrc7_atomic_batch_transfers= null; 16 | icrc7_default_take_value= null; 17 | icrc7_logo= null; 18 | icrc7_name= "ICP Flower"; 19 | approval_init= null; 20 | archive_init= opt record { 21 | maxRecordsToArchive= 2; 22 | archiveIndexType= variant {Stable}; 23 | maxArchivePages= 3; 24 | settleToRecords= 2; 25 | archiveCycles= 1000000000000; 26 | maxActiveRecords= 4; 27 | maxRecordsInArchiveInstance= 4; 28 | archiveControllers= null 29 | } 30 | })' 31 | 32 | dfx canister call icrc7 icrc7_mint '(record{ 33 | to= record { 34 | owner = principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe"; 35 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 36 | }; 37 | token_id=1; 38 | memo= null; 39 | from_subaccount= null; 40 | token_description= opt "Token Number 1"; 41 | token_logo= null; 42 | token_name= null 43 | })' 44 | 45 | dfx canister call icrc7 icrc7_transfer '(vec{ 46 | record{ 47 | to=record { 48 | owner = principal "t4egw-clf4w-qbpli-svryg-7yqq6-jt2yj-7v755-mabir-zmx6i-vp4fr-fqe"; 49 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 50 | }; 51 | token_id= 1; 52 | from_subaccount= null; 53 | memo= null; 54 | created_at_time= null 55 | } 56 | })' 57 | 58 | dfx canister call icrc7 icrc7_mint '(record{ 59 | to= record { 60 | owner = principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe"; 61 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 62 | }; 63 | token_id=2; 64 | memo= null; 65 | from_subaccount= null; 66 | token_description= opt "Token Number 1"; 67 | token_logo= null; 68 | token_name= null 69 | })' 70 | 71 | dfx canister call icrc7 icrc7_transfer '(vec{ 72 | record{ 73 | to= record { 74 | owner = principal "t4egw-clf4w-qbpli-svryg-7yqq6-jt2yj-7v755-mabir-zmx6i-vp4fr-fqe"; 75 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 76 | }; 77 | token_id= 2; 78 | memo= opt blob "123"; 79 | from_subaccount= null; 80 | created_at_time= null 81 | } 82 | })' 83 | 84 | dfx canister call icrc7 icrc37_approve_tokens '(vec{ 85 | record{ 86 | token_id= 2; 87 | approval_info= record { 88 | memo= null; 89 | from_subaccount= null; 90 | created_at_time= null; 91 | expires_at= null; 92 | spender= record { 93 | owner = principal "o2zom-piy75-ifbnk-nhhlq-362su-4vsx5-ptl2s-ec4jw-osbv4-nygtw-dae"; 94 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 95 | } 96 | } 97 | } 98 | })' 99 | 100 | dfx canister call icrc7 icrc37_transfer_from '(vec{ 101 | record{ 102 | from= record { 103 | owner = principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe"; 104 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 105 | }; 106 | to= record { 107 | owner = principal "t4egw-clf4w-qbpli-svryg-7yqq6-jt2yj-7v755-mabir-zmx6i-vp4fr-fqe"; 108 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 109 | }; 110 | spender_subaccount= null; 111 | token_id= 2; 112 | memo= opt blob "123"; 113 | created_at_time= null 114 | } 115 | })' -------------------------------------------------------------------------------- /src/icrc7_types/src/icrc37_types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use candid::{CandidType, Decode, Encode}; 4 | use ic_stable_structures::{storable::Bound, Storable}; 5 | use icrc_ledger_types::{ 6 | icrc::generic_value::Map, 7 | icrc1::account::{Account, Subaccount}, 8 | }; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::errors::{ 12 | ApproveCollectionError, ApproveTokenError, RevokeCollectionApprovalError, 13 | RevokeTokenApprovalError, TransferFromError, 14 | }; 15 | 16 | pub type Metadata = Map; 17 | 18 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 19 | pub struct UserAccount(Account); 20 | 21 | impl Storable for UserAccount { 22 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 23 | Decode!(bytes.as_ref(), Self).unwrap() 24 | } 25 | 26 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 27 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 28 | } 29 | 30 | const BOUND: Bound = Bound::Unbounded; 31 | } 32 | 33 | impl UserAccount { 34 | pub fn new(user: Account) -> Self { 35 | UserAccount(user) 36 | } 37 | } 38 | 39 | impl From for Account { 40 | fn from(user_account: UserAccount) -> Self { 41 | user_account.0 42 | } 43 | } 44 | 45 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 46 | pub struct LedgerInfo { 47 | pub max_approvals_per_token_or_collection: u16, 48 | pub max_revoke_approvals: u16, 49 | pub max_approvals: u16, 50 | pub settle_to_approvals: u16, 51 | pub collection_approval_requires_token: bool, 52 | } 53 | 54 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 55 | pub struct ApprovalInfo { 56 | pub from_subaccount: Option, 57 | pub spender: Account, // Approval is given to an ICRC Account 58 | pub memo: Option>, 59 | pub expires_at: Option, 60 | pub created_at_time: Option, 61 | } 62 | 63 | impl Storable for ApprovalInfo { 64 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 65 | Decode!(bytes.as_ref(), Self).unwrap() 66 | } 67 | 68 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 69 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 70 | } 71 | 72 | const BOUND: Bound = Bound::Unbounded; 73 | } 74 | 75 | impl ApprovalInfo { 76 | pub fn new( 77 | from_subaccount: Option, 78 | spender: Account, 79 | memo: Option>, 80 | expires_at: Option, 81 | created_at_time: Option, 82 | ) -> Self { 83 | Self { 84 | from_subaccount, 85 | spender, 86 | memo, 87 | expires_at, 88 | created_at_time, 89 | } 90 | } 91 | } 92 | 93 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 94 | pub struct TokenApprovalInfo(BTreeMap>); 95 | 96 | impl Storable for TokenApprovalInfo { 97 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 98 | Decode!(bytes.as_ref(), Self).unwrap() 99 | } 100 | 101 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 102 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 103 | } 104 | 105 | const BOUND: Bound = Bound::Unbounded; 106 | } 107 | 108 | impl TokenApprovalInfo { 109 | pub fn into_map(self) -> BTreeMap> { 110 | self.0 111 | } 112 | 113 | pub fn new(owner: Account, approval: ApprovalInfo) -> Self { 114 | let mut token_approval = BTreeMap::new(); 115 | let mut approval_info = BTreeMap::new(); 116 | approval_info.insert(approval.spender, approval); 117 | token_approval.insert(owner, approval_info); 118 | TokenApprovalInfo(token_approval) 119 | } 120 | 121 | pub fn approve(&mut self, owner: Account, approval: ApprovalInfo) { 122 | match self.0.get_mut(&owner) { 123 | None => { 124 | let mut approval_info = BTreeMap::new(); 125 | approval_info.insert(approval.spender, approval); 126 | self.0.insert(owner, approval_info); 127 | } 128 | Some(approvals) => { 129 | approvals.insert(approval.spender, approval); 130 | } 131 | } 132 | } 133 | 134 | pub fn remove_approve(&mut self, owner: Account, spender: Option) { 135 | match self.0.get_mut(&owner) { 136 | None => (), 137 | Some(approvals) => match spender { 138 | None => { 139 | self.0.remove(&owner); 140 | } 141 | Some(spender) => { 142 | approvals.remove(&spender); 143 | } 144 | }, 145 | } 146 | } 147 | } 148 | 149 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 150 | pub struct CollectionApprovalInfo(BTreeMap); 151 | 152 | impl Storable for CollectionApprovalInfo { 153 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 154 | Decode!(bytes.as_ref(), Self).unwrap() 155 | } 156 | 157 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 158 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 159 | } 160 | 161 | const BOUND: Bound = Bound::Unbounded; 162 | } 163 | 164 | impl CollectionApprovalInfo { 165 | pub fn new(spender: Account, approval: ApprovalInfo) -> Self { 166 | let mut collection_approval = BTreeMap::new(); 167 | collection_approval.insert(spender, approval); 168 | CollectionApprovalInfo(collection_approval) 169 | } 170 | 171 | pub fn into_map(self) -> BTreeMap { 172 | self.0 173 | } 174 | 175 | pub fn approve(&mut self, spender: Account, approval: ApprovalInfo) { 176 | self.0.insert(spender, approval); 177 | } 178 | 179 | pub fn remove_approve(&mut self, spender: Account) { 180 | match self.0.get_mut(&spender) { 181 | None => (), 182 | Some(_) => { 183 | self.0.remove(&spender); 184 | } 185 | } 186 | } 187 | } 188 | 189 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 190 | pub struct CollectionApprovalAccount { 191 | pub owner: Account, 192 | pub spender: Account, 193 | } 194 | 195 | impl Storable for CollectionApprovalAccount { 196 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 197 | Decode!(bytes.as_ref(), Self).unwrap() 198 | } 199 | 200 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 201 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 202 | } 203 | 204 | const BOUND: Bound = Bound::Unbounded; 205 | } 206 | 207 | #[derive(CandidType, Deserialize)] 208 | pub struct InitApprovalsArg { 209 | pub max_approvals: Option, 210 | pub max_approvals_per_token_or_collection: Option, 211 | pub max_revoke_approvals: Option, 212 | pub settle_to_approvals: Option, 213 | pub collection_approval_requires_token: Option, 214 | } 215 | 216 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 217 | pub struct ApproveTokenArg { 218 | pub token_id: u128, 219 | pub approval_info: ApprovalInfo, 220 | } 221 | 222 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 223 | pub struct TokenApproval { 224 | pub token_id: u128, 225 | pub approval_info: ApprovalInfo, 226 | } 227 | 228 | pub type CollectionApproval = ApprovalInfo; 229 | 230 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 231 | pub struct ApproveCollectionArg { 232 | pub approval_info: ApprovalInfo, 233 | } 234 | 235 | pub type ApproveTokenResult = Result; 236 | 237 | pub type ApproveCollectionResult = Result; 238 | 239 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 240 | pub struct RevokeTokenApprovalArg { 241 | pub token_id: u128, 242 | pub from_subaccount: Option, 243 | pub spender: Option, 244 | pub memo: Option>, 245 | pub created_at_time: Option, 246 | } 247 | 248 | pub type RevokeTokenApprovalResult = Result; 249 | 250 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 251 | pub struct RevokeCollectionApprovalArg { 252 | pub from_subaccount: Option, 253 | pub spender: Option, 254 | pub memo: Option>, 255 | pub created_at_time: Option, 256 | } 257 | 258 | pub type RevokeCollectionApprovalResult = Result; 259 | 260 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 261 | pub struct IsApprovedArg { 262 | pub spender: Account, 263 | pub from_subaccount: Option, 264 | pub token_id: u128, 265 | } 266 | 267 | #[derive(CandidType, Deserialize, Clone, Debug)] 268 | pub struct TransferFromArg { 269 | pub spender_subaccount: Option, 270 | pub from: Account, 271 | pub to: Account, 272 | pub token_id: u128, 273 | pub memo: Option>, 274 | pub created_at_time: Option, 275 | } 276 | 277 | pub type TransferFromResult = Result; 278 | -------------------------------------------------------------------------------- /src/icrc7/src/icrc37_types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use candid::{CandidType, Decode, Encode}; 4 | use ic_stable_structures::{storable::Bound, Storable}; 5 | use icrc_ledger_types::{ 6 | icrc::generic_value::Map, 7 | icrc1::account::{Account, Subaccount}, 8 | }; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::{ 12 | errors::{ 13 | ApproveCollectionError, ApproveTokenError, RevokeCollectionApprovalError, 14 | RevokeTokenApprovalError, TransferFromError, 15 | }, 16 | TransferArg, 17 | }; 18 | 19 | pub type Metadata = Map; 20 | 21 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 22 | pub struct UserAccount(Account); 23 | 24 | impl Storable for UserAccount { 25 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 26 | Decode!(bytes.as_ref(), Self).unwrap() 27 | } 28 | 29 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 30 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 31 | } 32 | 33 | const BOUND: Bound = Bound::Unbounded; 34 | } 35 | 36 | impl UserAccount { 37 | pub fn new(user: Account) -> Self { 38 | UserAccount(user) 39 | } 40 | } 41 | 42 | impl From for Account { 43 | fn from(user_account: UserAccount) -> Self { 44 | user_account.0 45 | } 46 | } 47 | 48 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 49 | pub struct LedgerInfo { 50 | pub max_approvals_per_token_or_collection: u16, 51 | pub max_revoke_approvals: u16, 52 | pub max_approvals: u16, 53 | pub settle_to_approvals: u16, 54 | pub collection_approval_requires_token: bool, 55 | } 56 | 57 | impl Default for LedgerInfo { 58 | fn default() -> Self { 59 | Self { 60 | max_approvals_per_token_or_collection: 10000, 61 | max_revoke_approvals: 10000, 62 | max_approvals: crate::state::State::DEFAULT_MAX_UPDATE_BATCH_SIZE, 63 | settle_to_approvals: 9975, 64 | collection_approval_requires_token: true, 65 | } 66 | } 67 | } 68 | 69 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 70 | pub struct ApprovalInfo { 71 | pub from_subaccount: Option, 72 | pub spender: Account, // Approval is given to an ICRC Account 73 | pub memo: Option>, 74 | pub expires_at: Option, 75 | pub created_at_time: Option, 76 | } 77 | 78 | impl Storable for ApprovalInfo { 79 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 80 | Decode!(bytes.as_ref(), Self).unwrap() 81 | } 82 | 83 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 84 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 85 | } 86 | 87 | const BOUND: Bound = Bound::Unbounded; 88 | } 89 | 90 | impl ApprovalInfo { 91 | pub fn new( 92 | from_subaccount: Option, 93 | spender: Account, 94 | memo: Option>, 95 | expires_at: Option, 96 | created_at_time: Option, 97 | ) -> Self { 98 | Self { 99 | from_subaccount, 100 | spender, 101 | memo, 102 | expires_at, 103 | created_at_time, 104 | } 105 | } 106 | } 107 | 108 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 109 | pub struct TokenApprovalInfo(BTreeMap>); 110 | 111 | impl Storable for TokenApprovalInfo { 112 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 113 | Decode!(bytes.as_ref(), Self).unwrap() 114 | } 115 | 116 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 117 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 118 | } 119 | 120 | const BOUND: Bound = Bound::Unbounded; 121 | } 122 | 123 | impl TokenApprovalInfo { 124 | pub fn into_map(self) -> BTreeMap> { 125 | self.0 126 | } 127 | 128 | pub fn new(owner: Account, approval: ApprovalInfo) -> Self { 129 | let mut token_approval = BTreeMap::new(); 130 | let mut approval_info = BTreeMap::new(); 131 | approval_info.insert(approval.spender, approval); 132 | token_approval.insert(owner, approval_info); 133 | TokenApprovalInfo(token_approval) 134 | } 135 | 136 | pub fn approve(&mut self, owner: Account, approval: ApprovalInfo) { 137 | match self.0.get_mut(&owner) { 138 | None => { 139 | let mut approval_info = BTreeMap::new(); 140 | approval_info.insert(approval.spender, approval); 141 | self.0.insert(owner, approval_info); 142 | } 143 | Some(approvals) => { 144 | approvals.insert(approval.spender, approval); 145 | } 146 | } 147 | } 148 | 149 | pub fn remove_approve(&mut self, owner: Account, spender: Option) { 150 | match self.0.get_mut(&owner) { 151 | None => (), 152 | Some(approvals) => match spender { 153 | None => { 154 | self.0.remove(&owner); 155 | } 156 | Some(spender) => { 157 | approvals.remove(&spender); 158 | } 159 | }, 160 | } 161 | } 162 | } 163 | 164 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 165 | pub struct CollectionApprovalInfo(BTreeMap); 166 | 167 | impl Storable for CollectionApprovalInfo { 168 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 169 | Decode!(bytes.as_ref(), Self).unwrap() 170 | } 171 | 172 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 173 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 174 | } 175 | 176 | const BOUND: Bound = Bound::Unbounded; 177 | } 178 | 179 | impl CollectionApprovalInfo { 180 | pub fn new(spender: Account, approval: ApprovalInfo) -> Self { 181 | let mut collection_approval = BTreeMap::new(); 182 | collection_approval.insert(spender, approval); 183 | CollectionApprovalInfo(collection_approval) 184 | } 185 | 186 | pub fn into_map(self) -> BTreeMap { 187 | self.0 188 | } 189 | 190 | pub fn approve(&mut self, spender: Account, approval: ApprovalInfo) { 191 | self.0.insert(spender, approval); 192 | } 193 | 194 | pub fn remove_approve(&mut self, spender: Account) { 195 | match self.0.get_mut(&spender) { 196 | None => (), 197 | Some(_) => { 198 | self.0.remove(&spender); 199 | } 200 | } 201 | } 202 | } 203 | 204 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 205 | pub struct CollectionApprovalAccount { 206 | pub owner: Account, 207 | pub spender: Account, 208 | } 209 | 210 | impl Storable for CollectionApprovalAccount { 211 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 212 | Decode!(bytes.as_ref(), Self).unwrap() 213 | } 214 | 215 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 216 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 217 | } 218 | 219 | const BOUND: Bound = Bound::Unbounded; 220 | } 221 | 222 | #[derive(CandidType, Deserialize)] 223 | pub struct InitApprovalsArg { 224 | pub max_approvals: Option, 225 | pub max_approvals_per_token_or_collection: Option, 226 | pub max_revoke_approvals: Option, 227 | pub settle_to_approvals: Option, 228 | pub collection_approval_requires_token: Option, 229 | } 230 | 231 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 232 | pub struct ApproveTokenArg { 233 | pub token_id: u128, 234 | pub approval_info: ApprovalInfo, 235 | } 236 | 237 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 238 | pub struct TokenApproval { 239 | pub token_id: u128, 240 | pub approval_info: ApprovalInfo, 241 | } 242 | 243 | pub type CollectionApproval = ApprovalInfo; 244 | 245 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 246 | pub struct ApproveCollectionArg { 247 | pub approval_info: ApprovalInfo, 248 | } 249 | 250 | pub type ApproveTokenResult = Result; 251 | 252 | pub type ApproveCollectionResult = Result; 253 | 254 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 255 | pub struct RevokeTokenApprovalArg { 256 | pub token_id: u128, 257 | pub from_subaccount: Option, 258 | pub spender: Option, 259 | pub memo: Option>, 260 | pub created_at_time: Option, 261 | } 262 | 263 | pub type RevokeTokenApprovalResult = Result; 264 | 265 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 266 | pub struct RevokeCollectionApprovalArg { 267 | pub from_subaccount: Option, 268 | pub spender: Option, 269 | pub memo: Option>, 270 | pub created_at_time: Option, 271 | } 272 | 273 | pub type RevokeCollectionApprovalResult = Result; 274 | 275 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 276 | pub struct IsApprovedArg { 277 | pub spender: Account, 278 | pub from_subaccount: Option, 279 | pub token_id: u128, 280 | } 281 | 282 | #[derive(CandidType, Deserialize, Clone, Debug)] 283 | pub struct TransferFromArg { 284 | pub spender_subaccount: Option, 285 | pub from: Account, 286 | pub to: Account, 287 | pub token_id: u128, 288 | pub memo: Option>, 289 | pub created_at_time: Option, 290 | } 291 | 292 | impl Into for TransferFromArg { 293 | fn into(self) -> TransferArg { 294 | TransferArg { 295 | from_subaccount: self.spender_subaccount, 296 | to: self.to, 297 | token_id: self.token_id, 298 | memo: self.memo, 299 | created_at_time: self.created_at_time, 300 | } 301 | } 302 | } 303 | 304 | pub type TransferFromResult = Result; 305 | -------------------------------------------------------------------------------- /wasm/icrc7.did: -------------------------------------------------------------------------------- 1 | type Account = record { owner : principal; subaccount : opt blob }; 2 | type ApprovalInfo = record { 3 | memo : opt blob; 4 | from_subaccount : opt blob; 5 | created_at_time : opt nat64; 6 | expires_at : opt nat64; 7 | spender : Account; 8 | }; 9 | type ApproveCollectionArg = record { approval_info : ApprovalInfo }; 10 | type ApproveCollectionError = variant { 11 | GenericError : record { message : text; error_code : nat }; 12 | Duplicate : record { duplicate_of : nat }; 13 | InvalidSpender; 14 | CreatedInFuture : record { ledger_time : nat64 }; 15 | GenericBatchError : record { message : text; error_code : nat }; 16 | TooOld; 17 | }; 18 | type ApproveTokenArg = record { token_id : nat; approval_info : ApprovalInfo }; 19 | type ApproveTokenError = variant { 20 | GenericError : record { message : text; error_code : nat }; 21 | Duplicate : record { duplicate_of : nat }; 22 | InvalidSpender; 23 | NonExistingTokenId; 24 | Unauthorized; 25 | CreatedInFuture : record { ledger_time : nat64 }; 26 | GenericBatchError : record { message : text; error_code : nat }; 27 | TooOld; 28 | }; 29 | type ArchivedTransactionResponse = record { 30 | args : vec TransactionRange; 31 | callback : func (vec QueryBlock) -> (GetTransactionsResult) query; 32 | }; 33 | type BTreeMap = vec record { 34 | text; 35 | variant { 36 | Int : int; 37 | Map : BTreeMap; 38 | Nat : nat; 39 | Nat64 : nat64; 40 | Blob : blob; 41 | Text : text; 42 | Array : vec Value; 43 | }; 44 | }; 45 | type Block = variant { 46 | Int : int; 47 | Map : BTreeMap; 48 | Nat : nat; 49 | Nat64 : nat64; 50 | Blob : blob; 51 | Text : text; 52 | Array : vec Value; 53 | }; 54 | type BlockType = record { url : text; block_type : text }; 55 | type BurnArg = record { 56 | token_id : nat; 57 | memo : opt blob; 58 | from_subaccount : opt blob; 59 | }; 60 | type BurnError = variant { 61 | GenericError : record { message : text; error_code : nat }; 62 | NonExistingTokenId; 63 | Unauthorized; 64 | GenericBatchError : record { message : text; error_code : nat }; 65 | }; 66 | type DataCertificate = record { certificate : opt blob; hash_tree : blob }; 67 | type GetArchiveArgs = record { from : opt principal }; 68 | type GetArchivesResultItem = record { 69 | end : nat; 70 | canister_id : principal; 71 | start : nat; 72 | }; 73 | type GetBlocksResult = record { 74 | log_length : nat; 75 | blocks : vec QueryBlock; 76 | archived_blocks : vec ArchivedTransactionResponse; 77 | }; 78 | type GetTransactionsResult = record { 79 | log_length : nat; 80 | blocks : Vec; 81 | archived_blocks : blob; 82 | }; 83 | type IndexType = variant { Stable; StableTyped; Managed }; 84 | type InitApprovalsArg = record { 85 | max_approvals : opt nat16; 86 | max_approvals_per_token_or_collection : opt nat16; 87 | settle_to_approvals : opt nat16; 88 | max_revoke_approvals : opt nat16; 89 | collection_approval_requires_token : opt bool; 90 | }; 91 | type InitArchiveArg = record { 92 | maxRecordsToArchive : nat; 93 | archiveIndexType : IndexType; 94 | maxArchivePages : nat; 95 | settleToRecords : nat; 96 | archiveCycles : nat; 97 | maxActiveRecords : nat; 98 | maxRecordsInArchiveInstance : nat; 99 | archiveControllers : opt opt vec principal; 100 | }; 101 | type InitArg = record { 102 | icrc7_supply_cap : opt nat; 103 | icrc7_description : opt text; 104 | tx_window : opt nat64; 105 | minting_account : opt Account; 106 | icrc7_max_query_batch_size : opt nat16; 107 | permitted_drift : opt nat64; 108 | archive_init : opt InitArchiveArg; 109 | icrc7_max_take_value : opt nat; 110 | icrc7_max_memo_size : opt nat32; 111 | icrc7_symbol : text; 112 | icrc7_max_update_batch_size : opt nat16; 113 | icrc7_atomic_batch_transfers : opt bool; 114 | approval_init : opt InitApprovalsArg; 115 | icrc7_default_take_value : opt nat; 116 | icrc7_logo : opt text; 117 | icrc7_name : text; 118 | }; 119 | type IsApprovedArg = record { 120 | token_id : nat; 121 | from_subaccount : opt blob; 122 | spender : Account; 123 | }; 124 | type MintArg = record { 125 | to : Account; 126 | token_id : nat; 127 | memo : opt blob; 128 | from_subaccount : opt blob; 129 | token_description : opt text; 130 | token_logo : opt text; 131 | token_name : opt text; 132 | }; 133 | type MintError = variant { 134 | GenericError : record { message : text; error_code : nat }; 135 | SupplyCapReached; 136 | TokenIdMinimumLimit; 137 | Unauthorized; 138 | GenericBatchError : record { message : text; error_code : nat }; 139 | TokenIdAlreadyExist; 140 | }; 141 | type QueryBlock = record { id : nat; block : Value }; 142 | type Result = variant { Ok : nat; Err : BurnError }; 143 | type Result_1 = variant { Ok : nat; Err : ApproveCollectionError }; 144 | type Result_2 = variant { Ok : nat; Err : ApproveTokenError }; 145 | type Result_3 = variant { Ok : nat; Err : RevokeCollectionApprovalError }; 146 | type Result_4 = variant { Ok : nat; Err : RevokeTokenApprovalError }; 147 | type Result_5 = variant { Ok : nat; Err : TransferFromError }; 148 | type Result_6 = variant { Ok : nat; Err : TransferError }; 149 | type Result_7 = variant { Ok : nat; Err : MintError }; 150 | type RevokeCollectionApprovalArg = record { 151 | memo : opt blob; 152 | from_subaccount : opt blob; 153 | created_at_time : opt nat64; 154 | spender : opt Account; 155 | }; 156 | type RevokeCollectionApprovalError = variant { 157 | GenericError : record { message : text; error_code : nat }; 158 | Duplicate : record { duplicate_of : nat }; 159 | Unauthorized; 160 | CreatedInFuture : record { ledger_time : nat64 }; 161 | ApprovalDoesNotExist; 162 | GenericBatchError : record { message : text; error_code : nat }; 163 | TooOld; 164 | }; 165 | type RevokeTokenApprovalArg = record { 166 | token_id : nat; 167 | memo : opt blob; 168 | from_subaccount : opt blob; 169 | created_at_time : opt nat64; 170 | spender : opt Account; 171 | }; 172 | type RevokeTokenApprovalError = variant { 173 | GenericError : record { message : text; error_code : nat }; 174 | Duplicate : record { duplicate_of : nat }; 175 | NonExistingTokenId; 176 | Unauthorized; 177 | CreatedInFuture : record { ledger_time : nat64 }; 178 | ApprovalDoesNotExist; 179 | GenericBatchError : record { message : text; error_code : nat }; 180 | TooOld; 181 | }; 182 | type Standard = record { url : text; name : text }; 183 | type Tip = record { 184 | last_block_index : blob; 185 | hash_tree : blob; 186 | last_block_hash : blob; 187 | }; 188 | type TokenApproval = record { token_id : nat; approval_info : ApprovalInfo }; 189 | type Transaction = record { 190 | op : text; 191 | to : opt Account; 192 | ts : nat64; 193 | exp : opt nat64; 194 | tid : nat; 195 | from : opt Account; 196 | memo : opt blob; 197 | meta : opt vec record { text; Block }; 198 | block : opt Block; 199 | spender : opt Account; 200 | }; 201 | type TransactionRange = record { start : nat; length : nat }; 202 | type TransferArg = record { 203 | to : Account; 204 | token_id : nat; 205 | memo : opt blob; 206 | from_subaccount : opt blob; 207 | created_at_time : opt nat64; 208 | }; 209 | type TransferError = variant { 210 | GenericError : record { message : text; error_code : nat }; 211 | Duplicate : record { duplicate_of : nat }; 212 | NonExistingTokenId; 213 | Unauthorized; 214 | CreatedInFuture : record { ledger_time : nat64 }; 215 | InvalidRecipient; 216 | GenericBatchError : record { message : text; error_code : nat }; 217 | TooOld; 218 | }; 219 | type TransferFromArg = record { 220 | to : Account; 221 | spender_subaccount : opt blob; 222 | token_id : nat; 223 | from : Account; 224 | memo : opt blob; 225 | created_at_time : opt nat64; 226 | }; 227 | type TransferFromError = variant { 228 | GenericError : record { message : text; error_code : nat }; 229 | Duplicate : record { duplicate_of : nat }; 230 | NonExistingTokenId; 231 | Unauthorized; 232 | CreatedInFuture : record { ledger_time : nat64 }; 233 | InvalidRecipient; 234 | GenericBatchError : record { message : text; error_code : nat }; 235 | TooOld; 236 | }; 237 | type Value = variant { 238 | Int : int; 239 | Map : BTreeMap; 240 | Nat : nat; 241 | Nat64 : nat64; 242 | Blob : blob; 243 | Text : text; 244 | Array : vec Value; 245 | }; 246 | type Vec = vec record { 247 | args : vec record { start : nat; length : nat }; 248 | callback : func ( 249 | vec record { 250 | id : nat; 251 | block : variant { 252 | Int : int; 253 | Map : BTreeMap; 254 | Nat : nat; 255 | Nat64 : nat64; 256 | Blob : blob; 257 | Text : text; 258 | Array : vec Value; 259 | }; 260 | }, 261 | ) -> ( 262 | record { log_length : nat; blocks : Vec; archived_blocks : blob }, 263 | ) query; 264 | }; 265 | type WalletReceiveResult = record { accepted : nat64 }; 266 | service : (InitArg) -> { 267 | __get_candid_interface_tmp_hack : () -> (text) query; 268 | archive_log_canister : () -> (opt principal) query; 269 | burn : (vec BurnArg) -> (vec opt Result); 270 | get_tip : () -> (Tip) query; 271 | icrc37_approve_collection : (vec ApproveCollectionArg) -> (vec opt Result_1); 272 | icrc37_approve_tokens : (vec ApproveTokenArg) -> (vec opt Result_2); 273 | icrc37_get_collection_approvals : (Account, opt ApprovalInfo, opt nat) -> ( 274 | vec ApprovalInfo, 275 | ) query; 276 | icrc37_get_token_approvals : (nat, opt TokenApproval, opt nat) -> ( 277 | vec TokenApproval, 278 | ) query; 279 | icrc37_is_approved : (vec IsApprovedArg) -> (vec bool) query; 280 | icrc37_max_approvals_per_token_or_collection : () -> (opt nat) query; 281 | icrc37_max_revoke_approvals : () -> (opt nat) query; 282 | icrc37_metadata : () -> (vec record { text; Value }) query; 283 | icrc37_revoke_collection_approvals : (vec RevokeCollectionApprovalArg) -> ( 284 | vec opt Result_3, 285 | ); 286 | icrc37_revoke_token_approvals : (vec RevokeTokenApprovalArg) -> ( 287 | vec opt Result_4, 288 | ); 289 | icrc37_transfer_from : (vec TransferFromArg) -> (vec opt Result_5); 290 | icrc3_get_archives : (GetArchiveArgs) -> (vec GetArchivesResultItem) query; 291 | icrc3_get_blocks : (vec TransactionRange) -> (GetBlocksResult) query; 292 | icrc3_get_tip_certificate : () -> (opt DataCertificate) query; 293 | icrc3_supported_block_types : () -> (vec BlockType) query; 294 | icrc7_atomic_batch_transfers : () -> (opt bool) query; 295 | icrc7_balance_of : (vec Account) -> (vec nat) query; 296 | icrc7_default_take_value : () -> (opt nat) query; 297 | icrc7_description : () -> (opt text) query; 298 | icrc7_logo : () -> (opt text) query; 299 | icrc7_max_memo_size : () -> (opt nat32) query; 300 | icrc7_max_query_batch_size : () -> (opt nat16) query; 301 | icrc7_max_take_value : () -> (opt nat) query; 302 | icrc7_max_update_batch_size : () -> (opt nat16) query; 303 | icrc7_name : () -> (text) query; 304 | icrc7_owner_of : (vec nat) -> (vec opt Account) query; 305 | icrc7_supply_cap : () -> (opt nat) query; 306 | icrc7_supported_standards : () -> (vec Standard) query; 307 | icrc7_symbol : () -> (text) query; 308 | icrc7_token_metadata : (vec nat) -> ( 309 | vec opt vec record { text; Value }, 310 | ) query; 311 | icrc7_tokens : (opt nat, opt nat) -> (vec nat) query; 312 | icrc7_tokens_of : (Account, opt nat, opt nat) -> (vec nat) query; 313 | icrc7_total_supply : () -> (nat) query; 314 | icrc7_transfer : (vec TransferArg) -> (vec opt Result_6); 315 | mint : (MintArg) -> (Result_7); 316 | minting_authority : () -> (opt Account) query; 317 | set_minting_authority : (Account) -> (bool); 318 | txn_logs : (nat32, nat32) -> (vec Transaction) query; 319 | wallet_balance : () -> (nat) query; 320 | wallet_receive : () -> (WalletReceiveResult); 321 | } -------------------------------------------------------------------------------- /src/icrc7/icrc7.did: -------------------------------------------------------------------------------- 1 | type Account = record { owner : principal; subaccount : opt blob }; 2 | type ApprovalInfo = record { 3 | memo : opt blob; 4 | from_subaccount : opt blob; 5 | created_at_time : opt nat64; 6 | expires_at : opt nat64; 7 | spender : Account; 8 | }; 9 | type ApproveCollectionArg = record { approval_info : ApprovalInfo }; 10 | type ApproveCollectionError = variant { 11 | GenericError : record { message : text; error_code : nat }; 12 | Duplicate : record { duplicate_of : nat }; 13 | InvalidSpender; 14 | CreatedInFuture : record { ledger_time : nat64 }; 15 | GenericBatchError : record { message : text; error_code : nat }; 16 | TooOld; 17 | }; 18 | type ApproveTokenArg = record { token_id : nat; approval_info : ApprovalInfo }; 19 | type ApproveTokenError = variant { 20 | GenericError : record { message : text; error_code : nat }; 21 | Duplicate : record { duplicate_of : nat }; 22 | InvalidSpender; 23 | NonExistingTokenId; 24 | Unauthorized; 25 | CreatedInFuture : record { ledger_time : nat64 }; 26 | GenericBatchError : record { message : text; error_code : nat }; 27 | TooOld; 28 | }; 29 | type ArchivedTransactionResponse = record { 30 | args : vec TransactionRange; 31 | callback : func (vec QueryBlock) -> (GetTransactionsResult) query; 32 | }; 33 | type BTreeMap = vec record { 34 | text; 35 | variant { 36 | Int : int; 37 | Map : BTreeMap; 38 | Nat : nat; 39 | Nat64 : nat64; 40 | Blob : blob; 41 | Text : text; 42 | Array : vec Value; 43 | }; 44 | }; 45 | type Block = variant { 46 | Int : int; 47 | Map : BTreeMap; 48 | Nat : nat; 49 | Nat64 : nat64; 50 | Blob : blob; 51 | Text : text; 52 | Array : vec Value; 53 | }; 54 | type BlockType = record { url : text; block_type : text }; 55 | type BurnArg = record { 56 | token_id : nat; 57 | memo : opt blob; 58 | from_subaccount : opt blob; 59 | }; 60 | type BurnError = variant { 61 | GenericError : record { message : text; error_code : nat }; 62 | NonExistingTokenId; 63 | Unauthorized; 64 | GenericBatchError : record { message : text; error_code : nat }; 65 | }; 66 | type DataCertificate = record { certificate : opt blob; hash_tree : blob }; 67 | type GetArchiveArgs = record { from : opt principal }; 68 | type GetArchivesResultItem = record { 69 | end : nat; 70 | canister_id : principal; 71 | start : nat; 72 | }; 73 | type GetBlocksResult = record { 74 | log_length : nat; 75 | blocks : vec QueryBlock; 76 | archived_blocks : vec ArchivedTransactionResponse; 77 | }; 78 | type GetTransactionsResult = record { 79 | log_length : nat; 80 | blocks : Vec; 81 | archived_blocks : blob; 82 | }; 83 | type IndexType = variant { Stable; StableTyped; Managed }; 84 | type InitApprovalsArg = record { 85 | max_approvals : opt nat16; 86 | max_approvals_per_token_or_collection : opt nat16; 87 | settle_to_approvals : opt nat16; 88 | max_revoke_approvals : opt nat16; 89 | collection_approval_requires_token : opt bool; 90 | }; 91 | type InitArchiveArg = record { 92 | maxRecordsToArchive : nat; 93 | archiveIndexType : IndexType; 94 | maxArchivePages : nat; 95 | settleToRecords : nat; 96 | archiveCycles : nat; 97 | maxActiveRecords : nat; 98 | maxRecordsInArchiveInstance : nat; 99 | archiveControllers : opt opt vec principal; 100 | }; 101 | type InitArg = record { 102 | icrc7_supply_cap : opt nat; 103 | icrc7_description : opt text; 104 | tx_window : opt nat64; 105 | minting_account : opt Account; 106 | icrc7_max_query_batch_size : opt nat16; 107 | permitted_drift : opt nat64; 108 | archive_init : opt InitArchiveArg; 109 | icrc7_max_take_value : opt nat; 110 | icrc7_max_memo_size : opt nat32; 111 | icrc7_symbol : text; 112 | icrc7_max_update_batch_size : opt nat16; 113 | icrc7_atomic_batch_transfers : opt bool; 114 | approval_init : opt InitApprovalsArg; 115 | icrc7_default_take_value : opt nat; 116 | icrc7_logo : opt text; 117 | icrc7_name : text; 118 | }; 119 | type IsApprovedArg = record { 120 | token_id : nat; 121 | from_subaccount : opt blob; 122 | spender : Account; 123 | }; 124 | type MintArg = record { 125 | to : Account; 126 | token_id : nat; 127 | memo : opt blob; 128 | from_subaccount : opt blob; 129 | token_description : opt text; 130 | token_logo : opt text; 131 | token_name : opt text; 132 | }; 133 | type MintError = variant { 134 | GenericError : record { message : text; error_code : nat }; 135 | SupplyCapReached; 136 | TokenIdMinimumLimit; 137 | Unauthorized; 138 | GenericBatchError : record { message : text; error_code : nat }; 139 | TokenIdAlreadyExist; 140 | }; 141 | type QueryBlock = record { id : nat; block : Value }; 142 | type Result = variant { Ok : nat; Err : BurnError }; 143 | type Result_1 = variant { Ok : nat; Err : ApproveCollectionError }; 144 | type Result_2 = variant { Ok : nat; Err : ApproveTokenError }; 145 | type Result_3 = variant { Ok : nat; Err : RevokeCollectionApprovalError }; 146 | type Result_4 = variant { Ok : nat; Err : RevokeTokenApprovalError }; 147 | type Result_5 = variant { Ok : nat; Err : TransferFromError }; 148 | type Result_6 = variant { Ok : nat; Err : TransferError }; 149 | type Result_7 = variant { Ok : nat; Err : MintError }; 150 | type RevokeCollectionApprovalArg = record { 151 | memo : opt blob; 152 | from_subaccount : opt blob; 153 | created_at_time : opt nat64; 154 | spender : opt Account; 155 | }; 156 | type RevokeCollectionApprovalError = variant { 157 | GenericError : record { message : text; error_code : nat }; 158 | Duplicate : record { duplicate_of : nat }; 159 | Unauthorized; 160 | CreatedInFuture : record { ledger_time : nat64 }; 161 | ApprovalDoesNotExist; 162 | GenericBatchError : record { message : text; error_code : nat }; 163 | TooOld; 164 | }; 165 | type RevokeTokenApprovalArg = record { 166 | token_id : nat; 167 | memo : opt blob; 168 | from_subaccount : opt blob; 169 | created_at_time : opt nat64; 170 | spender : opt Account; 171 | }; 172 | type RevokeTokenApprovalError = variant { 173 | GenericError : record { message : text; error_code : nat }; 174 | Duplicate : record { duplicate_of : nat }; 175 | NonExistingTokenId; 176 | Unauthorized; 177 | CreatedInFuture : record { ledger_time : nat64 }; 178 | ApprovalDoesNotExist; 179 | GenericBatchError : record { message : text; error_code : nat }; 180 | TooOld; 181 | }; 182 | type Standard = record { url : text; name : text }; 183 | type Tip = record { 184 | last_block_index : blob; 185 | hash_tree : blob; 186 | last_block_hash : blob; 187 | }; 188 | type TokenApproval = record { token_id : nat; approval_info : ApprovalInfo }; 189 | type Transaction = record { 190 | op : text; 191 | to : opt Account; 192 | ts : nat64; 193 | exp : opt nat64; 194 | tid : nat; 195 | from : opt Account; 196 | memo : opt blob; 197 | meta : opt vec record { text; Block }; 198 | block : opt Block; 199 | spender : opt Account; 200 | }; 201 | type TransactionRange = record { start : nat; length : nat }; 202 | type TransferArg = record { 203 | to : Account; 204 | token_id : nat; 205 | memo : opt blob; 206 | from_subaccount : opt blob; 207 | created_at_time : opt nat64; 208 | }; 209 | type TransferError = variant { 210 | GenericError : record { message : text; error_code : nat }; 211 | Duplicate : record { duplicate_of : nat }; 212 | NonExistingTokenId; 213 | Unauthorized; 214 | CreatedInFuture : record { ledger_time : nat64 }; 215 | InvalidRecipient; 216 | GenericBatchError : record { message : text; error_code : nat }; 217 | TooOld; 218 | }; 219 | type TransferFromArg = record { 220 | to : Account; 221 | spender_subaccount : opt blob; 222 | token_id : nat; 223 | from : Account; 224 | memo : opt blob; 225 | created_at_time : opt nat64; 226 | }; 227 | type TransferFromError = variant { 228 | GenericError : record { message : text; error_code : nat }; 229 | Duplicate : record { duplicate_of : nat }; 230 | NonExistingTokenId; 231 | Unauthorized; 232 | CreatedInFuture : record { ledger_time : nat64 }; 233 | InvalidRecipient; 234 | GenericBatchError : record { message : text; error_code : nat }; 235 | TooOld; 236 | }; 237 | type Value = variant { 238 | Int : int; 239 | Map : BTreeMap; 240 | Nat : nat; 241 | Nat64 : nat64; 242 | Blob : blob; 243 | Text : text; 244 | Array : vec Value; 245 | }; 246 | type Vec = vec record { 247 | args : vec record { start : nat; length : nat }; 248 | callback : func ( 249 | vec record { 250 | id : nat; 251 | block : variant { 252 | Int : int; 253 | Map : BTreeMap; 254 | Nat : nat; 255 | Nat64 : nat64; 256 | Blob : blob; 257 | Text : text; 258 | Array : vec Value; 259 | }; 260 | }, 261 | ) -> ( 262 | record { log_length : nat; blocks : Vec; archived_blocks : blob }, 263 | ) query; 264 | }; 265 | type WalletReceiveResult = record { accepted : nat64 }; 266 | service : (InitArg) -> { 267 | __get_candid_interface_tmp_hack : () -> (text) query; 268 | archive_log_canister : () -> (opt principal) query; 269 | burn : (vec BurnArg) -> (vec opt Result); 270 | get_tip : () -> (Tip) query; 271 | icrc37_approve_collection : (vec ApproveCollectionArg) -> (vec opt Result_1); 272 | icrc37_approve_tokens : (vec ApproveTokenArg) -> (vec opt Result_2); 273 | icrc37_get_collection_approvals : (Account, opt ApprovalInfo, opt nat) -> ( 274 | vec ApprovalInfo, 275 | ) query; 276 | icrc37_get_token_approvals : (nat, opt TokenApproval, opt nat) -> ( 277 | vec TokenApproval, 278 | ) query; 279 | icrc37_is_approved : (vec IsApprovedArg) -> (vec bool) query; 280 | icrc37_max_approvals_per_token_or_collection : () -> (opt nat) query; 281 | icrc37_max_revoke_approvals : () -> (opt nat) query; 282 | icrc37_metadata : () -> (vec record { text; Value }) query; 283 | icrc37_revoke_collection_approvals : (vec RevokeCollectionApprovalArg) -> ( 284 | vec opt Result_3, 285 | ); 286 | icrc37_revoke_token_approvals : (vec RevokeTokenApprovalArg) -> ( 287 | vec opt Result_4, 288 | ); 289 | icrc37_transfer_from : (vec TransferFromArg) -> (vec opt Result_5); 290 | icrc3_get_archives : (GetArchiveArgs) -> (vec GetArchivesResultItem) query; 291 | icrc3_get_blocks : (vec TransactionRange) -> (GetBlocksResult) query; 292 | icrc3_get_tip_certificate : () -> (opt DataCertificate) query; 293 | icrc3_supported_block_types : () -> (vec BlockType) query; 294 | icrc7_atomic_batch_transfers : () -> (opt bool) query; 295 | icrc7_balance_of : (vec Account) -> (vec nat) query; 296 | icrc7_default_take_value : () -> (opt nat) query; 297 | icrc7_description : () -> (opt text) query; 298 | icrc7_logo : () -> (opt text) query; 299 | icrc7_max_memo_size : () -> (opt nat32) query; 300 | icrc7_max_query_batch_size : () -> (opt nat16) query; 301 | icrc7_max_take_value : () -> (opt nat) query; 302 | icrc7_max_update_batch_size : () -> (opt nat16) query; 303 | icrc7_name : () -> (text) query; 304 | icrc7_owner_of : (vec nat) -> (vec opt Account) query; 305 | icrc7_supply_cap : () -> (opt nat) query; 306 | icrc7_supported_standards : () -> (vec Standard) query; 307 | icrc7_symbol : () -> (text) query; 308 | icrc7_token_metadata : (vec nat) -> ( 309 | vec opt vec record { text; Value }, 310 | ) query; 311 | icrc7_tokens : (opt nat, opt nat) -> (vec nat) query; 312 | icrc7_tokens_of : (Account, opt nat, opt nat) -> (vec nat) query; 313 | icrc7_total_supply : () -> (nat) query; 314 | icrc7_transfer : (vec TransferArg) -> (vec opt Result_6); 315 | mint : (MintArg) -> (Result_7); 316 | minting_authority : () -> (opt Account) query; 317 | set_minting_authority : (Account) -> (bool); 318 | txn_logs : (nat32, nat32) -> (vec Transaction) query; 319 | wallet_balance : () -> (nat) query; 320 | wallet_receive : () -> (WalletReceiveResult); 321 | } -------------------------------------------------------------------------------- /src/icrc7/src/icrc7_types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use candid::{CandidType, Decode, Encode}; 4 | use ic_stable_structures::{storable::Bound, Storable}; 5 | use icrc_ledger_types::{ 6 | icrc::generic_value::Value, 7 | icrc1::account::{Account, Subaccount}, 8 | }; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::{ 12 | errors::{BurnError, InsertTransactionError, MintError, TransferError}, 13 | icrc37_types::InitApprovalsArg, 14 | icrc3_types::{Block, InitArchiveArg}, 15 | }; 16 | 17 | pub static TRANSACTION_TRANSFER_OP: &str = "7xfer"; 18 | pub static TRANSACTION_TRANSFER_FROM_OP: &str = "37xfer"; 19 | 20 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 21 | pub enum TransactionType { 22 | Mint { 23 | tid: u128, 24 | from: Account, 25 | to: Account, 26 | meta: Icrc7TokenMetadata, 27 | }, 28 | Burn { 29 | tid: u128, 30 | from: Account, 31 | to: Account, 32 | }, 33 | Transfer { 34 | tid: u128, 35 | from: Account, 36 | to: Account, 37 | }, 38 | TransferFrom { 39 | tid: u128, 40 | from: Account, 41 | to: Account, 42 | spender: Account, 43 | }, 44 | Approval { 45 | tid: u128, 46 | from: Account, 47 | to: Account, 48 | exp_sec: Option, 49 | }, 50 | ApproveCollection { 51 | from: Account, 52 | to: Account, 53 | exp_sec: Option, 54 | }, 55 | Revoke { 56 | tid: u128, 57 | from: Account, 58 | to: Option, 59 | }, 60 | RevokeCollection { 61 | from: Account, 62 | to: Option, 63 | }, 64 | } 65 | 66 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone, Default)] 67 | pub struct Transaction { 68 | pub ts: u64, 69 | pub op: String, // "7mint" | "7burn" | "7xfer" | "7update" | "37appr" | "37appr_coll | "37revoke" | "37revoke_coll" | "37xfer" 70 | pub tid: u128, 71 | pub from: Option, 72 | pub to: Option, 73 | pub spender: Option, 74 | pub exp: Option, 75 | pub meta: Option, 76 | pub memo: Option>, 77 | pub block: Option, 78 | } 79 | 80 | impl Transaction { 81 | pub fn mint( 82 | now_sec: u64, 83 | tid: u128, 84 | from: Option, 85 | to: Account, 86 | meta: Icrc7TokenMetadata, 87 | memo: Option>, 88 | ) -> Self { 89 | Transaction { 90 | ts: now_sec, 91 | op: "7mint".to_string(), 92 | tid, 93 | from, 94 | to: Some(to), 95 | meta: Some(meta), 96 | memo, 97 | ..Default::default() 98 | } 99 | } 100 | 101 | pub fn burn( 102 | now_sec: u64, 103 | tid: u128, 104 | from: Account, 105 | to: Option, 106 | memo: Option>, 107 | ) -> Self { 108 | Transaction { 109 | ts: now_sec, 110 | op: "7burn".to_string(), 111 | tid, 112 | from: Some(from), 113 | to, 114 | memo, 115 | ..Default::default() 116 | } 117 | } 118 | 119 | pub fn transfer( 120 | now_sec: u64, 121 | tid: u128, 122 | from: Account, 123 | to: Account, 124 | memo: Option>, 125 | ) -> Self { 126 | Transaction { 127 | ts: now_sec, 128 | op: "7xfer".to_string(), 129 | tid, 130 | from: Some(from), 131 | to: Some(to), 132 | memo, 133 | ..Default::default() 134 | } 135 | } 136 | 137 | pub fn update( 138 | now_sec: u64, 139 | tid: u128, 140 | from: Account, 141 | meta: Icrc7TokenMetadata, 142 | memo: Option>, 143 | ) -> Self { 144 | Transaction { 145 | ts: now_sec, 146 | op: "7update".to_string(), 147 | tid, 148 | from: Some(from), 149 | meta: Some(meta), 150 | memo, 151 | ..Default::default() 152 | } 153 | } 154 | 155 | pub fn approve( 156 | now_sec: u64, 157 | tid: u128, 158 | from: Account, 159 | spender: Account, 160 | exp_sec: Option, 161 | memo: Option>, 162 | ) -> Self { 163 | Transaction { 164 | ts: now_sec, 165 | op: "37appr".to_string(), 166 | tid, 167 | from: Some(from), 168 | spender: Some(spender), 169 | exp: exp_sec, 170 | memo, 171 | ..Default::default() 172 | } 173 | } 174 | 175 | pub fn approve_collection( 176 | now_sec: u64, 177 | from: Account, 178 | spender: Account, 179 | exp_sec: Option, 180 | memo: Option>, 181 | ) -> Self { 182 | Transaction { 183 | ts: now_sec, 184 | op: "37appr_coll".to_string(), 185 | from: Some(from), 186 | spender: Some(spender), 187 | exp: exp_sec, 188 | memo, 189 | ..Default::default() 190 | } 191 | } 192 | 193 | pub fn revoke( 194 | now_sec: u64, 195 | tid: u128, 196 | from: Account, 197 | spender: Option, 198 | memo: Option>, 199 | ) -> Self { 200 | Transaction { 201 | ts: now_sec, 202 | op: "37revoke".to_string(), 203 | tid, 204 | from: Some(from), 205 | spender, 206 | memo, 207 | ..Default::default() 208 | } 209 | } 210 | 211 | pub fn revoke_collection( 212 | now_sec: u64, 213 | from: Account, 214 | spender: Option, 215 | memo: Option>, 216 | ) -> Self { 217 | Transaction { 218 | ts: now_sec, 219 | op: "37revoke_coll".to_string(), 220 | from: Some(from), 221 | spender, 222 | memo, 223 | ..Default::default() 224 | } 225 | } 226 | 227 | pub fn transfer_from( 228 | now_sec: u64, 229 | tid: u128, 230 | from: Account, 231 | to: Account, 232 | spender: Account, 233 | memo: Option>, 234 | ) -> Self { 235 | Transaction { 236 | ts: now_sec, 237 | op: "37xfer".to_string(), 238 | tid, 239 | from: Some(from), 240 | to: Some(to), 241 | spender: Some(spender), 242 | memo, 243 | ..Default::default() 244 | } 245 | } 246 | 247 | pub fn new(_txn_id: u128, txn_type: TransactionType, at: u64, memo: Option>) -> Self { 248 | let transaction = match &txn_type { 249 | TransactionType::Transfer { tid, from, to } => { 250 | Self::transfer(at, tid.clone(), from.clone(), to.clone(), memo) 251 | } 252 | TransactionType::Mint { 253 | tid, 254 | from, 255 | to, 256 | meta, 257 | } => Self::mint( 258 | at, 259 | tid.clone(), 260 | Some(from.clone()), 261 | to.clone(), 262 | meta.clone(), 263 | memo, 264 | ), 265 | TransactionType::Burn { tid, from, to } => { 266 | Self::burn(at, tid.clone(), from.clone(), Some(to.clone()), memo) 267 | } 268 | TransactionType::Approval { 269 | tid, 270 | from, 271 | to, 272 | exp_sec, 273 | } => Self::approve( 274 | at, 275 | tid.clone(), 276 | from.clone(), 277 | to.clone(), 278 | exp_sec.clone(), 279 | memo, 280 | ), 281 | TransactionType::ApproveCollection { from, to, exp_sec } => { 282 | Self::approve_collection(at, from.clone(), to.clone(), exp_sec.clone(), memo) 283 | } 284 | TransactionType::Revoke { tid, from, to: _ } => { 285 | Self::revoke(at, tid.clone(), from.clone(), None, memo) 286 | } 287 | TransactionType::RevokeCollection { from, to: _ } => { 288 | Self::revoke_collection(at, from.clone(), None, memo) 289 | } 290 | TransactionType::TransferFrom { 291 | tid, 292 | from, 293 | to, 294 | spender, 295 | } => Self::transfer_from( 296 | at, 297 | tid.clone(), 298 | from.clone(), 299 | to.clone(), 300 | spender.clone(), 301 | memo, 302 | ), 303 | }; 304 | return transaction; 305 | } 306 | } 307 | 308 | impl Storable for Transaction { 309 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 310 | Decode!(bytes.as_ref(), Self).unwrap() 311 | } 312 | 313 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 314 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 315 | } 316 | 317 | const BOUND: Bound = Bound::Unbounded; 318 | } 319 | 320 | #[derive(CandidType, Deserialize, Clone, Debug)] 321 | pub struct TransferArg { 322 | pub from_subaccount: Option, 323 | pub to: Account, 324 | pub token_id: u128, 325 | pub memo: Option>, 326 | pub created_at_time: Option, 327 | } 328 | 329 | pub type TransferResult = Result; 330 | 331 | pub type Icrc7TokenMetadata = BTreeMap; 332 | 333 | #[derive(CandidType, Deserialize, Clone)] 334 | pub struct MintArg { 335 | pub from_subaccount: Option, 336 | pub to: Account, 337 | pub token_id: u128, 338 | pub memo: Option>, 339 | // if None, then the combination of Collection's symbol and token's id will be provided 340 | // for e.g.: "ICRC7 100" 341 | pub token_name: Option, 342 | pub token_description: Option, 343 | pub token_logo: Option, 344 | } 345 | 346 | pub type MintResult = Result; 347 | 348 | #[derive(CandidType, Deserialize, Clone)] 349 | pub struct BurnArg { 350 | pub from_subaccount: Option, 351 | pub token_id: u128, 352 | pub memo: Option>, 353 | } 354 | 355 | pub type BurnResult = Result; 356 | 357 | #[derive(CandidType, Deserialize)] 358 | pub struct InitArg { 359 | pub minting_account: Option, 360 | pub icrc7_symbol: String, 361 | pub icrc7_name: String, 362 | pub icrc7_description: Option, 363 | pub icrc7_logo: Option, 364 | pub icrc7_supply_cap: Option, 365 | pub icrc7_max_query_batch_size: Option, 366 | pub icrc7_max_update_batch_size: Option, 367 | pub icrc7_max_take_value: Option, 368 | pub icrc7_default_take_value: Option, 369 | pub icrc7_max_memo_size: Option, 370 | pub icrc7_atomic_batch_transfers: Option, 371 | pub tx_window: Option, 372 | pub permitted_drift: Option, 373 | pub approval_init: Option, 374 | pub archive_init: Option, 375 | } 376 | 377 | #[derive(CandidType)] 378 | pub struct Standard { 379 | pub name: String, 380 | pub url: String, 381 | } 382 | 383 | #[derive(CandidType, Deserialize)] 384 | pub struct ApprovalArg { 385 | pub from_subaccount: Option, 386 | pub spender: Account, 387 | pub token_id: u128, 388 | pub expires_at: Option, 389 | pub memo: Option>, 390 | } 391 | 392 | pub type SyncReceipt = Result; 393 | -------------------------------------------------------------------------------- /src/icrc7_types/src/icrc7_types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use candid::{CandidType, Decode, Encode}; 4 | use ic_stable_structures::{storable::Bound, Storable}; 5 | use icrc_ledger_types::{ 6 | icrc::generic_value::Value, 7 | icrc1::account::{Account, Subaccount}, 8 | }; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::{ 12 | errors::{BurnError, InsertTransactionError, MintError, TransferError}, 13 | icrc37_types::InitApprovalsArg, 14 | icrc3_types::{Block, InitArchiveArg}, 15 | }; 16 | 17 | pub static TRANSACTION_TRANSFER_OP: &str = "7xfer"; 18 | pub static TRANSACTION_TRANSFER_FROM_OP: &str = "37xfer"; 19 | 20 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 21 | pub enum TransactionType { 22 | Mint { 23 | tid: u128, 24 | from: Account, 25 | to: Account, 26 | meta: Icrc7TokenMetadata, 27 | }, 28 | Burn { 29 | tid: u128, 30 | from: Account, 31 | to: Account, 32 | }, 33 | Transfer { 34 | tid: u128, 35 | from: Account, 36 | to: Account, 37 | }, 38 | TransferFrom { 39 | tid: u128, 40 | from: Account, 41 | to: Account, 42 | spender: Account, 43 | }, 44 | Approval { 45 | tid: u128, 46 | from: Account, 47 | to: Account, 48 | exp_sec: Option, 49 | }, 50 | ApproveCollection { 51 | from: Account, 52 | to: Account, 53 | exp_sec: Option, 54 | }, 55 | Revoke { 56 | tid: u128, 57 | from: Account, 58 | to: Option, 59 | }, 60 | RevokeCollection { 61 | from: Account, 62 | to: Option, 63 | }, 64 | } 65 | 66 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone, Default)] 67 | pub struct Transaction { 68 | pub ts: u64, 69 | pub op: String, // "7mint" | "7burn" | "7xfer" | "7update" | "37appr" | "37appr_coll | "37revoke" | "37revoke_coll" | "37xfer" 70 | pub tid: u128, 71 | pub from: Option, 72 | pub to: Option, 73 | pub spender: Option, 74 | pub exp: Option, 75 | pub meta: Option, 76 | pub memo: Option>, 77 | pub block: Option, 78 | } 79 | 80 | impl Transaction { 81 | pub fn mint( 82 | now_sec: u64, 83 | tid: u128, 84 | from: Option, 85 | to: Account, 86 | meta: Icrc7TokenMetadata, 87 | memo: Option>, 88 | ) -> Self { 89 | Transaction { 90 | ts: now_sec, 91 | op: "7mint".to_string(), 92 | tid, 93 | from, 94 | to: Some(to), 95 | meta: Some(meta), 96 | memo, 97 | ..Default::default() 98 | } 99 | } 100 | 101 | pub fn burn( 102 | now_sec: u64, 103 | tid: u128, 104 | from: Account, 105 | to: Option, 106 | memo: Option>, 107 | ) -> Self { 108 | Transaction { 109 | ts: now_sec, 110 | op: "7burn".to_string(), 111 | tid, 112 | from: Some(from), 113 | to, 114 | memo, 115 | ..Default::default() 116 | } 117 | } 118 | 119 | pub fn transfer( 120 | now_sec: u64, 121 | tid: u128, 122 | from: Account, 123 | to: Account, 124 | memo: Option>, 125 | ) -> Self { 126 | Transaction { 127 | ts: now_sec, 128 | op: "7xfer".to_string(), 129 | tid, 130 | from: Some(from), 131 | to: Some(to), 132 | memo, 133 | ..Default::default() 134 | } 135 | } 136 | 137 | pub fn update( 138 | now_sec: u64, 139 | tid: u128, 140 | from: Account, 141 | meta: Icrc7TokenMetadata, 142 | memo: Option>, 143 | ) -> Self { 144 | Transaction { 145 | ts: now_sec, 146 | op: "7update".to_string(), 147 | tid, 148 | from: Some(from), 149 | meta: Some(meta), 150 | memo, 151 | ..Default::default() 152 | } 153 | } 154 | 155 | pub fn approve( 156 | now_sec: u64, 157 | tid: u128, 158 | from: Account, 159 | spender: Account, 160 | exp_sec: Option, 161 | memo: Option>, 162 | ) -> Self { 163 | Transaction { 164 | ts: now_sec, 165 | op: "37appr".to_string(), 166 | tid, 167 | from: Some(from), 168 | spender: Some(spender), 169 | exp: exp_sec, 170 | memo, 171 | ..Default::default() 172 | } 173 | } 174 | 175 | pub fn approve_collection( 176 | now_sec: u64, 177 | from: Account, 178 | spender: Account, 179 | exp_sec: Option, 180 | memo: Option>, 181 | ) -> Self { 182 | Transaction { 183 | ts: now_sec, 184 | op: "37appr_coll".to_string(), 185 | from: Some(from), 186 | spender: Some(spender), 187 | exp: exp_sec, 188 | memo, 189 | ..Default::default() 190 | } 191 | } 192 | 193 | pub fn revoke( 194 | now_sec: u64, 195 | tid: u128, 196 | from: Account, 197 | spender: Option, 198 | memo: Option>, 199 | ) -> Self { 200 | Transaction { 201 | ts: now_sec, 202 | op: "37revoke".to_string(), 203 | tid, 204 | from: Some(from), 205 | spender, 206 | memo, 207 | ..Default::default() 208 | } 209 | } 210 | 211 | pub fn revoke_collection( 212 | now_sec: u64, 213 | from: Account, 214 | spender: Option, 215 | memo: Option>, 216 | ) -> Self { 217 | Transaction { 218 | ts: now_sec, 219 | op: "37revoke_coll".to_string(), 220 | from: Some(from), 221 | spender, 222 | memo, 223 | ..Default::default() 224 | } 225 | } 226 | 227 | pub fn transfer_from( 228 | now_sec: u64, 229 | tid: u128, 230 | from: Account, 231 | to: Account, 232 | spender: Account, 233 | memo: Option>, 234 | ) -> Self { 235 | Transaction { 236 | ts: now_sec, 237 | op: "37xfer".to_string(), 238 | tid, 239 | from: Some(from), 240 | to: Some(to), 241 | spender: Some(spender), 242 | memo, 243 | ..Default::default() 244 | } 245 | } 246 | 247 | pub fn new(_txn_id: u128, txn_type: TransactionType, at: u64, memo: Option>) -> Self { 248 | let transaction = match &txn_type { 249 | TransactionType::Transfer { tid, from, to } => { 250 | Self::transfer(at, tid.clone(), from.clone(), to.clone(), memo) 251 | } 252 | TransactionType::Mint { 253 | tid, 254 | from, 255 | to, 256 | meta, 257 | } => Self::mint( 258 | at, 259 | tid.clone(), 260 | Some(from.clone()), 261 | to.clone(), 262 | meta.clone(), 263 | memo, 264 | ), 265 | TransactionType::Burn { tid, from, to } => { 266 | Self::burn(at, tid.clone(), from.clone(), Some(to.clone()), memo) 267 | } 268 | TransactionType::Approval { 269 | tid, 270 | from, 271 | to, 272 | exp_sec, 273 | } => Self::approve( 274 | at, 275 | tid.clone(), 276 | from.clone(), 277 | to.clone(), 278 | exp_sec.clone(), 279 | memo, 280 | ), 281 | TransactionType::ApproveCollection { from, to, exp_sec } => { 282 | Self::approve_collection(at, from.clone(), to.clone(), exp_sec.clone(), memo) 283 | } 284 | TransactionType::Revoke { tid, from, to: _ } => { 285 | Self::revoke(at, tid.clone(), from.clone(), None, memo) 286 | } 287 | TransactionType::RevokeCollection { from, to: _ } => { 288 | Self::revoke_collection(at, from.clone(), None, memo) 289 | } 290 | TransactionType::TransferFrom { 291 | tid, 292 | from, 293 | to, 294 | spender, 295 | } => Self::transfer_from( 296 | at, 297 | tid.clone(), 298 | from.clone(), 299 | to.clone(), 300 | spender.clone(), 301 | memo, 302 | ), 303 | }; 304 | return transaction; 305 | } 306 | } 307 | 308 | impl Storable for Transaction { 309 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 310 | Decode!(bytes.as_ref(), Self).unwrap() 311 | } 312 | 313 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 314 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 315 | } 316 | 317 | const BOUND: Bound = Bound::Unbounded; 318 | } 319 | 320 | #[derive(CandidType, Deserialize, Clone, Debug)] 321 | pub struct TransferArg { 322 | pub from_subaccount: Option, 323 | pub to: Account, 324 | pub token_id: u128, 325 | pub memo: Option>, 326 | pub created_at_time: Option, 327 | } 328 | 329 | pub type TransferResult = Result; 330 | 331 | pub type Icrc7TokenMetadata = BTreeMap; 332 | 333 | #[derive(CandidType, Deserialize, Clone)] 334 | pub struct MintArg { 335 | pub from_subaccount: Option, 336 | pub to: Account, 337 | pub token_id: u128, 338 | pub memo: Option>, 339 | // if None, then the combination of Collection's symbol and token's id will be provided 340 | // for e.g.: "ICRC7 100" 341 | pub token_name: Option, 342 | pub token_description: Option, 343 | pub token_logo: Option, 344 | } 345 | 346 | pub type MintResult = Result; 347 | 348 | #[derive(CandidType, Deserialize, Clone)] 349 | pub struct BurnArg { 350 | pub from_subaccount: Option, 351 | pub token_id: u128, 352 | pub memo: Option>, 353 | } 354 | 355 | pub type BurnResult = Result; 356 | 357 | #[derive(CandidType, Deserialize)] 358 | pub struct InitArg { 359 | pub minting_account: Option, 360 | pub icrc7_symbol: String, 361 | pub icrc7_name: String, 362 | pub icrc7_description: Option, 363 | pub icrc7_logo: Option, 364 | pub icrc7_supply_cap: Option, 365 | pub icrc7_max_query_batch_size: Option, 366 | pub icrc7_max_update_batch_size: Option, 367 | pub icrc7_max_take_value: Option, 368 | pub icrc7_default_take_value: Option, 369 | pub icrc7_max_memo_size: Option, 370 | pub icrc7_atomic_batch_transfers: Option, 371 | pub tx_window: Option, 372 | pub permitted_drift: Option, 373 | pub approval_init: Option, 374 | pub archive_init: Option, 375 | } 376 | 377 | #[derive(CandidType)] 378 | pub struct Standard { 379 | pub name: String, 380 | pub url: String, 381 | } 382 | 383 | #[derive(CandidType, Deserialize)] 384 | pub struct ApprovalArg { 385 | pub from_subaccount: Option, 386 | pub spender: Account, 387 | pub token_id: u128, 388 | pub expires_at: Option, 389 | pub memo: Option>, 390 | } 391 | 392 | pub type SyncReceipt = Result; 393 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # icrc7_launchpad 2 | 3 | **ICRC7 Launchpad** is a powerful tool designed to streamline the creation of ICRC7 NFT canisters. With the **ICRC7 Launchpad**, users can easily deploy ICRC7-based NFT canisters tailored to their needs by simply modifying the `icrc7_launchpad.sh` script. 4 | 5 | ## Key Components 6 | 7 | 1. **ICRC7 Launchpad**: 8 | A canister-based launcher that simplifies the deployment of ICRC7 NFT canisters. It abstracts the complexities of configuration and setup, enabling users to focus on their projects. 9 | 10 | 2. **ICRC7**: 11 | The core ICRC canister contract code. This serves as the foundation for ICRC7-based NFTs, providing essential functionality for token operations. 12 | 13 | 3. **ICRC7 Types**: 14 | A dedicated type library for ICRC7. This library defines the structures and data types required to interact with ICRC7 canisters, ensuring consistency and ease of integration. 15 | 16 | ## Use Cases 17 | 18 | - Launch new NFT collections with ease. 19 | - Experiment with ICRC7 token features and functionalities. 20 | - Provide an infrastructure for decentralized applications leveraging NFTs. 21 | 22 | ## How It Works 23 | 24 | 1. Modify the `icrc7_launchpad.sh` script to include your desired configuration, such as token name, symbol, metadata, and supply parameters. 25 | 2. Execute the script to deploy a new ICRC7 canister on the Internet Computer. 26 | 3. Interact with the newly created NFT canister using the ICRC7 Types library for seamless integration into your application. 27 | 28 | --- 29 | 30 | Get started with the **ICRC7 Launchpad** and unlock the potential of ICRC7 NFTs on the Internet Computer! 31 | 32 | 33 | ``` 34 | 35 | dfx start --background 36 | 37 | dfx deploy icrc7_launchpad 38 | 39 | sh icrc7_launchpad.sh 40 | 41 | ``` 42 | 43 | ```bash 44 | 45 | #!/bin/bash 46 | 47 | ICRC7_LAUNCHPAD_CANISTER_ID="icrc7_launchpad" 48 | 49 | # Arguments for the `mint_collection_canister` method 50 | ARG=$(cat <, 135 | pub icrc7_symbol: String, 136 | pub icrc7_name: String, 137 | pub icrc7_description: Option, 138 | pub icrc7_logo: Option, 139 | pub icrc7_supply_cap: Option, 140 | pub icrc7_max_query_batch_size: Option, 141 | pub icrc7_max_update_batch_size: Option, 142 | pub icrc7_max_take_value: Option, 143 | pub icrc7_default_take_value: Option, 144 | pub icrc7_max_memo_size: Option, 145 | pub icrc7_atomic_batch_transfers: Option, 146 | pub tx_window: Option, 147 | pub permitted_drift: Option, 148 | pub approval_init: Option, // ICRC37 Init args 149 | pub archive_init: Option, // ICRC3 Init args 150 | } 151 | ``` 152 | 153 | ICRC37 Init args: 154 | 155 | ``` 156 | type InitApprovalsArg = record { 157 | max_approvals : opt nat16; 158 | max_approvals_per_token_or_collection : opt nat16; 159 | settle_to_approvals : opt nat16; 160 | max_revoke_approvals : opt nat16; 161 | collection_approval_requires_token : opt bool; 162 | } 163 | ``` 164 | 165 | ICRC3 Init args: 166 | 167 | ``` 168 | type InitArchiveArg = record { 169 | maxRecordsToArchive : nat; //Max number of archive items to archive in one round 170 | archiveIndexType : IndexType; //Index type to use for the memory of the archive 171 | maxArchivePages : nat; //Max number of pages allowed on the archivserver 172 | settleToRecords : nat; //number of records to settle to during the clean up process 173 | archiveCycles : nat; //number of cycles to sent to a new archive canister; 174 | maxActiveRecords : nat; //allowed max active records on this canister 175 | maxRecordsInArchiveInstance : nat; //specify the max number of archive items to put on an archive instance 176 | archiveControllers : opt opt vec principal; //override the default controllers. The canister will always add itself to this group; 177 | } 178 | ``` 179 | 180 | ## ICIC7 181 | 182 | ### ICRC-7 183 | 184 | ICRC-7 is the minimal standard for the implementation of Non-Fungible Tokens (NFTs) on the Internet Computer. 185 | 186 | A token ledger implementation following this standard hosts an NFT collection (collection), which is a set of NFTs. 187 | 188 | ICRC-7 does not handle approval-related operations such as approve and transfer_from itself. Those operations are specified by ICRC-37 which extends ICRC-7 with approval semantics. 189 | 190 | [ICRC-7](https://github.com/dfinity/ICRC/blob/icrc_7_and_37/ICRCs/ICRC-7/ICRC-7.md) 191 | 192 | ### ICRC-37 193 | 194 | [ICRC-37](https://github.com/dfinity/ICRC/blob/icrc_7_and_37/ICRCs/ICRC-37/ICRC-37.md) 195 | 196 | ### ICRC-3 197 | 198 | [ICRC-3](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-3/README.md) 199 | 200 | 201 | ### Scripts 202 | 203 | #### Deploying Icrc7 Canister 204 | 205 | ```bash 206 | dfx deploy icrc7 --argument '(record{ 207 | minting_account= opt record { 208 | owner = principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe"; 209 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 210 | }; 211 | icrc7_supply_cap= null; 212 | icrc7_description= opt "ICP Flower Collection"; 213 | tx_window= null; 214 | permitted_drift= null; 215 | icrc7_max_take_value= opt 100; 216 | icrc7_max_memo_size= opt 1000; 217 | icrc7_symbol= "ICFL"; 218 | icrc7_max_update_batch_size= opt 100; 219 | icrc7_max_query_batch_size= opt 100; 220 | icrc7_atomic_batch_transfers= null; 221 | icrc7_default_take_value= opt 100; 222 | icrc7_logo= null; 223 | icrc7_name= "ICP Flower"; 224 | approval_init= null; 225 | archive_init= null 226 | })' 227 | ``` 228 | 229 | ```bash 230 | dfx deploy icrc7 --argument '(record{ 231 | minting_account= opt record { 232 | owner = principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe"; 233 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 234 | }; 235 | icrc7_supply_cap= null; 236 | icrc7_description= opt "ICP Flower Collection"; 237 | tx_window= null; 238 | permitted_drift= null; 239 | icrc7_max_take_value= null; 240 | icrc7_max_memo_size= null; 241 | icrc7_symbol= "ICFL"; 242 | icrc7_max_update_batch_size= null; 243 | icrc7_max_query_batch_size= null; 244 | icrc7_atomic_batch_transfers= null; 245 | icrc7_default_take_value= null; 246 | icrc7_logo= null; 247 | icrc7_name= "ICP Flower"; 248 | approval_init= null; 249 | archive_init= opt record { 250 | maxRecordsToArchive= 2; 251 | archiveIndexType= variant {Stable}; 252 | maxArchivePages= 3; 253 | settleToRecords= 2; 254 | archiveCycles= 1000000000000; 255 | maxActiveRecords= 4; 256 | maxRecordsInArchiveInstance= 4; 257 | archiveControllers= null 258 | } 259 | })' 260 | ``` 261 | 262 | #### Minting NFT 263 | 264 | ```bash 265 | dfx canister call icrc7 mint '(record{ 266 | to= record { 267 | owner = principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe"; 268 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 269 | }; 270 | token_id=1; 271 | memo= null; 272 | from_subaccount= null; 273 | token_description= opt "Token Number 1"; 274 | token_logo= null; 275 | token_name= null 276 | })' 277 | ``` 278 | 279 | 280 | #### Transfer NFT 281 | 282 | ```bash 283 | dfx canister call icrc7 icrc7_transfer '(vec{ 284 | record{ 285 | to=record { 286 | owner = principal "t4egw-clf4w-qbpli-svryg-7yqq6-jt2yj-7v755-mabir-zmx6i-vp4fr-fqe"; 287 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 288 | }; 289 | token_id= 1; 290 | from_subaccount= null; 291 | memo= null; 292 | created_at_time= null 293 | } 294 | })' 295 | ``` 296 | 297 | #### Approve NFT 298 | 299 | ```bash 300 | dfx canister call icrc7 icrc37_approve_tokens '(vec{ 301 | record{ 302 | token_id= 2; 303 | approval_info= record { 304 | memo= null; 305 | from_subaccount= null; 306 | created_at_time= null; 307 | expires_at= null; 308 | spender= record { 309 | owner = principal "o2zom-piy75-ifbnk-nhhlq-362su-4vsx5-ptl2s-ec4jw-osbv4-nygtw-dae"; 310 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 311 | } 312 | } 313 | } 314 | })' 315 | 316 | dfx canister call icrc7 icrc37_approve_collection '(vec{ 317 | record{ 318 | token_id= 2; 319 | approval_info= record { 320 | memo= null; 321 | from_subaccount= null; 322 | created_at_time= null; 323 | expires_at= null; 324 | spender= record { 325 | owner = principal "o2zom-piy75-ifbnk-nhhlq-362su-4vsx5-ptl2s-ec4jw-osbv4-nygtw-dae"; 326 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 327 | } 328 | } 329 | } 330 | })' 331 | ``` 332 | 333 | #### Transfer From NFT 334 | 335 | ```bash 336 | dfx canister call icrc7 icrc37_transfer_from '(vec{ 337 | record{ 338 | from= record { 339 | owner = principal "3yyxm-t5fpe-v32em-ac6lr-xyort-wuscb-dvl4x-3wnwi-hqkyj-xortw-oqe"; 340 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 341 | }; 342 | to= record { 343 | owner = principal "t4egw-clf4w-qbpli-svryg-7yqq6-jt2yj-7v755-mabir-zmx6i-vp4fr-fqe"; 344 | subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"; 345 | }; 346 | spender_subaccount= null; 347 | token_id= 2; 348 | memo= opt blob "123"; 349 | created_at_time= null 350 | } 351 | })' 352 | ``` 353 | 354 | #### Burn NFT 355 | 356 | The implementation of the burn method does not delete the token; rather, it transfers the token to a burn_address (akin to a zero address). 357 | 358 | ```bash 359 | dfx canister call icrc7 burn '(vec { 360 | record { 361 | token_id = 1 : nat; 362 | memo = opt blob "Burning token 1"; 363 | from_subaccount = null 364 | } 365 | })' 366 | ``` -------------------------------------------------------------------------------- /src/icrc7/src/icrc3_types.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Decode, Deserialize, Encode, Principal}; 2 | use ic_stable_structures::{storable::Bound, Storable}; 3 | use icrc_ledger_types::{ 4 | icrc::generic_value::{Hash, Map, Value}, 5 | icrc1::account::Account, 6 | }; 7 | use std::{collections::BTreeMap, marker::PhantomData}; 8 | 9 | use serde::Serialize; 10 | use serde_bytes::ByteBuf; 11 | use std::ops::Deref; 12 | 13 | use crate::Transaction; 14 | 15 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 16 | pub struct Block(Value); 17 | 18 | impl Storable for Block { 19 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 20 | Decode!(bytes.as_ref(), Self).unwrap() 21 | } 22 | 23 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 24 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 25 | } 26 | 27 | const BOUND: Bound = Bound::Unbounded; 28 | } 29 | 30 | impl Block { 31 | pub fn value(&self) -> &Value { 32 | &self.0 33 | } 34 | 35 | pub fn value_mut(&mut self) -> &mut Value { 36 | &mut self.0 37 | } 38 | } 39 | 40 | impl AsRef for Block { 41 | #[inline] 42 | fn as_ref(&self) -> &Value { 43 | &self.0 44 | } 45 | } 46 | 47 | impl Deref for Block { 48 | type Target = Value; 49 | #[inline] 50 | fn deref(&self) -> &Self::Target { 51 | &self.0 52 | } 53 | } 54 | 55 | impl From for Block { 56 | fn from(value: Map) -> Self { 57 | Self(Value::Map(value)) 58 | } 59 | } 60 | 61 | impl TryFrom for Block { 62 | type Error = String; 63 | fn try_from(value: Value) -> Result { 64 | match value { 65 | Value::Map(map) => Ok(Self(Value::Map(map))), 66 | _ => Err("block must be a map value".to_string()), 67 | } 68 | } 69 | } 70 | 71 | impl Block { 72 | pub fn new(phash: Option, tx: Transaction) -> Self { 73 | let mut block = Map::new(); 74 | if let Some(phash) = phash { 75 | block.insert("phash".to_string(), Value::Blob(ByteBuf::from(phash))); 76 | }; 77 | 78 | block.insert("btype".to_string(), Value::Text(tx.op)); 79 | block.insert("ts".to_string(), Value::Nat(tx.ts.into())); 80 | 81 | let mut val = Map::new(); 82 | val.insert("tid".to_string(), Value::Nat(tx.tid.into())); 83 | if let Some(from) = tx.from { 84 | val.insert("from".to_string(), account_value(from)); 85 | } 86 | if let Some(to) = tx.to { 87 | val.insert("to".to_string(), account_value(to)); 88 | } 89 | if let Some(spender) = tx.spender { 90 | val.insert("spender".to_string(), account_value(spender)); 91 | } 92 | if let Some(exp) = tx.exp { 93 | val.insert("exp".to_string(), Value::Nat(exp.into())); 94 | } 95 | if let Some(meta) = tx.meta { 96 | val.insert("meta".to_string(), Value::Map(meta)); 97 | } 98 | if let Some(memo) = tx.memo { 99 | val.insert("memo".to_string(), Value::Blob(ByteBuf::from(memo))); 100 | } 101 | val.insert("ts".to_string(), Value::Nat(tx.ts.into())); 102 | block.insert("tx".to_string(), Value::Map(val)); 103 | Self(Value::Map(block)) 104 | } 105 | 106 | pub fn into_inner(self) -> Value { 107 | self.0 108 | } 109 | 110 | pub fn into_map(self) -> Map { 111 | match self.0 { 112 | Value::Map(map) => map, 113 | _ => unreachable!(), 114 | } 115 | } 116 | } 117 | 118 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 119 | pub struct BlockType { 120 | pub block_type: String, 121 | pub url: String, 122 | } 123 | 124 | fn account_value(Account { owner, subaccount }: Account) -> Value { 125 | let mut parts = vec![Value::blob(owner.as_slice())]; 126 | if let Some(subaccount) = subaccount { 127 | parts.push(Value::blob(subaccount.as_slice())); 128 | } 129 | Value::Array(parts) 130 | } 131 | 132 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 133 | pub enum IndexType { 134 | Managed, 135 | Stable, 136 | StableTyped, 137 | } 138 | 139 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 140 | pub struct TransactionRange { 141 | pub start: u128, 142 | pub length: u128, 143 | } 144 | 145 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 146 | pub struct ArchiveLedgerInfo { 147 | pub archives: BTreeMap, 148 | pub local_ledger_size: u128, 149 | pub supported_blocks: Vec, 150 | pub last_index: u128, 151 | pub first_index: u128, 152 | pub is_cleaning: bool, 153 | pub latest_hash: Option, 154 | pub setting: ArchiveSetting, 155 | } 156 | 157 | impl Default for ArchiveLedgerInfo { 158 | fn default() -> Self { 159 | Self { 160 | archives: BTreeMap::new(), 161 | local_ledger_size: 0, 162 | supported_blocks: vec![], 163 | last_index: 0, 164 | first_index: 0, 165 | is_cleaning: false, 166 | latest_hash: None, 167 | setting: ArchiveSetting::default(), 168 | } 169 | } 170 | } 171 | 172 | impl ArchiveLedgerInfo { 173 | pub fn new(setting: Option) -> Self { 174 | let setting = setting.unwrap_or(ArchiveSetting::default()); 175 | Self { 176 | archives: BTreeMap::new(), 177 | local_ledger_size: 0, 178 | last_index: 0, 179 | first_index: 0, 180 | is_cleaning: false, 181 | latest_hash: None, 182 | setting, 183 | supported_blocks: vec![ 184 | BlockType { 185 | block_type: "7mint".into(), 186 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md".into(), 187 | }, 188 | BlockType { 189 | block_type: "7burn".into(), 190 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md".into(), 191 | }, 192 | BlockType { 193 | block_type: "7xfer".into(), 194 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md".into(), 195 | }, 196 | BlockType { 197 | block_type: "7update".into(), 198 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md".into(), 199 | }, 200 | BlockType { 201 | block_type: "37appr".into(), 202 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 203 | .into(), 204 | }, 205 | BlockType { 206 | block_type: "37appr_coll".into(), 207 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 208 | .into(), 209 | }, 210 | BlockType { 211 | block_type: "37revoke".into(), 212 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 213 | .into(), 214 | }, 215 | BlockType { 216 | block_type: "37revoke_coll".into(), 217 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 218 | .into(), 219 | }, 220 | BlockType { 221 | block_type: "37xfer".into(), 222 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 223 | .into(), 224 | }, 225 | ], 226 | } 227 | } 228 | } 229 | 230 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 231 | pub struct ArchiveSetting { 232 | pub archive_controllers: Option>>, 233 | pub archive_cycles: u128, 234 | pub archive_index_type: IndexType, 235 | pub max_active_records: u128, 236 | pub max_archive_pages: u128, 237 | pub max_records_in_archive_instance: u128, 238 | pub max_records_to_archive: u128, 239 | pub settle_to_records: u128, 240 | } 241 | 242 | impl Default for ArchiveSetting { 243 | fn default() -> Self { 244 | Self { 245 | archive_controllers: None, 246 | archive_cycles: 2_000_000_000_000, 247 | archive_index_type: IndexType::Stable, 248 | max_active_records: 2000, 249 | max_archive_pages: 62500, 250 | max_records_in_archive_instance: 10_000_000, 251 | max_records_to_archive: 10_000, 252 | settle_to_records: 1000, 253 | } 254 | } 255 | } 256 | 257 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 258 | pub struct InitArchiveArg { 259 | #[serde(rename = "archiveControllers")] 260 | pub archive_controllers: Option>>, 261 | #[serde(rename = "archiveCycles")] 262 | pub archive_cycles: u128, 263 | #[serde(rename = "archiveIndexType")] 264 | pub archive_index_type: IndexType, 265 | #[serde(rename = "maxActiveRecords")] 266 | pub max_active_records: u128, 267 | #[serde(rename = "maxArchivePages")] 268 | pub max_archive_pages: u128, 269 | #[serde(rename = "maxRecordsInArchiveInstance")] 270 | pub max_records_in_archive_instance: u128, 271 | #[serde(rename = "maxRecordsToArchive")] 272 | pub max_records_to_archive: u128, 273 | #[serde(rename = "settleToRecords")] 274 | pub settle_to_records: u128, 275 | } 276 | 277 | impl InitArchiveArg { 278 | pub fn to_archive_setting(self) -> ArchiveSetting { 279 | ArchiveSetting { 280 | archive_controllers: self.archive_controllers, 281 | archive_cycles: self.archive_cycles, 282 | archive_index_type: self.archive_index_type, 283 | max_active_records: self.max_active_records, 284 | max_archive_pages: self.max_archive_pages, 285 | max_records_in_archive_instance: self.max_records_in_archive_instance, 286 | max_records_to_archive: self.max_records_to_archive, 287 | settle_to_records: self.settle_to_records, 288 | } 289 | } 290 | } 291 | 292 | #[derive(CandidType, Deserialize, Debug)] 293 | pub struct GetArchiveArgs { 294 | pub from: Option, 295 | } 296 | 297 | pub type GetBlocksArgs = Vec; 298 | 299 | #[derive(CandidType, Deserialize, Debug)] 300 | pub struct QueryBlock { 301 | pub id: u128, 302 | pub block: Value, 303 | } 304 | 305 | #[derive(CandidType, Deserialize, Debug)] 306 | pub struct Tip { 307 | pub hash_tree: Vec, 308 | pub last_block_hash: Hash, 309 | pub last_block_index: Vec, 310 | } 311 | 312 | #[derive(CandidType, Deserialize, Debug)] 313 | pub struct GetTransactionsResult { 314 | pub blocks: Vec, 315 | pub log_length: u128, 316 | pub archived_blocks: Vec, 317 | } 318 | 319 | #[derive(CandidType, Deserialize, Debug)] 320 | pub struct GetBlocksResult { 321 | pub blocks: Vec, 322 | pub log_length: u128, 323 | pub archived_blocks: Vec, 324 | } 325 | 326 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 327 | #[serde(try_from = "candid::types::reference::Func")] 328 | pub struct GetTransactionsFn { 329 | pub canister_id: Principal, 330 | pub method: String, 331 | pub _marker: PhantomData<(Input, Output)>, 332 | } 333 | 334 | impl GetTransactionsFn { 335 | pub fn new(canister_id: Principal, method: impl Into) -> Self { 336 | Self { 337 | canister_id, 338 | method: method.into(), 339 | _marker: PhantomData, 340 | } 341 | } 342 | } 343 | 344 | impl Clone for GetTransactionsFn { 345 | fn clone(&self) -> Self { 346 | Self { 347 | canister_id: self.canister_id, 348 | method: self.method.clone(), 349 | _marker: PhantomData, 350 | } 351 | } 352 | } 353 | 354 | impl From> 355 | for candid::types::reference::Func 356 | { 357 | fn from(get_transactions_fn: GetTransactionsFn) -> Self { 358 | let p: &Principal = &Principal::try_from(get_transactions_fn.canister_id.as_ref()) 359 | .expect("could not deserialize principal"); 360 | Self { 361 | principal: *p, 362 | method: get_transactions_fn.method, 363 | } 364 | } 365 | } 366 | 367 | impl TryFrom 368 | for GetTransactionsFn 369 | { 370 | type Error = String; 371 | fn try_from(func: candid::types::reference::Func) -> Result { 372 | let canister_id = Principal::try_from(func.principal.as_slice()) 373 | .map_err(|e| format!("principal is not a canister id: {}", e))?; 374 | Ok(GetTransactionsFn { 375 | canister_id, 376 | method: func.method, 377 | _marker: PhantomData, 378 | }) 379 | } 380 | } 381 | 382 | impl CandidType for GetTransactionsFn { 383 | fn _ty() -> candid::types::Type { 384 | candid::func!((Input) -> (Output) query) 385 | } 386 | 387 | fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> 388 | where 389 | S: candid::types::Serializer, 390 | { 391 | candid::types::reference::Func::from(self.clone()).idl_serialize(serializer) 392 | } 393 | } 394 | 395 | #[derive(CandidType, Deserialize, Debug)] 396 | pub struct ArchivedTransactionResponse { 397 | pub args: Vec, 398 | pub callback: QueryTransactionsFn, 399 | } 400 | 401 | pub type QueryTransactionsFn = GetTransactionsFn, GetTransactionsResult>; 402 | 403 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 404 | pub struct GetArchivesResultItem { 405 | pub canister_id: Principal, 406 | pub start: u128, 407 | pub end: u128, 408 | } 409 | 410 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 411 | pub struct ArchiveCreateArgs { 412 | pub max_pages: u128, 413 | pub max_records: u128, 414 | pub first_index: u128, 415 | pub controllers: Option>>, 416 | } 417 | -------------------------------------------------------------------------------- /src/icrc7_types/src/icrc3_types.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Decode, Deserialize, Encode, Principal}; 2 | use ic_stable_structures::{storable::Bound, Storable}; 3 | use icrc_ledger_types::{ 4 | icrc::generic_value::{Hash, Map, Value}, 5 | icrc1::account::Account, 6 | }; 7 | use std::{collections::BTreeMap, marker::PhantomData}; 8 | 9 | use serde::Serialize; 10 | use serde_bytes::ByteBuf; 11 | use std::ops::Deref; 12 | 13 | use crate::icrc7_types::Transaction; 14 | 15 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 16 | pub struct Block(Value); 17 | 18 | impl Storable for Block { 19 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 20 | Decode!(bytes.as_ref(), Self).unwrap() 21 | } 22 | 23 | fn to_bytes(&self) -> std::borrow::Cow<[u8]> { 24 | std::borrow::Cow::Owned(Encode!(self).unwrap()) 25 | } 26 | 27 | const BOUND: Bound = Bound::Unbounded; 28 | } 29 | 30 | impl Block { 31 | pub fn value(&self) -> &Value { 32 | &self.0 33 | } 34 | 35 | pub fn value_mut(&mut self) -> &mut Value { 36 | &mut self.0 37 | } 38 | } 39 | 40 | impl AsRef for Block { 41 | #[inline] 42 | fn as_ref(&self) -> &Value { 43 | &self.0 44 | } 45 | } 46 | 47 | impl Deref for Block { 48 | type Target = Value; 49 | #[inline] 50 | fn deref(&self) -> &Self::Target { 51 | &self.0 52 | } 53 | } 54 | 55 | impl From for Block { 56 | fn from(value: Map) -> Self { 57 | Self(Value::Map(value)) 58 | } 59 | } 60 | 61 | impl TryFrom for Block { 62 | type Error = String; 63 | fn try_from(value: Value) -> Result { 64 | match value { 65 | Value::Map(map) => Ok(Self(Value::Map(map))), 66 | _ => Err("block must be a map value".to_string()), 67 | } 68 | } 69 | } 70 | 71 | impl Block { 72 | pub fn new(phash: Option, tx: Transaction) -> Self { 73 | let mut block = Map::new(); 74 | if let Some(phash) = phash { 75 | block.insert("phash".to_string(), Value::Blob(ByteBuf::from(phash))); 76 | }; 77 | 78 | block.insert("btype".to_string(), Value::Text(tx.op)); 79 | block.insert("ts".to_string(), Value::Nat(tx.ts.into())); 80 | 81 | let mut val = Map::new(); 82 | val.insert("tid".to_string(), Value::Nat(tx.tid.into())); 83 | if let Some(from) = tx.from { 84 | val.insert("from".to_string(), account_value(from)); 85 | } 86 | if let Some(to) = tx.to { 87 | val.insert("to".to_string(), account_value(to)); 88 | } 89 | if let Some(spender) = tx.spender { 90 | val.insert("spender".to_string(), account_value(spender)); 91 | } 92 | if let Some(exp) = tx.exp { 93 | val.insert("exp".to_string(), Value::Nat(exp.into())); 94 | } 95 | if let Some(meta) = tx.meta { 96 | val.insert("meta".to_string(), Value::Map(meta)); 97 | } 98 | if let Some(memo) = tx.memo { 99 | val.insert("memo".to_string(), Value::Blob(ByteBuf::from(memo))); 100 | } 101 | val.insert("ts".to_string(), Value::Nat(tx.ts.into())); 102 | block.insert("tx".to_string(), Value::Map(val)); 103 | Self(Value::Map(block)) 104 | } 105 | 106 | pub fn into_inner(self) -> Value { 107 | self.0 108 | } 109 | 110 | pub fn into_map(self) -> Map { 111 | match self.0 { 112 | Value::Map(map) => map, 113 | _ => unreachable!(), 114 | } 115 | } 116 | } 117 | 118 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 119 | pub struct BlockType { 120 | pub block_type: String, 121 | pub url: String, 122 | } 123 | 124 | fn account_value(Account { owner, subaccount }: Account) -> Value { 125 | let mut parts = vec![Value::blob(owner.as_slice())]; 126 | if let Some(subaccount) = subaccount { 127 | parts.push(Value::blob(subaccount.as_slice())); 128 | } 129 | Value::Array(parts) 130 | } 131 | 132 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 133 | pub enum IndexType { 134 | Managed, 135 | Stable, 136 | StableTyped, 137 | } 138 | 139 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 140 | pub struct TransactionRange { 141 | pub start: u128, 142 | pub length: u128, 143 | } 144 | 145 | #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] 146 | pub struct ArchiveLedgerInfo { 147 | pub archives: BTreeMap, 148 | pub local_ledger_size: u128, 149 | pub supported_blocks: Vec, 150 | pub last_index: u128, 151 | pub first_index: u128, 152 | pub is_cleaning: bool, 153 | pub latest_hash: Option, 154 | pub setting: ArchiveSetting, 155 | } 156 | 157 | impl Default for ArchiveLedgerInfo { 158 | fn default() -> Self { 159 | Self { 160 | archives: BTreeMap::new(), 161 | local_ledger_size: 0, 162 | supported_blocks: vec![], 163 | last_index: 0, 164 | first_index: 0, 165 | is_cleaning: false, 166 | latest_hash: None, 167 | setting: ArchiveSetting::default(), 168 | } 169 | } 170 | } 171 | 172 | impl ArchiveLedgerInfo { 173 | pub fn new(setting: Option) -> Self { 174 | let setting = setting.unwrap_or(ArchiveSetting::default()); 175 | Self { 176 | archives: BTreeMap::new(), 177 | local_ledger_size: 0, 178 | last_index: 0, 179 | first_index: 0, 180 | is_cleaning: false, 181 | latest_hash: None, 182 | setting, 183 | supported_blocks: vec![ 184 | BlockType { 185 | block_type: "7mint".into(), 186 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md".into(), 187 | }, 188 | BlockType { 189 | block_type: "7burn".into(), 190 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md".into(), 191 | }, 192 | BlockType { 193 | block_type: "7xfer".into(), 194 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md".into(), 195 | }, 196 | BlockType { 197 | block_type: "7update".into(), 198 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-7/ICRC-7.md".into(), 199 | }, 200 | BlockType { 201 | block_type: "37appr".into(), 202 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 203 | .into(), 204 | }, 205 | BlockType { 206 | block_type: "37appr_coll".into(), 207 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 208 | .into(), 209 | }, 210 | BlockType { 211 | block_type: "37revoke".into(), 212 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 213 | .into(), 214 | }, 215 | BlockType { 216 | block_type: "37revoke_coll".into(), 217 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 218 | .into(), 219 | }, 220 | BlockType { 221 | block_type: "37xfer".into(), 222 | url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-37/ICRC-37.md" 223 | .into(), 224 | }, 225 | ], 226 | } 227 | } 228 | } 229 | 230 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 231 | pub struct ArchiveSetting { 232 | pub archive_controllers: Option>>, 233 | pub archive_cycles: u128, 234 | pub archive_index_type: IndexType, 235 | pub max_active_records: u128, 236 | pub max_archive_pages: u128, 237 | pub max_records_in_archive_instance: u128, 238 | pub max_records_to_archive: u128, 239 | pub settle_to_records: u128, 240 | } 241 | 242 | impl Default for ArchiveSetting { 243 | fn default() -> Self { 244 | Self { 245 | archive_controllers: None, 246 | archive_cycles: 2_000_000_000_000, 247 | archive_index_type: IndexType::Stable, 248 | max_active_records: 2000, 249 | max_archive_pages: 62500, 250 | max_records_in_archive_instance: 10_000_000, 251 | max_records_to_archive: 10_000, 252 | settle_to_records: 1000, 253 | } 254 | } 255 | } 256 | 257 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 258 | pub struct InitArchiveArg { 259 | #[serde(rename = "archiveControllers")] 260 | pub archive_controllers: Option>>, 261 | #[serde(rename = "archiveCycles")] 262 | pub archive_cycles: u128, 263 | #[serde(rename = "archiveIndexType")] 264 | pub archive_index_type: IndexType, 265 | #[serde(rename = "maxActiveRecords")] 266 | pub max_active_records: u128, 267 | #[serde(rename = "maxArchivePages")] 268 | pub max_archive_pages: u128, 269 | #[serde(rename = "maxRecordsInArchiveInstance")] 270 | pub max_records_in_archive_instance: u128, 271 | #[serde(rename = "maxRecordsToArchive")] 272 | pub max_records_to_archive: u128, 273 | #[serde(rename = "settleToRecords")] 274 | pub settle_to_records: u128, 275 | } 276 | 277 | impl InitArchiveArg { 278 | pub fn to_archive_setting(self) -> ArchiveSetting { 279 | ArchiveSetting { 280 | archive_controllers: self.archive_controllers, 281 | archive_cycles: self.archive_cycles, 282 | archive_index_type: self.archive_index_type, 283 | max_active_records: self.max_active_records, 284 | max_archive_pages: self.max_archive_pages, 285 | max_records_in_archive_instance: self.max_records_in_archive_instance, 286 | max_records_to_archive: self.max_records_to_archive, 287 | settle_to_records: self.settle_to_records, 288 | } 289 | } 290 | } 291 | 292 | #[derive(CandidType, Deserialize, Debug)] 293 | pub struct GetArchiveArgs { 294 | pub from: Option, 295 | } 296 | 297 | pub type GetBlocksArgs = Vec; 298 | 299 | #[derive(CandidType, Deserialize, Debug)] 300 | pub struct QueryBlock { 301 | pub id: u128, 302 | pub block: Value, 303 | } 304 | 305 | #[derive(CandidType, Deserialize, Debug)] 306 | pub struct Tip { 307 | pub hash_tree: Vec, 308 | pub last_block_hash: Hash, 309 | pub last_block_index: Vec, 310 | } 311 | 312 | #[derive(CandidType, Deserialize, Debug)] 313 | pub struct GetTransactionsResult { 314 | pub blocks: Vec, 315 | pub log_length: u128, 316 | pub archived_blocks: Vec, 317 | } 318 | 319 | #[derive(CandidType, Deserialize, Debug)] 320 | pub struct GetBlocksResult { 321 | pub blocks: Vec, 322 | pub log_length: u128, 323 | pub archived_blocks: Vec, 324 | } 325 | 326 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 327 | #[serde(try_from = "candid::types::reference::Func")] 328 | pub struct GetTransactionsFn { 329 | pub canister_id: Principal, 330 | pub method: String, 331 | pub _marker: PhantomData<(Input, Output)>, 332 | } 333 | 334 | impl GetTransactionsFn { 335 | pub fn new(canister_id: Principal, method: impl Into) -> Self { 336 | Self { 337 | canister_id, 338 | method: method.into(), 339 | _marker: PhantomData, 340 | } 341 | } 342 | } 343 | 344 | impl Clone for GetTransactionsFn { 345 | fn clone(&self) -> Self { 346 | Self { 347 | canister_id: self.canister_id, 348 | method: self.method.clone(), 349 | _marker: PhantomData, 350 | } 351 | } 352 | } 353 | 354 | impl From> 355 | for candid::types::reference::Func 356 | { 357 | fn from(get_transactions_fn: GetTransactionsFn) -> Self { 358 | let p: &Principal = &Principal::try_from(get_transactions_fn.canister_id.as_ref()) 359 | .expect("could not deserialize principal"); 360 | Self { 361 | principal: *p, 362 | method: get_transactions_fn.method, 363 | } 364 | } 365 | } 366 | 367 | impl TryFrom 368 | for GetTransactionsFn 369 | { 370 | type Error = String; 371 | fn try_from(func: candid::types::reference::Func) -> Result { 372 | let canister_id = Principal::try_from(func.principal.as_slice()) 373 | .map_err(|e| format!("principal is not a canister id: {}", e))?; 374 | Ok(GetTransactionsFn { 375 | canister_id, 376 | method: func.method, 377 | _marker: PhantomData, 378 | }) 379 | } 380 | } 381 | 382 | impl CandidType for GetTransactionsFn { 383 | fn _ty() -> candid::types::Type { 384 | candid::func!((Input) -> (Output) query) 385 | } 386 | 387 | fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> 388 | where 389 | S: candid::types::Serializer, 390 | { 391 | candid::types::reference::Func::from(self.clone()).idl_serialize(serializer) 392 | } 393 | } 394 | 395 | #[derive(CandidType, Deserialize, Debug)] 396 | pub struct ArchivedTransactionResponse { 397 | pub args: Vec, 398 | pub callback: QueryTransactionsFn, 399 | } 400 | 401 | pub type QueryTransactionsFn = GetTransactionsFn, GetTransactionsResult>; 402 | 403 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 404 | pub struct GetArchivesResultItem { 405 | pub canister_id: Principal, 406 | pub start: u128, 407 | pub end: u128, 408 | } 409 | 410 | #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] 411 | pub struct ArchiveCreateArgs { 412 | pub max_pages: u128, 413 | pub max_records: u128, 414 | pub first_index: u128, 415 | pub controllers: Option>>, 416 | } 417 | -------------------------------------------------------------------------------- /tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | const IDENTITY: &str = "deploy"; 2 | const NETWORK: &str = "local"; 3 | // const NETWORK: &str = "ic"; 4 | const ASSETS_DIR: &str = "../assets"; 5 | // const ASSETS_DIR: &str = "empty"; // delete all files 6 | // const ASSETS_DIR: &str = "assets-test"; // test files 7 | const IGNORE_FILES: [&str; 4] = [".DS_Store", ".gitkeep", ".gitignore", ".git"]; 8 | const CHUNK_SIZE: u64 = 1024 * 1024 * 2 - 1024 * 128; 9 | const DFX_PROGRAM: &str = "/usr/local/bin/dfx"; 10 | 11 | #[derive(Debug, Clone)] 12 | struct LocalFile { 13 | pub path: String, 14 | pub size: u64, 15 | pub headers: Vec<(String, String)>, 16 | pub modified: u64, 17 | pub hash: String, 18 | pub data: Vec, 19 | } 20 | 21 | #[allow(dead_code)] 22 | #[derive(Debug)] 23 | struct RemoteFile { 24 | pub path: String, 25 | pub size: u64, 26 | pub headers: Vec<(String, String)>, 27 | pub created: u64, 28 | pub modified: u64, 29 | pub hash: String, 30 | } 31 | 32 | #[derive(Debug)] 33 | struct UploadFile { 34 | pub file: LocalFile, 35 | pub chunks: u64, 36 | pub chunk_size: u64, 37 | pub index: u64, 38 | pub offset: usize, 39 | pub offset_end: usize, 40 | } 41 | 42 | // Content-Type 43 | const EXT_CONTENT_TYPES: [(&str, &str); 50] = [ 44 | ("txt", "text/plain"), // 文本 45 | ("html", "text/html"), 46 | ("htm", "text/html"), 47 | ("htx", "text/html"), 48 | ("xhtml", "text/html"), 49 | ("css", "text/css"), 50 | ("js", "text/javascript"), 51 | ("md", "text/markdown"), 52 | ("ics", "text/calendar"), 53 | ("csv", "text/csv"), 54 | ("xml", "text/xml"), 55 | ("json", "application/json"), 56 | ("pdf", "application/pdf"), 57 | ("zip", "application/zip"), 58 | ("prefab", "application/zip"), 59 | ("7z", "application/x-7z-compressed"), 60 | ("eot", "application/vnd.ms-fontobject"), 61 | ("png", "image/png"), 62 | ("gif", "image/gif"), 63 | ("jpg", "image/jpeg"), 64 | ("jpeg", "image/jpeg"), 65 | ("svg", "image/svg+xml"), 66 | ("webp", "image/webp"), 67 | ("tif", "image/tiff"), 68 | ("tiff", "image/tiff"), 69 | ("ico", "image/x-icon"), 70 | ("mp4", "video/mp4"), 71 | ("avi", "video/x-msvideo"), 72 | ("mov", "video/quicktime"), 73 | ("mpeg", "video/mpeg"), 74 | ("ogv", "video/ogg"), 75 | ("webm", "video/webm"), 76 | ("mp3", "audio/mp3"), 77 | ("wav", "audio/wav"), 78 | ("flac", "audio/flac"), 79 | ("aac", "audio/aac"), 80 | ("webm", "audio/webm"), 81 | ("oga", "audio/ogg"), 82 | ("wma", "audio/x-ms-wma"), 83 | ("mid", "audio/midi"), 84 | ("midi", "audio/midi"), 85 | ("ra", "audio/x-realaudio"), 86 | ("ram", "audio/x-realaudio"), 87 | ("otf", "font/otf"), 88 | ("ttf", "font/ttf"), 89 | ("woff", "font/woff"), 90 | ("woff2", "font/woff2"), 91 | ("dat", ""), 92 | ("plot", ""), 93 | ("cache", ""), 94 | ]; 95 | 96 | #[test] 97 | fn upload() { 98 | let mut local_files: Vec = vec![]; 99 | load_local_files(ASSETS_DIR, ASSETS_DIR, &mut local_files); 100 | let local_file_names: Vec = local_files.iter().map(|f| f.path.clone()).collect(); 101 | println!("local files: {:?}", local_file_names); 102 | 103 | let remote_files = load_remote_files(); 104 | // println!("remote files: {:?}", remote_files); 105 | 106 | let deletes: Vec = remote_files 107 | .iter() 108 | .map(|f| f.path.clone()) 109 | .filter(|p| !local_file_names.contains(p)) 110 | .collect(); 111 | if !deletes.is_empty() { 112 | delete_files(deletes); 113 | } 114 | 115 | let local_files: Vec = local_files 116 | .into_iter() 117 | .filter(|local_file| { 118 | let remote_file = remote_files.iter().find(|f| f.path == local_file.path); 119 | if remote_file.is_none() { 120 | return true; 121 | } 122 | let remote_file = remote_file.unwrap(); 123 | let mut file_headers: Vec = local_file 124 | .headers 125 | .iter() 126 | .map(|h| format!("{}:{}", h.0, h.1)) 127 | .collect(); 128 | file_headers.sort(); 129 | let mut remote_file_headers: Vec = remote_file 130 | .headers 131 | .iter() 132 | .map(|h| format!("{}:{}", h.0, h.1)) 133 | .collect(); 134 | remote_file_headers.sort(); 135 | let changed = local_file.size != remote_file.size 136 | || file_headers.join(";") != remote_file_headers.join(";") 137 | || local_file.hash != remote_file.hash 138 | || remote_file.modified < local_file.modified * 1000000; 139 | if !changed { 140 | println!("file: {} has not changed. do nothing.", local_file.path) 141 | } 142 | changed 143 | }) 144 | .collect(); 145 | if local_files.is_empty() { 146 | println!("Nothing to do"); 147 | return; 148 | } 149 | upload_files(local_files); 150 | } 151 | 152 | fn load_local_files(prefix: &str, dir_path: &str, files: &mut Vec) { 153 | let entries = std::fs::read_dir(dir_path).unwrap(); 154 | 155 | for entry in entries { 156 | let entry = entry.unwrap(); 157 | let file_name = entry.file_name(); 158 | let file_type = entry.file_type().unwrap(); 159 | 160 | let path = format!("{}/{}", dir_path, file_name.to_str().unwrap().to_string()); 161 | fn is_ignore(path: &str) -> bool { 162 | for ignore in IGNORE_FILES { 163 | if path.ends_with(ignore) { 164 | return true; 165 | } 166 | } 167 | false 168 | } 169 | 170 | if is_ignore(&path) { 171 | continue; 172 | } 173 | 174 | if file_type.is_file() { 175 | let mut file = load_local_file(&path); 176 | file.path = (&file.path[prefix.len()..]).to_string(); 177 | files.push(file); 178 | } else if file_type.is_dir() { 179 | load_local_files(prefix, &path, files); 180 | } 181 | } 182 | } 183 | 184 | fn load_local_file(path: &str) -> LocalFile { 185 | let metadata = std::fs::metadata(path).unwrap(); 186 | let file_size = metadata.len(); 187 | 188 | use std::time::UNIX_EPOCH; 189 | let modified_time = metadata 190 | .modified() 191 | .unwrap() 192 | .duration_since(UNIX_EPOCH) 193 | .unwrap() 194 | .as_millis(); 195 | 196 | let mut file = std::fs::File::open(path).unwrap(); 197 | let mut buffer = Vec::new(); 198 | use std::io::Read; 199 | file.read_to_end(&mut buffer).unwrap(); 200 | 201 | LocalFile { 202 | path: path.to_string(), 203 | size: file_size, 204 | headers: get_headers(&path), 205 | modified: modified_time as u64, 206 | hash: do_hash(&buffer), 207 | data: buffer, 208 | } 209 | } 210 | 211 | fn do_hash(data: &Vec) -> String { 212 | use sha2::Digest; 213 | let mut hasher = sha2::Sha256::new(); 214 | hasher.update(&data[..]); 215 | let digest: [u8; 32] = hasher.finalize().into(); 216 | hex::encode(&digest) 217 | } 218 | 219 | fn get_headers(file: &str) -> Vec<(String, String)> { 220 | let mut headers: Vec<(String, String)> = vec![]; 221 | 222 | let mut content_type: String = String::from(""); 223 | 224 | use std::path::Path; 225 | let file_path = Path::new(file); 226 | if let Some(extension) = file_path.extension() { 227 | if let Some(ext_str) = extension.to_str() { 228 | let file_name = file.to_string(); 229 | let ext = ext_str.to_lowercase(); 230 | 231 | fn get_content_type(ext_str: &str) -> String { 232 | for (ext, content) in EXT_CONTENT_TYPES { 233 | if ext == ext_str { 234 | return content.to_string(); 235 | } 236 | } 237 | panic!("Unknown file type: {}", ext_str); 238 | } 239 | 240 | if &ext == "gz" { 241 | let mut ext = ""; 242 | let mut s = (&file_name[0..(file_name.len() - 3)]).split("."); 243 | while let Some(e) = s.next() { 244 | ext = e; 245 | } 246 | content_type = get_content_type(ext); 247 | } else { 248 | content_type = get_content_type(&ext); 249 | } 250 | } else { 251 | println!("Invalid extension"); 252 | } 253 | } else { 254 | println!("No extension: {}", file); 255 | } 256 | 257 | if !content_type.is_empty() { 258 | headers.push(("Content-Type".to_string(), content_type.to_string())); 259 | } 260 | 261 | headers.push(( 262 | "Cache-Control".to_string(), 263 | "public, max-age=31536000".to_string(), 264 | )); 265 | 266 | if file.ends_with(".gz") { 267 | headers.push(("Content-Encoding".to_string(), "gzip".to_string())); 268 | } 269 | 270 | headers 271 | } 272 | 273 | fn load_remote_files() -> Vec { 274 | use std::process::Command; 275 | 276 | let _start = std::time::SystemTime::now() 277 | .duration_since(std::time::UNIX_EPOCH) 278 | .expect("Time went backwards"); 279 | 280 | let output = Command::new(DFX_PROGRAM) 281 | .current_dir(".") 282 | .arg("--identity") 283 | .arg(IDENTITY) 284 | .arg("canister") 285 | .arg("--network") 286 | .arg(NETWORK) 287 | .arg("call") 288 | .arg("ic_canister_assets") 289 | .arg("files") 290 | .arg("()") 291 | .arg("--output") 292 | .arg("idl") 293 | .output() 294 | .expect("error"); 295 | 296 | let _end = std::time::SystemTime::now() 297 | .duration_since(std::time::UNIX_EPOCH) 298 | .expect("Time went backwards"); 299 | 300 | // println!("api: {} -> {:?}", "files", _end - _start); 301 | // println!("status: {}", output.status); 302 | 303 | if format!("{}", output.status).eq("exit status: 0") { 304 | let output = String::from_utf8(output.stdout.clone()).unwrap(); 305 | // println!("output: {}", output); 306 | return parse_remote_files(output); 307 | } 308 | 309 | eprintln!(">>>>>>>>>> ERROR <<<<<<<<<<<"); 310 | eprintln!("identity: {}", IDENTITY); 311 | eprintln!("api: {}", "files"); 312 | eprintln!("arg: {}", ""); 313 | eprintln!("status: {}", output.status); 314 | if format!("{}", output.status).eq("exit status: 0") { 315 | eprintln!( 316 | "output: {}", 317 | String::from_utf8(output.stdout).unwrap().trim_end() 318 | ); 319 | } else { 320 | eprintln!( 321 | "error : {}", 322 | String::from_utf8(output.stderr).unwrap().trim_end() 323 | ); 324 | } 325 | panic!("error"); 326 | } 327 | 328 | fn parse_remote_files(output: String) -> Vec { 329 | let output = output.trim(); 330 | let output = (&output[6..(output.len() - 2)]).to_string(); 331 | let output = output.trim(); 332 | 333 | if output.len() == 0 { 334 | return vec![]; 335 | } 336 | 337 | let output = (&output[9..(output.len() - 4)]).to_string(); 338 | let output = output.trim(); 339 | 340 | let mut files = vec![]; 341 | let mut splitted = output.split("};}; record { "); 342 | while let Some(content) = splitted.next() { 343 | let content = (&content[10..]).to_string(); 344 | let created: u64 = content 345 | .split(r#" : nat64; modified = "#) 346 | .next() 347 | .unwrap() 348 | .to_string() 349 | .replace("_", "") 350 | .parse() 351 | .unwrap(); 352 | let mut content = content.split(r#" : nat64; modified = "#); 353 | content.next(); 354 | let content = content.next().unwrap(); 355 | 356 | let modified: u64 = content 357 | .split(r#" : nat64; hash = ""#) 358 | .next() 359 | .unwrap() 360 | .to_string() 361 | .replace("_", "") 362 | .parse() 363 | .unwrap(); 364 | let mut content = content.split(r#" : nat64; hash = ""#); 365 | content.next(); 366 | let content = content.next().unwrap(); 367 | 368 | let hash = (&content[0..64]).to_string(); 369 | let mut content = content.split(r#""; path = ""#); 370 | content.next(); 371 | let content = content.next().unwrap(); 372 | 373 | let path = content.split(r#""; size = "#).next().unwrap().to_string(); 374 | let mut content = content.split(r#""; size = "#); 375 | content.next(); 376 | let content = content.next().unwrap(); 377 | 378 | let size: u64 = content 379 | .split(r#" : nat64; headers = "#) 380 | .next() 381 | .unwrap() 382 | .to_string() 383 | .replace("_", "") 384 | .parse() 385 | .unwrap(); 386 | let mut content = content.split(r#" : nat64; headers = "#); 387 | content.next(); 388 | let content = content.next().unwrap(); 389 | 390 | let headers: Vec<(String, String)> = if 5 < content.len() { 391 | let content = &content[16..(content.len() - 4)]; 392 | let mut headers = vec![]; 393 | let mut cs = content.split(r#"";}; record { ""#); 394 | while let Some(s) = cs.next() { 395 | let mut ss = s.split(r#""; ""#); 396 | let key = ss.next().unwrap().to_string(); 397 | let value = ss.next().unwrap().to_string(); 398 | headers.push((key, value)); 399 | } 400 | headers 401 | } else { 402 | vec![] 403 | }; 404 | 405 | files.push(RemoteFile { 406 | path, 407 | size, 408 | headers, 409 | created, 410 | modified, 411 | hash, 412 | }); 413 | } 414 | files 415 | } 416 | 417 | fn delete_files(names: Vec) { 418 | use std::process::Command; 419 | 420 | let _start = std::time::SystemTime::now() 421 | .duration_since(std::time::UNIX_EPOCH) 422 | .expect("Time went backwards"); 423 | 424 | let args = format!( 425 | "(vec {{{}}})", 426 | names 427 | .iter() 428 | .map(|name| format!("\"{}\"", name)) 429 | .collect::>() 430 | .join(";") 431 | ); 432 | 433 | let output = Command::new(DFX_PROGRAM) 434 | .current_dir(".") 435 | .arg("--identity") 436 | .arg(IDENTITY) 437 | .arg("canister") 438 | .arg("--network") 439 | .arg(NETWORK) 440 | .arg("call") 441 | .arg("ic_canister_assets") 442 | .arg("delete") 443 | .arg(&args) 444 | .arg("--output") 445 | .arg("idl") 446 | .output() 447 | .expect("error"); 448 | 449 | let _end = std::time::SystemTime::now() 450 | .duration_since(std::time::UNIX_EPOCH) 451 | .expect("Time went backwards"); 452 | 453 | if format!("{}", output.status).eq("exit status: 0") { 454 | for name in names.iter() { 455 | println!("delete file: {}", name) 456 | } 457 | return; 458 | } 459 | 460 | eprintln!(">>>>>>>>>> ERROR <<<<<<<<<<<"); 461 | eprintln!("identity: {}", IDENTITY); 462 | eprintln!("api: {}", "delete"); 463 | eprintln!("arg: {}", args); 464 | eprintln!("status: {}", output.status); 465 | if format!("{}", output.status).eq("exit status: 0") { 466 | eprintln!( 467 | "output: {}", 468 | String::from_utf8(output.stdout).unwrap().trim_end() 469 | ); 470 | } else { 471 | eprintln!( 472 | "error : {}", 473 | String::from_utf8(output.stderr).unwrap().trim_end() 474 | ); 475 | } 476 | panic!("error"); 477 | } 478 | 479 | fn upload_files(local_files: Vec) { 480 | let mut upload_files: Vec> = vec![]; 481 | 482 | let mut all_count = 0; 483 | let mut count = 0; 484 | let mut upload_file: Vec = vec![]; 485 | for file in local_files.iter() { 486 | let size = file.size; 487 | let mut splitted = size / CHUNK_SIZE; 488 | if splitted * CHUNK_SIZE < size { 489 | splitted += 1; 490 | } 491 | for i in 0..splitted { 492 | let (current_size, offset, offset_end) = if i < splitted - 1 { 493 | (CHUNK_SIZE, CHUNK_SIZE * i, CHUNK_SIZE * (i + 1)) 494 | } else { 495 | (size - (splitted - 1) * CHUNK_SIZE, CHUNK_SIZE * i, size) 496 | }; 497 | if CHUNK_SIZE < count + current_size { 498 | upload_files.push(upload_file); 499 | count = 0; 500 | upload_file = vec![] 501 | } 502 | 503 | count += current_size; 504 | all_count += current_size; 505 | upload_file.push(UploadFile { 506 | file: file.clone(), 507 | chunks: splitted, 508 | chunk_size: CHUNK_SIZE, 509 | index: i, 510 | offset: offset as usize, 511 | offset_end: offset_end as usize, 512 | }); 513 | } 514 | } 515 | if !upload_file.is_empty() { 516 | upload_files.push(upload_file); 517 | } 518 | 519 | use std::time::{SystemTime, UNIX_EPOCH}; 520 | let start = SystemTime::now() 521 | .duration_since(UNIX_EPOCH) 522 | .unwrap() 523 | .as_millis(); 524 | 525 | use std::thread; 526 | let mut handles = vec![]; 527 | for (i, upload_file) in upload_files.into_iter().enumerate() { 528 | let handle = thread::spawn(move || { 529 | do_upload_file(&upload_file, i); 530 | }); 531 | handles.push(handle); 532 | } 533 | for handle in handles { 534 | handle.join().unwrap(); 535 | } 536 | 537 | let end = SystemTime::now() 538 | .duration_since(UNIX_EPOCH) 539 | .unwrap() 540 | .as_millis(); 541 | println!( 542 | "all done: total: {:.2}MB time: {}s average: {:.2}MB/s", 543 | all_count as f64 / 1024f64 / 1024f64, 544 | (end - start) / 1000, 545 | all_count as f64 / 1024f64 / 1024f64 / (((end - start) / 1000) as f64) 546 | ); 547 | } 548 | 549 | fn do_upload_file(local_files: &Vec, index: usize) { 550 | let mut arg = String::from(""); 551 | arg.push_str("(vec{"); 552 | arg.push_str( 553 | &local_files 554 | .iter() 555 | .map(|file| { 556 | format!( 557 | "record{{ path=\"{}\"; headers=vec{{{}}}; size={}:nat64; chunk_size={}:nat64; index={}:nat32; chunk=vec{{{}}} }}", 558 | file.file.path, 559 | file.file 560 | .headers 561 | .iter() 562 | .map(|header| { format!("record{{\"{}\";\"{}\"}}", header.0, header.1) }) 563 | .collect::>() 564 | .join(";"), 565 | file.file.size, 566 | file.chunk_size, 567 | file.index, 568 | (&file.file.data[file.offset..file.offset_end]).iter().map(|u|format!("{}:nat8", u)).collect::>().join(";") 569 | ) 570 | }) 571 | .collect::>() 572 | .join(";"), 573 | ); 574 | arg.push_str("})"); 575 | let arg_file = format!(".args.{}.temp", index); 576 | write_file(&arg_file, &arg); 577 | 578 | let r = do_upload_file_to_canister(&arg_file, local_files); 579 | if let Err(msg) = r { 580 | println!("{}. try again", msg); 581 | do_upload_file_to_canister(&arg_file, local_files).unwrap(); 582 | } 583 | 584 | std::fs::remove_file(arg_file).unwrap(); 585 | } 586 | 587 | fn write_file(path: &str, content: &str) { 588 | use std::io::Write; 589 | if let Ok(_) = std::fs::File::open(path) { 590 | std::fs::remove_file(path).unwrap(); 591 | } 592 | std::fs::File::create(&path) 593 | .expect("create failed") 594 | .write_all(content.as_bytes()) 595 | .expect("write candid failed"); 596 | } 597 | 598 | fn do_upload_file_to_canister(arg: &str, local_files: &Vec) -> Result<(), String> { 599 | use std::process::Command; 600 | 601 | let _start = std::time::SystemTime::now() 602 | .duration_since(std::time::UNIX_EPOCH) 603 | .expect("Time went backwards"); 604 | 605 | let output = Command::new(DFX_PROGRAM) 606 | .current_dir(".") 607 | .arg("--identity") 608 | .arg(IDENTITY) 609 | .arg("canister") 610 | .arg("--network") 611 | .arg(NETWORK) 612 | .arg("call") 613 | .arg("--argument-file") 614 | .arg(arg) 615 | .arg("ic_canister_assets") 616 | .arg("upload") 617 | .arg("--output") 618 | .arg("idl") 619 | .output(); 620 | if let Err(e) = output { 621 | println!("error: {}", e); 622 | return Err("Upload failed".to_string()); 623 | } 624 | let output = output.unwrap(); 625 | 626 | let _end = std::time::SystemTime::now() 627 | .duration_since(std::time::UNIX_EPOCH) 628 | .expect("Time went backwards"); 629 | 630 | if format!("{}", output.status).eq("exit status: 0") { 631 | for file in local_files.iter() { 632 | println!( 633 | "upload file: {} {}/{} ({} bytes) hash: {}", 634 | file.file.path, 635 | file.index + 1, 636 | file.chunks, 637 | file.offset_end - file.offset, 638 | file.file.hash 639 | ) 640 | } 641 | return Ok({}); 642 | } 643 | 644 | eprintln!(">>>>>>>>>> ERROR <<<<<<<<<<<"); 645 | eprintln!("identity: {}", IDENTITY); 646 | eprintln!("api: {}", "upload"); 647 | eprintln!("arg: {}", arg); 648 | eprintln!("status: {}", output.status); 649 | if format!("{}", output.status).eq("exit status: 0") { 650 | eprintln!( 651 | "output: {}", 652 | String::from_utf8(output.stdout).unwrap().trim_end() 653 | ); 654 | } else { 655 | eprintln!( 656 | "error : {}", 657 | String::from_utf8(output.stderr).unwrap().trim_end() 658 | ); 659 | } 660 | Err("Upload failed".to_string()) 661 | } 662 | --------------------------------------------------------------------------------