From 4a44b8a24e958c2c4143348971f6c4b433da54f4 Mon Sep 17 00:00:00 2001 From: X Date: Sat, 25 Nov 2023 13:40:32 +0100 Subject: [PATCH] feat: listing --- Cargo.lock | 141 ++++++++++++++++++- src/backend/Cargo.toml | 4 + src/backend/icp.rs | 82 +++++++++++ src/backend/icrc1.rs | 254 ++++++++++++++++++++++++++++++++++ src/backend/lib.rs | 133 +++++++++++++++--- src/backend/order_book/mod.rs | 92 ++++++++++++ src/backend/xdr_rate.rs | 28 ++++ src/frontend/src/api.ts | 2 +- src/frontend/src/common.tsx | 43 ++++++ src/frontend/src/index.html | 49 ++++++- src/frontend/src/index.tsx | 51 +++++-- src/frontend/src/token.tsx | 106 ++++++++++++++ src/frontend/src/types.ts | 14 ++ 13 files changed, 961 insertions(+), 38 deletions(-) create mode 100644 src/backend/icp.rs create mode 100644 src/backend/icrc1.rs create mode 100644 src/backend/order_book/mod.rs create mode 100644 src/backend/xdr_rate.rs create mode 100644 src/frontend/src/common.tsx create mode 100644 src/frontend/src/token.tsx create mode 100644 src/frontend/src/types.ts diff --git a/Cargo.lock b/Cargo.lock index 7dfd816..88d0d88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,8 +44,12 @@ version = "0.1.0" dependencies = [ "base64", "candid", + "crc32fast", + "data-encoding", + "hex", "ic-cdk", "ic-cdk-macros", + "ic-cdk-timers", "ic-certified-map", "ic-ledger-types", "serde", @@ -236,9 +240,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "diff" @@ -325,6 +329,95 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -406,6 +499,20 @@ dependencies = [ "syn 1.0.105", ] +[[package]] +name = "ic-cdk-timers" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fa62243d3a412ceae88d6ad213614769e8b0bbcaeb98abb2e7985074257356" +dependencies = [ + "futures", + "ic-cdk", + "ic0", + "serde", + "serde_bytes", + "slotmap", +] + [[package]] name = "ic-certified-map" version = "0.4.0" @@ -717,6 +824,18 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -937,6 +1056,24 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.10.0" diff --git a/src/backend/Cargo.toml b/src/backend/Cargo.toml index 18d2ac1..222a02b 100644 --- a/src/backend/Cargo.toml +++ b/src/backend/Cargo.toml @@ -10,8 +10,12 @@ crate-type = ["cdylib"] [dependencies] base64 = "0.21.5" candid = { version = "0.9.11", features = ["parser"] } +crc32fast = "1.3.2" +data-encoding = "2.5.0" +hex = "0.4.3" ic-cdk = "0.11.3" ic-cdk-macros = "0.8.1" +ic-cdk-timers = "0.5.1" ic-certified-map = "0.4.0" ic-ledger-types = "0.8.0" serde = { version = "1.0.192", features = ["derive"] } diff --git a/src/backend/icp.rs b/src/backend/icp.rs new file mode 100644 index 0000000..30e7dd4 --- /dev/null +++ b/src/backend/icp.rs @@ -0,0 +1,82 @@ +use candid::Principal; +use ic_cdk::api::id; +use ic_ledger_types::{ + AccountBalanceArgs, AccountIdentifier, BlockIndex, Memo, Subaccount, Tokens, TransferArgs, + TransferResult, DEFAULT_FEE, DEFAULT_SUBACCOUNT, MAINNET_LEDGER_CANISTER_ID, +}; + +use crate::read; + +pub fn revenue_account() -> AccountIdentifier { + AccountIdentifier::new( + &read(|state| state.revenue_account).expect("no revenue subaccount set"), + &DEFAULT_SUBACCOUNT, + ) +} +pub fn user_account(principal: Principal) -> AccountIdentifier { + AccountIdentifier::new(&id(), &principal_to_subaccount(&principal)) +} + +pub fn main_account() -> AccountIdentifier { + AccountIdentifier::new(&id(), &DEFAULT_SUBACCOUNT) +} + +pub async fn transfer( + to: AccountIdentifier, + amount: Tokens, + memo: Memo, + sub_account: Option, +) -> Result { + if amount < DEFAULT_FEE { + return Err("can't transfer amounts smaller than the fee".into()); + } + let (result,): (TransferResult,) = ic_cdk::call( + MAINNET_LEDGER_CANISTER_ID, + "transfer", + (TransferArgs { + created_at_time: None, + memo, + amount: amount - DEFAULT_FEE, + fee: DEFAULT_FEE, + to, + from_subaccount: sub_account, + },), + ) + .await + .map_err(|err| format!("call to ledger failed: {:?}", err))?; + result.map_err(|err| { + format!( + "transfer of `{}` ICP from `{}` failed: {:?}", + amount, + AccountIdentifier::new(&id(), &sub_account.unwrap_or(DEFAULT_SUBACCOUNT)), + err + ) + }) +} + +pub async fn account_balance_of_principal(principal: Principal) -> Tokens { + account_balance(AccountIdentifier::new( + &id(), + &principal_to_subaccount(&principal), + )) + .await +} + +async fn account_balance(account: AccountIdentifier) -> Tokens { + let (balance,): (Tokens,) = ic_cdk::call( + MAINNET_LEDGER_CANISTER_ID, + "account_balance", + (AccountBalanceArgs { account },), + ) + .await + .expect("couldn't check balance"); + balance +} + +pub fn principal_to_subaccount(principal_id: &Principal) -> Subaccount { + let mut subaccount = [0; std::mem::size_of::()]; + let principal_id = principal_id.as_slice(); + subaccount[0] = principal_id.len() as u8; + subaccount[1..1 + principal_id.len()].copy_from_slice(principal_id); + Subaccount(subaccount) +} diff --git a/src/backend/icrc1.rs b/src/backend/icrc1.rs new file mode 100644 index 0000000..589b80f --- /dev/null +++ b/src/backend/icrc1.rs @@ -0,0 +1,254 @@ +use data_encoding::Specification; +use std::collections::BTreeMap; + +use candid::{CandidType, Deserialize, Principal}; +use serde::Serialize; + +use crate::order_book::{TokenId, Tokens}; + +type Timestamp = u64; + +pub type Subaccount = Vec; + +type Memo = [u8; 32]; + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct Account { + pub owner: Principal, + pub subaccount: Option, +} + +pub fn crc32(data: &[u8]) -> u32 { + use crc32fast::Hasher; + let mut hasher = Hasher::new(); + hasher.update(data); + hasher.finalize() +} + +fn base32_lowercase_no_padding(data: &[u8]) -> String { + let mut spec = Specification::new(); + spec.symbols.push_str("abcdefghijklmnopqrstuvwxyz234567"); + spec.padding = None; + let encoding = spec.encoding().unwrap(); + encoding.encode(data) +} + +impl Account { + pub fn to_string(&self) -> String { + let mut bytes = self.owner.as_slice().to_vec(); + if let Some(sub) = self.subaccount.as_ref() { + bytes.extend_from_slice(sub); + let checksum = crc32(&bytes).to_be_bytes(); + format!( + "{}-{}.{}", + self.owner.to_string(), + base32_lowercase_no_padding(checksum.as_slice()), + hex::encode(sub).trim_start_matches('0').to_string() + ) + } else { + self.owner.to_text() + } + } + + #[allow(dead_code)] + pub fn from_string(value: &str) -> Result { + match value.split(".").collect::>().as_slice() { + [owner] => Ok(Account { + owner: Principal::from_text(owner).map_err(|err| err.to_string())?, + subaccount: None, + }), + [owner_checksum, hex] => { + let mut parts = owner_checksum.split("-").collect::>(); + let checksum_str = parts.pop().ok_or("couldn't parse the account")?; + let owner = Principal::from_text(parts.join("-")).map_err(|err| err.to_string())?; + let mut bytes = owner.as_slice().to_vec(); + let subaccount = + hex::decode(format!("{:0>64}", hex)).map_err(|err| err.to_string())?; + bytes.extend_from_slice(&subaccount); + let checksum = crc32(&bytes).to_be_bytes(); + if checksum_str != base32_lowercase_no_padding(checksum.as_slice()).as_str() { + return Err("couldn't parse the account".into()); + } + Ok(Account { + owner, + subaccount: Some(subaccount), + }) + } + _ => Err("couldn't parse the account".into()), + } + } +} + +#[derive(CandidType, Serialize, Deserialize)] +pub struct TransferArgs { + from_subaccount: Option, + to: Account, + amount: u128, + fee: Option, + memo: Option, + created_at_time: Option, +} + +#[derive(CandidType, Debug, PartialEq, Deserialize, Serialize)] +pub struct InsufficientFunds { + balance: u128, +} + +#[derive(CandidType, Debug, PartialEq, Deserialize, Serialize)] +pub struct CreatedInFuture { + ledger_time: Timestamp, +} + +#[derive(CandidType, Debug, PartialEq, Deserialize, Serialize)] +pub struct GenericError { + error_code: u128, + message: String, +} + +#[derive(CandidType, Debug, PartialEq, Deserialize, Serialize)] +pub struct BadFee { + expected_fee: u128, +} + +#[derive(CandidType, Debug, PartialEq, Deserialize, Serialize)] +pub enum TransferError { + BadFee(BadFee), + // BadBurn(BadBurn), + // Duplicate(Duplicate), + TemporarilyUnavailable, + InsufficientFunds(InsufficientFunds), + TooOld, + CreatedInFuture(CreatedInFuture), + GenericError(GenericError), +} + +#[derive(Debug, CandidType, Deserialize)] +pub enum Value { + Nat(u128), + Text(String), + // Int(i64), + // Blob(Vec), +} + +pub async fn balance_of(token: TokenId, account: Account) -> Result { + let (result,): (Tokens,) = ic_cdk::call(token, "icrc1_balance_of", (account,)) + .await + .map_err(|err| format!("call failed: {:?}", err))?; + Ok(result) +} + +pub async fn metadata(token: TokenId) -> Result, String> { + let (result,): (Vec<(String, Value)>,) = ic_cdk::call(token, "icrc1_metadata", ((),)) + .await + .map_err(|err| format!("call failed: {:?}", err))?; + let mut data = result.into_iter().collect::>(); + + if !data.contains_key("icrc1:symbol") { + let (symbol,): (String,) = ic_cdk::call(token, "icrc1_symbol", ((),)) + .await + .map_err(|err| format!("call failed: {:?}", err))?; + data.insert("icrc1:symbol".to_string(), Value::Text(symbol)); + } + + if !data.contains_key("icrc1:fee") { + let (fee,): (u128,) = ic_cdk::call(token, "icrc1_fee", ((),)) + .await + .map_err(|err| format!("call failed: {:?}", err))?; + data.insert("icrc1:fee".to_string(), Value::Nat(fee)); + } + + if !data.contains_key("icrc1:decimals") { + let (decimals,): (u8,) = ic_cdk::call(token, "icrc1_decimals", ((),)) + .await + .map_err(|err| format!("call failed: {:?}", err))?; + data.insert("icrc1:decimals".to_string(), Value::Nat(decimals as u128)); + } + + Ok(data) +} + +pub async fn transfer(token: TokenId, args: TransferArgs) -> Result { + let (result,): (Result,) = ic_cdk::call(token, "icrc1_transfer", (args,)) + .await + .map_err(|err| format!("call failed: {:?}", err))?; + result.map_err(|err| format!("{:?}", err)) +} + +pub fn account(owner: Principal, user: Principal) -> Account { + let mut subaccount = user.as_slice().to_vec(); + subaccount.resize(32, 0); + Account { + owner, + subaccount: Some(subaccount), + } +} + +#[cfg(test)] +mod tests { + use candid::Principal; + + use super::Account; + + #[test] + fn test_account_encoding() { + let acc = Account { + owner: Principal::from_text( + "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", + ) + .unwrap(), + subaccount: None, + }; + + let encoded = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae"; + assert_eq!(&acc.to_string(), encoded); + assert_eq!(acc, Account::from_string(encoded).unwrap()); + + let acc = Account { + owner: Principal::from_text( + "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", + ) + .unwrap(), + subaccount: Some(vec![ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]), + }; + + let encoded = + "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; + assert_eq!(&acc.to_string(), encoded); + assert_eq!(acc, Account::from_string(encoded).unwrap()); + + let acc = Account { + owner: Principal::from_text( + "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", + ) + .unwrap(), + subaccount: Some(vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + }; + + let encoded = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.1"; + assert_eq!(&acc.to_string(), encoded); + assert_eq!(acc, Account::from_string(encoded).unwrap()); + + let acc = Account { + owner: Principal::from_text( + "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", + ) + .unwrap(), + subaccount: Some(vec![ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x1f, 0x20, + ]), + }; + + let encoded = + "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; + assert_eq!(&acc.to_string(), encoded); + assert_eq!(acc, Account::from_string(encoded).unwrap()); + } +} diff --git a/src/backend/lib.rs b/src/backend/lib.rs index 4004e97..1480b39 100644 --- a/src/backend/lib.rs +++ b/src/backend/lib.rs @@ -1,13 +1,26 @@ use std::cell::RefCell; -use ic_cdk::api::{ - call::{arg_data_raw, reply_raw}, - stable, +use candid::Principal; +use ic_cdk::{ + api::{ + call::{arg_data_raw, reply_raw}, + stable, + }, + caller, spawn, }; use ic_cdk_macros::*; -use serde::{Deserialize, Serialize}; +use ic_cdk_timers::{set_timer, set_timer_interval}; +use ic_ledger_types::{Memo, Tokens}; +use icp::{account_balance_of_principal, principal_to_subaccount}; +use icrc1::Value; +use order_book::{State, TokenId}; +use xdr_rate::get_xdr_in_e8s; mod assets; +mod icp; +mod icrc1; +mod order_book; +mod xdr_rate; thread_local! { static STATE: RefCell = Default::default(); @@ -27,9 +40,45 @@ where STATE.with(|cell| f(&mut cell.borrow_mut())) } -#[derive(Default, Serialize, Deserialize)] -struct State { - last_greeted_name: String, +#[export_name = "canister_query token"] +fn token() { + let id: Principal = parse(&arg_data_raw()); + read(|state| reply(state.get_token(id))); +} + +#[export_name = "canister_query subaccount"] +fn subaccount() { + reply(icp::user_account(caller()).to_string()); +} + +#[export_name = "canister_query params"] +fn params() { + read(|state| reply(state.e8s_per_xdr)); +} + +#[update] +fn set_revenue_account(new_address: Principal) { + mutate(|state| { + if state.revenue_account.is_none() || state.revenue_account == Some(caller()) { + state.revenue_account = Some(new_address); + } + }) +} + +#[export_name = "canister_update list_token"] +fn list_token() { + spawn(async { + let token: TokenId = parse(&arg_data_raw()); + reply(list_token_core(token).await) + }); +} + +fn parse<'a, T: serde::Deserialize<'a>>(bytes: &'a [u8]) -> T { + serde_json::from_slice(bytes).expect("couldn't parse the input") +} + +fn reply(data: T) { + reply_raw(serde_json::json!(data).to_string().as_bytes()); } #[init] @@ -64,19 +113,67 @@ fn post_upgrade() { ) }); assets::load(); + let fetch_rate = || { + spawn(async { + if let Ok(e8s) = get_xdr_in_e8s().await { + mutate(|state| state.e8s_per_xdr = e8s); + } + }) + }; + set_timer(std::time::Duration::from_secs(1), fetch_rate); + set_timer_interval(std::time::Duration::from_secs(60 * 60), fetch_rate); } -#[export_name = "canister_query greet"] -fn greet() { - let name: String = parse(&arg_data_raw()); - mutate(|state| state.last_greeted_name = name.clone()); - reply(format!("Hello {}!", name)); -} +async fn list_token_core(token: TokenId) -> Result<(), String> { + let balance = account_balance_of_principal(caller()).await; + let listing_price = Tokens::from_e8s(read(|state| state.e8s_per_xdr * 100)); + if balance < listing_price { + return Err(format!( + "Balance too low! Expected: {}, found: {}.", + listing_price, balance + )); + } -fn parse<'a, T: serde::Deserialize<'a>>(bytes: &'a [u8]) -> T { - serde_json::from_slice(bytes).expect("couldn't parse the input") -} + let metadata = icrc1::metadata(token) + .await + .map_err(|err| format!("couldn't fetch metadata: {}", err))?; -fn reply(data: T) { - reply_raw(serde_json::json!(data).to_string().as_bytes()); + match ( + metadata.get("icrc1:symbol"), + metadata.get("icrc1:fee"), + metadata.get("icrc1:decimals"), + metadata.get("icrc1:logo"), + ) { + (Some(Value::Text(symbol)), Some(Value::Nat(fee)), Some(Value::Nat(decimals)), logo) => { + mutate(|state| { + state.add_token( + token, + symbol.clone(), + *fee, + *decimals as u32, + match logo { + Some(Value::Text(hex)) => Some(hex.clone()), + _ => None, + }, + ) + }) + } + (symbol, fee, decimals, _) => { + return Err(format!( + "one of the required values missing: symbol={:?}, fee={:?}, decimals={:?}", + symbol, fee, decimals + )); + } + } + + icp::transfer( + icp::revenue_account(), + balance, + Memo(0), + Some(principal_to_subaccount(&caller())), + ) + .await + .map_err(|err| format!("transfer failed: {}", err))?; + + Ok(()) } diff --git a/src/backend/order_book/mod.rs b/src/backend/order_book/mod.rs new file mode 100644 index 0000000..64b3351 --- /dev/null +++ b/src/backend/order_book/mod.rs @@ -0,0 +1,92 @@ +use std::{ + cmp::Ordering, + collections::{BTreeMap, BTreeSet}, +}; + +use candid::Principal; +use serde::{Deserialize, Serialize}; + +pub type Tokens = u128; +pub type TokenId = Principal; + +#[derive(Serialize, Deserialize, PartialEq, Eq)] +struct Order { + owner: Principal, + amount: Tokens, +} + +impl PartialOrd for Order { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.amount.cmp(&other.amount)) + } +} + +impl Ord for Order { + fn cmp(&self, other: &Self) -> Ordering { + self.amount.cmp(&other.amount) + } +} + +#[derive(Serialize, Deserialize)] +struct Book { + buyers: BTreeSet, + sellers: BTreeSet, +} + +type Timestamp = u64; +type PriceDelta = i64; + +#[derive(Clone, Serialize, Deserialize)] +pub struct Metadata { + symbol: String, + fee: Tokens, + decimals: u32, + logo: Option, +} + +#[derive(Default, Serialize, Deserialize)] +pub struct State { + orders: BTreeMap, + price_moves: BTreeMap>, + pools: BTreeMap, + tokens: BTreeMap, + pub e8s_per_xdr: u64, + pub revenue_account: Option, +} + +impl State { + pub fn get_token(&self, id: TokenId) -> Result { + self.tokens + .get(&id) + .cloned() + .ok_or("no token listed".into()) + } + + pub fn add_token( + &mut self, + id: TokenId, + symbol: String, + fee: Tokens, + decimals: u32, + logo: Option, + ) { + self.tokens.insert( + id, + Metadata { + symbol, + logo, + fee, + decimals, + }, + ); + } + + pub async fn create_order( + &mut self, + caller: Principal, + id: TokenId, + amount: Tokens, + ) -> Result<(), String> { + panic!() + } +} diff --git a/src/backend/xdr_rate.rs b/src/backend/xdr_rate.rs new file mode 100644 index 0000000..b1c6357 --- /dev/null +++ b/src/backend/xdr_rate.rs @@ -0,0 +1,28 @@ +use candid::CandidType; +use ic_ledger_types::MAINNET_CYCLES_MINTING_CANISTER_ID; +use serde::Deserialize; + +#[derive(CandidType, Deserialize)] +struct IcpXdrConversionRate { + xdr_permyriad_per_icp: u64, +} + +#[derive(CandidType, Deserialize)] +struct IcpXdrConversionRateCertifiedResponse { + data: IcpXdrConversionRate, +} + +pub async fn get_xdr_in_e8s() -> Result { + let (IcpXdrConversionRateCertifiedResponse { + data: IcpXdrConversionRate { + xdr_permyriad_per_icp, + }, + },) = ic_cdk::call( + MAINNET_CYCLES_MINTING_CANISTER_ID, + "get_icp_xdr_conversion_rate", + (), + ) + .await + .map_err(|err| format!("couldn't get ICP/XDR ratio: {:?}", err))?; + Ok((100_000_000.0 / xdr_permyriad_per_icp as f64) as u64 * 10_000) +} diff --git a/src/frontend/src/api.ts b/src/frontend/src/api.ts index c607d09..8a0e8a1 100644 --- a/src/frontend/src/api.ts +++ b/src/frontend/src/api.ts @@ -1,5 +1,6 @@ import { Principal } from "@dfinity/principal"; import { HttpAgent, HttpAgentOptions, Identity, polling } from "@dfinity/agent"; +import { mainnetMode } from "./common"; export type Backend = { query: ( @@ -32,7 +33,6 @@ export const ApiGenerator = ( identity?: Identity, ): Backend => { const canisterId = Principal.fromText(defaultCanisterId); - const mainnetMode = process.env.NODE_ENV == "production"; const options: HttpAgentOptions = { identity }; if (mainnetMode) options.host = `https://${defaultCanisterId}.icp0.io`; const agent = new HttpAgent(options); diff --git a/src/frontend/src/common.tsx b/src/frontend/src/common.tsx new file mode 100644 index 0000000..2f6729a --- /dev/null +++ b/src/frontend/src/common.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +export const Error = ({ text }: { text: string }) =>

Error: {text}

; + +export const mainnetMode = process.env.NODE_ENV == "production"; + +export const II_URL = mainnetMode + ? "https://identity.ic0.app" + : "http://127.0.0.1:8080/?canisterId=qhbym-qaaaa-aaaaa-aaafq-cai"; + +export const II_DERIVATION_URL = mainnetMode + ? `https://${process.env.CANISTER_ID}.icp0.io` + : window.location.origin; + +export const icp = (e8s: BigInt, decimals: number = 2) => { + let n = Number(e8s); + let base = Math.pow(10, 8); + let v = n / base; + return (decimals ? v : Math.floor(v)).toLocaleString(undefined, { + minimumFractionDigits: decimals, + }); +}; + +export const Button = ({ + onClick, + label, +}: { + onClick: () => Promise; + label: string; +}) => { + const [loading, setLoading] = React.useState(false); + return ( + + ); +}; diff --git a/src/frontend/src/index.html b/src/frontend/src/index.html index 054544b..0db97e8 100644 --- a/src/frontend/src/index.html +++ b/src/frontend/src/index.html @@ -17,7 +17,7 @@ sizes="180x180" href="https://xxxx.raw.icp0.io/_/raw/apple-touch-icon.png" /> - +