From bee08e2b1456f9758109b0d065d7c0a147946ab2 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 13 Sep 2023 17:07:41 +0100 Subject: [PATCH 01/39] Exchange bot WIP --- Cargo.lock | 73 +++++++++++++++ Cargo.toml | 5 + backend/canister_installer/Cargo.toml | 1 + .../local-bin/exchange_bot.wasm.gz | 1 + backend/canister_installer/src/lib.rs | 20 +++- backend/canister_installer/src/main.rs | 4 + backend/canister_upgrader/Cargo.toml | 1 + .../local-bin/exchange_bot.wasm.gz | 1 + backend/canister_upgrader/src/lib.rs | 19 ++++ backend/canister_upgrader/src/main.rs | 4 + backend/canisters/exchange_bot/CHANGELOG.md | 6 ++ backend/canisters/exchange_bot/api/Cargo.toml | 13 +++ backend/canisters/exchange_bot/api/can.did | 42 +++++++++ backend/canisters/exchange_bot/api/src/lib.rs | 5 + .../exchange_bot/api/src/lifecycle/init.rs | 12 +++ .../exchange_bot/api/src/lifecycle/mod.rs | 2 + .../api/src/lifecycle/post_upgrade.rs | 8 ++ .../canisters/exchange_bot/api/src/main.rs | 5 + .../exchange_bot/api/src/updates/mod.rs | 1 + .../canisters/exchange_bot/impl/Cargo.toml | 31 +++++++ .../canisters/exchange_bot/impl/src/guards.rs | 9 ++ .../exchange_bot/impl/src/jobs/mod.rs | 3 + .../canisters/exchange_bot/impl/src/lib.rs | 93 +++++++++++++++++++ .../exchange_bot/impl/src/lifecycle/init.rs | 27 ++++++ .../impl/src/lifecycle/inspect_message.rs | 19 ++++ .../exchange_bot/impl/src/lifecycle/mod.rs | 38 ++++++++ .../impl/src/lifecycle/post_upgrade.rs | 28 ++++++ .../impl/src/lifecycle/pre_upgrade.rs | 24 +++++ .../canisters/exchange_bot/impl/src/memory.rs | 21 +++++ .../exchange_bot/impl/src/model/mod.rs | 1 + .../impl/src/queries/http_request.rs | 26 ++++++ .../exchange_bot/impl/src/queries/mod.rs | 1 + .../exchange_bot/impl/src/updates/mod.rs | 1 + .../impl/src/updates/wallet_receive.rs | 9 ++ .../icpswap_swap_pool/api/Cargo.toml | 12 +++ .../icpswap_swap_pool/api/src/lib.rs | 23 +++++ .../icpswap_swap_pool/api/src/queries/mod.rs | 1 + .../api/src/queries/quote.rs | 1 + .../api/src/updates/deposit.rs | 11 +++ .../icpswap_swap_pool/api/src/updates/mod.rs | 3 + .../icpswap_swap_pool/api/src/updates/swap.rs | 14 +++ .../api/src/updates/withdraw.rs | 1 + .../icpswap_swap_pool/c2c_client/Cargo.toml | 13 +++ .../icpswap_swap_pool/c2c_client/src/lib.rs | 8 ++ .../local-bin/exchange_bot.wasm.gz | 1 + .../libraries/canister_agent_utils/src/lib.rs | 4 + backend/libraries/icpswap_client/Cargo.toml | 16 ++++ backend/libraries/icpswap_client/src/lib.rs | 70 ++++++++++++++ canister_ids.json | 3 + dfx.json | 6 ++ scripts/deploy-local.sh | 1 + scripts/deploy-testnet.sh | 1 + scripts/deploy.sh | 2 + scripts/download-all-canister-wasms.sh | 1 + scripts/generate-all-canister-wasms.sh | 1 + scripts/upgrade-canister.sh | 2 + 56 files changed, 747 insertions(+), 2 deletions(-) create mode 120000 backend/canister_installer/local-bin/exchange_bot.wasm.gz create mode 120000 backend/canister_upgrader/local-bin/exchange_bot.wasm.gz create mode 100644 backend/canisters/exchange_bot/CHANGELOG.md create mode 100644 backend/canisters/exchange_bot/api/Cargo.toml create mode 100644 backend/canisters/exchange_bot/api/can.did create mode 100644 backend/canisters/exchange_bot/api/src/lib.rs create mode 100644 backend/canisters/exchange_bot/api/src/lifecycle/init.rs create mode 100644 backend/canisters/exchange_bot/api/src/lifecycle/mod.rs create mode 100644 backend/canisters/exchange_bot/api/src/lifecycle/post_upgrade.rs create mode 100644 backend/canisters/exchange_bot/api/src/main.rs create mode 100644 backend/canisters/exchange_bot/api/src/updates/mod.rs create mode 100644 backend/canisters/exchange_bot/impl/Cargo.toml create mode 100644 backend/canisters/exchange_bot/impl/src/guards.rs create mode 100644 backend/canisters/exchange_bot/impl/src/jobs/mod.rs create mode 100644 backend/canisters/exchange_bot/impl/src/lib.rs create mode 100644 backend/canisters/exchange_bot/impl/src/lifecycle/init.rs create mode 100644 backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs create mode 100644 backend/canisters/exchange_bot/impl/src/lifecycle/mod.rs create mode 100644 backend/canisters/exchange_bot/impl/src/lifecycle/post_upgrade.rs create mode 100644 backend/canisters/exchange_bot/impl/src/lifecycle/pre_upgrade.rs create mode 100644 backend/canisters/exchange_bot/impl/src/memory.rs create mode 100644 backend/canisters/exchange_bot/impl/src/model/mod.rs create mode 100644 backend/canisters/exchange_bot/impl/src/queries/http_request.rs create mode 100644 backend/canisters/exchange_bot/impl/src/queries/mod.rs create mode 100644 backend/canisters/exchange_bot/impl/src/updates/mod.rs create mode 100644 backend/canisters/exchange_bot/impl/src/updates/wallet_receive.rs create mode 100644 backend/external_canisters/icpswap_swap_pool/api/Cargo.toml create mode 100644 backend/external_canisters/icpswap_swap_pool/api/src/lib.rs create mode 100644 backend/external_canisters/icpswap_swap_pool/api/src/queries/mod.rs create mode 100644 backend/external_canisters/icpswap_swap_pool/api/src/queries/quote.rs create mode 100644 backend/external_canisters/icpswap_swap_pool/api/src/updates/deposit.rs create mode 100644 backend/external_canisters/icpswap_swap_pool/api/src/updates/mod.rs create mode 100644 backend/external_canisters/icpswap_swap_pool/api/src/updates/swap.rs create mode 100644 backend/external_canisters/icpswap_swap_pool/api/src/updates/withdraw.rs create mode 100644 backend/external_canisters/icpswap_swap_pool/c2c_client/Cargo.toml create mode 100644 backend/external_canisters/icpswap_swap_pool/c2c_client/src/lib.rs create mode 120000 backend/integration_tests/local-bin/exchange_bot.wasm.gz create mode 100644 backend/libraries/icpswap_client/Cargo.toml create mode 100644 backend/libraries/icpswap_client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index aa060b0499..6a9157ae69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -826,6 +826,7 @@ dependencies = [ "canister_agent_utils", "clap", "cycles_dispenser_canister", + "exchange_bot_canister", "futures", "group_canister", "group_index_canister", @@ -908,6 +909,7 @@ dependencies = [ "canister_agent_utils", "clap", "cycles_dispenser_canister", + "exchange_bot_canister", "group_canister", "group_index_canister", "group_index_canister_client", @@ -1552,6 +1554,42 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exchange_bot_canister" +version = "0.1.0" +dependencies = [ + "candid", + "candid_gen", + "human_readable", + "serde", + "types", +] + +[[package]] +name = "exchange_bot_canister_impl" +version = "0.1.0" +dependencies = [ + "candid", + "canister_api_macros", + "canister_logger", + "canister_state_macros", + "canister_tracing_macros", + "exchange_bot_canister", + "http_request", + "human_readable", + "ic-cdk", + "ic-cdk-macros", + "ic-cdk-timers", + "ic-stable-structures", + "icpswap_client", + "msgpack", + "serde", + "serializer", + "tracing", + "types", + "utils", +] + [[package]] name = "fallible_collections" version = "0.4.9" @@ -2525,6 +2563,41 @@ dependencies = [ "types", ] +[[package]] +name = "icpswap_client" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk", + "icpswap_swap_pool_canister", + "icpswap_swap_pool_canister_c2c_client", + "icrc1_ledger_canister_c2c_client", + "ledger_utils", + "serde", + "types", +] + +[[package]] +name = "icpswap_swap_pool_canister" +version = "0.1.0" +dependencies = [ + "candid", + "candid_gen", + "serde", + "types", +] + +[[package]] +name = "icpswap_swap_pool_canister_c2c_client" +version = "0.1.0" +dependencies = [ + "candid", + "canister_client", + "ic-cdk", + "icpswap_swap_pool_canister", + "types", +] + [[package]] name = "icrc1_ledger_canister" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b3d737052e..f4cd942b64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ members = [ "backend/canisters/community/impl", "backend/canisters/cycles_dispenser/api", "backend/canisters/cycles_dispenser/impl", + "backend/canisters/exchange_bot/api", + "backend/canisters/exchange_bot/impl", "backend/canisters/group/api", "backend/canisters/group/c2c_client", "backend/canisters/group/client", @@ -72,6 +74,8 @@ members = [ "backend/external_canisters/icdex/c2c_client", "backend/external_canisters/icp_ledger/api", "backend/external_canisters/icp_ledger/c2c_client", + "backend/external_canisters/icpswap_swap_pool/api", + "backend/external_canisters/icpswap_swap_pool/c2c_client", "backend/external_canisters/icrc1_ledger/api", "backend/external_canisters/icrc1_ledger/c2c_client", "backend/external_canisters/sns_governance/api", @@ -101,6 +105,7 @@ members = [ "backend/libraries/human_readable", "backend/libraries/human_readable_derive", "backend/libraries/icdex_client", + "backend/libraries/icpswap_client", "backend/libraries/index_store", "backend/libraries/instruction_counts_log", "backend/libraries/ledger_utils", diff --git a/backend/canister_installer/Cargo.toml b/backend/canister_installer/Cargo.toml index 1cd4066d37..e0da7923aa 100644 --- a/backend/canister_installer/Cargo.toml +++ b/backend/canister_installer/Cargo.toml @@ -10,6 +10,7 @@ candid = { workspace = true } canister_agent_utils = { path = "../libraries/canister_agent_utils" } clap = { workspace = true, features = ["derive"] } cycles_dispenser_canister = { path = "../canisters/cycles_dispenser/api" } +exchange_bot_canister = { path = "../canisters/exchange_bot/api" } futures = { workspace = true } group_canister = { path = "../canisters/group/api" } group_index_canister = { path = "../canisters/group_index/api" } diff --git a/backend/canister_installer/local-bin/exchange_bot.wasm.gz b/backend/canister_installer/local-bin/exchange_bot.wasm.gz new file mode 120000 index 0000000000..a159383582 --- /dev/null +++ b/backend/canister_installer/local-bin/exchange_bot.wasm.gz @@ -0,0 +1 @@ +../../../wasms/exchange_bot.wasm.gz \ No newline at end of file diff --git a/backend/canister_installer/src/lib.rs b/backend/canister_installer/src/lib.rs index 26125b7614..c7daf4e329 100644 --- a/backend/canister_installer/src/lib.rs +++ b/backend/canister_installer/src/lib.rs @@ -32,7 +32,8 @@ async fn install_service_canisters_impl( set_controllers(management_canister, &canister_ids.storage_index, controllers.clone()), set_controllers(management_canister, &canister_ids.cycles_dispenser, controllers.clone()), set_controllers(management_canister, &canister_ids.registry, controllers.clone()), - set_controllers(management_canister, &canister_ids.market_maker, controllers), + set_controllers(management_canister, &canister_ids.market_maker, controllers.clone()), + set_controllers(management_canister, &canister_ids.exchange_bot, controllers), set_controllers( management_canister, &canister_ids.local_user_index, @@ -171,6 +172,15 @@ async fn install_service_canisters_impl( test_mode, }; + let exchange_bot_canister_wasm = get_canister_wasm(CanisterName::ExchangeBot, version); + let exchange_bot_init_args = exchange_bot_canister::init::Args { + governance_principals: vec![principal], + local_user_index_canister_id: canister_ids.local_user_index, + cycles_dispenser_canister_id: canister_ids.cycles_dispenser, + wasm_version: version, + test_mode, + }; + futures::future::join5( install_wasm( management_canister, @@ -205,7 +215,7 @@ async fn install_service_canisters_impl( ) .await; - futures::future::join4( + futures::future::join5( install_wasm( management_canister, &canister_ids.storage_index, @@ -230,6 +240,12 @@ async fn install_service_canisters_impl( &market_maker_canister_wasm.module, market_maker_init_args, ), + install_wasm( + management_canister, + &canister_ids.exchange_bot, + &exchange_bot_canister_wasm.module, + exchange_bot_init_args, + ), ) .await; diff --git a/backend/canister_installer/src/main.rs b/backend/canister_installer/src/main.rs index a5e231f262..215a3c42b3 100644 --- a/backend/canister_installer/src/main.rs +++ b/backend/canister_installer/src/main.rs @@ -20,6 +20,7 @@ async fn main() { cycles_dispenser: opts.cycles_dispenser, registry: opts.registry, market_maker: opts.market_maker, + exchange_bot: opts.exchange_bot, nns_root: opts.nns_root, nns_governance: opts.nns_governance, nns_internet_identity: opts.nns_internet_identity, @@ -80,6 +81,9 @@ struct Opts { #[arg(long)] market_maker: CanisterId, + #[arg(long)] + exchange_bot: CanisterId, + #[arg(long)] nns_root: CanisterId, diff --git a/backend/canister_upgrader/Cargo.toml b/backend/canister_upgrader/Cargo.toml index 8874247b39..45449b1f7c 100644 --- a/backend/canister_upgrader/Cargo.toml +++ b/backend/canister_upgrader/Cargo.toml @@ -10,6 +10,7 @@ candid = { workspace = true } canister_agent_utils = { path = "../libraries/canister_agent_utils" } clap = { workspace = true, features = ["derive"] } cycles_dispenser_canister = { path = "../canisters/cycles_dispenser/api" } +exchange_bot_canister = { path = "../canisters/exchange_bot/api" } group_canister = { path = "../canisters/group/api" } group_index_canister = { path = "../canisters/group_index/api" } group_index_canister_client = { path = "../canisters/group_index/client" } diff --git a/backend/canister_upgrader/local-bin/exchange_bot.wasm.gz b/backend/canister_upgrader/local-bin/exchange_bot.wasm.gz new file mode 120000 index 0000000000..a159383582 --- /dev/null +++ b/backend/canister_upgrader/local-bin/exchange_bot.wasm.gz @@ -0,0 +1 @@ +../../../wasms/exchange_bot.wasm.gz \ No newline at end of file diff --git a/backend/canister_upgrader/src/lib.rs b/backend/canister_upgrader/src/lib.rs index d1e2e15976..8a8c981acf 100644 --- a/backend/canister_upgrader/src/lib.rs +++ b/backend/canister_upgrader/src/lib.rs @@ -178,6 +178,25 @@ pub async fn upgrade_market_maker_canister( println!("Market maker canister upgraded"); } +pub async fn upgrade_exchange_bot_canister( + identity: BasicIdentity, + url: String, + exchange_bot_canister_id: CanisterId, + version: BuildVersion, +) { + upgrade_top_level_canister( + identity, + url, + exchange_bot_canister_id, + version, + exchange_bot_canister::post_upgrade::Args { wasm_version: version }, + CanisterName::ExchangeBot, + ) + .await; + + println!("Exchange bot canister upgraded"); +} + pub async fn upgrade_local_group_index_canister( identity: BasicIdentity, url: String, diff --git a/backend/canister_upgrader/src/main.rs b/backend/canister_upgrader/src/main.rs index 8573a4e548..08c01f7c5f 100644 --- a/backend/canister_upgrader/src/main.rs +++ b/backend/canister_upgrader/src/main.rs @@ -14,6 +14,7 @@ async fn main() { CanisterName::CyclesDispenser => { upgrade_cycles_dispenser_canister(identity, opts.url, opts.cycles_dispenser, opts.version).await } + CanisterName::ExchangeBot => upgrade_exchange_bot_canister(identity, opts.url, opts.exchange_bot, opts.version).await, CanisterName::Group => upgrade_group_canister(identity, opts.url, opts.group_index, opts.version).await, CanisterName::LocalGroupIndex => { upgrade_local_group_index_canister(identity, opts.url, opts.group_index, opts.version).await @@ -80,6 +81,9 @@ struct Opts { #[arg(long)] market_maker: CanisterId, + #[arg(long)] + exchange_bot: CanisterId, + #[arg(long)] canister_to_upgrade: CanisterName, diff --git a/backend/canisters/exchange_bot/CHANGELOG.md b/backend/canisters/exchange_bot/CHANGELOG.md new file mode 100644 index 0000000000..8646827d5a --- /dev/null +++ b/backend/canisters/exchange_bot/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [unreleased] diff --git a/backend/canisters/exchange_bot/api/Cargo.toml b/backend/canisters/exchange_bot/api/Cargo.toml new file mode 100644 index 0000000000..80fcf88a52 --- /dev/null +++ b/backend/canisters/exchange_bot/api/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "exchange_bot_canister" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +candid = { workspace = true } +candid_gen = { path = "../../../libraries/candid_gen" } +human_readable = { path = "../../../libraries/human_readable" } +serde = { workspace = true } +types = { path = "../../../libraries/types" } \ No newline at end of file diff --git a/backend/canisters/exchange_bot/api/can.did b/backend/canisters/exchange_bot/api/can.did new file mode 100644 index 0000000000..1687b30ead --- /dev/null +++ b/backend/canisters/exchange_bot/api/can.did @@ -0,0 +1,42 @@ +import "../../../libraries/types/can.did"; + +type AddGovernanceCanisterArgs = record { + governance_canister_id : CanisterId; + community_id : opt CommunityId; + name : text; + description : opt text; + avatar : opt Document; +}; + +type AddGovernanceCanisterResponse = variant { + Success; + AlreadyAdded; + InternalError : text; +}; + +type RemoveGovernanceCanisterArgs = record { + governance_canister_id : CanisterId; + delete_group : bool; +}; + +type RemoveGovernanceCanisterResponse = variant { + Success; + NotFound; + InternalError : text; +}; + +type AppointAdminsArgs = record { + governance_canister_id : CanisterId; + users : vec UserId; +}; + +type AppointAdminsResponse = variant { + Success; + NotFound; +}; + +service : { + add_governance_canister : (AddGovernanceCanisterArgs) -> (AddGovernanceCanisterResponse); + remove_governance_canister : (RemoveGovernanceCanisterArgs) -> (RemoveGovernanceCanisterResponse); + appoint_admins : (AppointAdminsArgs) -> (AppointAdminsResponse); +}; diff --git a/backend/canisters/exchange_bot/api/src/lib.rs b/backend/canisters/exchange_bot/api/src/lib.rs new file mode 100644 index 0000000000..9550e16025 --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/lib.rs @@ -0,0 +1,5 @@ +mod lifecycle; +mod updates; + +pub use lifecycle::*; +pub use updates::*; diff --git a/backend/canisters/exchange_bot/api/src/lifecycle/init.rs b/backend/canisters/exchange_bot/api/src/lifecycle/init.rs new file mode 100644 index 0000000000..7d1ece3047 --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/lifecycle/init.rs @@ -0,0 +1,12 @@ +use candid::{CandidType, Principal}; +use serde::{Deserialize, Serialize}; +use types::{BuildVersion, CanisterId}; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub governance_principals: Vec, + pub local_user_index_canister_id: CanisterId, + pub cycles_dispenser_canister_id: CanisterId, + pub wasm_version: BuildVersion, + pub test_mode: bool, +} diff --git a/backend/canisters/exchange_bot/api/src/lifecycle/mod.rs b/backend/canisters/exchange_bot/api/src/lifecycle/mod.rs new file mode 100644 index 0000000000..70bd4f5a23 --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/lifecycle/mod.rs @@ -0,0 +1,2 @@ +pub mod init; +pub mod post_upgrade; diff --git a/backend/canisters/exchange_bot/api/src/lifecycle/post_upgrade.rs b/backend/canisters/exchange_bot/api/src/lifecycle/post_upgrade.rs new file mode 100644 index 0000000000..470a25ac40 --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/lifecycle/post_upgrade.rs @@ -0,0 +1,8 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use types::BuildVersion; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub wasm_version: BuildVersion, +} diff --git a/backend/canisters/exchange_bot/api/src/main.rs b/backend/canisters/exchange_bot/api/src/main.rs new file mode 100644 index 0000000000..37e8c25054 --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/main.rs @@ -0,0 +1,5 @@ +#[allow(deprecated)] +fn main() { + candid::export_service!(); + std::print!("{}", __export_service()); +} diff --git a/backend/canisters/exchange_bot/api/src/updates/mod.rs b/backend/canisters/exchange_bot/api/src/updates/mod.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/updates/mod.rs @@ -0,0 +1 @@ + diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml new file mode 100644 index 0000000000..e8ee63327f --- /dev/null +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "exchange_bot_canister_impl" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +candid = { workspace = true } +canister_api_macros = { path = "../../../libraries/canister_api_macros" } +canister_logger = { path = "../../../libraries/canister_logger" } +canister_state_macros = { path = "../../../libraries/canister_state_macros" } +canister_tracing_macros = { path = "../../../libraries/canister_tracing_macros" } +exchange_bot_canister = { path = "../api" } +http_request = { path = "../../../libraries/http_request" } +human_readable = { path = "../../../libraries/human_readable" } +ic-cdk = { workspace = true } +ic-cdk-macros = { workspace = true } +ic-cdk-timers = { workspace = true } +ic-stable-structures = { workspace = true } +icpswap_client = { path = "../../../libraries/icpswap_client" } +msgpack = { path = "../../../libraries/msgpack" } +serde = { workspace = true } +serializer = { path = "../../../libraries/serializer" } +tracing = { workspace = true } +types = { path = "../../../libraries/types" } +utils = { path = "../../../libraries/utils" } diff --git a/backend/canisters/exchange_bot/impl/src/guards.rs b/backend/canisters/exchange_bot/impl/src/guards.rs new file mode 100644 index 0000000000..8b50787738 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/guards.rs @@ -0,0 +1,9 @@ +use crate::read_state; + +pub fn caller_is_governance_principal() -> Result<(), String> { + if read_state(|state| state.is_caller_governance_principal()) { + Ok(()) + } else { + Err("Caller is not the service owner".to_owned()) + } +} diff --git a/backend/canisters/exchange_bot/impl/src/jobs/mod.rs b/backend/canisters/exchange_bot/impl/src/jobs/mod.rs new file mode 100644 index 0000000000..f24c5a7cd5 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/jobs/mod.rs @@ -0,0 +1,3 @@ +use crate::RuntimeState; + +pub(crate) fn start(state: &RuntimeState) {} diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs new file mode 100644 index 0000000000..e2b83d2f99 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -0,0 +1,93 @@ +use candid::Principal; +use canister_state_macros::canister_state; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::collections::HashSet; +use types::{BuildVersion, CanisterId, Cycles, TimestampMillis, Timestamped}; +use utils::env::Environment; + +mod guards; +mod jobs; +mod lifecycle; +mod memory; +mod model; +mod queries; +mod updates; + +thread_local! { + static WASM_VERSION: RefCell> = RefCell::default(); +} + +canister_state!(RuntimeState); + +struct RuntimeState { + pub env: Box, + pub data: Data, +} + +impl RuntimeState { + pub fn new(env: Box, data: Data) -> RuntimeState { + RuntimeState { env, data } + } + + pub fn is_caller_governance_principal(&self) -> bool { + let caller = self.env.caller(); + self.data.governance_principals.contains(&caller) + } + + pub fn metrics(&self) -> Metrics { + Metrics { + memory_used: utils::memory::used(), + now: self.env.now(), + cycles_balance: self.env.cycles_balance(), + wasm_version: WASM_VERSION.with(|v| **v.borrow()), + git_commit_id: utils::git::git_commit_id().to_string(), + governance_principals: self.data.governance_principals.iter().copied().collect(), + canister_ids: CanisterIds { + local_user_index: self.data.local_user_index_canister_id, + cycles_dispenser: self.data.cycles_dispenser_canister_id, + }, + } + } +} + +#[derive(Serialize, Deserialize)] +struct Data { + pub governance_principals: HashSet, + pub local_user_index_canister_id: CanisterId, + pub cycles_dispenser_canister_id: CanisterId, + pub test_mode: bool, +} + +impl Data { + pub fn new( + governance_principals: HashSet, + local_user_index_canister_id: CanisterId, + cycles_dispenser_canister_id: CanisterId, + test_mode: bool, + ) -> Data { + Data { + governance_principals, + local_user_index_canister_id, + cycles_dispenser_canister_id, + test_mode, + } + } +} + +#[derive(Serialize, Debug)] +pub struct Metrics { + pub now: TimestampMillis, + pub memory_used: u64, + pub cycles_balance: Cycles, + pub wasm_version: BuildVersion, + pub git_commit_id: String, + pub governance_principals: Vec, + pub canister_ids: CanisterIds, +} + +#[derive(Serialize, Debug)] +pub struct CanisterIds { + pub local_user_index: CanisterId, + pub cycles_dispenser: CanisterId, +} diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/init.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/init.rs new file mode 100644 index 0000000000..5b6ca1e014 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/init.rs @@ -0,0 +1,27 @@ +use crate::lifecycle::{init_env, init_state}; +use crate::Data; +use canister_tracing_macros::trace; +use exchange_bot_canister::init::Args; +use ic_cdk_macros::init; +use tracing::info; +use utils::cycles::init_cycles_dispenser_client; + +#[init] +#[trace] +fn init(args: Args) { + canister_logger::init(args.test_mode); + init_cycles_dispenser_client(args.cycles_dispenser_canister_id); + + let env = init_env(); + + let data = Data::new( + args.governance_principals.into_iter().collect(), + args.local_user_index_canister_id, + args.cycles_dispenser_canister_id, + args.test_mode, + ); + + init_state(env, data, args.wasm_version); + + info!(version = %args.wasm_version, "Initialization complete"); +} diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs new file mode 100644 index 0000000000..3ac9fd8284 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs @@ -0,0 +1,19 @@ +use crate::{read_state, RuntimeState}; +use ic_cdk_macros::inspect_message; + +#[inspect_message] +fn inspect_message() { + read_state(accept_if_valid); +} + +fn accept_if_valid(state: &RuntimeState) { + let method_name = ic_cdk::api::call::method_name(); + + let is_valid = match method_name.as_str() { + _ => false, + }; + + if is_valid { + ic_cdk::api::call::accept_message(); + } +} diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/mod.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/mod.rs new file mode 100644 index 0000000000..883457343d --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/mod.rs @@ -0,0 +1,38 @@ +use crate::{mutate_state, Data, RuntimeState, WASM_VERSION}; +use std::time::Duration; +use tracing::trace; +use types::{BuildVersion, Timestamped}; +use utils::canister::get_random_seed; +use utils::env::canister::CanisterEnv; +use utils::env::Environment; + +mod init; +mod inspect_message; +mod post_upgrade; +mod pre_upgrade; + +const UPGRADE_BUFFER_SIZE: usize = 1024 * 1024; // 1MB + +fn init_env() -> Box { + ic_cdk_timers::set_timer(Duration::ZERO, reseed_rng); + Box::default() +} + +fn init_state(env: Box, data: Data, wasm_version: BuildVersion) { + let now = env.now(); + let state = RuntimeState::new(env, data); + + crate::jobs::start(&state); + crate::init_state(state); + WASM_VERSION.with(|v| *v.borrow_mut() = Timestamped::new(wasm_version, now)); +} + +fn reseed_rng() { + ic_cdk::spawn(reseed_rng_inner()); + + async fn reseed_rng_inner() { + let seed = get_random_seed().await; + mutate_state(|state| state.env = Box::new(CanisterEnv::new(seed))); + trace!("Successfully reseeded rng"); + } +} diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/post_upgrade.rs new file mode 100644 index 0000000000..758b3dee6e --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/post_upgrade.rs @@ -0,0 +1,28 @@ +use crate::lifecycle::{init_env, init_state, UPGRADE_BUFFER_SIZE}; +use crate::memory::get_upgrades_memory; +use crate::Data; +use canister_logger::LogEntry; +use canister_tracing_macros::trace; +use exchange_bot_canister::post_upgrade::Args; +use ic_cdk_macros::post_upgrade; +use ic_stable_structures::reader::{BufferedReader, Reader}; +use tracing::info; +use utils::cycles::init_cycles_dispenser_client; + +#[post_upgrade] +#[trace] +fn post_upgrade(args: Args) { + let env = init_env(); + + let memory = get_upgrades_memory(); + let reader = BufferedReader::new(UPGRADE_BUFFER_SIZE, Reader::new(&memory, 0)); + + let (data, logs, traces): (Data, Vec, Vec) = serializer::deserialize(reader).unwrap(); + + canister_logger::init_with_logs(data.test_mode, logs, traces); + + init_cycles_dispenser_client(data.cycles_dispenser_canister_id); + init_state(env, data, args.wasm_version); + + info!(version = %args.wasm_version, "Post-upgrade complete"); +} diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/pre_upgrade.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/pre_upgrade.rs new file mode 100644 index 0000000000..080c91ed88 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/pre_upgrade.rs @@ -0,0 +1,24 @@ +use crate::lifecycle::UPGRADE_BUFFER_SIZE; +use crate::memory::get_upgrades_memory; +use crate::take_state; +use canister_tracing_macros::trace; +use ic_cdk_macros::pre_upgrade; +use ic_stable_structures::writer::{BufferedWriter, Writer}; +use tracing::info; + +#[pre_upgrade] +#[trace] +fn pre_upgrade() { + info!("Pre-upgrade starting"); + + let state = take_state(); + let logs = canister_logger::export_logs(); + let traces = canister_logger::export_traces(); + + let stable_state = (state.data, logs, traces); + + let mut memory = get_upgrades_memory(); + let writer = BufferedWriter::new(UPGRADE_BUFFER_SIZE, Writer::new(&mut memory, 0)); + + serializer::serialize(stable_state, writer).unwrap(); +} diff --git a/backend/canisters/exchange_bot/impl/src/memory.rs b/backend/canisters/exchange_bot/impl/src/memory.rs new file mode 100644 index 0000000000..d3a1de0b7b --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/memory.rs @@ -0,0 +1,21 @@ +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + DefaultMemoryImpl, +}; + +const UPGRADES: MemoryId = MemoryId::new(0); + +pub type Memory = VirtualMemory; + +thread_local! { + static MEMORY_MANAGER: MemoryManager + = MemoryManager::init(DefaultMemoryImpl::default()); +} + +pub fn get_upgrades_memory() -> Memory { + get_memory(UPGRADES) +} + +fn get_memory(id: MemoryId) -> Memory { + MEMORY_MANAGER.with(|m| m.get(id)) +} diff --git a/backend/canisters/exchange_bot/impl/src/model/mod.rs b/backend/canisters/exchange_bot/impl/src/model/mod.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/model/mod.rs @@ -0,0 +1 @@ + diff --git a/backend/canisters/exchange_bot/impl/src/queries/http_request.rs b/backend/canisters/exchange_bot/impl/src/queries/http_request.rs new file mode 100644 index 0000000000..fea6d8d402 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/queries/http_request.rs @@ -0,0 +1,26 @@ +use crate::{read_state, RuntimeState}; +use http_request::{build_json_response, encode_logs, extract_route, Route}; +use ic_cdk_macros::query; +use types::{HttpRequest, HttpResponse, TimestampMillis}; + +#[query] +fn http_request(request: HttpRequest) -> HttpResponse { + fn get_logs_impl(since: Option) -> HttpResponse { + encode_logs(canister_logger::export_logs(), since.unwrap_or(0)) + } + + fn get_traces_impl(since: Option) -> HttpResponse { + encode_logs(canister_logger::export_traces(), since.unwrap_or(0)) + } + + fn get_metrics_impl(state: &RuntimeState) -> HttpResponse { + build_json_response(&state.metrics()) + } + + match extract_route(&request.url) { + Route::Logs(since) => get_logs_impl(since), + Route::Traces(since) => get_traces_impl(since), + Route::Metrics => read_state(get_metrics_impl), + _ => HttpResponse::not_found(), + } +} diff --git a/backend/canisters/exchange_bot/impl/src/queries/mod.rs b/backend/canisters/exchange_bot/impl/src/queries/mod.rs new file mode 100644 index 0000000000..1cfa1ad736 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/queries/mod.rs @@ -0,0 +1 @@ +mod http_request; diff --git a/backend/canisters/exchange_bot/impl/src/updates/mod.rs b/backend/canisters/exchange_bot/impl/src/updates/mod.rs new file mode 100644 index 0000000000..f77132607f --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/updates/mod.rs @@ -0,0 +1 @@ +mod wallet_receive; diff --git a/backend/canisters/exchange_bot/impl/src/updates/wallet_receive.rs b/backend/canisters/exchange_bot/impl/src/updates/wallet_receive.rs new file mode 100644 index 0000000000..d08987204c --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/updates/wallet_receive.rs @@ -0,0 +1,9 @@ +use canister_tracing_macros::trace; +use ic_cdk_macros::update; +use utils::cycles::accept_cycles; + +#[update] +#[trace] +fn wallet_receive() { + accept_cycles(); +} diff --git a/backend/external_canisters/icpswap_swap_pool/api/Cargo.toml b/backend/external_canisters/icpswap_swap_pool/api/Cargo.toml new file mode 100644 index 0000000000..9147bcc2bd --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/api/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "icpswap_swap_pool_canister" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +candid = { workspace = true } +candid_gen = { path = "../../../libraries/candid_gen" } +serde = { workspace = true } +types = { path = "../../../libraries/types" } diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/lib.rs b/backend/external_canisters/icpswap_swap_pool/api/src/lib.rs new file mode 100644 index 0000000000..23ad7b46a5 --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/api/src/lib.rs @@ -0,0 +1,23 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; + +mod queries; +mod updates; + +pub use queries::*; +pub use updates::*; + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub enum ICPSwapResult { + Ok(T), + Err(ICPSwapError), +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub enum ICPSwapError { + CommonError, + InternalError(String), + UnsupportedToken(String), + InsufficientFunds, +} diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/queries/mod.rs b/backend/external_canisters/icpswap_swap_pool/api/src/queries/mod.rs new file mode 100644 index 0000000000..9e1a6d4369 --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/api/src/queries/mod.rs @@ -0,0 +1 @@ +pub mod quote; diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/queries/quote.rs b/backend/external_canisters/icpswap_swap_pool/api/src/queries/quote.rs new file mode 100644 index 0000000000..3d0a9f859d --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/api/src/queries/quote.rs @@ -0,0 +1 @@ +pub use crate::updates::swap::*; diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/updates/deposit.rs b/backend/external_canisters/icpswap_swap_pool/api/src/updates/deposit.rs new file mode 100644 index 0000000000..30213cb4be --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/api/src/updates/deposit.rs @@ -0,0 +1,11 @@ +use crate::ICPSwapResult; +use candid::{CandidType, Nat}; +use serde::Deserialize; + +#[derive(CandidType, Deserialize)] +pub struct Args { + pub token: String, + pub amount: Nat, +} + +pub type Response = ICPSwapResult; diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/updates/mod.rs b/backend/external_canisters/icpswap_swap_pool/api/src/updates/mod.rs new file mode 100644 index 0000000000..91650cd835 --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/api/src/updates/mod.rs @@ -0,0 +1,3 @@ +pub mod deposit; +pub mod swap; +pub mod withdraw; diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/updates/swap.rs b/backend/external_canisters/icpswap_swap_pool/api/src/updates/swap.rs new file mode 100644 index 0000000000..a9f182251b --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/api/src/updates/swap.rs @@ -0,0 +1,14 @@ +use crate::ICPSwapResult; +use candid::{CandidType, Nat, Principal}; +use serde::{Deserialize, Serialize}; + +#[derive(CandidType, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Args { + pub operator: Principal, + pub amount_in: String, + pub zero_for_one: bool, + pub amount_out_minimum: String, +} + +pub type Response = ICPSwapResult; diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/updates/withdraw.rs b/backend/external_canisters/icpswap_swap_pool/api/src/updates/withdraw.rs new file mode 100644 index 0000000000..b432acc049 --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/api/src/updates/withdraw.rs @@ -0,0 +1 @@ +pub use crate::updates::deposit::*; diff --git a/backend/external_canisters/icpswap_swap_pool/c2c_client/Cargo.toml b/backend/external_canisters/icpswap_swap_pool/c2c_client/Cargo.toml new file mode 100644 index 0000000000..e6d37c418f --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/c2c_client/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "icpswap_swap_pool_canister_c2c_client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +candid = { workspace = true } +canister_client = { path = "../../../libraries/canister_client" } +ic-cdk = { workspace = true } +icpswap_swap_pool_canister = { path = "../api" } +types = { path = "../../../libraries/types" } diff --git a/backend/external_canisters/icpswap_swap_pool/c2c_client/src/lib.rs b/backend/external_canisters/icpswap_swap_pool/c2c_client/src/lib.rs new file mode 100644 index 0000000000..137ae5b32d --- /dev/null +++ b/backend/external_canisters/icpswap_swap_pool/c2c_client/src/lib.rs @@ -0,0 +1,8 @@ +use canister_client::generate_candid_c2c_call; +use icpswap_swap_pool_canister::*; + +// Queries +generate_candid_c2c_call!(quote); + +// Updates +generate_candid_c2c_call!(swap); diff --git a/backend/integration_tests/local-bin/exchange_bot.wasm.gz b/backend/integration_tests/local-bin/exchange_bot.wasm.gz new file mode 120000 index 0000000000..a159383582 --- /dev/null +++ b/backend/integration_tests/local-bin/exchange_bot.wasm.gz @@ -0,0 +1 @@ +../../../wasms/exchange_bot.wasm.gz \ No newline at end of file diff --git a/backend/libraries/canister_agent_utils/src/lib.rs b/backend/libraries/canister_agent_utils/src/lib.rs index f741813a0e..81f12576ea 100644 --- a/backend/libraries/canister_agent_utils/src/lib.rs +++ b/backend/libraries/canister_agent_utils/src/lib.rs @@ -15,6 +15,7 @@ use types::{BuildVersion, CanisterId, CanisterWasm}; pub enum CanisterName { Community, CyclesDispenser, + ExchangeBot, Group, GroupIndex, LocalGroupIndex, @@ -38,6 +39,7 @@ impl FromStr for CanisterName { match s { "community" => Ok(CanisterName::Community), "cycles_dispenser" => Ok(CanisterName::CyclesDispenser), + "exchange_bot" => Ok(CanisterName::ExchangeBot), "group" => Ok(CanisterName::Group), "group_index" => Ok(CanisterName::GroupIndex), "local_group_index" => Ok(CanisterName::LocalGroupIndex), @@ -62,6 +64,7 @@ impl Display for CanisterName { let name = match self { CanisterName::Community => "community", CanisterName::CyclesDispenser => "cycles_dispenser", + CanisterName::ExchangeBot => "exchange_bot", CanisterName::Group => "group", CanisterName::GroupIndex => "group_index", CanisterName::LocalGroupIndex => "local_group_index", @@ -96,6 +99,7 @@ pub struct CanisterIds { pub cycles_dispenser: CanisterId, pub registry: CanisterId, pub market_maker: CanisterId, + pub exchange_bot: CanisterId, pub nns_root: CanisterId, pub nns_governance: CanisterId, pub nns_internet_identity: CanisterId, diff --git a/backend/libraries/icpswap_client/Cargo.toml b/backend/libraries/icpswap_client/Cargo.toml new file mode 100644 index 0000000000..65d99d1e5a --- /dev/null +++ b/backend/libraries/icpswap_client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "icpswap_client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +candid = { workspace = true } +ic-cdk = { workspace = true } +icpswap_swap_pool_canister = { path = "../../external_canisters/icpswap_swap_pool/api" } +icpswap_swap_pool_canister_c2c_client = { path = "../../external_canisters/icpswap_swap_pool/c2c_client" } +icrc1_ledger_canister_c2c_client = { path = "../../external_canisters/icrc1_ledger/c2c_client" } +ledger_utils = { path = "../ledger_utils" } +serde = { workspace = true } +types = { path = "../types" } diff --git a/backend/libraries/icpswap_client/src/lib.rs b/backend/libraries/icpswap_client/src/lib.rs new file mode 100644 index 0000000000..2264ecfc7b --- /dev/null +++ b/backend/libraries/icpswap_client/src/lib.rs @@ -0,0 +1,70 @@ +use ic_cdk::api::call::{CallResult, RejectionCode}; +use icpswap_swap_pool_canister::ICPSwapResult; +use ledger_utils::convert_to_subaccount; +use types::icrc1::{Account, TransferArg}; +use types::{CanisterId, TokenInfo}; + +pub struct ICPSwapClient { + pub this_canister_id: CanisterId, + pub swap_canister_id: CanisterId, + pub token0: TokenInfo, + pub token1: TokenInfo, +} + +impl ICPSwapClient { + pub fn new(this_canister_id: CanisterId, swap_canister_id: CanisterId, token0: TokenInfo, token1: TokenInfo) -> Self { + ICPSwapClient { + this_canister_id, + swap_canister_id, + token0, + token1, + } + } + + pub async fn quote(&self, amount: u128, zero_for_one: bool) -> CallResult { + let args = icpswap_swap_pool_canister::quote::Args { + operator: self.this_canister_id, + amount_in: amount.to_string(), + zero_for_one, + amount_out_minimum: "0".to_string(), + }; + + match icpswap_swap_pool_canister_c2c_client::quote(self.swap_canister_id, &args).await? { + ICPSwapResult::Ok(amount_out) => Ok(amount_out.0.try_into().unwrap()), + ICPSwapResult::Err(e) => Err((RejectionCode::CanisterError, format!("{e:?}"))), + } + } + + pub async fn swap(&self, amount: u128, zero_for_one: bool) -> CallResult { + let ledger_canister_id = if zero_for_one { self.token0.ledger } else { self.token1.ledger }; + + icrc1_ledger_canister_c2c_client::icrc1_transfer( + ledger_canister_id, + &TransferArg { + from_subaccount: None, + to: Account { + owner: self.swap_canister_id, + subaccount: Some(convert_to_subaccount(&self.this_canister_id).0), + }, + fee: None, + created_at_time: None, + memo: None, + amount: amount.into(), + }, + ) + .await? + .map_err(|t| (RejectionCode::Unknown, format!("{t:?}")))?; + + let swap_args = icpswap_swap_pool_canister::swap::Args { + operator: self.this_canister_id, + amount_in: amount.to_string(), + zero_for_one, + amount_out_minimum: "0".to_string(), + }; + + match icpswap_swap_pool_canister_c2c_client::swap(self.swap_canister_id, &swap_args).await? { + ICPSwapResult::Ok(amount_out) => Ok(amount_out.0.try_into().unwrap()), + ICPSwapResult::Err(e) => Err((RejectionCode::Unknown, format!("{e:?}"))), + } + } +} diff --git a/canister_ids.json b/canister_ids.json index 9df3c8fa43..5652d7b392 100644 --- a/canister_ids.json +++ b/canister_ids.json @@ -8,6 +8,9 @@ "ic": "gonut-hqaaa-aaaaf-aby7a-cai", "ic_test": "mq2tp-baaaa-aaaaf-aucva-cai" }, + "exchange_bot": { + "ic_test": "tz2or-siaaa-aaaaf-biffa-cai" + } "group_index": { "ic": "4ijyc-kiaaa-aaaaf-aaaja-cai", "ic_test": "7kifq-3yaaa-aaaaf-ab2cq-cai" diff --git a/dfx.json b/dfx.json index 20b21d6583..cd47c3572d 100644 --- a/dfx.json +++ b/dfx.json @@ -91,6 +91,12 @@ "wasm": "wasms/market_maker.wasm.gz", "build": "./scripts/generate-wasm.sh market_maker" }, + "exchange_bot": { + "type": "custom", + "candid": "backend/canisters/exchange_bot/api/can.did", + "wasm": "wasms/exchange_bot.wasm.gz", + "build": "./scripts/generate-wasm.sh exchange_bot" + }, "website": { "source": ["frontend/app/build", "frontend/app/public"], "type": "assets" diff --git a/scripts/deploy-local.sh b/scripts/deploy-local.sh index 61729a66af..466b77d999 100755 --- a/scripts/deploy-local.sh +++ b/scripts/deploy-local.sh @@ -31,6 +31,7 @@ dfx --identity $IDENTITY canister create --no-wallet --with-cycles 1000000000000 dfx --identity $IDENTITY canister create --no-wallet --with-cycles 1000000000000000 cycles_dispenser dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 registry dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 market_maker +dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 exchange_bot # Install the OpenChat canisters ./scripts/deploy.sh local \ diff --git a/scripts/deploy-testnet.sh b/scripts/deploy-testnet.sh index 650c3efcbe..64718a15ee 100755 --- a/scripts/deploy-testnet.sh +++ b/scripts/deploy-testnet.sh @@ -28,6 +28,7 @@ dfx --identity $IDENTITY canister create --provisional-create-canister-effective dfx --identity $IDENTITY canister create --provisional-create-canister-effective-canister-id jrlun-jiaaa-aaaab-aaaaa-cai --network $NETWORK --no-wallet --with-cycles 1000000000000000 cycles_dispenser dfx --identity $IDENTITY canister create --provisional-create-canister-effective-canister-id jrlun-jiaaa-aaaab-aaaaa-cai --network $NETWORK --no-wallet --with-cycles 100000000000000 registry dfx --identity $IDENTITY canister create --provisional-create-canister-effective-canister-id jrlun-jiaaa-aaaab-aaaaa-cai --network $NETWORK --no-wallet --with-cycles 100000000000000 market_maker +dfx --identity $IDENTITY canister create --provisional-create-canister-effective-canister-id jrlun-jiaaa-aaaab-aaaaa-cai --network $NETWORK --no-wallet --with-cycles 100000000000000 exchange_bot # Install the OpenChat canisters ./scripts/deploy.sh $NETWORK \ diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 34461ed045..d51d211a16 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -39,6 +39,7 @@ STORAGE_INDEX_CANISTER_ID=$(dfx canister --network $NETWORK id storage_index) CYCLES_DISPENSER_CANISTER_ID=$(dfx canister --network $NETWORK id cycles_dispenser) REGISTRY_CANISTER_ID=$(dfx canister --network $NETWORK id registry) MARKET_MAKER_CANISTER_ID=$(dfx canister --network $NETWORK id market_maker) +EXCHANGE_BOT_CANISTER_ID=$(dfx canister --network $NETWORK id exchange_bot) cargo run \ --manifest-path backend/canister_installer/Cargo.toml -- \ @@ -57,6 +58,7 @@ cargo run \ --cycles-dispenser $CYCLES_DISPENSER_CANISTER_ID \ --registry $REGISTRY_CANISTER_ID \ --market-maker $MARKET_MAKER_CANISTER_ID \ + --exchange-bot $EXCHANGE_BOT_CANISTER_ID \ --nns-root $NNS_ROOT_CANISTER_ID \ --nns-governance $NNS_GOVERNANCE_CANISTER_ID \ --nns-internet-identity $NNS_INTERNET_IDENTITY_CANISTER_ID \ diff --git a/scripts/download-all-canister-wasms.sh b/scripts/download-all-canister-wasms.sh index 7e7679dce0..b8568e4b2b 100755 --- a/scripts/download-all-canister-wasms.sh +++ b/scripts/download-all-canister-wasms.sh @@ -15,6 +15,7 @@ echo "Downloading wasms" ./download-canister-wasm.sh community $WASM_SRC || exit 1 ./download-canister-wasm.sh cycles_dispenser $WASM_SRC || exit 1 +./download-canister-wasm.sh exchange_bot $WASM_SRC || exit 1 ./download-canister-wasm.sh group $WASM_SRC || exit 1 ./download-canister-wasm.sh group_index $WASM_SRC || exit 1 ./download-canister-wasm.sh local_group_index $WASM_SRC || exit 1 diff --git a/scripts/generate-all-canister-wasms.sh b/scripts/generate-all-canister-wasms.sh index 885e125a8f..c6b0be5cde 100755 --- a/scripts/generate-all-canister-wasms.sh +++ b/scripts/generate-all-canister-wasms.sh @@ -6,6 +6,7 @@ cd $SCRIPT_DIR/.. ./scripts/generate-wasm.sh community ./scripts/generate-wasm.sh cycles_dispenser +./scripts/generate-wasm.sh exchange_bot ./scripts/generate-wasm.sh group ./scripts/generate-wasm.sh group_index ./scripts/generate-wasm.sh local_group_index diff --git a/scripts/upgrade-canister.sh b/scripts/upgrade-canister.sh index b3cf262b54..a2ec8df58d 100755 --- a/scripts/upgrade-canister.sh +++ b/scripts/upgrade-canister.sh @@ -31,6 +31,7 @@ STORAGE_INDEX_CANISTER_ID=$(dfx canister --network $NETWORK id storage_index) CYCLES_DISPENSER_CANISTER_ID=$(dfx canister --network $NETWORK id cycles_dispenser) REGISTRY_CANISTER_ID=$(dfx canister --network $NETWORK id registry) MARKET_MAKER_CANISTER_ID=$(dfx canister --network $NETWORK id market_maker) +EXCHANGE_BOT_CANISTER_ID=$(dfx canister --network $NETWORK id exchange_bot) cargo run \ --manifest-path backend/canister_upgrader/Cargo.toml -- \ @@ -45,5 +46,6 @@ cargo run \ --cycles-dispenser $CYCLES_DISPENSER_CANISTER_ID \ --registry $REGISTRY_CANISTER_ID \ --market-maker $MARKET_MAKER_CANISTER_ID \ + --exchange-bot $EXCHANGE_BOT_CANISTER_ID \ --canister-to-upgrade $CANISTER_NAME \ --version $VERSION \ \ No newline at end of file From e550295fd8561f719c35e18113d9a34cae744cdb Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 13 Sep 2023 17:10:18 +0100 Subject: [PATCH 02/39] Add prod canister Id --- canister_ids.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/canister_ids.json b/canister_ids.json index 5652d7b392..9a78f77b10 100644 --- a/canister_ids.json +++ b/canister_ids.json @@ -9,8 +9,9 @@ "ic_test": "mq2tp-baaaa-aaaaf-aucva-cai" }, "exchange_bot": { - "ic_test": "tz2or-siaaa-aaaaf-biffa-cai" - } + "ic_test": "tz2or-siaaa-aaaaf-biffa-cai", + "ic": "no74g-2qaaa-aaaaf-bihlq-cai" + }, "group_index": { "ic": "4ijyc-kiaaa-aaaaf-aaaja-cai", "ic_test": "7kifq-3yaaa-aaaaf-ab2cq-cai" From f1e92a34a4ac9d686784afed534e24fce9dad582 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 13 Sep 2023 22:18:55 +0100 Subject: [PATCH 03/39] More --- backend/canisters/exchange_bot/api/can.did | 47 ++++++------------- .../exchange_bot/api/src/updates/exchange.rs | 10 ++++ .../exchange_bot/api/src/updates/mod.rs | 2 +- .../exchange_bot/impl/src/jobs/mod.rs | 2 +- .../canisters/exchange_bot/impl/src/lib.rs | 20 +++++++- .../impl/src/lifecycle/inspect_message.rs | 1 + .../exchange_bot/impl/src/updates/exchange.rs | 16 +++++++ .../exchange_bot/impl/src/updates/mod.rs | 1 + .../icpswap_swap_pool/api/src/lib.rs | 2 + .../icpswap_swap_pool/api/src/updates/swap.rs | 3 ++ dfx.json | 2 +- 11 files changed, 69 insertions(+), 37 deletions(-) create mode 100644 backend/canisters/exchange_bot/api/src/updates/exchange.rs create mode 100644 backend/canisters/exchange_bot/impl/src/updates/exchange.rs diff --git a/backend/canisters/exchange_bot/api/can.did b/backend/canisters/exchange_bot/api/can.did index 1687b30ead..20073800ef 100644 --- a/backend/canisters/exchange_bot/api/can.did +++ b/backend/canisters/exchange_bot/api/can.did @@ -1,42 +1,23 @@ import "../../../libraries/types/can.did"; -type AddGovernanceCanisterArgs = record { - governance_canister_id : CanisterId; - community_id : opt CommunityId; - name : text; - description : opt text; - avatar : opt Document; +type ExchangeArgs = record { + amount : nat; + zero_for_one : bool; }; -type AddGovernanceCanisterResponse = variant { - Success; - AlreadyAdded; - InternalError : text; +type ExchangeResponse = variant { + Ok : nat; + Err : text; }; -type RemoveGovernanceCanisterArgs = record { - governance_canister_id : CanisterId; - delete_group : bool; +type InitArgs = record { + governance_principals : vec CanisterId; + local_user_index_canister_id : CanisterId; + cycles_dispenser_canister_id : CanisterId; + wasm_version : BuildVersion; + test_mode : bool; }; -type RemoveGovernanceCanisterResponse = variant { - Success; - NotFound; - InternalError : text; -}; - -type AppointAdminsArgs = record { - governance_canister_id : CanisterId; - users : vec UserId; -}; - -type AppointAdminsResponse = variant { - Success; - NotFound; -}; - -service : { - add_governance_canister : (AddGovernanceCanisterArgs) -> (AddGovernanceCanisterResponse); - remove_governance_canister : (RemoveGovernanceCanisterArgs) -> (RemoveGovernanceCanisterResponse); - appoint_admins : (AppointAdminsArgs) -> (AppointAdminsResponse); +service : (InitArgs) -> { + exchange : (ExchangeArgs) -> (ExchangeResponse); }; diff --git a/backend/canisters/exchange_bot/api/src/updates/exchange.rs b/backend/canisters/exchange_bot/api/src/updates/exchange.rs new file mode 100644 index 0000000000..b6edc6de72 --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/updates/exchange.rs @@ -0,0 +1,10 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub amount: u128, + pub zero_for_one: bool, +} + +pub type Response = Result; diff --git a/backend/canisters/exchange_bot/api/src/updates/mod.rs b/backend/canisters/exchange_bot/api/src/updates/mod.rs index 8b13789179..470f172eed 100644 --- a/backend/canisters/exchange_bot/api/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/api/src/updates/mod.rs @@ -1 +1 @@ - +pub mod exchange; diff --git a/backend/canisters/exchange_bot/impl/src/jobs/mod.rs b/backend/canisters/exchange_bot/impl/src/jobs/mod.rs index f24c5a7cd5..01f9150d4a 100644 --- a/backend/canisters/exchange_bot/impl/src/jobs/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/jobs/mod.rs @@ -1,3 +1,3 @@ use crate::RuntimeState; -pub(crate) fn start(state: &RuntimeState) {} +pub(crate) fn start(_state: &RuntimeState) {} diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index e2b83d2f99..d96a617528 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -1,9 +1,10 @@ use candid::Principal; use canister_state_macros::canister_state; +use icpswap_client::ICPSwapClient; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::HashSet; -use types::{BuildVersion, CanisterId, Cycles, TimestampMillis, Timestamped}; +use types::{BuildVersion, CanisterId, Cryptocurrency, Cycles, TimestampMillis, Timestamped, TokenInfo}; use utils::env::Environment; mod guards; @@ -30,6 +31,23 @@ impl RuntimeState { RuntimeState { env, data } } + pub fn get_icpswap_client(&self) -> ICPSwapClient { + ICPSwapClient::new( + self.env.canister_id(), + CanisterId::from_text("ne2vj-6yaaa-aaaag-qb3ia-cai").unwrap(), + TokenInfo { + token: Cryptocurrency::InternetComputer, + ledger: Cryptocurrency::InternetComputer.ledger_canister_id().unwrap(), + decimals: 8, + }, + TokenInfo { + token: Cryptocurrency::CHAT, + ledger: Cryptocurrency::InternetComputer.ledger_canister_id().unwrap(), + decimals: 8, + }, + ) + } + pub fn is_caller_governance_principal(&self) -> bool { let caller = self.env.caller(); self.data.governance_principals.contains(&caller) diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs index 3ac9fd8284..e865526d97 100644 --- a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs @@ -10,6 +10,7 @@ fn accept_if_valid(state: &RuntimeState) { let method_name = ic_cdk::api::call::method_name(); let is_valid = match method_name.as_str() { + "exchange" => state.is_caller_governance_principal(), _ => false, }; diff --git a/backend/canisters/exchange_bot/impl/src/updates/exchange.rs b/backend/canisters/exchange_bot/impl/src/updates/exchange.rs new file mode 100644 index 0000000000..9529bb9246 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/updates/exchange.rs @@ -0,0 +1,16 @@ +use crate::guards::caller_is_governance_principal; +use crate::read_state; +use canister_tracing_macros::trace; +use exchange_bot_canister::exchange::*; +use ic_cdk_macros::update; + +#[update(guard = "caller_is_governance_principal")] +#[trace] +async fn exchange(args: Args) -> Response { + let client = read_state(|state| state.get_icpswap_client()); + + client + .quote(args.amount, args.zero_for_one) + .await + .map_err(|e| format!("{e:?}")) +} diff --git a/backend/canisters/exchange_bot/impl/src/updates/mod.rs b/backend/canisters/exchange_bot/impl/src/updates/mod.rs index f77132607f..cf925117d3 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/mod.rs @@ -1 +1,2 @@ +mod exchange; mod wallet_receive; diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/lib.rs b/backend/external_canisters/icpswap_swap_pool/api/src/lib.rs index 23ad7b46a5..b40c713a20 100644 --- a/backend/external_canisters/icpswap_swap_pool/api/src/lib.rs +++ b/backend/external_canisters/icpswap_swap_pool/api/src/lib.rs @@ -10,7 +10,9 @@ pub use updates::*; #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub enum ICPSwapResult { + #[serde(rename = "ok")] Ok(T), + #[serde(rename = "err")] Err(ICPSwapError), } diff --git a/backend/external_canisters/icpswap_swap_pool/api/src/updates/swap.rs b/backend/external_canisters/icpswap_swap_pool/api/src/updates/swap.rs index a9f182251b..2cc26090c0 100644 --- a/backend/external_canisters/icpswap_swap_pool/api/src/updates/swap.rs +++ b/backend/external_canisters/icpswap_swap_pool/api/src/updates/swap.rs @@ -6,8 +6,11 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct Args { pub operator: Principal, + #[serde(rename = "amountIn")] pub amount_in: String, + #[serde(rename = "zeroForOne")] pub zero_for_one: bool, + #[serde(rename = "amountOutMinimum")] pub amount_out_minimum: String, } diff --git a/dfx.json b/dfx.json index cd47c3572d..f94b2cca76 100644 --- a/dfx.json +++ b/dfx.json @@ -39,7 +39,7 @@ }, "online_users": { "type": "custom", - "candid": "backend/canisters/online_users_agg/api/can.did", + "candid": "backend/canisters/online_users/api/can.did", "wasm": "wasms/online_users.wasm.gz", "build": "./scripts/generate-wasm.sh online_users" }, From 87e19d59b4d0a0bec1a6c8933efa835e02c52866 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 13 Sep 2023 23:56:08 +0100 Subject: [PATCH 04/39] More --- backend/canisters/exchange_bot/api/can.did | 1 + .../exchange_bot/api/src/updates/mod.rs | 1 + .../exchange_bot/api/src/updates/quote.rs | 2 ++ .../canisters/exchange_bot/impl/src/lib.rs | 2 +- .../impl/src/lifecycle/inspect_message.rs | 2 +- .../exchange_bot/impl/src/updates/exchange.rs | 2 +- .../exchange_bot/impl/src/updates/mod.rs | 1 + .../exchange_bot/impl/src/updates/quote.rs | 16 +++++++++ .../icpswap_swap_pool/c2c_client/src/lib.rs | 2 ++ backend/libraries/icpswap_client/src/lib.rs | 34 ++++++++++++++++--- 10 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 backend/canisters/exchange_bot/api/src/updates/quote.rs create mode 100644 backend/canisters/exchange_bot/impl/src/updates/quote.rs diff --git a/backend/canisters/exchange_bot/api/can.did b/backend/canisters/exchange_bot/api/can.did index 20073800ef..0869e82ccd 100644 --- a/backend/canisters/exchange_bot/api/can.did +++ b/backend/canisters/exchange_bot/api/can.did @@ -20,4 +20,5 @@ type InitArgs = record { service : (InitArgs) -> { exchange : (ExchangeArgs) -> (ExchangeResponse); + quote : (ExchangeArgs) -> (ExchangeResponse); }; diff --git a/backend/canisters/exchange_bot/api/src/updates/mod.rs b/backend/canisters/exchange_bot/api/src/updates/mod.rs index 470f172eed..d7f26b9a25 100644 --- a/backend/canisters/exchange_bot/api/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/api/src/updates/mod.rs @@ -1 +1,2 @@ pub mod exchange; +pub mod quote; diff --git a/backend/canisters/exchange_bot/api/src/updates/quote.rs b/backend/canisters/exchange_bot/api/src/updates/quote.rs new file mode 100644 index 0000000000..bcf20cdbbe --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/updates/quote.rs @@ -0,0 +1,2 @@ +pub type Args = crate::exchange::Args; +pub type Response = crate::exchange::Response; diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index d96a617528..e2ab7acd91 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -42,7 +42,7 @@ impl RuntimeState { }, TokenInfo { token: Cryptocurrency::CHAT, - ledger: Cryptocurrency::InternetComputer.ledger_canister_id().unwrap(), + ledger: Cryptocurrency::CHAT.ledger_canister_id().unwrap(), decimals: 8, }, ) diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs index e865526d97..7925f3e189 100644 --- a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs @@ -10,7 +10,7 @@ fn accept_if_valid(state: &RuntimeState) { let method_name = ic_cdk::api::call::method_name(); let is_valid = match method_name.as_str() { - "exchange" => state.is_caller_governance_principal(), + "exchange" | "quote" => state.is_caller_governance_principal(), _ => false, }; diff --git a/backend/canisters/exchange_bot/impl/src/updates/exchange.rs b/backend/canisters/exchange_bot/impl/src/updates/exchange.rs index 9529bb9246..3751e36b12 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/exchange.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/exchange.rs @@ -10,7 +10,7 @@ async fn exchange(args: Args) -> Response { let client = read_state(|state| state.get_icpswap_client()); client - .quote(args.amount, args.zero_for_one) + .swap(args.amount, args.zero_for_one) .await .map_err(|e| format!("{e:?}")) } diff --git a/backend/canisters/exchange_bot/impl/src/updates/mod.rs b/backend/canisters/exchange_bot/impl/src/updates/mod.rs index cf925117d3..03488f6e90 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/mod.rs @@ -1,2 +1,3 @@ mod exchange; +mod quote; mod wallet_receive; diff --git a/backend/canisters/exchange_bot/impl/src/updates/quote.rs b/backend/canisters/exchange_bot/impl/src/updates/quote.rs new file mode 100644 index 0000000000..8018b7b1f9 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/updates/quote.rs @@ -0,0 +1,16 @@ +use crate::guards::caller_is_governance_principal; +use crate::read_state; +use canister_tracing_macros::trace; +use exchange_bot_canister::quote::*; +use ic_cdk_macros::update; + +#[update(guard = "caller_is_governance_principal")] +#[trace] +async fn quote(args: Args) -> Response { + let client = read_state(|state| state.get_icpswap_client()); + + client + .quote(args.amount, args.zero_for_one) + .await + .map_err(|e| format!("{e:?}")) +} diff --git a/backend/external_canisters/icpswap_swap_pool/c2c_client/src/lib.rs b/backend/external_canisters/icpswap_swap_pool/c2c_client/src/lib.rs index 137ae5b32d..49eee71e29 100644 --- a/backend/external_canisters/icpswap_swap_pool/c2c_client/src/lib.rs +++ b/backend/external_canisters/icpswap_swap_pool/c2c_client/src/lib.rs @@ -5,4 +5,6 @@ use icpswap_swap_pool_canister::*; generate_candid_c2c_call!(quote); // Updates +generate_candid_c2c_call!(deposit); generate_candid_c2c_call!(swap); +generate_candid_c2c_call!(withdraw); diff --git a/backend/libraries/icpswap_client/src/lib.rs b/backend/libraries/icpswap_client/src/lib.rs index 2264ecfc7b..f52563f39e 100644 --- a/backend/libraries/icpswap_client/src/lib.rs +++ b/backend/libraries/icpswap_client/src/lib.rs @@ -1,5 +1,5 @@ use ic_cdk::api::call::{CallResult, RejectionCode}; -use icpswap_swap_pool_canister::ICPSwapResult; +use icpswap_swap_pool_canister::{ICPSwapError, ICPSwapResult}; use ledger_utils::convert_to_subaccount; use types::icrc1::{Account, TransferArg}; use types::{CanisterId, TokenInfo}; @@ -36,10 +36,11 @@ impl ICPSwapClient { } pub async fn swap(&self, amount: u128, zero_for_one: bool) -> CallResult { - let ledger_canister_id = if zero_for_one { self.token0.ledger } else { self.token1.ledger }; + let input_ledger = if zero_for_one { self.token0.ledger } else { self.token1.ledger }; + let output_ledger = if zero_for_one { self.token1.ledger } else { self.token0.ledger }; icrc1_ledger_canister_c2c_client::icrc1_transfer( - ledger_canister_id, + input_ledger, &TransferArg { from_subaccount: None, to: Account { @@ -55,6 +56,16 @@ impl ICPSwapClient { .await? .map_err(|t| (RejectionCode::Unknown, format!("{t:?}")))?; + let deposit_args = icpswap_swap_pool_canister::deposit::Args { + token: input_ledger.to_string(), + amount: amount.into(), + }; + if let ICPSwapResult::Err(error) = + icpswap_swap_pool_canister_c2c_client::deposit(self.swap_canister_id, &deposit_args).await? + { + return Err(convert_error(error)); + } + let swap_args = icpswap_swap_pool_canister::swap::Args { operator: self.this_canister_id, amount_in: amount.to_string(), @@ -62,9 +73,22 @@ impl ICPSwapClient { amount_out_minimum: "0".to_string(), }; - match icpswap_swap_pool_canister_c2c_client::swap(self.swap_canister_id, &swap_args).await? { + let amount_to_withdraw = match icpswap_swap_pool_canister_c2c_client::swap(self.swap_canister_id, &swap_args).await? { + ICPSwapResult::Ok(amount_out) => amount_out, + ICPSwapResult::Err(error) => return Err(convert_error(error)), + }; + + let withdraw_arg = icpswap_swap_pool_canister::withdraw::Args { + token: output_ledger.to_string(), + amount: amount_to_withdraw, + }; + match icpswap_swap_pool_canister_c2c_client::withdraw(self.swap_canister_id, &withdraw_arg).await? { ICPSwapResult::Ok(amount_out) => Ok(amount_out.0.try_into().unwrap()), - ICPSwapResult::Err(e) => Err((RejectionCode::Unknown, format!("{e:?}"))), + ICPSwapResult::Err(error) => Err(convert_error(error)), } } } + +fn convert_error(error: ICPSwapError) -> (RejectionCode, String) { + (RejectionCode::Unknown, format!("{error:?}")) +} From ca0021d65bc6b4ec9b30f2dd6cbdf3c16734181a Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Thu, 14 Sep 2023 11:50:59 +0100 Subject: [PATCH 05/39] More --- Cargo.lock | 2 + backend/canisters/exchange_bot/api/can.did | 26 +++-- backend/canisters/exchange_bot/api/src/lib.rs | 8 ++ .../canisters/exchange_bot/api/src/main.rs | 4 + .../exchange_bot/api/src/updates/exchange.rs | 10 -- .../exchange_bot/api/src/updates/mod.rs | 2 +- .../exchange_bot/api/src/updates/quote.rs | 31 +++++- .../exchange_bot/api/src/updates/swap.rs | 19 ++++ .../canisters/exchange_bot/impl/Cargo.toml | 2 + .../exchange_bot/impl/src/icpswap/mod.rs | 80 ++++++++++++++ .../canisters/exchange_bot/impl/src/lib.rs | 85 +++++++++++---- .../impl/src/lifecycle/inspect_message.rs | 6 +- .../exchange_bot/impl/src/swap_client.rs | 24 +++++ .../exchange_bot/impl/src/updates/exchange.rs | 16 --- .../exchange_bot/impl/src/updates/mod.rs | 2 +- .../exchange_bot/impl/src/updates/quote.rs | 28 ++++- .../exchange_bot/impl/src/updates/swap.rs | 18 ++++ .../canisters/market_maker/impl/src/lib.rs | 2 + backend/libraries/icpswap_client/src/lib.rs | 102 ++++++++++-------- backend/libraries/types/src/exchanges.rs | 1 + 20 files changed, 354 insertions(+), 114 deletions(-) delete mode 100644 backend/canisters/exchange_bot/api/src/updates/exchange.rs create mode 100644 backend/canisters/exchange_bot/api/src/updates/swap.rs create mode 100644 backend/canisters/exchange_bot/impl/src/icpswap/mod.rs create mode 100644 backend/canisters/exchange_bot/impl/src/swap_client.rs delete mode 100644 backend/canisters/exchange_bot/impl/src/updates/exchange.rs create mode 100644 backend/canisters/exchange_bot/impl/src/updates/swap.rs diff --git a/Cargo.lock b/Cargo.lock index 6a9157ae69..07f9b9c49f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1569,12 +1569,14 @@ dependencies = [ name = "exchange_bot_canister_impl" version = "0.1.0" dependencies = [ + "async-trait", "candid", "canister_api_macros", "canister_logger", "canister_state_macros", "canister_tracing_macros", "exchange_bot_canister", + "futures", "http_request", "human_readable", "ic-cdk", diff --git a/backend/canisters/exchange_bot/api/can.did b/backend/canisters/exchange_bot/api/can.did index 0869e82ccd..bd0fbcfd1f 100644 --- a/backend/canisters/exchange_bot/api/can.did +++ b/backend/canisters/exchange_bot/api/can.did @@ -1,13 +1,20 @@ import "../../../libraries/types/can.did"; -type ExchangeArgs = record { +type QuoteArgs = record { + input_token : CanisterId; + output_token : CanisterId; amount : nat; - zero_for_one : bool; }; -type ExchangeResponse = variant { - Ok : nat; - Err : text; +type QuoteResponse = record { + quotes : vec record { + exchange_id : ExchangeId; + amount_out : nat; + }; + failures : vec record { + exchange_id : ExchangeId; + error: text; + } }; type InitArgs = record { @@ -18,7 +25,10 @@ type InitArgs = record { test_mode : bool; }; -service : (InitArgs) -> { - exchange : (ExchangeArgs) -> (ExchangeResponse); - quote : (ExchangeArgs) -> (ExchangeResponse); +type ExchangeId = variant { + ICPSwap; +}; + +service : { + quote : (QuoteArgs) -> (QuoteResponse); }; diff --git a/backend/canisters/exchange_bot/api/src/lib.rs b/backend/canisters/exchange_bot/api/src/lib.rs index 9550e16025..a6edd7101c 100644 --- a/backend/canisters/exchange_bot/api/src/lib.rs +++ b/backend/canisters/exchange_bot/api/src/lib.rs @@ -1,5 +1,13 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; + mod lifecycle; mod updates; pub use lifecycle::*; pub use updates::*; + +#[derive(CandidType, Serialize, Deserialize, Clone, Copy, Debug)] +pub enum ExchangeId { + ICPSwap, +} diff --git a/backend/canisters/exchange_bot/api/src/main.rs b/backend/canisters/exchange_bot/api/src/main.rs index 37e8c25054..4699490d35 100644 --- a/backend/canisters/exchange_bot/api/src/main.rs +++ b/backend/canisters/exchange_bot/api/src/main.rs @@ -1,5 +1,9 @@ +use candid_gen::generate_candid_method; + #[allow(deprecated)] fn main() { + generate_candid_method!(exchange_bot, quote, update); + candid::export_service!(); std::print!("{}", __export_service()); } diff --git a/backend/canisters/exchange_bot/api/src/updates/exchange.rs b/backend/canisters/exchange_bot/api/src/updates/exchange.rs deleted file mode 100644 index b6edc6de72..0000000000 --- a/backend/canisters/exchange_bot/api/src/updates/exchange.rs +++ /dev/null @@ -1,10 +0,0 @@ -use candid::CandidType; -use serde::{Deserialize, Serialize}; - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct Args { - pub amount: u128, - pub zero_for_one: bool, -} - -pub type Response = Result; diff --git a/backend/canisters/exchange_bot/api/src/updates/mod.rs b/backend/canisters/exchange_bot/api/src/updates/mod.rs index d7f26b9a25..2bf6e3e43a 100644 --- a/backend/canisters/exchange_bot/api/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/api/src/updates/mod.rs @@ -1,2 +1,2 @@ -pub mod exchange; pub mod quote; +pub mod swap; diff --git a/backend/canisters/exchange_bot/api/src/updates/quote.rs b/backend/canisters/exchange_bot/api/src/updates/quote.rs index bcf20cdbbe..f17ec67bc7 100644 --- a/backend/canisters/exchange_bot/api/src/updates/quote.rs +++ b/backend/canisters/exchange_bot/api/src/updates/quote.rs @@ -1,2 +1,29 @@ -pub type Args = crate::exchange::Args; -pub type Response = crate::exchange::Response; +use crate::ExchangeId; +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use types::CanisterId; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub input_token: CanisterId, + pub output_token: CanisterId, + pub amount: u128, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Response { + pub quotes: Vec, + pub failures: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Quote { + pub exchange_id: ExchangeId, + pub amount_out: u128, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Failure { + pub exchange_id: ExchangeId, + pub error: String, +} diff --git a/backend/canisters/exchange_bot/api/src/updates/swap.rs b/backend/canisters/exchange_bot/api/src/updates/swap.rs new file mode 100644 index 0000000000..cee80a2ded --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/updates/swap.rs @@ -0,0 +1,19 @@ +use crate::ExchangeId; +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use types::CanisterId; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub exchange_id: ExchangeId, + pub input_token: CanisterId, + pub output_token: CanisterId, + pub amount: u128, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum Response { + Success(u128), + PairNotSupportedByExchange, + InternalError(String), +} diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml index e8ee63327f..4b6e42e148 100644 --- a/backend/canisters/exchange_bot/impl/Cargo.toml +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -10,12 +10,14 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] +async-trait = { workspace = true } candid = { workspace = true } canister_api_macros = { path = "../../../libraries/canister_api_macros" } canister_logger = { path = "../../../libraries/canister_logger" } canister_state_macros = { path = "../../../libraries/canister_state_macros" } canister_tracing_macros = { path = "../../../libraries/canister_tracing_macros" } exchange_bot_canister = { path = "../api" } +futures = { workspace = true } http_request = { path = "../../../libraries/http_request" } human_readable = { path = "../../../libraries/human_readable" } ic-cdk = { workspace = true } diff --git a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs new file mode 100644 index 0000000000..92843a72ed --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs @@ -0,0 +1,80 @@ +use crate::swap_client::{SwapClient, SwapClientFactory}; +use async_trait::async_trait; +use exchange_bot_canister::ExchangeId; +use ic_cdk::api::call::CallResult; +use icpswap_client::ICPSwapClient; +use types::icrc1::Account; +use types::{CanisterId, Cryptocurrency, TokenInfo}; + +pub struct ICPSwapClientFactory {} + +impl ICPSwapClientFactory { + pub fn new() -> ICPSwapClientFactory { + ICPSwapClientFactory {} + } + + fn lookup_swap_canister_id(&self, token0: &TokenInfo, token1: &TokenInfo) -> Option { + match (token0.token.clone(), token1.token.clone()) { + (Cryptocurrency::InternetComputer, Cryptocurrency::CHAT) => { + Some(CanisterId::from_text("ne2vj-6yaaa-aaaag-qb3ia-cai").unwrap()) + } + _ => None, + } + } +} + +impl SwapClientFactory for ICPSwapClientFactory { + fn build( + &self, + this_canister_id: CanisterId, + input_token: TokenInfo, + output_token: TokenInfo, + ) -> Option> { + if let Some(swap_canister_id) = self.lookup_swap_canister_id(&input_token, &output_token) { + Some(Box::new(ICPSwapClient::new( + this_canister_id, + swap_canister_id, + input_token.clone(), + output_token.clone(), + true, + ))) + } else if let Some(swap_canister_id) = self.lookup_swap_canister_id(&output_token, &input_token) { + Some(Box::new(ICPSwapClient::new( + this_canister_id, + swap_canister_id, + output_token.clone(), + input_token.clone(), + false, + ))) + } else { + None + } + } +} + +#[async_trait] +impl SwapClient for ICPSwapClient { + fn exchange_id(&self) -> ExchangeId { + ExchangeId::ICPSwap + } + + async fn quote(&self, amount: u128) -> CallResult { + self.quote(amount).await + } + + async fn deposit_account(&self) -> CallResult { + Ok(self.deposit_account()) + } + + async fn deposit(&self, amount: u128) -> CallResult { + self.deposit(amount).await + } + + async fn swap(&self, amount: u128) -> CallResult { + self.swap(amount).await + } + + async fn withdraw(&self, amount: u128) -> CallResult { + self.withdraw(amount).await + } +} diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index e2ab7acd91..b2969fa6f1 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -1,18 +1,22 @@ +use crate::icpswap::ICPSwapClientFactory; +use crate::swap_client::{SwapClient, SwapClientFactory}; use candid::Principal; use canister_state_macros::canister_state; -use icpswap_client::ICPSwapClient; +use exchange_bot_canister::ExchangeId; use serde::{Deserialize, Serialize}; use std::cell::RefCell; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use types::{BuildVersion, CanisterId, Cryptocurrency, Cycles, TimestampMillis, Timestamped, TokenInfo}; use utils::env::Environment; mod guards; +mod icpswap; mod jobs; mod lifecycle; mod memory; mod model; mod queries; +mod swap_client; mod updates; thread_local! { @@ -31,21 +35,34 @@ impl RuntimeState { RuntimeState { env, data } } - pub fn get_icpswap_client(&self) -> ICPSwapClient { - ICPSwapClient::new( - self.env.canister_id(), - CanisterId::from_text("ne2vj-6yaaa-aaaag-qb3ia-cai").unwrap(), - TokenInfo { - token: Cryptocurrency::InternetComputer, - ledger: Cryptocurrency::InternetComputer.ledger_canister_id().unwrap(), - decimals: 8, - }, - TokenInfo { - token: Cryptocurrency::CHAT, - ledger: Cryptocurrency::CHAT.ledger_canister_id().unwrap(), - decimals: 8, - }, - ) + pub fn get_all_swap_clients(&self, input_token: CanisterId, output_token: CanisterId) -> Vec> { + if let Some((input_token_info, output_token_info)) = self.get_token_info(input_token, output_token) { + let this_canister_id = self.env.canister_id(); + + vec![ICPSwapClientFactory::new().build(this_canister_id, input_token_info.clone(), output_token_info.clone())] + .into_iter() + .flatten() + .collect() + } else { + Vec::new() + } + } + + pub fn get_swap_client( + &self, + exchange_id: ExchangeId, + input_token: CanisterId, + output_token: CanisterId, + ) -> Option> { + let (input_token_info, output_token_info) = self.get_token_info(input_token, output_token)?; + + let this_canister_id = self.env.canister_id(); + + match exchange_id { + ExchangeId::ICPSwap => { + ICPSwapClientFactory::new().build(this_canister_id, input_token_info.clone(), output_token_info.clone()) + } + } } pub fn is_caller_governance_principal(&self) -> bool { @@ -67,14 +84,22 @@ impl RuntimeState { }, } } + + fn get_token_info(&self, input_token: CanisterId, output_token: CanisterId) -> Option<(TokenInfo, TokenInfo)> { + let input_token_info = self.data.token_info.get(&input_token)?; + let output_token_info = self.data.token_info.get(&output_token)?; + + Some((input_token_info.clone(), output_token_info.clone())) + } } #[derive(Serialize, Deserialize)] struct Data { - pub governance_principals: HashSet, - pub local_user_index_canister_id: CanisterId, - pub cycles_dispenser_canister_id: CanisterId, - pub test_mode: bool, + governance_principals: HashSet, + local_user_index_canister_id: CanisterId, + cycles_dispenser_canister_id: CanisterId, + token_info: HashMap, + test_mode: bool, } impl Data { @@ -88,11 +113,29 @@ impl Data { governance_principals, local_user_index_canister_id, cycles_dispenser_canister_id, + token_info: build_token_info().into_iter().map(|t| (t.ledger, t)).collect(), test_mode, } } } +fn build_token_info() -> Vec { + vec![ + TokenInfo { + token: Cryptocurrency::InternetComputer, + ledger: Cryptocurrency::InternetComputer.ledger_canister_id().unwrap(), + decimals: 8, + fee: 10_000, + }, + TokenInfo { + token: Cryptocurrency::CHAT, + ledger: Cryptocurrency::CHAT.ledger_canister_id().unwrap(), + decimals: 8, + fee: 100_000, + }, + ] +} + #[derive(Serialize, Debug)] pub struct Metrics { pub now: TimestampMillis, diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs index 7925f3e189..c3e4bc54ec 100644 --- a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs @@ -10,11 +10,9 @@ fn accept_if_valid(state: &RuntimeState) { let method_name = ic_cdk::api::call::method_name(); let is_valid = match method_name.as_str() { - "exchange" | "quote" => state.is_caller_governance_principal(), + "quote" | "swap" => state.is_caller_governance_principal(), _ => false, }; - if is_valid { - ic_cdk::api::call::accept_message(); - } + ic_cdk::api::call::accept_message(); } diff --git a/backend/canisters/exchange_bot/impl/src/swap_client.rs b/backend/canisters/exchange_bot/impl/src/swap_client.rs new file mode 100644 index 0000000000..e43de43409 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/swap_client.rs @@ -0,0 +1,24 @@ +use async_trait::async_trait; +use exchange_bot_canister::ExchangeId; +use ic_cdk::api::call::CallResult; +use types::icrc1::Account; +use types::{CanisterId, TokenInfo}; + +pub trait SwapClientFactory { + fn build( + &self, + this_canister_id: CanisterId, + input_token: TokenInfo, + output_token: TokenInfo, + ) -> Option>; +} + +#[async_trait] +pub trait SwapClient { + fn exchange_id(&self) -> ExchangeId; + async fn quote(&self, amount: u128) -> CallResult; + async fn deposit_account(&self) -> CallResult; + async fn deposit(&self, amount: u128) -> CallResult; + async fn swap(&self, amount: u128) -> CallResult; + async fn withdraw(&self, amount: u128) -> CallResult; +} diff --git a/backend/canisters/exchange_bot/impl/src/updates/exchange.rs b/backend/canisters/exchange_bot/impl/src/updates/exchange.rs deleted file mode 100644 index 3751e36b12..0000000000 --- a/backend/canisters/exchange_bot/impl/src/updates/exchange.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::guards::caller_is_governance_principal; -use crate::read_state; -use canister_tracing_macros::trace; -use exchange_bot_canister::exchange::*; -use ic_cdk_macros::update; - -#[update(guard = "caller_is_governance_principal")] -#[trace] -async fn exchange(args: Args) -> Response { - let client = read_state(|state| state.get_icpswap_client()); - - client - .swap(args.amount, args.zero_for_one) - .await - .map_err(|e| format!("{e:?}")) -} diff --git a/backend/canisters/exchange_bot/impl/src/updates/mod.rs b/backend/canisters/exchange_bot/impl/src/updates/mod.rs index 03488f6e90..61134495fd 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/mod.rs @@ -1,3 +1,3 @@ -mod exchange; mod quote; +mod swap; mod wallet_receive; diff --git a/backend/canisters/exchange_bot/impl/src/updates/quote.rs b/backend/canisters/exchange_bot/impl/src/updates/quote.rs index 8018b7b1f9..fc017f57e7 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/quote.rs @@ -1,16 +1,34 @@ use crate::guards::caller_is_governance_principal; use crate::read_state; +use crate::swap_client::SwapClient; use canister_tracing_macros::trace; use exchange_bot_canister::quote::*; +use exchange_bot_canister::ExchangeId; use ic_cdk_macros::update; #[update(guard = "caller_is_governance_principal")] #[trace] async fn quote(args: Args) -> Response { - let client = read_state(|state| state.get_icpswap_client()); + let clients = read_state(|state| state.get_all_swap_clients(args.input_token, args.output_token)); - client - .quote(args.amount, args.zero_for_one) - .await - .map_err(|e| format!("{e:?}")) + let futures: Vec<_> = clients.into_iter().map(|c| quote_single(c, args.amount)).collect(); + + let results = futures::future::join_all(futures).await; + + let mut quotes = Vec::new(); + let mut failures = Vec::new(); + for (exchange_id, result) in results { + match result { + Ok(amount_out) => quotes.push(Quote { exchange_id, amount_out }), + Err(error) => failures.push(Failure { exchange_id, error }), + } + } + + Response { quotes, failures } +} + +async fn quote_single(client: Box, amount: u128) -> (ExchangeId, Result) { + let result = client.quote(amount).await.map_err(|e| format!("{e:?}")); + + (client.exchange_id(), result) } diff --git a/backend/canisters/exchange_bot/impl/src/updates/swap.rs b/backend/canisters/exchange_bot/impl/src/updates/swap.rs new file mode 100644 index 0000000000..dd49bce7ef --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/updates/swap.rs @@ -0,0 +1,18 @@ +use crate::guards::caller_is_governance_principal; +use crate::read_state; +use canister_tracing_macros::trace; +use exchange_bot_canister::swap::{Response::*, *}; +use ic_cdk_macros::update; + +#[update(guard = "caller_is_governance_principal")] +#[trace] +async fn swap(args: Args) -> Response { + if let Some(client) = read_state(|state| state.get_swap_client(args.exchange_id, args.input_token, args.output_token)) { + match client.swap(args.amount).await { + Ok(amount_out) => Success(amount_out), + Err(error) => InternalError(format!("{error:?}")), + } + } else { + PairNotSupportedByExchange + } +} diff --git a/backend/canisters/market_maker/impl/src/lib.rs b/backend/canisters/market_maker/impl/src/lib.rs index 8613e9af39..c348301277 100644 --- a/backend/canisters/market_maker/impl/src/lib.rs +++ b/backend/canisters/market_maker/impl/src/lib.rs @@ -45,11 +45,13 @@ impl RuntimeState { token: Cryptocurrency::InternetComputer, ledger: self.data.icp_ledger_canister_id, decimals: 8, + fee: 10_000, }, TokenInfo { token: Cryptocurrency::CHAT, ledger: self.data.chat_ledger_canister_id, decimals: 8, + fee: 100_000, }, 10_000_000, |order| on_order_made(ICDEX_EXCHANGE_ID, order), diff --git a/backend/libraries/icpswap_client/src/lib.rs b/backend/libraries/icpswap_client/src/lib.rs index f52563f39e..da25e5c0ee 100644 --- a/backend/libraries/icpswap_client/src/lib.rs +++ b/backend/libraries/icpswap_client/src/lib.rs @@ -1,31 +1,47 @@ +use candid::Nat; use ic_cdk::api::call::{CallResult, RejectionCode}; use icpswap_swap_pool_canister::{ICPSwapError, ICPSwapResult}; use ledger_utils::convert_to_subaccount; -use types::icrc1::{Account, TransferArg}; +use types::icrc1::Account; use types::{CanisterId, TokenInfo}; pub struct ICPSwapClient { - pub this_canister_id: CanisterId, - pub swap_canister_id: CanisterId, - pub token0: TokenInfo, - pub token1: TokenInfo, + this_canister_id: CanisterId, + swap_canister_id: CanisterId, + token0: TokenInfo, + token1: TokenInfo, + zero_for_one: bool, } impl ICPSwapClient { - pub fn new(this_canister_id: CanisterId, swap_canister_id: CanisterId, token0: TokenInfo, token1: TokenInfo) -> Self { + pub fn new( + this_canister_id: CanisterId, + swap_canister_id: CanisterId, + token0: TokenInfo, + token1: TokenInfo, + zero_for_one: bool, + ) -> Self { ICPSwapClient { this_canister_id, swap_canister_id, token0, token1, + zero_for_one, + } + } + + pub fn deposit_account(&self) -> Account { + Account { + owner: self.swap_canister_id, + subaccount: Some(convert_to_subaccount(&self.this_canister_id).0), } } - pub async fn quote(&self, amount: u128, zero_for_one: bool) -> CallResult { + pub async fn quote(&self, amount: u128) -> CallResult { let args = icpswap_swap_pool_canister::quote::Args { operator: self.this_canister_id, amount_in: amount.to_string(), - zero_for_one, + zero_for_one: self.zero_for_one, amount_out_minimum: "0".to_string(), }; @@ -35,58 +51,52 @@ impl ICPSwapClient { } } - pub async fn swap(&self, amount: u128, zero_for_one: bool) -> CallResult { - let input_ledger = if zero_for_one { self.token0.ledger } else { self.token1.ledger }; - let output_ledger = if zero_for_one { self.token1.ledger } else { self.token0.ledger }; - - icrc1_ledger_canister_c2c_client::icrc1_transfer( - input_ledger, - &TransferArg { - from_subaccount: None, - to: Account { - owner: self.swap_canister_id, - subaccount: Some(convert_to_subaccount(&self.this_canister_id).0), - }, - fee: None, - created_at_time: None, - memo: None, - amount: amount.into(), - }, - ) - .await? - .map_err(|t| (RejectionCode::Unknown, format!("{t:?}")))?; - - let deposit_args = icpswap_swap_pool_canister::deposit::Args { - token: input_ledger.to_string(), + pub async fn deposit(&self, amount: u128) -> CallResult { + let args = icpswap_swap_pool_canister::deposit::Args { + token: self.get_ledger(self.zero_for_one).to_string(), amount: amount.into(), }; - if let ICPSwapResult::Err(error) = - icpswap_swap_pool_canister_c2c_client::deposit(self.swap_canister_id, &deposit_args).await? - { - return Err(convert_error(error)); + match icpswap_swap_pool_canister_c2c_client::deposit(self.swap_canister_id, &args).await? { + ICPSwapResult::Ok(amount_deposited) => Ok(nat_to_u128(amount_deposited)), + ICPSwapResult::Err(error) => Err(convert_error(error)), } + } - let swap_args = icpswap_swap_pool_canister::swap::Args { + pub async fn swap(&self, amount: u128) -> CallResult { + let args = icpswap_swap_pool_canister::swap::Args { operator: self.this_canister_id, amount_in: amount.to_string(), - zero_for_one, + zero_for_one: self.zero_for_one, amount_out_minimum: "0".to_string(), }; - - let amount_to_withdraw = match icpswap_swap_pool_canister_c2c_client::swap(self.swap_canister_id, &swap_args).await? { - ICPSwapResult::Ok(amount_out) => amount_out, + match icpswap_swap_pool_canister_c2c_client::swap(self.swap_canister_id, &args).await? { + ICPSwapResult::Ok(amount_out) => Ok(nat_to_u128(amount_out)), ICPSwapResult::Err(error) => return Err(convert_error(error)), - }; + } + } - let withdraw_arg = icpswap_swap_pool_canister::withdraw::Args { - token: output_ledger.to_string(), - amount: amount_to_withdraw, + pub async fn withdraw(&self, amount: u128) -> CallResult { + let args = icpswap_swap_pool_canister::withdraw::Args { + token: self.get_ledger(!self.zero_for_one).to_string(), + amount: amount.into(), }; - match icpswap_swap_pool_canister_c2c_client::withdraw(self.swap_canister_id, &withdraw_arg).await? { - ICPSwapResult::Ok(amount_out) => Ok(amount_out.0.try_into().unwrap()), + match icpswap_swap_pool_canister_c2c_client::withdraw(self.swap_canister_id, &args).await? { + ICPSwapResult::Ok(amount_out) => Ok(nat_to_u128(amount_out)), ICPSwapResult::Err(error) => Err(convert_error(error)), } } + + fn get_ledger(&self, token0: bool) -> CanisterId { + if token0 { + self.token0.ledger + } else { + self.token1.ledger + } + } +} + +fn nat_to_u128(value: Nat) -> u128 { + value.0.try_into().unwrap() } fn convert_error(error: ICPSwapError) -> (RejectionCode, String) { diff --git a/backend/libraries/types/src/exchanges.rs b/backend/libraries/types/src/exchanges.rs index 0329d811d6..36ae0eb6fa 100644 --- a/backend/libraries/types/src/exchanges.rs +++ b/backend/libraries/types/src/exchanges.rs @@ -21,6 +21,7 @@ pub struct TokenInfo { pub token: Cryptocurrency, pub ledger: CanisterId, pub decimals: u8, + pub fee: u128, } #[derive(CandidType, Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq)] From 4e16b8e4badf70384c42282abd60e9320f89d217 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Thu, 14 Sep 2023 13:26:20 +0100 Subject: [PATCH 06/39] More --- Cargo.lock | 2 +- backend/canisters/exchange_bot/api/can.did | 41 ++++++++-- .../canisters/exchange_bot/api/src/main.rs | 1 + .../exchange_bot/api/src/updates/quote.rs | 11 ++- .../exchange_bot/api/src/updates/swap.rs | 1 + .../canisters/exchange_bot/impl/Cargo.toml | 1 + .../exchange_bot/impl/src/icpswap/mod.rs | 2 +- .../canisters/exchange_bot/impl/src/lib.rs | 48 ++++++------ .../impl/src/lifecycle/inspect_message.rs | 4 +- .../exchange_bot/impl/src/swap_client.rs | 2 +- .../exchange_bot/impl/src/updates/quote.rs | 35 ++++++++- .../exchange_bot/impl/src/updates/swap.rs | 74 +++++++++++++++++-- backend/libraries/icpswap_client/Cargo.toml | 1 - backend/libraries/icpswap_client/src/lib.rs | 13 ++-- 14 files changed, 181 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07f9b9c49f..207463326a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,6 +1584,7 @@ dependencies = [ "ic-cdk-timers", "ic-stable-structures", "icpswap_client", + "icrc1_ledger_canister_c2c_client", "msgpack", "serde", "serializer", @@ -2573,7 +2574,6 @@ dependencies = [ "ic-cdk", "icpswap_swap_pool_canister", "icpswap_swap_pool_canister_c2c_client", - "icrc1_ledger_canister_c2c_client", "ledger_utils", "serde", "types", diff --git a/backend/canisters/exchange_bot/api/can.did b/backend/canisters/exchange_bot/api/can.did index bd0fbcfd1f..5f4c9995d2 100644 --- a/backend/canisters/exchange_bot/api/can.did +++ b/backend/canisters/exchange_bot/api/can.did @@ -6,15 +6,39 @@ type QuoteArgs = record { amount : nat; }; -type QuoteResponse = record { - quotes : vec record { - exchange_id : ExchangeId; - amount_out : nat; +type QuoteResponse = variant { + Success : vec Quote; + PartialSuccess : record { + quotes : vec Quote; + failures : vec ExchangeError; }; - failures : vec record { - exchange_id : ExchangeId; - error: text; - } + Failed : vec ExchangeError; + UnsupportedTokens : vec CanisterId; + PairNotSupported; +}; + +type Quote = record { + exchange_id : ExchangeId; + amount_out : nat; +}; + +type ExchangeError = record { + exchange_id : ExchangeId; + error : text; +}; + +type SwapArgs = record { + exchange_id : ExchangeId; + input_token : CanisterId; + output_token : CanisterId; + amount : nat; +}; + +type SwapResponse = variant { + Success : nat; + UnsupportedTokens : vec CanisterId; + PairNotSupportedByExchange; + InternalError : text; }; type InitArgs = record { @@ -31,4 +55,5 @@ type ExchangeId = variant { service : { quote : (QuoteArgs) -> (QuoteResponse); + swap : (SwapArgs) -> (SwapResponse); }; diff --git a/backend/canisters/exchange_bot/api/src/main.rs b/backend/canisters/exchange_bot/api/src/main.rs index 4699490d35..dff4d3807d 100644 --- a/backend/canisters/exchange_bot/api/src/main.rs +++ b/backend/canisters/exchange_bot/api/src/main.rs @@ -3,6 +3,7 @@ use candid_gen::generate_candid_method; #[allow(deprecated)] fn main() { generate_candid_method!(exchange_bot, quote, update); + generate_candid_method!(exchange_bot, swap, update); candid::export_service!(); std::print!("{}", __export_service()); diff --git a/backend/canisters/exchange_bot/api/src/updates/quote.rs b/backend/canisters/exchange_bot/api/src/updates/quote.rs index f17ec67bc7..03a6e9cc24 100644 --- a/backend/canisters/exchange_bot/api/src/updates/quote.rs +++ b/backend/canisters/exchange_bot/api/src/updates/quote.rs @@ -11,7 +11,16 @@ pub struct Args { } #[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct Response { +pub enum Response { + Success(Vec), + PartialSuccess(PartialSuccessResult), + Failed(Vec), + UnsupportedTokens(Vec), + PairNotSupported, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct PartialSuccessResult { pub quotes: Vec, pub failures: Vec, } diff --git a/backend/canisters/exchange_bot/api/src/updates/swap.rs b/backend/canisters/exchange_bot/api/src/updates/swap.rs index cee80a2ded..5b8082b556 100644 --- a/backend/canisters/exchange_bot/api/src/updates/swap.rs +++ b/backend/canisters/exchange_bot/api/src/updates/swap.rs @@ -14,6 +14,7 @@ pub struct Args { #[derive(CandidType, Serialize, Deserialize, Debug)] pub enum Response { Success(u128), + UnsupportedTokens(Vec), PairNotSupportedByExchange, InternalError(String), } diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml index 4b6e42e148..d5b75d8966 100644 --- a/backend/canisters/exchange_bot/impl/Cargo.toml +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -25,6 +25,7 @@ ic-cdk-macros = { workspace = true } ic-cdk-timers = { workspace = true } ic-stable-structures = { workspace = true } icpswap_client = { path = "../../../libraries/icpswap_client" } +icrc1_ledger_canister_c2c_client = { path = "../../../external_canisters/icrc1_ledger/c2c_client" } msgpack = { path = "../../../libraries/msgpack" } serde = { workspace = true } serializer = { path = "../../../libraries/serializer" } diff --git a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs index 92843a72ed..54a573bfaa 100644 --- a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs @@ -62,7 +62,7 @@ impl SwapClient for ICPSwapClient { self.quote(amount).await } - async fn deposit_account(&self) -> CallResult { + async fn deposit_account(&self) -> CallResult<(CanisterId, Account)> { Ok(self.deposit_account()) } diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index b2969fa6f1..e45d93baa3 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -35,33 +35,25 @@ impl RuntimeState { RuntimeState { env, data } } - pub fn get_all_swap_clients(&self, input_token: CanisterId, output_token: CanisterId) -> Vec> { - if let Some((input_token_info, output_token_info)) = self.get_token_info(input_token, output_token) { - let this_canister_id = self.env.canister_id(); - - vec![ICPSwapClientFactory::new().build(this_canister_id, input_token_info.clone(), output_token_info.clone())] - .into_iter() - .flatten() - .collect() - } else { - Vec::new() - } + pub fn get_all_swap_clients(&self, input_token: TokenInfo, output_token: TokenInfo) -> Vec> { + let this_canister_id = self.env.canister_id(); + + vec![ICPSwapClientFactory::new().build(this_canister_id, input_token.clone(), output_token.clone())] + .into_iter() + .flatten() + .collect() } pub fn get_swap_client( &self, exchange_id: ExchangeId, - input_token: CanisterId, - output_token: CanisterId, + input_token: TokenInfo, + output_token: TokenInfo, ) -> Option> { - let (input_token_info, output_token_info) = self.get_token_info(input_token, output_token)?; - let this_canister_id = self.env.canister_id(); match exchange_id { - ExchangeId::ICPSwap => { - ICPSwapClientFactory::new().build(this_canister_id, input_token_info.clone(), output_token_info.clone()) - } + ExchangeId::ICPSwap => ICPSwapClientFactory::new().build(this_canister_id, input_token, output_token), } } @@ -84,13 +76,6 @@ impl RuntimeState { }, } } - - fn get_token_info(&self, input_token: CanisterId, output_token: CanisterId) -> Option<(TokenInfo, TokenInfo)> { - let input_token_info = self.data.token_info.get(&input_token)?; - let output_token_info = self.data.token_info.get(&output_token)?; - - Some((input_token_info.clone(), output_token_info.clone())) - } } #[derive(Serialize, Deserialize)] @@ -117,6 +102,19 @@ impl Data { test_mode, } } + + pub fn get_token_info( + &self, + input_token: CanisterId, + output_token: CanisterId, + ) -> Result<(TokenInfo, TokenInfo), Vec> { + match (self.token_info.get(&input_token), self.token_info.get(&output_token)) { + (Some(i), Some(o)) => Ok((i.clone(), o.clone())), + (None, Some(_)) => Err(vec![input_token]), + (Some(_), None) => Err(vec![output_token]), + (None, None) => Err(vec![input_token, output_token]), + } + } } fn build_token_info() -> Vec { diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs index c3e4bc54ec..0cc4d098b5 100644 --- a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs @@ -14,5 +14,7 @@ fn accept_if_valid(state: &RuntimeState) { _ => false, }; - ic_cdk::api::call::accept_message(); + if is_valid { + ic_cdk::api::call::accept_message(); + } } diff --git a/backend/canisters/exchange_bot/impl/src/swap_client.rs b/backend/canisters/exchange_bot/impl/src/swap_client.rs index e43de43409..edef47a7db 100644 --- a/backend/canisters/exchange_bot/impl/src/swap_client.rs +++ b/backend/canisters/exchange_bot/impl/src/swap_client.rs @@ -17,7 +17,7 @@ pub trait SwapClientFactory { pub trait SwapClient { fn exchange_id(&self) -> ExchangeId; async fn quote(&self, amount: u128) -> CallResult; - async fn deposit_account(&self) -> CallResult; + async fn deposit_account(&self) -> CallResult<(CanisterId, Account)>; async fn deposit(&self, amount: u128) -> CallResult; async fn swap(&self, amount: u128) -> CallResult; async fn withdraw(&self, amount: u128) -> CallResult; diff --git a/backend/canisters/exchange_bot/impl/src/updates/quote.rs b/backend/canisters/exchange_bot/impl/src/updates/quote.rs index fc017f57e7..fc4213e4c8 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/quote.rs @@ -1,15 +1,18 @@ use crate::guards::caller_is_governance_principal; -use crate::read_state; use crate::swap_client::SwapClient; +use crate::{read_state, RuntimeState}; use canister_tracing_macros::trace; -use exchange_bot_canister::quote::*; +use exchange_bot_canister::quote::{Response::*, *}; use exchange_bot_canister::ExchangeId; use ic_cdk_macros::update; #[update(guard = "caller_is_governance_principal")] #[trace] async fn quote(args: Args) -> Response { - let clients = read_state(|state| state.get_all_swap_clients(args.input_token, args.output_token)); + let PrepareResult { clients } = match read_state(|state| prepare(&args, state)) { + Ok(ok) => ok, + Err(response) => return response, + }; let futures: Vec<_> = clients.into_iter().map(|c| quote_single(c, args.amount)).collect(); @@ -24,7 +27,31 @@ async fn quote(args: Args) -> Response { } } - Response { quotes, failures } + if failures.is_empty() { + Success(quotes) + } else if quotes.is_empty() { + Failed(failures) + } else { + PartialSuccess(PartialSuccessResult { quotes, failures }) + } +} + +struct PrepareResult { + clients: Vec>, +} + +fn prepare(args: &Args, state: &RuntimeState) -> Result { + match state.data.get_token_info(args.input_token, args.output_token) { + Ok((input_token, output_token)) => { + let clients = state.get_all_swap_clients(input_token, output_token); + if !clients.is_empty() { + Ok(PrepareResult { clients }) + } else { + Err(PairNotSupported) + } + } + Err(tokens) => Err(UnsupportedTokens(tokens)), + } } async fn quote_single(client: Box, amount: u128) -> (ExchangeId, Result) { diff --git a/backend/canisters/exchange_bot/impl/src/updates/swap.rs b/backend/canisters/exchange_bot/impl/src/updates/swap.rs index dd49bce7ef..e450d71c4e 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/swap.rs @@ -1,18 +1,78 @@ use crate::guards::caller_is_governance_principal; -use crate::read_state; +use crate::swap_client::SwapClient; +use crate::{read_state, RuntimeState}; use canister_tracing_macros::trace; use exchange_bot_canister::swap::{Response::*, *}; +use ic_cdk::api::call::{CallResult, RejectionCode}; use ic_cdk_macros::update; +use types::{icrc1, TokenInfo}; #[update(guard = "caller_is_governance_principal")] #[trace] async fn swap(args: Args) -> Response { - if let Some(client) = read_state(|state| state.get_swap_client(args.exchange_id, args.input_token, args.output_token)) { - match client.swap(args.amount).await { - Ok(amount_out) => Success(amount_out), - Err(error) => InternalError(format!("{error:?}")), + let PrepareResult { + client, + input_token, + output_token, + } = match read_state(|state| prepare(&args, state)) { + Ok(ok) => ok, + Err(response) => return response, + }; + + match swap_impl(client, args.amount, input_token, output_token).await { + Ok(amount_out) => Success(amount_out), + Err(error) => InternalError(format!("{error:?}")), + } +} + +struct PrepareResult { + client: Box, + input_token: TokenInfo, + output_token: TokenInfo, +} + +fn prepare(args: &Args, state: &RuntimeState) -> Result { + match state.data.get_token_info(args.input_token, args.output_token) { + Ok((input_token, output_token)) => { + if let Some(client) = state.get_swap_client(args.exchange_id, input_token.clone(), output_token.clone()) { + Ok(PrepareResult { + client, + input_token, + output_token, + }) + } else { + Err(PairNotSupportedByExchange) + } } - } else { - PairNotSupportedByExchange + Err(tokens) => Err(UnsupportedTokens(tokens)), + } +} + +async fn swap_impl( + client: Box, + amount: u128, + input_token: TokenInfo, + output_token: TokenInfo, +) -> CallResult { + let (ledger_canister_id, deposit_account) = client.deposit_account().await?; + + let transfer_args = icrc1::TransferArg { + from_subaccount: None, + to: deposit_account, + fee: Some(input_token.fee.into()), + created_at_time: None, + memo: None, + amount: amount.into(), + }; + if let Err(error) = icrc1_ledger_canister_c2c_client::icrc1_transfer(ledger_canister_id, &transfer_args).await? { + return Err((RejectionCode::Unknown, format!("{error:?}"))); } + + let amount_deposited = client.deposit(amount.saturating_sub(input_token.fee)).await?; + + let amount_out = client.swap(amount_deposited).await?; + + let amount_withdrawn = client.withdraw(amount_out.saturating_sub(output_token.fee)).await?; + + Ok(amount_withdrawn) } diff --git a/backend/libraries/icpswap_client/Cargo.toml b/backend/libraries/icpswap_client/Cargo.toml index 65d99d1e5a..7055927624 100644 --- a/backend/libraries/icpswap_client/Cargo.toml +++ b/backend/libraries/icpswap_client/Cargo.toml @@ -10,7 +10,6 @@ candid = { workspace = true } ic-cdk = { workspace = true } icpswap_swap_pool_canister = { path = "../../external_canisters/icpswap_swap_pool/api" } icpswap_swap_pool_canister_c2c_client = { path = "../../external_canisters/icpswap_swap_pool/c2c_client" } -icrc1_ledger_canister_c2c_client = { path = "../../external_canisters/icrc1_ledger/c2c_client" } ledger_utils = { path = "../ledger_utils" } serde = { workspace = true } types = { path = "../types" } diff --git a/backend/libraries/icpswap_client/src/lib.rs b/backend/libraries/icpswap_client/src/lib.rs index da25e5c0ee..409bc4a968 100644 --- a/backend/libraries/icpswap_client/src/lib.rs +++ b/backend/libraries/icpswap_client/src/lib.rs @@ -30,11 +30,14 @@ impl ICPSwapClient { } } - pub fn deposit_account(&self) -> Account { - Account { - owner: self.swap_canister_id, - subaccount: Some(convert_to_subaccount(&self.this_canister_id).0), - } + pub fn deposit_account(&self) -> (CanisterId, Account) { + ( + self.get_ledger(self.zero_for_one), + Account { + owner: self.swap_canister_id, + subaccount: Some(convert_to_subaccount(&self.this_canister_id).0), + }, + ) } pub async fn quote(&self, amount: u128) -> CallResult { From 4e7ad3d8afcdaaf3dbcf09336fb0a00eb16924d2 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Thu, 14 Sep 2023 13:39:46 +0100 Subject: [PATCH 07/39] clippy --- backend/libraries/icpswap_client/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/libraries/icpswap_client/src/lib.rs b/backend/libraries/icpswap_client/src/lib.rs index 409bc4a968..b8f862344b 100644 --- a/backend/libraries/icpswap_client/src/lib.rs +++ b/backend/libraries/icpswap_client/src/lib.rs @@ -74,7 +74,7 @@ impl ICPSwapClient { }; match icpswap_swap_pool_canister_c2c_client::swap(self.swap_canister_id, &args).await? { ICPSwapResult::Ok(amount_out) => Ok(nat_to_u128(amount_out)), - ICPSwapResult::Err(error) => return Err(convert_error(error)), + ICPSwapResult::Err(error) => Err(convert_error(error)), } } From 46df9b7a38e2d58c0b552ac974475160ae76d732 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 15 Sep 2023 09:34:07 +0100 Subject: [PATCH 08/39] More --- Cargo.lock | 13 +- .../api/src/updates/handle_direct_message.rs | 2 + .../impl/src/jobs/send_prizes.rs | 2 +- .../impl/src/updates/handle_direct_message.rs | 6 +- .../impl/src/lifecycle/heartbeat.rs | 7 +- .../impl/src/updates/handle_direct_message.rs | 6 +- .../impl/src/jobs/process_pending_actions.rs | 13 +- .../impl/src/model/pending_actions_queue.rs | 4 +- .../impl/src/updates/c2c_add_user.rs | 10 +- .../impl/src/updates/handle_direct_message.rs | 6 +- .../impl/src/updates/handle_direct_message.rs | 6 +- backend/canister_installer/src/lib.rs | 1 + backend/canisters/exchange_bot/api/Cargo.toml | 4 +- backend/canisters/exchange_bot/api/can.did | 48 +------ backend/canisters/exchange_bot/api/src/lib.rs | 9 ++ .../exchange_bot/api/src/lifecycle/init.rs | 1 + .../canisters/exchange_bot/api/src/main.rs | 5 - .../api/src/updates/handle_direct_message.rs | 1 + .../exchange_bot/api/src/updates/mod.rs | 3 +- .../exchange_bot/api/src/updates/quote.rs | 38 ------ .../api/src/updates/register_bot.rs | 1 + .../exchange_bot/api/src/updates/swap.rs | 7 +- .../canisters/exchange_bot/impl/Cargo.toml | 6 + .../impl/src/commands/common_errors.rs | 27 ++++ .../exchange_bot/impl/src/commands/mod.rs | 14 ++ .../exchange_bot/impl/src/commands/quote.rs | 123 ++++++++++++++++++ .../canisters/exchange_bot/impl/src/lib.rs | 76 +++++++++-- .../exchange_bot/impl/src/lifecycle/init.rs | 1 + .../impl/src/lifecycle/inspect_message.rs | 2 +- .../canisters/exchange_bot/impl/src/quote.rs | 63 +++++++++ .../impl/src/updates/handle_direct_message.rs | 60 +++++++++ .../exchange_bot/impl/src/updates/mod.rs | 3 +- .../exchange_bot/impl/src/updates/quote.rs | 61 --------- .../impl/src/updates/register_bot.rs | 35 +++++ .../exchange_bot/impl/src/updates/swap.rs | 2 +- backend/canisters/group/impl/Cargo.toml | 1 - .../group/impl/src/new_joiner_rewards.rs | 6 +- backend/canisters/user/impl/Cargo.toml | 2 +- .../impl/src/updates/c2c_send_messages.rs | 8 +- .../user/impl/src/updates/send_message.rs | 10 +- .../impl/src/updates/set_message_reminder.rs | 2 +- backend/integration_tests/src/rng.rs | 4 +- .../libraries/chat_events/src/chat_events.rs | 3 +- backend/libraries/types/Cargo.toml | 2 +- backend/libraries/types/src/bots.rs | 6 +- backend/libraries/types/src/message_id.rs | 12 +- 46 files changed, 495 insertions(+), 227 deletions(-) create mode 100644 backend/canisters/exchange_bot/api/src/updates/handle_direct_message.rs delete mode 100644 backend/canisters/exchange_bot/api/src/updates/quote.rs create mode 100644 backend/canisters/exchange_bot/api/src/updates/register_bot.rs create mode 100644 backend/canisters/exchange_bot/impl/src/commands/common_errors.rs create mode 100644 backend/canisters/exchange_bot/impl/src/commands/mod.rs create mode 100644 backend/canisters/exchange_bot/impl/src/commands/quote.rs create mode 100644 backend/canisters/exchange_bot/impl/src/quote.rs create mode 100644 backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs delete mode 100644 backend/canisters/exchange_bot/impl/src/updates/quote.rs create mode 100644 backend/canisters/exchange_bot/impl/src/updates/register_bot.rs diff --git a/Cargo.lock b/Cargo.lock index b06fa49fcc..731fc49e31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,11 +1558,13 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" name = "exchange_bot_canister" version = "0.1.0" dependencies = [ + "bot_api", "candid", "candid_gen", "human_readable", "serde", "types", + "user_index_canister", ] [[package]] @@ -1585,11 +1587,17 @@ dependencies = [ "ic-stable-structures", "icpswap_client", "icrc1_ledger_canister_c2c_client", + "itertools", + "lazy_static", + "local_user_index_canister_c2c_client", "msgpack", + "rand", + "regex", "serde", "serializer", "tracing", "types", + "user_index_canister_c2c_client", "utils", ] @@ -1899,7 +1907,6 @@ dependencies = [ "icp_ledger_canister_c2c_client", "instruction_counts_log", "itertools", - "lazy_static", "ledger_utils", "local_user_index_canister", "local_user_index_canister_c2c_client", @@ -5379,7 +5386,7 @@ dependencies = [ "candid", "human_readable", "ic-ledger-types", - "rand_core", + "rand", "range-set", "serde", "serde_bytes", @@ -5521,7 +5528,7 @@ dependencies = [ "notifications_canister", "notifications_canister_c2c_client", "num-traits", - "rand_core", + "rand", "search", "serde", "serde_bytes", diff --git a/backend/bots/api/src/updates/handle_direct_message.rs b/backend/bots/api/src/updates/handle_direct_message.rs index dd156faa1b..b045f7a5b5 100644 --- a/backend/bots/api/src/updates/handle_direct_message.rs +++ b/backend/bots/api/src/updates/handle_direct_message.rs @@ -21,6 +21,8 @@ pub enum Response { #[derive(Serialize, Deserialize, Debug)] pub struct SuccessResult { pub bot_name: String, + #[serde(default)] + pub bot_display_name: Option, pub messages: Vec, } diff --git a/backend/bots/examples/group_prize_bot/impl/src/jobs/send_prizes.rs b/backend/bots/examples/group_prize_bot/impl/src/jobs/send_prizes.rs index ed36f6555d..cce7dfbab2 100644 --- a/backend/bots/examples/group_prize_bot/impl/src/jobs/send_prizes.rs +++ b/backend/bots/examples/group_prize_bot/impl/src/jobs/send_prizes.rs @@ -86,7 +86,7 @@ async fn send_next_prize() -> bool { }; // 3. Generate a random MessageId - let new_message_id = mutate_state(|state| MessageId::generate(state.env.rng())); + let new_message_id = mutate_state(|state| state.env.rng().gen()); // 4. Send the prize message to the group if let Err(error_message) = diff --git a/backend/bots/examples/group_prize_bot/impl/src/updates/handle_direct_message.rs b/backend/bots/examples/group_prize_bot/impl/src/updates/handle_direct_message.rs index 927ab00f47..2cc8b4cd84 100644 --- a/backend/bots/examples/group_prize_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/bots/examples/group_prize_bot/impl/src/updates/handle_direct_message.rs @@ -2,7 +2,7 @@ use crate::{mutate_state, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; use group_prize_bot::handle_direct_message::*; -use types::{BotMessage, MessageContent, TextContent}; +use types::{BotMessage, MessageContentInitial, TextContent}; #[update_msgpack] #[trace] @@ -14,8 +14,10 @@ fn handle_message(state: &mut RuntimeState) -> Response { let text = "Keep an eye out for prize messages in public groups - you've got to be quick to claim a prize!".to_string(); Success(SuccessResult { bot_name: state.data.username.clone(), + bot_display_name: None, messages: vec![BotMessage { - content: MessageContent::Text(TextContent { text }), + content: MessageContentInitial::Text(TextContent { text }), + message_id: None, }], }) } diff --git a/backend/bots/examples/icp_dispenser/impl/src/lifecycle/heartbeat.rs b/backend/bots/examples/icp_dispenser/impl/src/lifecycle/heartbeat.rs index 760972c038..4e8c013a66 100644 --- a/backend/bots/examples/icp_dispenser/impl/src/lifecycle/heartbeat.rs +++ b/backend/bots/examples/icp_dispenser/impl/src/lifecycle/heartbeat.rs @@ -5,8 +5,7 @@ use ic_ledger_types::{TransferArgs, MAINNET_LEDGER_CANISTER_ID}; use ledger_utils::default_ledger_account; use tracing::{error, info}; use types::{ - nns, BotMessage, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, MessageContent, - TransactionHash, UserId, + nns, BotMessage, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, TransactionHash, UserId, }; #[heartbeat] @@ -16,6 +15,7 @@ fn heartbeat() { mod process_pending_actions { use super::*; + use types::MessageContentInitial; pub fn run() { if let Some(action) = mutate_state(get_next) { @@ -38,7 +38,7 @@ mod process_pending_actions { Ok(Ok(block_index)) => { let this_canister_id = read_state(|state| state.env.canister_id()); let message = BotMessage { - content: MessageContent::Crypto(CryptoContent { + content: MessageContentInitial::Crypto(CryptoContent { recipient, transfer: CryptoTransaction::Completed(CompletedCryptoTransaction::NNS( nns::CompletedCryptoTransaction { @@ -56,6 +56,7 @@ mod process_pending_actions { )), caption: None, }), + message_id: None, }; PendingAction::SendMessages(recipient, vec![message]) } diff --git a/backend/bots/examples/icp_dispenser/impl/src/updates/handle_direct_message.rs b/backend/bots/examples/icp_dispenser/impl/src/updates/handle_direct_message.rs index d4f4cace70..e5ed3e3224 100644 --- a/backend/bots/examples/icp_dispenser/impl/src/updates/handle_direct_message.rs +++ b/backend/bots/examples/icp_dispenser/impl/src/updates/handle_direct_message.rs @@ -4,7 +4,7 @@ use crate::{mutate_state, RewardCodes, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; use icp_dispenser_bot::handle_direct_message::*; -use types::{BotMessage, MessageContent, TextContent, UserId}; +use types::{BotMessage, MessageContent, MessageContentInitial, TextContent, UserId}; #[update_msgpack] #[trace] @@ -40,8 +40,10 @@ fn handle_message(args: Args, state: &mut RuntimeState) -> Response { Success(SuccessResult { bot_name: state.data.bot_name.clone(), + bot_display_name: None, messages: vec![BotMessage { - content: MessageContent::Text(TextContent { text: text.to_string() }), + content: MessageContentInitial::Text(TextContent { text: text.to_string() }), + message_id: None, }], }) } diff --git a/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs b/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs index 992806d9de..36cac68a83 100644 --- a/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs +++ b/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs @@ -7,7 +7,8 @@ use std::time::Duration; use tracing::{error, trace}; use types::icrc1::{Account, TransferArg, TransferError}; use types::{ - icrc1, BotMessage, CanisterId, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, MessageContent, + icrc1, BotMessage, CanisterId, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, + MessageContentInitial, }; const MAX_BATCH_SIZE: usize = 5; @@ -56,7 +57,13 @@ async fn process_action(action: Action) { CanisterId::from(user_id), &user_canister::c2c_handle_bot_messages::Args { bot_name: read_state(|state| state.data.username.clone()), - messages: messages.into_iter().map(|m| BotMessage { content: m }).collect(), + messages: messages + .into_iter() + .map(|m| BotMessage { + content: m, + message_id: None, + }) + .collect(), }, ) .await @@ -91,7 +98,7 @@ async fn process_action(action: Action) { mutate_state(|state| { state.enqueue_pending_action(Action::SendMessages( user_id, - vec![MessageContent::Crypto(CryptoContent { + vec![MessageContentInitial::Crypto(CryptoContent { recipient: user_id, transfer: CryptoTransaction::Completed(CompletedCryptoTransaction::ICRC1( icrc1::CompletedCryptoTransaction { diff --git a/backend/bots/examples/satoshi_dice/impl/src/model/pending_actions_queue.rs b/backend/bots/examples/satoshi_dice/impl/src/model/pending_actions_queue.rs index 0e6b58d727..586393b729 100644 --- a/backend/bots/examples/satoshi_dice/impl/src/model/pending_actions_queue.rs +++ b/backend/bots/examples/satoshi_dice/impl/src/model/pending_actions_queue.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use std::collections::vec_deque::VecDeque; -use types::{MessageContent, UserId}; +use types::{MessageContentInitial, UserId}; #[derive(Serialize, Deserialize, Default)] pub struct PendingActionsQueue { @@ -23,7 +23,7 @@ impl PendingActionsQueue { #[derive(Serialize, Deserialize, Clone)] pub enum Action { - SendMessages(UserId, Vec), + SendMessages(UserId, Vec), TransferCkbtc(TransferCkbtc), } diff --git a/backend/bots/examples/satoshi_dice/impl/src/updates/c2c_add_user.rs b/backend/bots/examples/satoshi_dice/impl/src/updates/c2c_add_user.rs index 16a994d47f..87bedaf08e 100644 --- a/backend/bots/examples/satoshi_dice/impl/src/updates/c2c_add_user.rs +++ b/backend/bots/examples/satoshi_dice/impl/src/updates/c2c_add_user.rs @@ -4,7 +4,7 @@ use crate::{mutate_state, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; use satoshi_dice_canister::c2c_add_user::{Response::*, *}; -use types::{BlobReference, CanisterId, ImageContent, MessageContent, TextContent, ThumbnailData, UserId}; +use types::{BlobReference, CanisterId, ImageContent, MessageContentInitial, TextContent, ThumbnailData, UserId}; #[update_msgpack(guard = "caller_is_local_user_index")] #[trace] @@ -18,11 +18,11 @@ pub(crate) fn c2c_add_user_impl(user_id: UserId, state: &mut RuntimeState) -> Re Success } -fn welcome_messages() -> Vec { +fn welcome_messages() -> Vec { Vec::from_iter([ to_text_content("Hey there! I am the SatoshiDice chatbot!"), to_text_content("I am here to help you experiment with sending ckBTC as a chat message"), - MessageContent::Image(ImageContent { + MessageContentInitial::Image(ImageContent { width: 348, height: 402, thumbnail_data: ThumbnailData("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAeCAYAAADZ7LXbAAAAAXNSR0IArs4c6QAABhdJREFUSEuNVktvW0UU/mbm2tdxEjsPO7aTOEkfgT4IbYpURCA07RJQJUBFsKMSi65YIbFA/IEuYFMhVrDlsUAsQF3wSiBFKSqhLVRpg5omTUFJmjiJ49ix770D58xcJ6gtYFn2fczMOec73/nOEdlcrwYCaC0wNPQEpqau48iRQczMzKKnpwuFlQJcN4rW1jZoaNy6NYf9+/ahWqvizvwdRKNRtLS2olwpw41Gcfv2HSwsLqFYLEIIAa01RCab1+EN6h8BQENIBa0DvqYvpIKAALSAED6/BxSgNcAHBoAOzFIhEAQ+ICTEsydf1J2nfkbfwms49+4HUCpqDoWkfbzffMyFEHLHPTlj3+qAPQeMMYpAC3JGQJx8/gV97v2z+Or8N3j7rbM4ceI4Kls1HHp0AI5SuHzlKi78OLHD2Ha8D7oKtM/GfJ8iAUQ2m9dskTyXEi3JFDzPx1a1iqbGRkTdCFZX1xGNRqAcicDXcByFYrGEpqY4fM+HVAqlUoljpVgCrZHu6MDd5SV4vs0J4wKNiBNHcyIJr+YZI01N1sga3EgUTsRh7xzHQWGtiGSiCYHnIeq6WF1bs4iS6zYnFmVOvAlbIBKNI51Ko6UliVyuA7FYA7797nv4PiWfqWD90fCDAEpKm3By0MHAwAFMTl4xa4WEJmJQ4uvsAsHQAO22QCCArJUsaUxyfSGhiI42136goZhdGpQBRewi0PlfAyrKsFEibCQUokJ595N4uWcL7tFT+OTsm8wkSa/Ck6VTZ1kAujawCHquyCDhwSTnCMnLgPZnsj3WPYFaWx/yvR2YK0i4sxd5sSRIwk1MX37KNKXDpXQMVRkaY4APJiLRak1w5frqlVCvUMMDZpuJgv4t/juu+b2MmDVhXTBnTb1IW6hiV/8e/cobLfjsvQ38kTyKavte8pPxJUwdKeBTwZMRqmALiVlh8CdJEoIzwd6nr52HgiQYTOIfG+7TR3okyqkAn08ewzPHRyBQw/y6h8XiFtobJK4slgHfh+/V/rsSAWSnvt6GmLUr16el0tCBQmnvMNzAA9ZXHnhY6O2DFlRyDyN165IJyRLG5sSkZbN/BOnCDE6/9By68t347ddrGB0dw+LdgoXJ6pSFJTQUJpXOXe8eRHJppm6A4c129pKW1Y20r82h1fFQKKwhlWrnCl9ZKfxv7drIDyK5eJMLkfgVa2qHyOZ6WMfJzmb/CaTWZjE08BCmf5/GwsISMh1pHDx4AOPjF1irMpksUy+d7sDo2A/Ys3sXCoVVrBRW+flG1yCSy/OIxB0EVQ+iOUc5yWvDWInN/mPIFG4iJjwIoVCplJHP59EQc3H9xjRqNa+eiu0W8M/sFLsPo2153tCaKN7YRpGQdhEVgVL/CDKFGQSb64w6NR3qfIRntfpvzNqmAxlpL/zJzavqSDTFrRHbjlDqPw7R0glVKyOgutABpFAshkIKKNKpMG4B+KBnxsUgME2qGkuga2qMi1cLHeYkbyMnuEbQ3L0XTx3aj8kbc3i8O4m5LYWJ67MQ3GIt/am6wxsAiQYX6WQjPnz9JF5951NsjX9p+rtSiDW2mpyEu73WPmzuGbbbDYQsgjRocA17CATFY4TQDcqoyFi90s05Grnp8bp4us2pMCemV/CvcKCUY5s7yYWiuYGJoWx/D1VZqBjJMIPGOiWNcJKUEH21FIgn0vcaCUQEycY40uk0KpUqt93ufB4TP02iM5vB4UOPYGOjiI3NKm7O3DYRuTHUaj5K5S3WOWknGyJMPHFPJOS5kXbTkAxYpLYmUivfJHp860BQ1GyKoqFkm44STi4xMpKxxciw8Iy14yAr9STaBIHhkel+tB7KYQdM8uhox4xAzCzzhOHKdfbq+ixlvQgHCzPEmSmQVUEoY8A2A44EZuAjipt1dliz/Ucq2+Mdx0VXdy8WFhbuI33bA9x2bd9fiz2/hlR7CsvLK9tTJZ1Ig0QslsSZM6fREIth4uIlJBKN6Mzl8Mvlq6xbH338BarVDWvDjDz3U0yC++nhIYyOjfNrmmZIYNmItKOp6dDWSw4gHElNruxQZHu5NVavKgmeHO1cXCfN3wz5C/AucUB0HvpOAAAAAElFTkSuQmCC".to_string()), @@ -44,6 +44,6 @@ fn welcome_messages() -> Vec { ]) } -fn to_text_content(str: &str) -> MessageContent { - MessageContent::Text(TextContent { text: str.to_string() }) +fn to_text_content(str: &str) -> MessageContentInitial { + MessageContentInitial::Text(TextContent { text: str.to_string() }) } diff --git a/backend/bots/examples/satoshi_dice/impl/src/updates/handle_direct_message.rs b/backend/bots/examples/satoshi_dice/impl/src/updates/handle_direct_message.rs index eaa7b0eeca..313ad8fb05 100644 --- a/backend/bots/examples/satoshi_dice/impl/src/updates/handle_direct_message.rs +++ b/backend/bots/examples/satoshi_dice/impl/src/updates/handle_direct_message.rs @@ -5,7 +5,7 @@ use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; use rand::RngCore; use satoshi_dice_canister::handle_direct_message::*; -use types::{BotMessage, CanisterId, Cryptocurrency, MessageContent, TextContent, UserId}; +use types::{BotMessage, CanisterId, Cryptocurrency, MessageContent, MessageContentInitial, TextContent, UserId}; use utils::time::MINUTE_IN_MS; const MAX_TOTAL_WINNINGS: u64 = 50_000; @@ -83,10 +83,12 @@ fn handle_message(args: Args, state: &mut RuntimeState) -> Response { Success(SuccessResult { bot_name: state.data.username.clone(), + bot_display_name: None, messages: messages .into_iter() .map(|m| BotMessage { - content: MessageContent::Text(TextContent { text: m }), + content: MessageContentInitial::Text(TextContent { text: m }), + message_id: None, }) .collect(), }) diff --git a/backend/bots/examples/sns1_airdrop/impl/src/updates/handle_direct_message.rs b/backend/bots/examples/sns1_airdrop/impl/src/updates/handle_direct_message.rs index cc4b504193..f55e484813 100644 --- a/backend/bots/examples/sns1_airdrop/impl/src/updates/handle_direct_message.rs +++ b/backend/bots/examples/sns1_airdrop/impl/src/updates/handle_direct_message.rs @@ -4,7 +4,7 @@ use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; use sns1_airdrop::handle_direct_message::*; use std::collections::hash_map::Entry::{Occupied, Vacant}; -use types::{BotMessage, MessageContent, TextContent, UserId}; +use types::{BotMessage, MessageContent, MessageContentInitial, TextContent, UserId}; #[update_msgpack] #[trace] @@ -17,8 +17,10 @@ fn handle_message(args: Args, state: &mut RuntimeState) -> Response { Success(SuccessResult { bot_name: state.data.bot_name.clone(), + bot_display_name: None, messages: vec![BotMessage { - content: MessageContent::Text(TextContent { text }), + content: MessageContentInitial::Text(TextContent { text }), + message_id: None, }], }) } diff --git a/backend/canister_installer/src/lib.rs b/backend/canister_installer/src/lib.rs index c7daf4e329..03e0474966 100644 --- a/backend/canister_installer/src/lib.rs +++ b/backend/canister_installer/src/lib.rs @@ -175,6 +175,7 @@ async fn install_service_canisters_impl( let exchange_bot_canister_wasm = get_canister_wasm(CanisterName::ExchangeBot, version); let exchange_bot_init_args = exchange_bot_canister::init::Args { governance_principals: vec![principal], + user_index_canister_id: canister_ids.user_index, local_user_index_canister_id: canister_ids.local_user_index, cycles_dispenser_canister_id: canister_ids.cycles_dispenser, wasm_version: version, diff --git a/backend/canisters/exchange_bot/api/Cargo.toml b/backend/canisters/exchange_bot/api/Cargo.toml index 80fcf88a52..95a38136fd 100644 --- a/backend/canisters/exchange_bot/api/Cargo.toml +++ b/backend/canisters/exchange_bot/api/Cargo.toml @@ -6,8 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bot_api = { path = "../../../bots/api" } candid = { workspace = true } candid_gen = { path = "../../../libraries/candid_gen" } human_readable = { path = "../../../libraries/human_readable" } serde = { workspace = true } -types = { path = "../../../libraries/types" } \ No newline at end of file +types = { path = "../../../libraries/types" } +user_index_canister = { path = "../../user_index/api" } \ No newline at end of file diff --git a/backend/canisters/exchange_bot/api/can.did b/backend/canisters/exchange_bot/api/can.did index 5f4c9995d2..41d622634d 100644 --- a/backend/canisters/exchange_bot/api/can.did +++ b/backend/canisters/exchange_bot/api/can.did @@ -1,59 +1,13 @@ import "../../../libraries/types/can.did"; -type QuoteArgs = record { - input_token : CanisterId; - output_token : CanisterId; - amount : nat; -}; - -type QuoteResponse = variant { - Success : vec Quote; - PartialSuccess : record { - quotes : vec Quote; - failures : vec ExchangeError; - }; - Failed : vec ExchangeError; - UnsupportedTokens : vec CanisterId; - PairNotSupported; -}; - -type Quote = record { - exchange_id : ExchangeId; - amount_out : nat; -}; - -type ExchangeError = record { - exchange_id : ExchangeId; - error : text; -}; - -type SwapArgs = record { - exchange_id : ExchangeId; - input_token : CanisterId; - output_token : CanisterId; - amount : nat; -}; - -type SwapResponse = variant { - Success : nat; - UnsupportedTokens : vec CanisterId; - PairNotSupportedByExchange; - InternalError : text; -}; - type InitArgs = record { governance_principals : vec CanisterId; + user_index_canister_id : CanisterId; local_user_index_canister_id : CanisterId; cycles_dispenser_canister_id : CanisterId; wasm_version : BuildVersion; test_mode : bool; }; -type ExchangeId = variant { - ICPSwap; -}; - service : { - quote : (QuoteArgs) -> (QuoteResponse); - swap : (SwapArgs) -> (SwapResponse); }; diff --git a/backend/canisters/exchange_bot/api/src/lib.rs b/backend/canisters/exchange_bot/api/src/lib.rs index a6edd7101c..047d553c6f 100644 --- a/backend/canisters/exchange_bot/api/src/lib.rs +++ b/backend/canisters/exchange_bot/api/src/lib.rs @@ -1,5 +1,6 @@ use candid::CandidType; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; mod lifecycle; mod updates; @@ -11,3 +12,11 @@ pub use updates::*; pub enum ExchangeId { ICPSwap, } + +impl Display for ExchangeId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ExchangeId::ICPSwap => f.write_str("ICPSwap"), + } + } +} diff --git a/backend/canisters/exchange_bot/api/src/lifecycle/init.rs b/backend/canisters/exchange_bot/api/src/lifecycle/init.rs index 7d1ece3047..6338a53fd0 100644 --- a/backend/canisters/exchange_bot/api/src/lifecycle/init.rs +++ b/backend/canisters/exchange_bot/api/src/lifecycle/init.rs @@ -5,6 +5,7 @@ use types::{BuildVersion, CanisterId}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub governance_principals: Vec, + pub user_index_canister_id: CanisterId, pub local_user_index_canister_id: CanisterId, pub cycles_dispenser_canister_id: CanisterId, pub wasm_version: BuildVersion, diff --git a/backend/canisters/exchange_bot/api/src/main.rs b/backend/canisters/exchange_bot/api/src/main.rs index dff4d3807d..37e8c25054 100644 --- a/backend/canisters/exchange_bot/api/src/main.rs +++ b/backend/canisters/exchange_bot/api/src/main.rs @@ -1,10 +1,5 @@ -use candid_gen::generate_candid_method; - #[allow(deprecated)] fn main() { - generate_candid_method!(exchange_bot, quote, update); - generate_candid_method!(exchange_bot, swap, update); - candid::export_service!(); std::print!("{}", __export_service()); } diff --git a/backend/canisters/exchange_bot/api/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/api/src/updates/handle_direct_message.rs new file mode 100644 index 0000000000..924a492d2c --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/updates/handle_direct_message.rs @@ -0,0 +1 @@ +pub use bot_api::handle_direct_message::{Response::*, *}; diff --git a/backend/canisters/exchange_bot/api/src/updates/mod.rs b/backend/canisters/exchange_bot/api/src/updates/mod.rs index 2bf6e3e43a..74a4a68810 100644 --- a/backend/canisters/exchange_bot/api/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/api/src/updates/mod.rs @@ -1,2 +1,3 @@ -pub mod quote; +pub mod handle_direct_message; +pub mod register_bot; pub mod swap; diff --git a/backend/canisters/exchange_bot/api/src/updates/quote.rs b/backend/canisters/exchange_bot/api/src/updates/quote.rs deleted file mode 100644 index 03a6e9cc24..0000000000 --- a/backend/canisters/exchange_bot/api/src/updates/quote.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::ExchangeId; -use candid::CandidType; -use serde::{Deserialize, Serialize}; -use types::CanisterId; - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct Args { - pub input_token: CanisterId, - pub output_token: CanisterId, - pub amount: u128, -} - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub enum Response { - Success(Vec), - PartialSuccess(PartialSuccessResult), - Failed(Vec), - UnsupportedTokens(Vec), - PairNotSupported, -} - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct PartialSuccessResult { - pub quotes: Vec, - pub failures: Vec, -} - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct Quote { - pub exchange_id: ExchangeId, - pub amount_out: u128, -} - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct Failure { - pub exchange_id: ExchangeId, - pub error: String, -} diff --git a/backend/canisters/exchange_bot/api/src/updates/register_bot.rs b/backend/canisters/exchange_bot/api/src/updates/register_bot.rs new file mode 100644 index 0000000000..c93500aef5 --- /dev/null +++ b/backend/canisters/exchange_bot/api/src/updates/register_bot.rs @@ -0,0 +1 @@ +pub use user_index_canister::c2c_register_bot::*; diff --git a/backend/canisters/exchange_bot/api/src/updates/swap.rs b/backend/canisters/exchange_bot/api/src/updates/swap.rs index 5b8082b556..72bbe72773 100644 --- a/backend/canisters/exchange_bot/api/src/updates/swap.rs +++ b/backend/canisters/exchange_bot/api/src/updates/swap.rs @@ -1,20 +1,19 @@ use crate::ExchangeId; use candid::CandidType; use serde::{Deserialize, Serialize}; -use types::CanisterId; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub exchange_id: ExchangeId, - pub input_token: CanisterId, - pub output_token: CanisterId, + pub input_token: String, + pub output_token: String, pub amount: u128, } #[derive(CandidType, Serialize, Deserialize, Debug)] pub enum Response { Success(u128), - UnsupportedTokens(Vec), + UnsupportedTokens(Vec), PairNotSupportedByExchange, InternalError(String), } diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml index d5b75d8966..e6f301fab4 100644 --- a/backend/canisters/exchange_bot/impl/Cargo.toml +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -26,9 +26,15 @@ ic-cdk-timers = { workspace = true } ic-stable-structures = { workspace = true } icpswap_client = { path = "../../../libraries/icpswap_client" } icrc1_ledger_canister_c2c_client = { path = "../../../external_canisters/icrc1_ledger/c2c_client" } +itertools = { workspace = true } +lazy_static = { workspace = true } +local_user_index_canister_c2c_client = { path = "../../local_user_index/c2c_client" } msgpack = { path = "../../../libraries/msgpack" } +rand = { workspace = true } +regex = { workspace = true } serde = { workspace = true } serializer = { path = "../../../libraries/serializer" } tracing = { workspace = true } types = { path = "../../../libraries/types" } +user_index_canister_c2c_client = { path = "../../user_index/c2c_client" } utils = { path = "../../../libraries/utils" } diff --git a/backend/canisters/exchange_bot/impl/src/commands/common_errors.rs b/backend/canisters/exchange_bot/impl/src/commands/common_errors.rs new file mode 100644 index 0000000000..954056f5c8 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/common_errors.rs @@ -0,0 +1,27 @@ +use crate::Data; + +pub(crate) enum CommonErrors { + UnsupportedTokens(Vec), + PairNotSupported, +} + +impl CommonErrors { + pub fn build_response_message(&self, data: &Data) -> String { + match self { + CommonErrors::UnsupportedTokens(tokens) => { + let mut message = "The following inputs were not recognised as supported tokens:".to_string(); + for token in tokens { + message.push_str(&format!("\n{token}")); + } + + message.push_str("\n\nSupported tokens:"); + for token in data.supported_tokens() { + message.push_str(&format!("\n{token}")); + } + + message + } + CommonErrors::PairNotSupported => todo!(), + } + } +} diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs new file mode 100644 index 0000000000..69e5df5761 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -0,0 +1,14 @@ +use crate::RuntimeState; +use types::MessageContent; + +pub(crate) mod common_errors; +pub(crate) mod quote; + +pub(crate) trait Command { + fn process_message(message: &MessageContent, state: &mut RuntimeState) -> ProcessCommandResult; +} + +pub enum ProcessCommandResult { + Success(exchange_bot_canister::handle_direct_message::Response), + Continue, +} diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs new file mode 100644 index 0000000000..4c4bfd28ae --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -0,0 +1,123 @@ +use crate::commands::common_errors::CommonErrors; +use crate::commands::{Command, ProcessCommandResult}; +use crate::swap_client::SwapClient; +use crate::{Data, RuntimeState}; +use exchange_bot_canister::ExchangeId; +use itertools::Itertools; +use lazy_static::lazy_static; +use rand::Rng; +use regex::{Regex, RegexBuilder}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use types::{MessageContent, MessageId, TokenInfo, UserId}; + +lazy_static! { + static ref REGEX: Regex = RegexBuilder::new(r"quote (?\S+) (?\S+) (?[\d.,]+)") + .case_insensitive(true) + .build() + .unwrap(); +} + +pub(crate) struct QuoteCommand { + user_id: UserId, + input_token: TokenInfo, + output_token: TokenInfo, + amount: u128, + clients: Vec>, + message_id: MessageId, + quote_statuses: Vec<(ExchangeId, QuoteStatus)>, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub enum QuoteStatus { + Success(u128, String), + Failed(String), + Pending, +} + +impl Display for QuoteStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + QuoteStatus::Success(_, text) => f.write_str(&text), + QuoteStatus::Failed(_) => f.write_str("Failed"), + QuoteStatus::Pending => f.write_str("Pending"), + } + } +} + +impl QuoteCommand { + pub fn build( + input_token: TokenInfo, + output_token: TokenInfo, + amount: u128, + state: &mut RuntimeState, + ) -> Result { + let clients = state.get_all_swap_clients(input_token.clone(), output_token.clone()); + + if !clients.is_empty() { + let quote_statuses = clients.iter().map(|c| (c.exchange_id(), QuoteStatus::Pending)).collect(); + + Ok(QuoteCommand { + user_id: state.env.caller().into(), + input_token, + output_token, + amount, + clients, + message_id: state.env.rng().gen(), + quote_statuses, + }) + } else { + Err(CommonErrors::PairNotSupported) + } + } + + pub fn build_message_text(&self) -> String { + let mut text = "Quotes:".to_string(); + for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { + let exchange_name = exchange_id.to_string(); + let status_text = status.to_string(); + text.push_str(&format!("\n{exchange_name}: {status_text}")); + } + text + } +} + +impl Command for QuoteCommand { + fn process_message(message: &MessageContent, state: &mut RuntimeState) -> ProcessCommandResult { + let text = message.text().unwrap_or_default(); + + if !REGEX.is_match(&text) { + return ProcessCommandResult::Continue; + } + + let matches = REGEX.captures_iter(&text).next().unwrap(); + let input_token = &matches["input_token"]; + let output_token = &matches["output_token"]; + let amount_decimal = f64::from_str(&matches["amount"]).unwrap(); + + let (input_token, output_token) = match state.data.get_token_pair(input_token, output_token) { + Ok((i, o)) => (i, o), + Err(tokens) => { + let error = CommonErrors::UnsupportedTokens(tokens); + return build_error_response(error, &state.data); + } + }; + + let amount = (amount_decimal * 10u128.pow(input_token.decimals as u32) as f64) as u128; + + match QuoteCommand::build(input_token, output_token, amount, state) { + Ok(command) => ProcessCommandResult::Success( + state + .data + .build_response(command.build_message_text(), Some(command.message_id)), + ), + Err(error) => build_error_response(error, &state.data), + } + } +} + +fn build_error_response(error: CommonErrors, data: &Data) -> ProcessCommandResult { + let response_message = error.build_response_message(data); + ProcessCommandResult::Success(data.build_response(response_message, None)) +} diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index e45d93baa3..b9e283372f 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -3,12 +3,17 @@ use crate::swap_client::{SwapClient, SwapClientFactory}; use candid::Principal; use canister_state_macros::canister_state; use exchange_bot_canister::ExchangeId; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use types::{BuildVersion, CanisterId, Cryptocurrency, Cycles, TimestampMillis, Timestamped, TokenInfo}; +use types::{ + BotMessage, BuildVersion, CanisterId, Cryptocurrency, Cycles, MessageContentInitial, MessageId, TextContent, + TimestampMillis, Timestamped, TokenInfo, +}; use utils::env::Environment; +mod commands; mod guards; mod icpswap; mod jobs; @@ -38,7 +43,7 @@ impl RuntimeState { pub fn get_all_swap_clients(&self, input_token: TokenInfo, output_token: TokenInfo) -> Vec> { let this_canister_id = self.env.canister_id(); - vec![ICPSwapClientFactory::new().build(this_canister_id, input_token.clone(), output_token.clone())] + vec![ICPSwapClientFactory::new().build(this_canister_id, input_token, output_token)] .into_iter() .flatten() .collect() @@ -81,40 +86,83 @@ impl RuntimeState { #[derive(Serialize, Deserialize)] struct Data { governance_principals: HashSet, + user_index_canister_id: CanisterId, local_user_index_canister_id: CanisterId, cycles_dispenser_canister_id: CanisterId, - token_info: HashMap, + token_info: Vec, + known_callers: HashMap, + username: String, + display_name: Option, + is_registered: bool, test_mode: bool, } impl Data { pub fn new( governance_principals: HashSet, + user_index_canister_id: CanisterId, local_user_index_canister_id: CanisterId, cycles_dispenser_canister_id: CanisterId, test_mode: bool, ) -> Data { Data { governance_principals, + user_index_canister_id, local_user_index_canister_id, cycles_dispenser_canister_id, - token_info: build_token_info().into_iter().map(|t| (t.ledger, t)).collect(), + token_info: build_token_info(), + known_callers: HashMap::new(), + username: "".to_string(), + display_name: None, + is_registered: false, test_mode, } } - pub fn get_token_info( - &self, - input_token: CanisterId, - output_token: CanisterId, - ) -> Result<(TokenInfo, TokenInfo), Vec> { - match (self.token_info.get(&input_token), self.token_info.get(&output_token)) { - (Some(i), Some(o)) => Ok((i.clone(), o.clone())), - (None, Some(_)) => Err(vec![input_token]), - (Some(_), None) => Err(vec![output_token]), - (None, None) => Err(vec![input_token, output_token]), + pub fn get_token_pair(&self, input_token: &str, output_token: &str) -> Result<(TokenInfo, TokenInfo), Vec> { + match (self.get_token(&input_token), self.get_token(&output_token)) { + (Some(i), Some(o)) => Ok((i, o)), + (None, Some(_)) => Err(vec![input_token.to_string()]), + (Some(_), None) => Err(vec![output_token.to_string()]), + (None, None) => Err(vec![input_token.to_string(), output_token.to_string()]), } } + + pub fn get_token(&self, token: &str) -> Option { + let token_upper = token.to_uppercase(); + + self.token_info + .iter() + .find(|t| t.token.token_symbol().to_uppercase() == token_upper) + .cloned() + } + + pub fn supported_tokens(&self) -> Vec { + self.token_info + .iter() + .map(|t| t.token.token_symbol().to_string()) + .sorted_unstable() + .collect() + } + + pub fn build_response( + &self, + text: String, + message_id: Option, + ) -> exchange_bot_canister::handle_direct_message::Response { + let (username, display_name) = (self.username.clone(), self.display_name.clone()); + + exchange_bot_canister::handle_direct_message::Response::Success( + exchange_bot_canister::handle_direct_message::SuccessResult { + bot_name: username, + bot_display_name: display_name, + messages: vec![BotMessage { + content: MessageContentInitial::Text(TextContent { text }), + message_id, + }], + }, + ) + } } fn build_token_info() -> Vec { diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/init.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/init.rs index 5b6ca1e014..1787a262b0 100644 --- a/backend/canisters/exchange_bot/impl/src/lifecycle/init.rs +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/init.rs @@ -16,6 +16,7 @@ fn init(args: Args) { let data = Data::new( args.governance_principals.into_iter().collect(), + args.user_index_canister_id, args.local_user_index_canister_id, args.cycles_dispenser_canister_id, args.test_mode, diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs index 0cc4d098b5..8c1922e963 100644 --- a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs @@ -10,7 +10,7 @@ fn accept_if_valid(state: &RuntimeState) { let method_name = ic_cdk::api::call::method_name(); let is_valid = match method_name.as_str() { - "quote" | "swap" => state.is_caller_governance_principal(), + "register_bot" | "swap" => state.is_caller_governance_principal(), _ => false, }; diff --git a/backend/canisters/exchange_bot/impl/src/quote.rs b/backend/canisters/exchange_bot/impl/src/quote.rs new file mode 100644 index 0000000000..b12c833df4 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/quote.rs @@ -0,0 +1,63 @@ +// use crate::swap_client::SwapClient; +// use crate::{read_state, RuntimeState}; +// use exchange_bot_canister::ExchangeId; +// use serde::{Deserialize, Serialize}; +// use types::{TokenInfo, UserId}; +// +// pub struct QuoteArgs { +// pub caller: UserId, +// pub input_token: TokenInfo, +// pub output_token: TokenInfo, +// pub amount: u128, +// } +// +// pub struct PrepareQuoteResult { +// message_text: String, +// clients: Vec>, +// } +// +// #[derive(Serialize, Deserialize, Debug)] +// pub struct QuoteResult { +// pub quotes: Vec, +// pub failures: Vec, +// } +// +// #[derive(Serialize, Deserialize, Debug)] +// pub struct Quote { +// pub exchange_id: ExchangeId, +// pub amount_out: u128, +// } +// +// #[derive(Serialize, Deserialize, Debug)] +// pub struct Failure { +// pub exchange_id: ExchangeId, +// pub error: String, +// } +// +// async fn quote(args: QuoteArgs) { +// let PrepareResult { clients } = match read_state(|state| prepare(&args, state)) { +// Ok(ok) => ok, +// Err(response) => return response, +// }; +// +// let futures: Vec<_> = clients.into_iter().map(|c| quote_single(c, args.amount)).collect(); +// +// let results = futures::future::join_all(futures).await; +// +// let mut quotes = Vec::new(); +// let mut failures = Vec::new(); +// for (exchange_id, result) in results { +// match result { +// Ok(amount_out) => quotes.push(Quote { exchange_id, amount_out }), +// Err(error) => failures.push(Failure { exchange_id, error }), +// } +// } +// +// if failures.is_empty() { +// Success(quotes) +// } else if quotes.is_empty() { +// Failed(failures) +// } else { +// PartialSuccess(PartialSuccessResult { quotes, failures }) +// } +// } diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs new file mode 100644 index 0000000000..713165aa14 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -0,0 +1,60 @@ +use crate::commands::quote::QuoteCommand; +use crate::commands::{Command, ProcessCommandResult}; +use crate::{mutate_state, read_state, RuntimeState}; +use candid::Principal; +use canister_api_macros::update_msgpack; +use canister_tracing_macros::trace; +use exchange_bot_canister::handle_direct_message::*; +use local_user_index_canister_c2c_client::LookupUserError; +use types::UserId; + +#[update_msgpack] +#[trace] +async fn handle_direct_message(args: Args) -> Response { + if let Err(message) = verify_caller().await { + return read_state(|state| state.data.build_response(message, None)); + }; + + mutate_state(|state| { + if let ProcessCommandResult::Success(response) = QuoteCommand::process_message(&args.content, state) { + response + } else { + todo!() + } + }) +} + +async fn verify_caller() -> Result { + match read_state(check_for_known_caller) { + CheckForKnownCallerResult::KnownUser(u) => Ok(u), + CheckForKnownCallerResult::Unknown(caller, local_user_index_canister_id) => { + match local_user_index_canister_c2c_client::lookup_user(caller, local_user_index_canister_id).await { + Ok(user) => { + mutate_state(|state| state.data.known_callers.insert(caller, true)); + Ok(user.user_id) + } + Err(LookupUserError::UserNotFound) => { + mutate_state(|state| state.data.known_callers.insert(caller, false)); + Err("User not found".to_string()) + } + Err(LookupUserError::InternalError(_)) => Err("An error occurred. Please try again later".to_string()), + } + } + CheckForKnownCallerResult::Blocked => panic!(), + } +} + +enum CheckForKnownCallerResult { + Unknown(Principal, Principal), // Caller, LocalUserIndex + KnownUser(UserId), + Blocked, +} + +fn check_for_known_caller(state: &RuntimeState) -> CheckForKnownCallerResult { + let caller = state.env.caller(); + match state.data.known_callers.get(&caller).copied() { + Some(true) => CheckForKnownCallerResult::KnownUser(caller.into()), + Some(false) => CheckForKnownCallerResult::Blocked, + None => CheckForKnownCallerResult::Unknown(caller, state.data.local_user_index_canister_id), + } +} diff --git a/backend/canisters/exchange_bot/impl/src/updates/mod.rs b/backend/canisters/exchange_bot/impl/src/updates/mod.rs index 61134495fd..fa2aaac01b 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/mod.rs @@ -1,3 +1,4 @@ -mod quote; +mod handle_direct_message; +mod register_bot; mod swap; mod wallet_receive; diff --git a/backend/canisters/exchange_bot/impl/src/updates/quote.rs b/backend/canisters/exchange_bot/impl/src/updates/quote.rs deleted file mode 100644 index fc4213e4c8..0000000000 --- a/backend/canisters/exchange_bot/impl/src/updates/quote.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::guards::caller_is_governance_principal; -use crate::swap_client::SwapClient; -use crate::{read_state, RuntimeState}; -use canister_tracing_macros::trace; -use exchange_bot_canister::quote::{Response::*, *}; -use exchange_bot_canister::ExchangeId; -use ic_cdk_macros::update; - -#[update(guard = "caller_is_governance_principal")] -#[trace] -async fn quote(args: Args) -> Response { - let PrepareResult { clients } = match read_state(|state| prepare(&args, state)) { - Ok(ok) => ok, - Err(response) => return response, - }; - - let futures: Vec<_> = clients.into_iter().map(|c| quote_single(c, args.amount)).collect(); - - let results = futures::future::join_all(futures).await; - - let mut quotes = Vec::new(); - let mut failures = Vec::new(); - for (exchange_id, result) in results { - match result { - Ok(amount_out) => quotes.push(Quote { exchange_id, amount_out }), - Err(error) => failures.push(Failure { exchange_id, error }), - } - } - - if failures.is_empty() { - Success(quotes) - } else if quotes.is_empty() { - Failed(failures) - } else { - PartialSuccess(PartialSuccessResult { quotes, failures }) - } -} - -struct PrepareResult { - clients: Vec>, -} - -fn prepare(args: &Args, state: &RuntimeState) -> Result { - match state.data.get_token_info(args.input_token, args.output_token) { - Ok((input_token, output_token)) => { - let clients = state.get_all_swap_clients(input_token, output_token); - if !clients.is_empty() { - Ok(PrepareResult { clients }) - } else { - Err(PairNotSupported) - } - } - Err(tokens) => Err(UnsupportedTokens(tokens)), - } -} - -async fn quote_single(client: Box, amount: u128) -> (ExchangeId, Result) { - let result = client.quote(amount).await.map_err(|e| format!("{e:?}")); - - (client.exchange_id(), result) -} diff --git a/backend/canisters/exchange_bot/impl/src/updates/register_bot.rs b/backend/canisters/exchange_bot/impl/src/updates/register_bot.rs new file mode 100644 index 0000000000..94a8448f90 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/updates/register_bot.rs @@ -0,0 +1,35 @@ +use crate::guards::caller_is_governance_principal; +use crate::{mutate_state, read_state}; +use canister_tracing_macros::trace; +use exchange_bot_canister::register_bot::{Response::*, *}; +use ic_cdk_macros::update; +use types::Cycles; + +const BOT_REGISTRATION_FEE: Cycles = 10_000_000_000_000; // 10T + +#[update(guard = "caller_is_governance_principal")] +#[trace] +async fn register_bot(args: Args) -> Response { + let (already_registered, user_index_canister_id) = + read_state(|state| (state.data.is_registered, state.data.user_index_canister_id)); + + if already_registered { + AlreadyRegistered + } else { + let response = + user_index_canister_c2c_client::c2c_register_bot(user_index_canister_id, &args, BOT_REGISTRATION_FEE).await; + + match response { + Ok(Success) => { + mutate_state(|state| { + state.data.username = args.username; + state.data.display_name = args.display_name; + state.data.is_registered = true; + }); + Success + } + Ok(response) => response, + Err(error) => InternalError(format!("{error:?}")), + } + } +} diff --git a/backend/canisters/exchange_bot/impl/src/updates/swap.rs b/backend/canisters/exchange_bot/impl/src/updates/swap.rs index e450d71c4e..551a95c42f 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/swap.rs @@ -32,7 +32,7 @@ struct PrepareResult { } fn prepare(args: &Args, state: &RuntimeState) -> Result { - match state.data.get_token_info(args.input_token, args.output_token) { + match state.data.get_token_pair(&args.input_token, &args.output_token) { Ok((input_token, output_token)) => { if let Some(client) = state.get_swap_client(args.exchange_id, input_token.clone(), output_token.clone()) { Ok(PrepareResult { diff --git a/backend/canisters/group/impl/Cargo.toml b/backend/canisters/group/impl/Cargo.toml index cdd09cce35..bdae23fe5b 100644 --- a/backend/canisters/group/impl/Cargo.toml +++ b/backend/canisters/group/impl/Cargo.toml @@ -34,7 +34,6 @@ ic-stable-structures = { workspace = true } icp_ledger_canister_c2c_client = { path = "../../../external_canisters/icp_ledger/c2c_client" } instruction_counts_log = { path = "../../../libraries/instruction_counts_log" } itertools = { workspace = true } -lazy_static = { workspace = true } ledger_utils = { path = "../../../libraries/ledger_utils" } local_user_index_canister = { path = "../../local_user_index/api" } local_user_index_canister_c2c_client = { path = "../../local_user_index/c2c_client" } diff --git a/backend/canisters/group/impl/src/new_joiner_rewards.rs b/backend/canisters/group/impl/src/new_joiner_rewards.rs index 2542138c2d..cb31021523 100644 --- a/backend/canisters/group/impl/src/new_joiner_rewards.rs +++ b/backend/canisters/group/impl/src/new_joiner_rewards.rs @@ -2,11 +2,11 @@ use crate::{mutate_state, NewJoinerRewardStatus, RuntimeState}; use chat_events::{MessageContentInternal, PushMessageArgs}; use ic_ledger_types::{Memo, Timestamp, TransferArgs, DEFAULT_FEE}; use ledger_utils::{calculate_transaction_hash, default_ledger_account}; +use rand::Rng; use tracing::error; use types::nns::CryptoAccount; use types::{ - nns, CanisterId, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, MessageId, TimestampMillis, - UserId, ICP, + nns, CanisterId, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, TimestampMillis, UserId, ICP, }; use utils::consts::OPENCHAT_BOT_USER_ID; @@ -75,7 +75,7 @@ fn send_reward_transferred_message(user_id: UserId, transfer: nns::CompletedCryp state.data.chat.events.push_message(PushMessageArgs { sender: OPENCHAT_BOT_USER_ID, thread_root_message_index: None, - message_id: MessageId::generate(state.env.rng()), + message_id: state.env.rng().gen(), content: MessageContentInternal::Crypto(CryptoContent { recipient: user_id, transfer: CryptoTransaction::Completed(CompletedCryptoTransaction::NNS(transfer)), diff --git a/backend/canisters/user/impl/Cargo.toml b/backend/canisters/user/impl/Cargo.toml index d96760de5e..ca6403c630 100644 --- a/backend/canisters/user/impl/Cargo.toml +++ b/backend/canisters/user/impl/Cargo.toml @@ -42,7 +42,7 @@ msgpack = { path = "../../../libraries/msgpack" } notifications_canister = { path = "../../notifications/api" } notifications_canister_c2c_client = { path = "../../notifications/c2c_client" } num-traits = { workspace = true } -rand_core = { workspace = true } +rand = { workspace = true } search = { path = "../../../libraries/search" } serde = { workspace = true } serde_bytes = { workspace = true } diff --git a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs index 56b8adcedf..472691d536 100644 --- a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs +++ b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs @@ -5,6 +5,7 @@ use canister_timer_jobs::TimerJobs; use canister_tracing_macros::trace; use chat_events::{MessageContentInternal, PushMessageArgs, Reader, ReplyContextInternal}; use ic_cdk_macros::update; +use rand::Rng; use types::{ BlobReference, CanisterId, DirectMessageNotification, EventWrapper, Message, MessageContent, MessageContentInitial, MessageId, MessageIndex, Notification, TimestampMillis, UserId, @@ -90,8 +91,7 @@ async fn c2c_handle_bot_messages( }; for message in args.messages.iter() { - let content: MessageContentInitial = message.content.clone().into(); - if let Err(error) = content.validate_for_new_direct_message(sender_user_id, false, now) { + if let Err(error) = message.content.validate_for_new_direct_message(sender_user_id, false, now) { return user_canister::c2c_handle_bot_messages::Response::ContentValidationError(error); } } @@ -106,7 +106,7 @@ async fn c2c_handle_bot_messages( sender_message_index: None, sender_name: args.bot_name.clone(), sender_display_name: None, - content: message.content, + content: message.content.into(), replies_to: None, forwarding: false, correlation_id: 0, @@ -182,7 +182,7 @@ pub(crate) fn handle_message_impl( let push_message_args = PushMessageArgs { thread_root_message_index: None, - message_id: args.message_id.unwrap_or_else(|| MessageId::generate(state.env.rng())), + message_id: args.message_id.unwrap_or_else(|| state.env.rng().gen()), sender, content, replies_to, diff --git a/backend/canisters/user/impl/src/updates/send_message.rs b/backend/canisters/user/impl/src/updates/send_message.rs index 5a85053264..42e63d4196 100644 --- a/backend/canisters/user/impl/src/updates/send_message.rs +++ b/backend/canisters/user/impl/src/updates/send_message.rs @@ -5,10 +5,11 @@ use crate::{mutate_state, read_state, run_regular_jobs, RuntimeState, TimerJob}; use canister_tracing_macros::trace; use chat_events::{PushMessageArgs, Reader}; use ic_cdk_macros::update; +use rand::Rng; use tracing::error; use types::{ - CanisterId, CompletedCryptoTransaction, ContentValidationError, CryptoTransaction, MessageContentInitial, MessageId, - MessageIndex, UserId, + CanisterId, CompletedCryptoTransaction, ContentValidationError, CryptoTransaction, MessageContentInitial, MessageIndex, + UserId, }; use user_canister::c2c_send_messages; use user_canister::c2c_send_messages::{C2CReplyContext, SendMessageArgs}; @@ -284,12 +285,11 @@ async fn send_to_bot_canister(recipient: UserId, message_index: MessageIndex, ar mutate_state(|state| { let now = state.env.now(); for message in result.messages { - let content: MessageContentInitial = message.content.into(); let push_message_args = PushMessageArgs { sender: recipient, thread_root_message_index: None, - message_id: MessageId::generate(state.env.rng()), - content: content.into(), + message_id: message.message_id.unwrap_or_else(|| state.env.rng().gen()), + content: message.content.into(), replies_to: None, forwarded: false, correlation_id: 0, diff --git a/backend/canisters/user/impl/src/updates/set_message_reminder.rs b/backend/canisters/user/impl/src/updates/set_message_reminder.rs index 7f3ca9884e..aa30f085cd 100644 --- a/backend/canisters/user/impl/src/updates/set_message_reminder.rs +++ b/backend/canisters/user/impl/src/updates/set_message_reminder.rs @@ -3,7 +3,7 @@ use crate::timer_job_types::{MessageReminderJob, TimerJob}; use crate::{mutate_state, openchat_bot, run_regular_jobs, RuntimeState}; use canister_tracing_macros::trace; use ic_cdk_macros::update; -use rand_core::RngCore; +use rand::RngCore; use types::{FieldTooLongResult, MessageContent, MessageReminderCreatedContent}; use user_canister::c2c_send_messages::C2CReplyContext; use user_canister::set_message_reminder_v2::{Response::*, *}; diff --git a/backend/integration_tests/src/rng.rs b/backend/integration_tests/src/rng.rs index 374b215fd8..8de65df8ce 100644 --- a/backend/integration_tests/src/rng.rs +++ b/backend/integration_tests/src/rng.rs @@ -1,6 +1,6 @@ use crate::NNS_INTERNET_IDENTITY_CANISTER_ID; use candid::Principal; -use rand::{Rng, RngCore}; +use rand::{random, Rng, RngCore}; use types::MessageId; pub fn random_principal() -> Principal { @@ -26,5 +26,5 @@ pub fn random_string() -> String { } pub fn random_message_id() -> MessageId { - MessageId::generate(rand::thread_rng()) + random() } diff --git a/backend/libraries/chat_events/src/chat_events.rs b/backend/libraries/chat_events/src/chat_events.rs index 08748702d0..2d7e52e75e 100644 --- a/backend/libraries/chat_events/src/chat_events.rs +++ b/backend/libraries/chat_events/src/chat_events.rs @@ -5,6 +5,7 @@ use candid::Principal; use ic_ledger_types::Tokens; use itertools::Itertools; use rand::rngs::StdRng; +use rand::Rng; use search::{Document, Query}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -600,7 +601,7 @@ impl ChatEvents { let message_event = self.push_message(PushMessageArgs { sender: OPENCHAT_BOT_USER_ID, thread_root_message_index: None, - message_id: MessageId::generate(rng), + message_id: rng.gen(), content: MessageContentInternal::PrizeWinner(PrizeWinnerContent { winner, transaction, diff --git a/backend/libraries/types/Cargo.toml b/backend/libraries/types/Cargo.toml index 1b69d2ab09..0c3d03b866 100644 --- a/backend/libraries/types/Cargo.toml +++ b/backend/libraries/types/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" candid = { workspace = true } human_readable = { path = "../human_readable" } ic-ledger-types = { workspace = true } -rand_core = { workspace = true } +rand = { workspace = true } range-set = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } diff --git a/backend/libraries/types/src/bots.rs b/backend/libraries/types/src/bots.rs index dab2b1d22e..34930fb596 100644 --- a/backend/libraries/types/src/bots.rs +++ b/backend/libraries/types/src/bots.rs @@ -1,8 +1,10 @@ -use crate::MessageContent; +use crate::{MessageContentInitial, MessageId}; use candid::CandidType; use serde::{Deserialize, Serialize}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct BotMessage { - pub content: MessageContent, + pub content: MessageContentInitial, + #[serde(default)] + pub message_id: Option, } diff --git a/backend/libraries/types/src/message_id.rs b/backend/libraries/types/src/message_id.rs index 67e969a3ef..81ecdcdb0a 100644 --- a/backend/libraries/types/src/message_id.rs +++ b/backend/libraries/types/src/message_id.rs @@ -1,17 +1,15 @@ use candid::CandidType; -use rand_core::RngCore; +use rand::distributions::{Distribution, Standard}; +use rand::Rng; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; #[derive(CandidType, Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MessageId(u128); -impl MessageId { - pub fn generate(mut rng: R) -> MessageId { - let mut message_id_bytes = [0; 16]; - rng.fill_bytes(&mut message_id_bytes); - - MessageId(u128::from_ne_bytes(message_id_bytes)) +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> MessageId { + MessageId(rng.gen()) } } From 8d921853d182cf14567acf4e324e04d6a55a0830 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 15 Sep 2023 10:59:51 +0100 Subject: [PATCH 09/39] Parser --- .../exchange_bot/impl/src/commands/mod.rs | 18 ++-- .../exchange_bot/impl/src/commands/quote.rs | 85 ++++++++++--------- .../canisters/exchange_bot/impl/src/lib.rs | 12 ++- .../impl/src/updates/handle_direct_message.rs | 18 ++-- 4 files changed, 79 insertions(+), 54 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 69e5df5761..0f61f7da64 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -1,14 +1,22 @@ use crate::RuntimeState; -use types::MessageContent; +use types::{MessageContent, MessageContentInitial, MessageId}; pub(crate) mod common_errors; pub(crate) mod quote; +pub(crate) trait CommandParser { + type Command: Command; + + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult; +} + pub(crate) trait Command { - fn process_message(message: &MessageContent, state: &mut RuntimeState) -> ProcessCommandResult; + fn message_id(&self) -> MessageId; + fn build_message(&self) -> MessageContentInitial; } -pub enum ProcessCommandResult { - Success(exchange_bot_canister::handle_direct_message::Response), - Continue, +pub(crate) enum ParseMessageResult { + Success(C), + Error(exchange_bot_canister::handle_direct_message::Response), + DoesNotMatch, } diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 4c4bfd28ae..8294882a15 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -1,5 +1,5 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::{Command, ProcessCommandResult}; +use crate::commands::{Command, CommandParser, ParseMessageResult}; use crate::swap_client::SwapClient; use crate::{Data, RuntimeState}; use exchange_bot_canister::ExchangeId; @@ -10,7 +10,7 @@ use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::str::FromStr; -use types::{MessageContent, MessageId, TokenInfo, UserId}; +use types::{MessageContent, MessageContentInitial, MessageId, TextContent, TokenInfo, UserId}; lazy_static! { static ref REGEX: Regex = RegexBuilder::new(r"quote (?\S+) (?\S+) (?[\d.,]+)") @@ -19,6 +19,40 @@ lazy_static! { .unwrap(); } +pub(crate) struct QuoteCommandParser; + +impl CommandParser for QuoteCommandParser { + type Command = QuoteCommand; + + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { + let text = message.text().unwrap_or_default(); + + if !REGEX.is_match(&text) { + return ParseMessageResult::DoesNotMatch; + } + + let matches = REGEX.captures_iter(&text).next().unwrap(); + let input_token = &matches["input_token"]; + let output_token = &matches["output_token"]; + let amount_decimal = f64::from_str(&matches["amount"]).unwrap(); + + let (input_token, output_token) = match state.data.get_token_pair(input_token, output_token) { + Ok((i, o)) => (i, o), + Err(tokens) => { + let error = CommonErrors::UnsupportedTokens(tokens); + return build_error_response(error, &state.data); + } + }; + + let amount = (amount_decimal * 10u128.pow(input_token.decimals as u32) as f64) as u128; + + match QuoteCommand::build(input_token, output_token, amount, state) { + Ok(command) => ParseMessageResult::Success(command), + Err(error) => build_error_response(error, &state.data), + } + } +} + pub(crate) struct QuoteCommand { user_id: UserId, input_token: TokenInfo, @@ -71,53 +105,26 @@ impl QuoteCommand { Err(CommonErrors::PairNotSupported) } } +} + +impl Command for QuoteCommand { + fn message_id(&self) -> MessageId { + self.message_id + } - pub fn build_message_text(&self) -> String { + fn build_message(&self) -> MessageContentInitial { let mut text = "Quotes:".to_string(); for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { let exchange_name = exchange_id.to_string(); let status_text = status.to_string(); text.push_str(&format!("\n{exchange_name}: {status_text}")); } - text - } -} - -impl Command for QuoteCommand { - fn process_message(message: &MessageContent, state: &mut RuntimeState) -> ProcessCommandResult { - let text = message.text().unwrap_or_default(); - if !REGEX.is_match(&text) { - return ProcessCommandResult::Continue; - } - - let matches = REGEX.captures_iter(&text).next().unwrap(); - let input_token = &matches["input_token"]; - let output_token = &matches["output_token"]; - let amount_decimal = f64::from_str(&matches["amount"]).unwrap(); - - let (input_token, output_token) = match state.data.get_token_pair(input_token, output_token) { - Ok((i, o)) => (i, o), - Err(tokens) => { - let error = CommonErrors::UnsupportedTokens(tokens); - return build_error_response(error, &state.data); - } - }; - - let amount = (amount_decimal * 10u128.pow(input_token.decimals as u32) as f64) as u128; - - match QuoteCommand::build(input_token, output_token, amount, state) { - Ok(command) => ProcessCommandResult::Success( - state - .data - .build_response(command.build_message_text(), Some(command.message_id)), - ), - Err(error) => build_error_response(error, &state.data), - } + MessageContentInitial::Text(TextContent { text }) } } -fn build_error_response(error: CommonErrors, data: &Data) -> ProcessCommandResult { +fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { let response_message = error.build_response_message(data); - ProcessCommandResult::Success(data.build_response(response_message, None)) + ParseMessageResult::Error(data.build_text_response(response_message, None)) } diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index b9e283372f..b7b5b79145 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -145,10 +145,18 @@ impl Data { .collect() } - pub fn build_response( + pub fn build_text_response( &self, text: String, message_id: Option, + ) -> exchange_bot_canister::handle_direct_message::Response { + self.build_response(MessageContentInitial::Text(TextContent { text }), message_id) + } + + pub fn build_response( + &self, + message: MessageContentInitial, + message_id: Option, ) -> exchange_bot_canister::handle_direct_message::Response { let (username, display_name) = (self.username.clone(), self.display_name.clone()); @@ -157,7 +165,7 @@ impl Data { bot_name: username, bot_display_name: display_name, messages: vec![BotMessage { - content: MessageContentInitial::Text(TextContent { text }), + content: message, message_id, }], }, diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 713165aa14..41281f4937 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -1,5 +1,5 @@ -use crate::commands::quote::QuoteCommand; -use crate::commands::{Command, ProcessCommandResult}; +use crate::commands::quote::QuoteCommandParser; +use crate::commands::{Command, CommandParser, ParseMessageResult}; use crate::{mutate_state, read_state, RuntimeState}; use candid::Principal; use canister_api_macros::update_msgpack; @@ -12,15 +12,17 @@ use types::UserId; #[trace] async fn handle_direct_message(args: Args) -> Response { if let Err(message) = verify_caller().await { - return read_state(|state| state.data.build_response(message, None)); + return read_state(|state| state.data.build_text_response(message, None)); }; - mutate_state(|state| { - if let ProcessCommandResult::Success(response) = QuoteCommand::process_message(&args.content, state) { - response - } else { - todo!() + mutate_state(|state| match QuoteCommandParser::try_parse(&args.content, state) { + ParseMessageResult::Success(command) => { + let message = command.build_message(); + let message_id = command.message_id(); + state.data.build_response(message, Some(message_id)) } + ParseMessageResult::Error(response) => response, + ParseMessageResult::DoesNotMatch => todo!(), }) } From 1bf7852e34cc2880b5762e5a2923340f4d0a8479 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 15 Sep 2023 12:09:21 +0100 Subject: [PATCH 10/39] More --- Cargo.lock | 40 +++++++++++++++++ Cargo.toml | 1 + .../canisters/exchange_bot/impl/Cargo.toml | 1 + .../impl/src/commands/common_errors.rs | 4 +- .../exchange_bot/impl/src/commands/mod.rs | 40 ++++++++++++----- .../exchange_bot/impl/src/commands/quote.rs | 43 +++++++++---------- .../exchange_bot/impl/src/icpswap/mod.rs | 1 + .../canisters/exchange_bot/impl/src/lib.rs | 9 ++++ .../impl/src/model/commands_pending.rs | 29 +++++++++++++ .../exchange_bot/impl/src/model/mod.rs | 2 +- .../exchange_bot/impl/src/swap_client.rs | 3 ++ .../impl/src/updates/handle_direct_message.rs | 4 +- backend/libraries/icpswap_client/src/lib.rs | 2 + 13 files changed, 142 insertions(+), 37 deletions(-) create mode 100644 backend/canisters/exchange_bot/impl/src/model/commands_pending.rs diff --git a/Cargo.lock b/Cargo.lock index 731fc49e31..93f0221853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,6 +1527,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.2" @@ -1597,6 +1606,7 @@ dependencies = [ "serializer", "tracing", "types", + "typetag", "user_index_canister_c2c_client", "utils", ] @@ -2724,6 +2734,12 @@ dependencies = [ "utils", ] +[[package]] +name = "inventory" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1be380c410bf0595e94992a648ea89db4dd3f3354ba54af206fd2a68cf5ac8e" + [[package]] name = "ipnet" version = "2.8.0" @@ -5394,6 +5410,30 @@ dependencies = [ "sha256", ] +[[package]] +name = "typetag" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80960fd143d4c96275c0e60b08f14b81fbb468e79bc0ef8fbda69fb0afafae43" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc13d450dc4a695200da3074dacf43d449b968baee95e341920e47f61a3b40f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index f4cd942b64..d937b33749 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,6 +175,7 @@ tokio = "1.32.0" tracing = "0.1.37" tracing-attributes = "0.1.26" tracing-subscriber = "0.3.17" +typetag = "0.2.13" web-push = { version = "0.10.0", default-features = false, features = ["hyper-client"] } x509-parser = "0.15.1" diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml index e6f301fab4..77df1d7dfc 100644 --- a/backend/canisters/exchange_bot/impl/Cargo.toml +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -36,5 +36,6 @@ serde = { workspace = true } serializer = { path = "../../../libraries/serializer" } tracing = { workspace = true } types = { path = "../../../libraries/types" } +typetag = { workspace = true } user_index_canister_c2c_client = { path = "../../user_index/c2c_client" } utils = { path = "../../../libraries/utils" } diff --git a/backend/canisters/exchange_bot/impl/src/commands/common_errors.rs b/backend/canisters/exchange_bot/impl/src/commands/common_errors.rs index 954056f5c8..5053960b57 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/common_errors.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/common_errors.rs @@ -1,12 +1,12 @@ use crate::Data; -pub(crate) enum CommonErrors { +pub enum CommonErrors { UnsupportedTokens(Vec), PairNotSupported, } impl CommonErrors { - pub fn build_response_message(&self, data: &Data) -> String { + pub(crate) fn build_response_message(&self, data: &Data) -> String { match self { CommonErrors::UnsupportedTokens(tokens) => { let mut message = "The following inputs were not recognised as supported tokens:".to_string(); diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 0f61f7da64..63740e1c2c 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -1,22 +1,42 @@ +use crate::commands::quote::QuoteCommand; use crate::RuntimeState; -use types::{MessageContent, MessageContentInitial, MessageId}; +use serde::{Deserialize, Serialize}; +use types::{MessageContent, MessageContentInitial, MessageId, UserId}; -pub(crate) mod common_errors; -pub(crate) mod quote; +pub mod common_errors; +pub mod quote; pub(crate) trait CommandParser { - type Command: Command; + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult; +} - fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult; +#[derive(Serialize, Deserialize)] +pub enum Command { + Quote(QuoteCommand), } -pub(crate) trait Command { - fn message_id(&self) -> MessageId; - fn build_message(&self) -> MessageContentInitial; +impl Command { + pub fn user_id(&self) -> UserId { + match self { + Command::Quote(q) => q.user_id, + } + } + + pub fn message_id(&self) -> MessageId { + match self { + Command::Quote(q) => q.message_id, + } + } + + pub fn build_message(&self) -> MessageContentInitial { + match self { + Command::Quote(q) => q.build_message(), + } + } } -pub(crate) enum ParseMessageResult { - Success(C), +pub enum ParseMessageResult { + Success(Command), Error(exchange_bot_canister::handle_direct_message::Response), DoesNotMatch, } diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 8294882a15..32bf7f2869 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -10,7 +10,7 @@ use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::str::FromStr; -use types::{MessageContent, MessageContentInitial, MessageId, TextContent, TokenInfo, UserId}; +use types::{MessageContent, MessageContentInitial, MessageId, TextContent, TimestampMillis, TokenInfo, UserId}; lazy_static! { static ref REGEX: Regex = RegexBuilder::new(r"quote (?\S+) (?\S+) (?[\d.,]+)") @@ -19,12 +19,10 @@ lazy_static! { .unwrap(); } -pub(crate) struct QuoteCommandParser; +pub struct QuoteCommandParser; impl CommandParser for QuoteCommandParser { - type Command = QuoteCommand; - - fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { let text = message.text().unwrap_or_default(); if !REGEX.is_match(&text) { @@ -47,20 +45,23 @@ impl CommandParser for QuoteCommandParser { let amount = (amount_decimal * 10u128.pow(input_token.decimals as u32) as f64) as u128; match QuoteCommand::build(input_token, output_token, amount, state) { - Ok(command) => ParseMessageResult::Success(command), + Ok(command) => ParseMessageResult::Success(Command::Quote(command)), Err(error) => build_error_response(error, &state.data), } } } -pub(crate) struct QuoteCommand { - user_id: UserId, - input_token: TokenInfo, - output_token: TokenInfo, - amount: u128, - clients: Vec>, - message_id: MessageId, - quote_statuses: Vec<(ExchangeId, QuoteStatus)>, +#[derive(Serialize, Deserialize)] +pub struct QuoteCommand { + pub created: TimestampMillis, + pub user_id: UserId, + pub input_token: TokenInfo, + pub output_token: TokenInfo, + pub amount: u128, + pub clients: Vec>, + pub message_id: MessageId, + pub quote_statuses: Vec<(ExchangeId, QuoteStatus)>, + pub in_progress: Option, // The time it started being processed } #[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] @@ -81,7 +82,7 @@ impl Display for QuoteStatus { } impl QuoteCommand { - pub fn build( + pub(crate) fn build( input_token: TokenInfo, output_token: TokenInfo, amount: u128, @@ -93,6 +94,7 @@ impl QuoteCommand { let quote_statuses = clients.iter().map(|c| (c.exchange_id(), QuoteStatus::Pending)).collect(); Ok(QuoteCommand { + created: state.env.now(), user_id: state.env.caller().into(), input_token, output_token, @@ -100,19 +102,14 @@ impl QuoteCommand { clients, message_id: state.env.rng().gen(), quote_statuses, + in_progress: None, }) } else { Err(CommonErrors::PairNotSupported) } } -} - -impl Command for QuoteCommand { - fn message_id(&self) -> MessageId { - self.message_id - } - fn build_message(&self) -> MessageContentInitial { + pub fn build_message(&self) -> MessageContentInitial { let mut text = "Quotes:".to_string(); for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { let exchange_name = exchange_id.to_string(); @@ -124,7 +121,7 @@ impl Command for QuoteCommand { } } -fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { +fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { let response_message = error.build_response_message(data); ParseMessageResult::Error(data.build_text_response(response_message, None)) } diff --git a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs index 54a573bfaa..90445acceb 100644 --- a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs @@ -53,6 +53,7 @@ impl SwapClientFactory for ICPSwapClientFactory { } #[async_trait] +#[typetag::serde] impl SwapClient for ICPSwapClient { fn exchange_id(&self) -> ExchangeId { ExchangeId::ICPSwap diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index b7b5b79145..1a8f1b6de8 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -1,4 +1,6 @@ +use crate::commands::Command; use crate::icpswap::ICPSwapClientFactory; +use crate::model::commands_pending::CommandsPending; use crate::swap_client::{SwapClient, SwapClientFactory}; use candid::Principal; use canister_state_macros::canister_state; @@ -67,6 +69,11 @@ impl RuntimeState { self.data.governance_principals.contains(&caller) } + pub fn enqueue_command(&mut self, command: Command) { + self.data.commands_pending.push(command); + // start job + } + pub fn metrics(&self) -> Metrics { Metrics { memory_used: utils::memory::used(), @@ -91,6 +98,7 @@ struct Data { cycles_dispenser_canister_id: CanisterId, token_info: Vec, known_callers: HashMap, + commands_pending: CommandsPending, username: String, display_name: Option, is_registered: bool, @@ -112,6 +120,7 @@ impl Data { cycles_dispenser_canister_id, token_info: build_token_info(), known_callers: HashMap::new(), + commands_pending: CommandsPending::default(), username: "".to_string(), display_name: None, is_registered: false, diff --git a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs new file mode 100644 index 0000000000..0d57119b96 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs @@ -0,0 +1,29 @@ +use crate::commands::Command; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use types::{MessageId, UserId}; + +#[derive(Serialize, Deserialize, Default)] +pub struct CommandsPending { + commands: Vec, +} + +impl CommandsPending { + pub fn push(&mut self, command: Command) { + self.commands.push(command); + } + + pub fn get(&self, user_id: UserId, message_id: MessageId) -> Option<&Command> { + self.commands + .iter() + .find(|c| c.user_id() == user_id && c.message_id() == message_id) + } + + pub fn remove(&mut self, user_id: UserId, message_id: MessageId) -> Option { + self.commands + .iter() + .find_position(|c| c.user_id() == user_id && c.message_id() == message_id) + .map(|(i, _)| i) + .map(|i| self.commands.remove(i)) + } +} diff --git a/backend/canisters/exchange_bot/impl/src/model/mod.rs b/backend/canisters/exchange_bot/impl/src/model/mod.rs index 8b13789179..add3cbae66 100644 --- a/backend/canisters/exchange_bot/impl/src/model/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/model/mod.rs @@ -1 +1 @@ - +pub mod commands_pending; diff --git a/backend/canisters/exchange_bot/impl/src/swap_client.rs b/backend/canisters/exchange_bot/impl/src/swap_client.rs index edef47a7db..360e4ab6ff 100644 --- a/backend/canisters/exchange_bot/impl/src/swap_client.rs +++ b/backend/canisters/exchange_bot/impl/src/swap_client.rs @@ -1,6 +1,8 @@ use async_trait::async_trait; use exchange_bot_canister::ExchangeId; use ic_cdk::api::call::CallResult; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; use types::icrc1::Account; use types::{CanisterId, TokenInfo}; @@ -14,6 +16,7 @@ pub trait SwapClientFactory { } #[async_trait] +#[typetag::serde(tag = "type")] pub trait SwapClient { fn exchange_id(&self) -> ExchangeId; async fn quote(&self, amount: u128) -> CallResult; diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 41281f4937..3bb9e80156 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -19,7 +19,9 @@ async fn handle_direct_message(args: Args) -> Response { ParseMessageResult::Success(command) => { let message = command.build_message(); let message_id = command.message_id(); - state.data.build_response(message, Some(message_id)) + let response = state.data.build_response(message, Some(message_id)); + state.enqueue_command(command); + response } ParseMessageResult::Error(response) => response, ParseMessageResult::DoesNotMatch => todo!(), diff --git a/backend/libraries/icpswap_client/src/lib.rs b/backend/libraries/icpswap_client/src/lib.rs index b8f862344b..b7830d3945 100644 --- a/backend/libraries/icpswap_client/src/lib.rs +++ b/backend/libraries/icpswap_client/src/lib.rs @@ -2,9 +2,11 @@ use candid::Nat; use ic_cdk::api::call::{CallResult, RejectionCode}; use icpswap_swap_pool_canister::{ICPSwapError, ICPSwapResult}; use ledger_utils::convert_to_subaccount; +use serde::{Deserialize, Serialize}; use types::icrc1::Account; use types::{CanisterId, TokenInfo}; +#[derive(Serialize, Deserialize)] pub struct ICPSwapClient { this_canister_id: CanisterId, swap_canister_id: CanisterId, From 2d04546ef5d803ec2c07c32f7db49ad409a077e2 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 15 Sep 2023 14:08:16 +0100 Subject: [PATCH 11/39] More --- Cargo.lock | 3 + .../canisters/community/impl/src/jobs/mod.rs | 4 +- backend/canisters/exchange_bot/api/src/lib.rs | 2 +- .../canisters/exchange_bot/impl/Cargo.toml | 3 + .../exchange_bot/impl/src/commands/mod.rs | 13 +++- .../exchange_bot/impl/src/commands/quote.rs | 57 ++++++++++++-- .../impl/src/jobs/edit_messages.rs | 75 +++++++++++++++++++ .../exchange_bot/impl/src/jobs/mod.rs | 6 +- .../canisters/exchange_bot/impl/src/lib.rs | 17 ++++- .../impl/src/model/commands_pending.rs | 6 ++ .../impl/src/model/message_edits_queue.rs | 33 ++++++++ .../exchange_bot/impl/src/model/mod.rs | 1 + .../canisters/exchange_bot/impl/src/quote.rs | 63 ---------------- .../exchange_bot/impl/src/swap_client.rs | 2 - .../impl/src/updates/handle_direct_message.rs | 4 +- .../chat_events/src/chat_event_internal.rs | 2 +- backend/libraries/ledger_utils/src/lib.rs | 4 +- backend/libraries/types/src/cryptocurrency.rs | 2 +- 18 files changed, 211 insertions(+), 86 deletions(-) create mode 100644 backend/canisters/exchange_bot/impl/src/jobs/edit_messages.rs create mode 100644 backend/canisters/exchange_bot/impl/src/model/message_edits_queue.rs delete mode 100644 backend/canisters/exchange_bot/impl/src/quote.rs diff --git a/Cargo.lock b/Cargo.lock index 93f0221853..aeba5ce89d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1598,6 +1598,7 @@ dependencies = [ "icrc1_ledger_canister_c2c_client", "itertools", "lazy_static", + "ledger_utils", "local_user_index_canister_c2c_client", "msgpack", "rand", @@ -1607,6 +1608,8 @@ dependencies = [ "tracing", "types", "typetag", + "user_canister", + "user_canister_c2c_client", "user_index_canister_c2c_client", "utils", ] diff --git a/backend/canisters/community/impl/src/jobs/mod.rs b/backend/canisters/community/impl/src/jobs/mod.rs index a63f7fa12b..b6203a40c9 100644 --- a/backend/canisters/community/impl/src/jobs/mod.rs +++ b/backend/canisters/community/impl/src/jobs/mod.rs @@ -1,7 +1,7 @@ -pub mod import_groups; - use crate::RuntimeState; +pub mod import_groups; + pub(crate) fn start(state: &RuntimeState) { import_groups::start_job_if_required(state); } diff --git a/backend/canisters/exchange_bot/api/src/lib.rs b/backend/canisters/exchange_bot/api/src/lib.rs index 047d553c6f..dc85062b07 100644 --- a/backend/canisters/exchange_bot/api/src/lib.rs +++ b/backend/canisters/exchange_bot/api/src/lib.rs @@ -8,7 +8,7 @@ mod updates; pub use lifecycle::*; pub use updates::*; -#[derive(CandidType, Serialize, Deserialize, Clone, Copy, Debug)] +#[derive(CandidType, Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)] pub enum ExchangeId { ICPSwap, } diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml index 77df1d7dfc..a235b92792 100644 --- a/backend/canisters/exchange_bot/impl/Cargo.toml +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -28,6 +28,7 @@ icpswap_client = { path = "../../../libraries/icpswap_client" } icrc1_ledger_canister_c2c_client = { path = "../../../external_canisters/icrc1_ledger/c2c_client" } itertools = { workspace = true } lazy_static = { workspace = true } +ledger_utils = { path = "../../../libraries/ledger_utils" } local_user_index_canister_c2c_client = { path = "../../local_user_index/c2c_client" } msgpack = { path = "../../../libraries/msgpack" } rand = { workspace = true } @@ -37,5 +38,7 @@ serializer = { path = "../../../libraries/serializer" } tracing = { workspace = true } types = { path = "../../../libraries/types" } typetag = { workspace = true } +user_canister = { path = "../../user/api" } +user_canister_c2c_client = { path = "../../user/c2c_client" } user_index_canister_c2c_client = { path = "../../user_index/c2c_client" } utils = { path = "../../../libraries/utils" } diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 63740e1c2c..fc1550f83d 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -1,7 +1,7 @@ use crate::commands::quote::QuoteCommand; use crate::RuntimeState; use serde::{Deserialize, Serialize}; -use types::{MessageContent, MessageContentInitial, MessageId, UserId}; +use types::{MessageContent, MessageContentInitial, MessageId, TextContent, UserId}; pub mod common_errors; pub mod quote; @@ -28,13 +28,22 @@ impl Command { } } + pub async fn process(self) { + match self { + Command::Quote(q) => q.process().await, + } + } + pub fn build_message(&self) -> MessageContentInitial { match self { - Command::Quote(q) => q.build_message(), + Command::Quote(q) => MessageContentInitial::Text(TextContent { + text: q.build_message_text(), + }), } } } +#[allow(clippy::large_enum_variant)] pub enum ParseMessageResult { Success(Command), Error(exchange_bot_canister::handle_direct_message::Response), diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 32bf7f2869..a96f595c00 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -1,16 +1,17 @@ use crate::commands::common_errors::CommonErrors; use crate::commands::{Command, CommandParser, ParseMessageResult}; use crate::swap_client::SwapClient; -use crate::{Data, RuntimeState}; +use crate::{mutate_state, Data, RuntimeState}; use exchange_bot_canister::ExchangeId; use itertools::Itertools; use lazy_static::lazy_static; +use ledger_utils::format_crypto_amount; use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::str::FromStr; -use types::{MessageContent, MessageContentInitial, MessageId, TextContent, TimestampMillis, TokenInfo, UserId}; +use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; lazy_static! { static ref REGEX: Regex = RegexBuilder::new(r"quote (?\S+) (?\S+) (?[\d.,]+)") @@ -25,11 +26,11 @@ impl CommandParser for QuoteCommandParser { fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { let text = message.text().unwrap_or_default(); - if !REGEX.is_match(&text) { + if !REGEX.is_match(text) { return ParseMessageResult::DoesNotMatch; } - let matches = REGEX.captures_iter(&text).next().unwrap(); + let matches = REGEX.captures_iter(text).next().unwrap(); let input_token = &matches["input_token"]; let output_token = &matches["output_token"]; let amount_decimal = f64::from_str(&matches["amount"]).unwrap(); @@ -74,7 +75,7 @@ pub enum QuoteStatus { impl Display for QuoteStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - QuoteStatus::Success(_, text) => f.write_str(&text), + QuoteStatus::Success(_, text) => f.write_str(text), QuoteStatus::Failed(_) => f.write_str("Failed"), QuoteStatus::Pending => f.write_str("Pending"), } @@ -109,18 +110,60 @@ impl QuoteCommand { } } - pub fn build_message(&self) -> MessageContentInitial { + pub async fn process(self) { + let futures: Vec<_> = self + .clients + .into_iter() + .map(|c| quote_single(c, self.user_id, self.message_id, self.amount, self.output_token.decimals)) + .collect(); + + futures::future::join_all(futures).await; + } + + pub fn build_message_text(&self) -> String { let mut text = "Quotes:".to_string(); for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { let exchange_name = exchange_id.to_string(); let status_text = status.to_string(); text.push_str(&format!("\n{exchange_name}: {status_text}")); } + text + } - MessageContentInitial::Text(TextContent { text }) + fn set_status(&mut self, exchange_id: ExchangeId, new_status: QuoteStatus) { + if let Some(status) = self + .quote_statuses + .iter_mut() + .find(|(e, _)| *e == exchange_id) + .map(|(_, s)| s) + { + *status = new_status; + } } } +async fn quote_single( + client: Box, + user_id: UserId, + message_id: MessageId, + amount: u128, + output_token_decimals: u8, +) { + let result = client.quote(amount).await; + + mutate_state(|state| { + if let Some(Command::Quote(command)) = state.data.commands_pending.get_mut(user_id, message_id) { + let status = match result { + Ok(amount_out) => QuoteStatus::Success(amount_out, format_crypto_amount(amount_out, output_token_decimals)), + Err(error) => QuoteStatus::Failed(format!("{error:?}")), + }; + command.set_status(client.exchange_id(), status); + let text = command.build_message_text(); + state.enqueue_message_edit(user_id, message_id, text, false); + } + }) +} + fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { let response_message = error.build_response_message(data); ParseMessageResult::Error(data.build_text_response(response_message, None)) diff --git a/backend/canisters/exchange_bot/impl/src/jobs/edit_messages.rs b/backend/canisters/exchange_bot/impl/src/jobs/edit_messages.rs new file mode 100644 index 0000000000..1dce7b65bc --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/jobs/edit_messages.rs @@ -0,0 +1,75 @@ +use crate::{mutate_state, RuntimeState}; +use ic_cdk_timers::TimerId; +use std::cell::Cell; +use std::time::Duration; +use tracing::trace; +use types::{MessageContent, MessageId, TextContent, UserId}; + +const MAX_BATCH_SIZE: usize = 10; + +thread_local! { + static TIMER_ID: Cell> = Cell::default(); +} + +pub(crate) fn start_job_if_required(state: &RuntimeState) -> bool { + if TIMER_ID.with(|t| t.get().is_none()) && !state.data.message_edits_queue.is_empty() { + let timer_id = ic_cdk_timers::set_timer_interval(Duration::ZERO, run); + TIMER_ID.with(|t| t.set(Some(timer_id))); + trace!("'edit_messages' job started"); + true + } else { + false + } +} + +fn run() { + match mutate_state(next_batch) { + Some(batch) => ic_cdk::spawn(process_batch(batch)), + None => { + if let Some(timer_id) = TIMER_ID.with(|t| t.take()) { + ic_cdk_timers::clear_timer(timer_id); + trace!("'edit_messages' job stopped"); + } + } + } +} + +fn next_batch(state: &mut RuntimeState) -> Option> { + let mut batch = Vec::new(); + while let Some(next) = state.data.message_edits_queue.pop() { + batch.push(next); + if batch.len() == MAX_BATCH_SIZE { + break; + } + } + if !batch.is_empty() { + Some(batch) + } else { + None + } +} + +async fn process_batch(batch: Vec<(UserId, MessageId, String)>) { + let futures: Vec<_> = batch + .into_iter() + .map(|(user_id, message_id, text)| process_single(user_id, message_id, text)) + .collect(); + + futures::future::join_all(futures).await; +} + +async fn process_single(user_id: UserId, message_id: MessageId, text: String) { + let args = user_canister::c2c_edit_message::Args { + message_id, + content: MessageContent::Text(TextContent { text: text.clone() }), + correlation_id: 0, + }; + if user_canister_c2c_client::c2c_edit_message(user_id.into(), &args) + .await + .is_err() + { + mutate_state(|state| { + state.enqueue_message_edit(user_id, message_id, text, false); + }); + } +} diff --git a/backend/canisters/exchange_bot/impl/src/jobs/mod.rs b/backend/canisters/exchange_bot/impl/src/jobs/mod.rs index 01f9150d4a..e508906341 100644 --- a/backend/canisters/exchange_bot/impl/src/jobs/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/jobs/mod.rs @@ -1,3 +1,7 @@ use crate::RuntimeState; -pub(crate) fn start(_state: &RuntimeState) {} +pub mod edit_messages; + +pub(crate) fn start(state: &RuntimeState) { + edit_messages::start_job_if_required(state); +} diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index 1a8f1b6de8..cae4df1fff 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -1,6 +1,7 @@ use crate::commands::Command; use crate::icpswap::ICPSwapClientFactory; use crate::model::commands_pending::CommandsPending; +use crate::model::message_edits_queue::MessageEditsQueue; use crate::swap_client::{SwapClient, SwapClientFactory}; use candid::Principal; use canister_state_macros::canister_state; @@ -11,7 +12,7 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use types::{ BotMessage, BuildVersion, CanisterId, Cryptocurrency, Cycles, MessageContentInitial, MessageId, TextContent, - TimestampMillis, Timestamped, TokenInfo, + TimestampMillis, Timestamped, TokenInfo, UserId, }; use utils::env::Environment; @@ -74,6 +75,16 @@ impl RuntimeState { // start job } + pub fn enqueue_message_edit(&mut self, user_id: UserId, message_id: MessageId, text: String, overwrite_existing: bool) { + if self + .data + .message_edits_queue + .push(user_id, message_id, text, overwrite_existing) + { + jobs::edit_messages::start_job_if_required(self); + } + } + pub fn metrics(&self) -> Metrics { Metrics { memory_used: utils::memory::used(), @@ -99,6 +110,7 @@ struct Data { token_info: Vec, known_callers: HashMap, commands_pending: CommandsPending, + message_edits_queue: MessageEditsQueue, username: String, display_name: Option, is_registered: bool, @@ -121,6 +133,7 @@ impl Data { token_info: build_token_info(), known_callers: HashMap::new(), commands_pending: CommandsPending::default(), + message_edits_queue: MessageEditsQueue::default(), username: "".to_string(), display_name: None, is_registered: false, @@ -129,7 +142,7 @@ impl Data { } pub fn get_token_pair(&self, input_token: &str, output_token: &str) -> Result<(TokenInfo, TokenInfo), Vec> { - match (self.get_token(&input_token), self.get_token(&output_token)) { + match (self.get_token(input_token), self.get_token(output_token)) { (Some(i), Some(o)) => Ok((i, o)), (None, Some(_)) => Err(vec![input_token.to_string()]), (Some(_), None) => Err(vec![output_token.to_string()]), diff --git a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs index 0d57119b96..1cd1e1ad30 100644 --- a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs +++ b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs @@ -19,6 +19,12 @@ impl CommandsPending { .find(|c| c.user_id() == user_id && c.message_id() == message_id) } + pub fn get_mut(&mut self, user_id: UserId, message_id: MessageId) -> Option<&mut Command> { + self.commands + .iter_mut() + .find(|c| c.user_id() == user_id && c.message_id() == message_id) + } + pub fn remove(&mut self, user_id: UserId, message_id: MessageId) -> Option { self.commands .iter() diff --git a/backend/canisters/exchange_bot/impl/src/model/message_edits_queue.rs b/backend/canisters/exchange_bot/impl/src/model/message_edits_queue.rs new file mode 100644 index 0000000000..4ddbfdab75 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/model/message_edits_queue.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; +use std::collections::btree_map::Entry::{Occupied, Vacant}; +use std::collections::BTreeMap; +use types::{MessageId, UserId}; + +#[derive(Serialize, Deserialize, Default)] +pub struct MessageEditsQueue { + messages: BTreeMap<(UserId, MessageId), String>, +} + +impl MessageEditsQueue { + pub fn push(&mut self, user_id: UserId, message_id: MessageId, text: String, overwrite_existing: bool) -> bool { + match self.messages.entry((user_id, message_id)) { + Vacant(e) => { + e.insert(text); + true + } + Occupied(mut e) if overwrite_existing => { + e.insert(text); + true + } + _ => false, + } + } + + pub fn pop(&mut self) -> Option<(UserId, MessageId, String)> { + self.messages.pop_first().map(|((u, m), t)| (u, m, t)) + } + + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } +} diff --git a/backend/canisters/exchange_bot/impl/src/model/mod.rs b/backend/canisters/exchange_bot/impl/src/model/mod.rs index add3cbae66..414569b755 100644 --- a/backend/canisters/exchange_bot/impl/src/model/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/model/mod.rs @@ -1 +1,2 @@ pub mod commands_pending; +pub mod message_edits_queue; diff --git a/backend/canisters/exchange_bot/impl/src/quote.rs b/backend/canisters/exchange_bot/impl/src/quote.rs deleted file mode 100644 index b12c833df4..0000000000 --- a/backend/canisters/exchange_bot/impl/src/quote.rs +++ /dev/null @@ -1,63 +0,0 @@ -// use crate::swap_client::SwapClient; -// use crate::{read_state, RuntimeState}; -// use exchange_bot_canister::ExchangeId; -// use serde::{Deserialize, Serialize}; -// use types::{TokenInfo, UserId}; -// -// pub struct QuoteArgs { -// pub caller: UserId, -// pub input_token: TokenInfo, -// pub output_token: TokenInfo, -// pub amount: u128, -// } -// -// pub struct PrepareQuoteResult { -// message_text: String, -// clients: Vec>, -// } -// -// #[derive(Serialize, Deserialize, Debug)] -// pub struct QuoteResult { -// pub quotes: Vec, -// pub failures: Vec, -// } -// -// #[derive(Serialize, Deserialize, Debug)] -// pub struct Quote { -// pub exchange_id: ExchangeId, -// pub amount_out: u128, -// } -// -// #[derive(Serialize, Deserialize, Debug)] -// pub struct Failure { -// pub exchange_id: ExchangeId, -// pub error: String, -// } -// -// async fn quote(args: QuoteArgs) { -// let PrepareResult { clients } = match read_state(|state| prepare(&args, state)) { -// Ok(ok) => ok, -// Err(response) => return response, -// }; -// -// let futures: Vec<_> = clients.into_iter().map(|c| quote_single(c, args.amount)).collect(); -// -// let results = futures::future::join_all(futures).await; -// -// let mut quotes = Vec::new(); -// let mut failures = Vec::new(); -// for (exchange_id, result) in results { -// match result { -// Ok(amount_out) => quotes.push(Quote { exchange_id, amount_out }), -// Err(error) => failures.push(Failure { exchange_id, error }), -// } -// } -// -// if failures.is_empty() { -// Success(quotes) -// } else if quotes.is_empty() { -// Failed(failures) -// } else { -// PartialSuccess(PartialSuccessResult { quotes, failures }) -// } -// } diff --git a/backend/canisters/exchange_bot/impl/src/swap_client.rs b/backend/canisters/exchange_bot/impl/src/swap_client.rs index 360e4ab6ff..3443b26c5f 100644 --- a/backend/canisters/exchange_bot/impl/src/swap_client.rs +++ b/backend/canisters/exchange_bot/impl/src/swap_client.rs @@ -1,8 +1,6 @@ use async_trait::async_trait; use exchange_bot_canister::ExchangeId; use ic_cdk::api::call::CallResult; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; use types::icrc1::Account; use types::{CanisterId, TokenInfo}; diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 3bb9e80156..8d9419bbb4 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -1,5 +1,5 @@ use crate::commands::quote::QuoteCommandParser; -use crate::commands::{Command, CommandParser, ParseMessageResult}; +use crate::commands::{CommandParser, ParseMessageResult}; use crate::{mutate_state, read_state, RuntimeState}; use candid::Principal; use canister_api_macros::update_msgpack; @@ -20,7 +20,7 @@ async fn handle_direct_message(args: Args) -> Response { let message = command.build_message(); let message_id = command.message_id(); let response = state.data.build_response(message, Some(message_id)); - state.enqueue_command(command); + ic_cdk::spawn(command.process()); response } ParseMessageResult::Error(response) => response, diff --git a/backend/libraries/chat_events/src/chat_event_internal.rs b/backend/libraries/chat_events/src/chat_event_internal.rs index 8031d1f03d..41581c563b 100644 --- a/backend/libraries/chat_events/src/chat_event_internal.rs +++ b/backend/libraries/chat_events/src/chat_event_internal.rs @@ -527,7 +527,7 @@ impl From<&MessageContentInternal> for Document { let amount = c.units(); // This is only used for string searching so it's better to default to 8 than to trap let decimals = c.token().decimals().unwrap_or(8); - let amount_string = format_crypto_amount(amount, decimals as u32); + let amount_string = format_crypto_amount(amount, decimals); document.add_field(amount_string, 1.0, false); } diff --git a/backend/libraries/ledger_utils/src/lib.rs b/backend/libraries/ledger_utils/src/lib.rs index fe63ed969d..319995b4cb 100644 --- a/backend/libraries/ledger_utils/src/lib.rs +++ b/backend/libraries/ledger_utils/src/lib.rs @@ -81,8 +81,8 @@ pub fn calculate_transaction_hash(sender: CanisterId, args: &TransferArgs) -> Tr transaction.hash() } -pub fn format_crypto_amount(units: u128, decimals: u32) -> String { - let subdividable_by = 10u128.pow(decimals); +pub fn format_crypto_amount(units: u128, decimals: u8) -> String { + let subdividable_by = 10u128.pow(decimals as u32); format!("{}.{:0}", units / subdividable_by, units % subdividable_by) } diff --git a/backend/libraries/types/src/cryptocurrency.rs b/backend/libraries/types/src/cryptocurrency.rs index 097da0063a..ac120c7c77 100644 --- a/backend/libraries/types/src/cryptocurrency.rs +++ b/backend/libraries/types/src/cryptocurrency.rs @@ -25,7 +25,7 @@ impl Cryptocurrency { } } - pub fn decimals(&self) -> Option { + pub fn decimals(&self) -> Option { match self { Cryptocurrency::InternetComputer => Some(8), Cryptocurrency::SNS1 => Some(8), From 748dd82b75764ced47e36da05f39d45eafcae0ee Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 15 Sep 2023 14:36:26 +0100 Subject: [PATCH 12/39] Fix call to register bot --- .../user_index/c2c_client/src/lib.rs | 4 +-- .../canister_client_macros/src/lib.rs | 34 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/backend/canisters/user_index/c2c_client/src/lib.rs b/backend/canisters/user_index/c2c_client/src/lib.rs index 211ac6e141..b75bac2e93 100644 --- a/backend/canisters/user_index/c2c_client/src/lib.rs +++ b/backend/canisters/user_index/c2c_client/src/lib.rs @@ -1,5 +1,5 @@ use candid::Principal; -use canister_client::{generate_c2c_call, generate_c2c_call_with_payment, generate_candid_c2c_call}; +use canister_client::{generate_c2c_call, generate_candid_c2c_call, generate_candid_c2c_call_with_payment}; use types::{CanisterId, UserDetails}; use user_index_canister::*; @@ -13,7 +13,7 @@ generate_c2c_call!(c2c_migrate_user_principal); generate_c2c_call!(c2c_notify_events); generate_c2c_call!(c2c_set_avatar); generate_c2c_call!(c2c_suspend_users); -generate_c2c_call_with_payment!(c2c_register_bot); +generate_candid_c2c_call_with_payment!(c2c_register_bot); #[derive(Debug)] pub enum LookupUserError { diff --git a/backend/libraries/canister_client_macros/src/lib.rs b/backend/libraries/canister_client_macros/src/lib.rs index 0a017e6359..37e7e62af1 100644 --- a/backend/libraries/canister_client_macros/src/lib.rs +++ b/backend/libraries/canister_client_macros/src/lib.rs @@ -65,40 +65,40 @@ macro_rules! generate_c2c_call { } #[macro_export] -macro_rules! generate_c2c_call_with_payment { +macro_rules! generate_candid_c2c_call { ($method_name:ident) => { pub async fn $method_name( - canister_id: types::CanisterId, + canister_id: ::types::CanisterId, args: &$method_name::Args, - cycles: types::Cycles, - ) -> ic_cdk::api::call::CallResult<$method_name::Response> { - let method_name = concat!(stringify!($method_name), "_msgpack"); + ) -> ::ic_cdk::api::call::CallResult<$method_name::Response> { + let method_name = stringify!($method_name); - canister_client::make_c2c_call_with_payment( - canister_id, - method_name, - args, - msgpack::serialize, - |r| msgpack::deserialize(r), - cycles, - ) + canister_client::make_c2c_call(canister_id, method_name, args, ::candid::encode_one, |r| { + ::candid::decode_one(r) + }) .await } }; } #[macro_export] -macro_rules! generate_candid_c2c_call { +macro_rules! generate_candid_c2c_call_with_payment { ($method_name:ident) => { pub async fn $method_name( canister_id: ::types::CanisterId, args: &$method_name::Args, + cycles: ::types::Cycles, ) -> ::ic_cdk::api::call::CallResult<$method_name::Response> { let method_name = stringify!($method_name); - canister_client::make_c2c_call(canister_id, method_name, args, ::candid::encode_one, |r| { - ::candid::decode_one(r) - }) + canister_client::make_c2c_call_with_payment( + canister_id, + method_name, + args, + ::candid::encode_one, + |r| ::candid::decode_one(r), + cycles, + ) .await } }; From b136440dc171142dd3a937cc080f17e25178b337 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 15 Sep 2023 15:02:37 +0100 Subject: [PATCH 13/39] More --- Cargo.lock | 40 ------------------- Cargo.toml | 1 - .../canisters/exchange_bot/impl/Cargo.toml | 1 - .../exchange_bot/impl/src/commands/mod.rs | 4 +- .../exchange_bot/impl/src/commands/quote.rs | 29 +++++++++++--- .../exchange_bot/impl/src/icpswap/mod.rs | 1 - .../exchange_bot/impl/src/swap_client.rs | 1 - .../impl/src/updates/handle_direct_message.rs | 2 +- .../user_index/c2c_client/src/lib.rs | 2 +- 9 files changed, 27 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aeba5ce89d..c2791c0781 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,15 +1527,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "erased-serde" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" -dependencies = [ - "serde", -] - [[package]] name = "errno" version = "0.3.2" @@ -1607,7 +1598,6 @@ dependencies = [ "serializer", "tracing", "types", - "typetag", "user_canister", "user_canister_c2c_client", "user_index_canister_c2c_client", @@ -2737,12 +2727,6 @@ dependencies = [ "utils", ] -[[package]] -name = "inventory" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1be380c410bf0595e94992a648ea89db4dd3f3354ba54af206fd2a68cf5ac8e" - [[package]] name = "ipnet" version = "2.8.0" @@ -5413,30 +5397,6 @@ dependencies = [ "sha256", ] -[[package]] -name = "typetag" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80960fd143d4c96275c0e60b08f14b81fbb468e79bc0ef8fbda69fb0afafae43" -dependencies = [ - "erased-serde", - "inventory", - "once_cell", - "serde", - "typetag-impl", -] - -[[package]] -name = "typetag-impl" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc13d450dc4a695200da3074dacf43d449b968baee95e341920e47f61a3b40f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.29", -] - [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index d937b33749..f4cd942b64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,7 +175,6 @@ tokio = "1.32.0" tracing = "0.1.37" tracing-attributes = "0.1.26" tracing-subscriber = "0.3.17" -typetag = "0.2.13" web-push = { version = "0.10.0", default-features = false, features = ["hyper-client"] } x509-parser = "0.15.1" diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml index a235b92792..157b0b33cb 100644 --- a/backend/canisters/exchange_bot/impl/Cargo.toml +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -37,7 +37,6 @@ serde = { workspace = true } serializer = { path = "../../../libraries/serializer" } tracing = { workspace = true } types = { path = "../../../libraries/types" } -typetag = { workspace = true } user_canister = { path = "../../user/api" } user_canister_c2c_client = { path = "../../user/c2c_client" } user_index_canister_c2c_client = { path = "../../user_index/c2c_client" } diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index fc1550f83d..c14b215a22 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -28,9 +28,9 @@ impl Command { } } - pub async fn process(self) { + pub(crate) fn process(self, state: &mut RuntimeState) { match self { - Command::Quote(q) => q.process().await, + Command::Quote(q) => q.process(state), } } diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index a96f595c00..6e0ee96cca 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -59,7 +59,7 @@ pub struct QuoteCommand { pub input_token: TokenInfo, pub output_token: TokenInfo, pub amount: u128, - pub clients: Vec>, + pub exchange_ids: Vec, pub message_id: MessageId, pub quote_statuses: Vec<(ExchangeId, QuoteStatus)>, pub in_progress: Option, // The time it started being processed @@ -100,7 +100,7 @@ impl QuoteCommand { input_token, output_token, amount, - clients, + exchange_ids: clients.iter().map(|c| c.exchange_id()).collect(), message_id: state.env.rng().gen(), quote_statuses, in_progress: None, @@ -110,14 +110,21 @@ impl QuoteCommand { } } - pub async fn process(self) { + pub(crate) fn process(mut self, state: &mut RuntimeState) { + self.in_progress = Some(state.env.now()); + let futures: Vec<_> = self - .clients - .into_iter() + .exchange_ids + .iter() + .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) .map(|c| quote_single(c, self.user_id, self.message_id, self.amount, self.output_token.decimals)) .collect(); - futures::future::join_all(futures).await; + state.enqueue_command(Command::Quote(self)); + + ic_cdk::spawn(async { + futures::future::join_all(futures).await; + }); } pub fn build_message_text(&self) -> String { @@ -140,6 +147,10 @@ impl QuoteCommand { *status = new_status; } } + + fn is_finished(&self) -> bool { + !self.quote_statuses.iter().any(|(_, s)| matches!(s, QuoteStatus::Pending)) + } } async fn quote_single( @@ -158,8 +169,14 @@ async fn quote_single( Err(error) => QuoteStatus::Failed(format!("{error:?}")), }; command.set_status(client.exchange_id(), status); + let is_finished = command.is_finished(); + let text = command.build_message_text(); state.enqueue_message_edit(user_id, message_id, text, false); + + if is_finished { + state.data.commands_pending.remove(user_id, message_id); + } } }) } diff --git a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs index 90445acceb..54a573bfaa 100644 --- a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs @@ -53,7 +53,6 @@ impl SwapClientFactory for ICPSwapClientFactory { } #[async_trait] -#[typetag::serde] impl SwapClient for ICPSwapClient { fn exchange_id(&self) -> ExchangeId { ExchangeId::ICPSwap diff --git a/backend/canisters/exchange_bot/impl/src/swap_client.rs b/backend/canisters/exchange_bot/impl/src/swap_client.rs index 3443b26c5f..edef47a7db 100644 --- a/backend/canisters/exchange_bot/impl/src/swap_client.rs +++ b/backend/canisters/exchange_bot/impl/src/swap_client.rs @@ -14,7 +14,6 @@ pub trait SwapClientFactory { } #[async_trait] -#[typetag::serde(tag = "type")] pub trait SwapClient { fn exchange_id(&self) -> ExchangeId; async fn quote(&self, amount: u128) -> CallResult; diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 8d9419bbb4..71fad45e88 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -20,7 +20,7 @@ async fn handle_direct_message(args: Args) -> Response { let message = command.build_message(); let message_id = command.message_id(); let response = state.data.build_response(message, Some(message_id)); - ic_cdk::spawn(command.process()); + command.process(state); response } ParseMessageResult::Error(response) => response, diff --git a/backend/canisters/user_index/c2c_client/src/lib.rs b/backend/canisters/user_index/c2c_client/src/lib.rs index b75bac2e93..c53e4d4b65 100644 --- a/backend/canisters/user_index/c2c_client/src/lib.rs +++ b/backend/canisters/user_index/c2c_client/src/lib.rs @@ -11,9 +11,9 @@ generate_c2c_call!(user); // Updates generate_c2c_call!(c2c_migrate_user_principal); generate_c2c_call!(c2c_notify_events); +generate_candid_c2c_call_with_payment!(c2c_register_bot); generate_c2c_call!(c2c_set_avatar); generate_c2c_call!(c2c_suspend_users); -generate_candid_c2c_call_with_payment!(c2c_register_bot); #[derive(Debug)] pub enum LookupUserError { From a1222b5cf412c0eeb03af69d64e5321c06ae29c0 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 15 Sep 2023 17:20:30 +0100 Subject: [PATCH 14/39] Make amount optional --- backend/canisters/exchange_bot/impl/src/commands/quote.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 6e0ee96cca..85a420e1a5 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -14,7 +14,7 @@ use std::str::FromStr; use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; lazy_static! { - static ref REGEX: Regex = RegexBuilder::new(r"quote (?\S+) (?\S+) (?[\d.,]+)") + static ref REGEX: Regex = RegexBuilder::new(r"quote\s+(?\S+)\s+(?\S+)(\s+(?[\d.,]+))?") .case_insensitive(true) .build() .unwrap(); @@ -33,7 +33,10 @@ impl CommandParser for QuoteCommandParser { let matches = REGEX.captures_iter(text).next().unwrap(); let input_token = &matches["input_token"]; let output_token = &matches["output_token"]; - let amount_decimal = f64::from_str(&matches["amount"]).unwrap(); + let amount_decimal = matches + .name("amount") + .map(|m| f64::from_str(m.as_str()).unwrap()) + .unwrap_or(1.0); let (input_token, output_token) = match state.data.get_token_pair(input_token, output_token) { Ok((i, o)) => (i, o), From bd0363d42707db01608c50069fb0000832ca7e4f Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 15 Sep 2023 17:20:42 +0100 Subject: [PATCH 15/39] Add help text --- .../canisters/exchange_bot/impl/src/commands/mod.rs | 2 ++ .../canisters/exchange_bot/impl/src/commands/quote.rs | 10 ++++++++++ .../impl/src/updates/handle_direct_message.rs | 6 +++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index c14b215a22..1803d0178a 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -7,6 +7,8 @@ pub mod common_errors; pub mod quote; pub(crate) trait CommandParser { + fn help_text() -> &'static str; + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult; } diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 85a420e1a5..4bc39fb06b 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -23,6 +23,16 @@ lazy_static! { pub struct QuoteCommandParser; impl CommandParser for QuoteCommandParser { + fn help_text() -> &'static str { + "**QUOTE** + +format: 'quote $InputToken $OutputToken $Amount' + +eg. 'quote ICP CHAT 100' + +'$Amount' will default to 1 if not provided." + } + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { let text = message.text().unwrap_or_default(); diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 71fad45e88..20f3143c25 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -24,7 +24,11 @@ async fn handle_direct_message(args: Args) -> Response { response } ParseMessageResult::Error(response) => response, - ParseMessageResult::DoesNotMatch => todo!(), + ParseMessageResult::DoesNotMatch => { + let mut text = "This bot currently supports the following message formats:\n\n".to_string(); + text.push_str(QuoteCommandParser::help_text()); + state.data.build_text_response(text, None) + } }) } From d26a0e2fad90a0ad8f11da2883eddbd563db3c10 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 18 Sep 2023 09:44:16 +0100 Subject: [PATCH 16/39] Fix --- Cargo.lock | 2 +- backend/canister_upgrader/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfb8744cec..8f074ccf42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2117,7 +2117,7 @@ dependencies = [ "ic-stable-structures", "icpswap_client", "icrc1_ledger_canister_c2c_client", - "itertools", + "itertools 0.11.0", "lazy_static", "ledger_utils", "local_user_index_canister_c2c_client", diff --git a/backend/canister_upgrader/src/lib.rs b/backend/canister_upgrader/src/lib.rs index 0807899fbf..ed4d2faacf 100644 --- a/backend/canister_upgrader/src/lib.rs +++ b/backend/canister_upgrader/src/lib.rs @@ -179,7 +179,7 @@ pub async fn upgrade_market_maker_canister( } pub async fn upgrade_exchange_bot_canister( - identity: BasicIdentity, + identity: Box, url: String, exchange_bot_canister_id: CanisterId, version: BuildVersion, From 10e3141f5c3686a8a907bf1d1fb0115c4c7b0df5 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 18 Sep 2023 12:46:53 +0100 Subject: [PATCH 17/39] Implement `withdraw` command --- Cargo.lock | 1 - .../exchange_bot/impl/src/commands/mod.rs | 70 ++++++- .../exchange_bot/impl/src/commands/quote.rs | 18 +- .../impl/src/commands/withdraw.rs | 197 ++++++++++++++++++ .../impl/src/updates/handle_direct_message.rs | 35 +++- .../icrc1_ledger/api/Cargo.toml | 1 - .../api/src/queries/icrc1_balance_of.rs | 5 + .../icrc1_ledger/api/src/queries/mod.rs | 1 + .../icrc1_ledger/c2c_client/src/lib.rs | 1 + backend/libraries/icpswap_client/src/lib.rs | 2 +- 10 files changed, 306 insertions(+), 25 deletions(-) create mode 100644 backend/canisters/exchange_bot/impl/src/commands/withdraw.rs create mode 100644 backend/external_canisters/icrc1_ledger/api/src/queries/icrc1_balance_of.rs diff --git a/Cargo.lock b/Cargo.lock index 8f074ccf42..920e1bfce0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3272,7 +3272,6 @@ version = "0.1.0" dependencies = [ "candid", "candid_gen", - "ic-ledger-types", "serde", "serde_bytes", "types", diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 1803d0178a..b2ae1c280a 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -1,10 +1,14 @@ +use crate::commands::common_errors::CommonErrors; use crate::commands::quote::QuoteCommand; -use crate::RuntimeState; +use crate::commands::withdraw::WithdrawCommand; +use crate::{Data, RuntimeState}; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; use types::{MessageContent, MessageContentInitial, MessageId, TextContent, UserId}; pub mod common_errors; pub mod quote; +pub mod withdraw; pub(crate) trait CommandParser { fn help_text() -> &'static str; @@ -15,24 +19,28 @@ pub(crate) trait CommandParser { #[derive(Serialize, Deserialize)] pub enum Command { Quote(QuoteCommand), + Withdraw(WithdrawCommand), } impl Command { pub fn user_id(&self) -> UserId { match self { Command::Quote(q) => q.user_id, + Command::Withdraw(w) => w.user_id, } } pub fn message_id(&self) -> MessageId { match self { Command::Quote(q) => q.message_id, + Command::Withdraw(w) => w.message_id, } } pub(crate) fn process(self, state: &mut RuntimeState) { match self { Command::Quote(q) => q.process(state), + Command::Withdraw(w) => w.process(state), } } @@ -41,6 +49,9 @@ impl Command { Command::Quote(q) => MessageContentInitial::Text(TextContent { text: q.build_message_text(), }), + Command::Withdraw(w) => MessageContentInitial::Text(TextContent { + text: w.build_message_text(), + }), } } } @@ -51,3 +62,60 @@ pub enum ParseMessageResult { Error(exchange_bot_canister::handle_direct_message::Response), DoesNotMatch, } + +fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { + let response_message = error.build_response_message(data); + ParseMessageResult::Error(data.build_text_response(response_message, None)) +} + +#[derive(Serialize, Deserialize)] +pub enum CommandSubTaskResult { + Pending, + Complete(T, Option), + Failed(String), +} + +impl CommandSubTaskResult { + pub fn is_pending(&self) -> bool { + matches!(self, Self::Pending) + } +} + +impl Display for CommandSubTaskResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + CommandSubTaskResult::Pending => f.write_str("pending"), + CommandSubTaskResult::Complete(_, s) => f.write_str(s.as_deref().unwrap_or("completed")), + CommandSubTaskResult::Failed(e) => write!(f, "failed ({e})"), + } + } +} + +#[derive(Serialize, Deserialize)] +pub enum OptionalCommandSubTaskResult { + NotRequired, + Pending, + Complete(T, Option), + Failed(String), +} + +impl OptionalCommandSubTaskResult { + pub fn is_required(&self) -> bool { + !matches!(self, Self::NotRequired) + } + + pub fn is_pending(&self) -> bool { + matches!(self, Self::Pending) + } +} + +impl Display for OptionalCommandSubTaskResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + OptionalCommandSubTaskResult::NotRequired => f.write_str("not required"), + OptionalCommandSubTaskResult::Pending => f.write_str("pending"), + OptionalCommandSubTaskResult::Complete(_, s) => f.write_str(s.as_deref().unwrap_or("completed")), + OptionalCommandSubTaskResult::Failed(e) => write!(f, "failed ({e})"), + } + } +} diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 4bc39fb06b..ed456425a0 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -1,7 +1,7 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::{Command, CommandParser, ParseMessageResult}; +use crate::commands::{build_error_response, Command, CommandParser, ParseMessageResult}; use crate::swap_client::SwapClient; -use crate::{mutate_state, Data, RuntimeState}; +use crate::{mutate_state, RuntimeState}; use exchange_bot_canister::ExchangeId; use itertools::Itertools; use lazy_static::lazy_static; @@ -14,10 +14,11 @@ use std::str::FromStr; use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; lazy_static! { - static ref REGEX: Regex = RegexBuilder::new(r"quote\s+(?\S+)\s+(?\S+)(\s+(?[\d.,]+))?") - .case_insensitive(true) - .build() - .unwrap(); + static ref REGEX: Regex = + RegexBuilder::new(r"^quote\s+(?\S+)\s+(?\S+)(\s+(?[\d.,]+))?$") + .case_insensitive(true) + .build() + .unwrap(); } pub struct QuoteCommandParser; @@ -193,8 +194,3 @@ async fn quote_single( } }) } - -fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { - let response_message = error.build_response_message(data); - ParseMessageResult::Error(data.build_text_response(response_message, None)) -} diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs new file mode 100644 index 0000000000..a3c7edc2fb --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -0,0 +1,197 @@ +use crate::commands::common_errors::CommonErrors; +use crate::commands::{ + build_error_response, Command, CommandParser, CommandSubTaskResult, OptionalCommandSubTaskResult, ParseMessageResult, +}; +use crate::{mutate_state, RuntimeState}; +use candid::Principal; +use lazy_static::lazy_static; +use ledger_utils::{convert_to_subaccount, format_crypto_amount}; +use rand::Rng; +use regex::{Regex, RegexBuilder}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use types::icrc1::{Account, BlockIndex, TransferArg}; +use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; + +lazy_static! { + static ref REGEX: Regex = RegexBuilder::new(r"^withdraw\s+(?\S+)(\s+(?[\d.,]+))?$") + .case_insensitive(true) + .build() + .unwrap(); +} + +pub struct WithdrawCommandParser; + +impl CommandParser for WithdrawCommandParser { + fn help_text() -> &'static str { + "**WITHDRAW** + +format: 'withdraw $Token $Amount' + +eg. 'withdraw CHAT 50' + +If '$Amount' is not provided, your ledger balance will be checked and then withdrawn" + } + + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { + let text = message.text().unwrap_or_default(); + + if !REGEX.is_match(text) { + return ParseMessageResult::DoesNotMatch; + } + + let matches = REGEX.captures_iter(text).next().unwrap(); + let token = &matches["token"]; + let amount_decimal = matches.name("amount").map(|m| f64::from_str(m.as_str()).unwrap()); + + let token = if let Some(t) = state.data.get_token(token) { + t + } else { + let error = CommonErrors::UnsupportedTokens(vec![token.to_string()]); + return build_error_response(error, &state.data); + }; + + let amount = amount_decimal.map(|a| (a * 10u128.pow(token.decimals as u32) as f64) as u128); + + let command = WithdrawCommand::build(token, amount, state); + ParseMessageResult::Success(Command::Withdraw(command)) + } +} + +#[derive(Serialize, Deserialize)] +pub struct WithdrawCommand { + pub created: TimestampMillis, + pub user_id: UserId, + pub token: TokenInfo, + pub amount_provided: Option, + pub message_id: MessageId, + pub in_progress: Option, // The time it started being processed + pub sub_tasks: WithdrawCommandSubTasks, +} + +#[derive(Serialize, Deserialize)] +pub struct WithdrawCommandSubTasks { + pub check_user_balance: OptionalCommandSubTaskResult, + pub withdraw: CommandSubTaskResult, +} + +impl WithdrawCommand { + pub(crate) fn build(token: TokenInfo, amount: Option, state: &mut RuntimeState) -> WithdrawCommand { + WithdrawCommand { + created: state.env.now(), + user_id: state.env.caller().into(), + token, + amount_provided: amount, + message_id: state.env.rng().gen(), + in_progress: None, + sub_tasks: WithdrawCommandSubTasks { + check_user_balance: if amount.is_some() { + OptionalCommandSubTaskResult::NotRequired + } else { + OptionalCommandSubTaskResult::Pending + }, + withdraw: CommandSubTaskResult::Pending, + }, + } + } + + pub(crate) fn process(mut self, state: &mut RuntimeState) { + self.in_progress = Some(state.env.now()); + + if !self.is_finished() { + if self.sub_tasks.check_user_balance.is_pending() { + ic_cdk::spawn(check_user_balance( + state.env.canister_id(), + self.user_id, + self.message_id, + self.token.clone(), + )); + } else if let Some(amount) = self.amount() { + ic_cdk::spawn(withdraw(self.user_id, self.message_id, self.token.clone(), amount)); + } + state.enqueue_command(Command::Withdraw(self)); + } + } + + pub fn build_message_text(&self) -> String { + let symbol = self.token.token.token_symbol(); + + let mut messages = Vec::new(); + if self.amount_provided.is_none() { + let status = self.sub_tasks.check_user_balance.to_string(); + messages.push(format!("Checking {symbol} balance: {status}")) + }; + if let Some(amount) = self.amount() { + let formatted = format_crypto_amount(amount, self.token.decimals); + let status = if matches!(self.sub_tasks.withdraw, CommandSubTaskResult::Complete(..)) { + "complete".to_string() + } else { + self.sub_tasks.withdraw.to_string() + }; + messages.push(format!("Withdrawing {formatted} {symbol}: {status}")); + }; + messages.join("\n") + } + + fn amount(&self) -> Option { + if let Some(a) = self.amount_provided { + Some(a) + } else if let OptionalCommandSubTaskResult::Complete(a, _) = self.sub_tasks.check_user_balance { + Some(a - self.token.fee) + } else { + None + } + } + + fn is_finished(&self) -> bool { + !matches!(self.sub_tasks.withdraw, CommandSubTaskResult::Pending) + } +} + +async fn check_user_balance(this_canister_id: CanisterId, user_id: UserId, message_id: MessageId, token: TokenInfo) { + let account = Account { + owner: this_canister_id, + subaccount: Some(convert_to_subaccount(&user_id.into()).0), + }; + let status = match icrc1_ledger_canister_c2c_client::icrc1_balance_of(token.ledger, &account) + .await + .map(|a| u128::try_from(a.0).unwrap()) + { + Ok(amount) => OptionalCommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, token.decimals))), + Err(error) => OptionalCommandSubTaskResult::Failed(format!("{error:?}")), + }; + mutate_state(|state| { + if let Some(Command::Withdraw(command)) = state.data.commands_pending.get_mut(user_id, message_id) { + command.sub_tasks.check_user_balance = status; + command.in_progress = None; + } + }); +} + +async fn withdraw(user_id: UserId, message_id: MessageId, token: TokenInfo, amount: u128) { + let subaccount = convert_to_subaccount(&user_id.into()).0; + + let status = match icrc1_ledger_canister_c2c_client::icrc1_transfer( + token.ledger, + &TransferArg { + from_subaccount: Some(subaccount), + to: Account::from(Principal::from(user_id)), + fee: None, + created_at_time: None, + memo: None, + amount: amount.into(), + }, + ) + .await + { + Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), + Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + }; + mutate_state(|state| { + if let Some(Command::Withdraw(command)) = state.data.commands_pending.get_mut(user_id, message_id) { + command.sub_tasks.withdraw = status; + command.in_progress = None; + } + }); +} diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 20f3143c25..acdee1786f 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -1,12 +1,13 @@ use crate::commands::quote::QuoteCommandParser; -use crate::commands::{CommandParser, ParseMessageResult}; +use crate::commands::withdraw::WithdrawCommandParser; +use crate::commands::{Command, CommandParser, ParseMessageResult}; use crate::{mutate_state, read_state, RuntimeState}; use candid::Principal; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; use exchange_bot_canister::handle_direct_message::*; use local_user_index_canister_c2c_client::LookupUserError; -use types::UserId; +use types::{MessageContent, UserId}; #[update_msgpack] #[trace] @@ -15,23 +16,37 @@ async fn handle_direct_message(args: Args) -> Response { return read_state(|state| state.data.build_text_response(message, None)); }; - mutate_state(|state| match QuoteCommandParser::try_parse(&args.content, state) { - ParseMessageResult::Success(command) => { + mutate_state(|state| match try_parse_message(args.content, state) { + Ok(command) => { let message = command.build_message(); let message_id = command.message_id(); let response = state.data.build_response(message, Some(message_id)); command.process(state); response } - ParseMessageResult::Error(response) => response, - ParseMessageResult::DoesNotMatch => { - let mut text = "This bot currently supports the following message formats:\n\n".to_string(); - text.push_str(QuoteCommandParser::help_text()); - state.data.build_text_response(text, None) - } + Err(response) => response, }) } +fn try_parse_message(message: MessageContent, state: &mut RuntimeState) -> Result { + match QuoteCommandParser::try_parse(&message, state) { + ParseMessageResult::Success(c) => return Ok(c), + ParseMessageResult::Error(response) => return Err(response), + ParseMessageResult::DoesNotMatch => {} + }; + + match WithdrawCommandParser::try_parse(&message, state) { + ParseMessageResult::Success(c) => return Ok(c), + ParseMessageResult::Error(response) => return Err(response), + ParseMessageResult::DoesNotMatch => {} + }; + + let mut text = "This bot currently supports the following message formats:\n\n".to_string(); + text.push_str(QuoteCommandParser::help_text()); + text.push_str(WithdrawCommandParser::help_text()); + Err(state.data.build_text_response(text, None)) +} + async fn verify_caller() -> Result { match read_state(check_for_known_caller) { CheckForKnownCallerResult::KnownUser(u) => Ok(u), diff --git a/backend/external_canisters/icrc1_ledger/api/Cargo.toml b/backend/external_canisters/icrc1_ledger/api/Cargo.toml index f7c636e6d1..5329f6d248 100644 --- a/backend/external_canisters/icrc1_ledger/api/Cargo.toml +++ b/backend/external_canisters/icrc1_ledger/api/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] candid = { workspace = true } candid_gen = { path = "../../../libraries/candid_gen" } -ic-ledger-types = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } types = { path = "../../../libraries/types" } diff --git a/backend/external_canisters/icrc1_ledger/api/src/queries/icrc1_balance_of.rs b/backend/external_canisters/icrc1_ledger/api/src/queries/icrc1_balance_of.rs new file mode 100644 index 0000000000..527d6cad45 --- /dev/null +++ b/backend/external_canisters/icrc1_ledger/api/src/queries/icrc1_balance_of.rs @@ -0,0 +1,5 @@ +use candid::Nat; +use types::icrc1::Account; + +pub type Args = Account; +pub type Response = Nat; diff --git a/backend/external_canisters/icrc1_ledger/api/src/queries/mod.rs b/backend/external_canisters/icrc1_ledger/api/src/queries/mod.rs index 717c82da01..565df08216 100644 --- a/backend/external_canisters/icrc1_ledger/api/src/queries/mod.rs +++ b/backend/external_canisters/icrc1_ledger/api/src/queries/mod.rs @@ -1,3 +1,4 @@ +pub mod icrc1_balance_of; pub mod icrc1_decimals; pub mod icrc1_fee; pub mod icrc1_metadata; diff --git a/backend/external_canisters/icrc1_ledger/c2c_client/src/lib.rs b/backend/external_canisters/icrc1_ledger/c2c_client/src/lib.rs index 29022eeab0..a52fafbd64 100644 --- a/backend/external_canisters/icrc1_ledger/c2c_client/src/lib.rs +++ b/backend/external_canisters/icrc1_ledger/c2c_client/src/lib.rs @@ -2,6 +2,7 @@ use canister_client::{generate_candid_c2c_call, generate_candid_c2c_call_no_args use icrc1_ledger_canister::*; // Queries +generate_candid_c2c_call!(icrc1_balance_of); generate_candid_c2c_call_no_args!(icrc1_decimals); generate_candid_c2c_call_no_args!(icrc1_fee); generate_candid_c2c_call_no_args!(icrc1_metadata); diff --git a/backend/libraries/icpswap_client/src/lib.rs b/backend/libraries/icpswap_client/src/lib.rs index b7830d3945..9442c05d1e 100644 --- a/backend/libraries/icpswap_client/src/lib.rs +++ b/backend/libraries/icpswap_client/src/lib.rs @@ -51,7 +51,7 @@ impl ICPSwapClient { }; match icpswap_swap_pool_canister_c2c_client::quote(self.swap_canister_id, &args).await? { - ICPSwapResult::Ok(amount_out) => Ok(amount_out.0.try_into().unwrap()), + ICPSwapResult::Ok(amount_out) => Ok(nat_to_u128(amount_out)), ICPSwapResult::Err(e) => Err((RejectionCode::CanisterError, format!("{e:?}"))), } } From eef9e9b35de2dc403fec148debe01e7349bece36 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 18 Sep 2023 15:32:28 +0100 Subject: [PATCH 18/39] More --- Cargo.lock | 1 + .../canisters/exchange_bot/impl/Cargo.toml | 1 + .../exchange_bot/impl/src/commands/mod.rs | 7 + .../exchange_bot/impl/src/commands/quote.rs | 4 +- .../exchange_bot/impl/src/commands/swap.rs | 189 ++++++++++++++++++ .../impl/src/commands/withdraw.rs | 36 ++-- .../impl/src/jobs/edit_messages.rs | 75 ------- .../exchange_bot/impl/src/jobs/mod.rs | 6 +- .../impl/src/jobs/process_commands.rs | 43 ++++ .../impl/src/jobs/process_messages.rs | 93 +++++++++ .../canisters/exchange_bot/impl/src/lib.rs | 39 ++-- .../impl/src/model/commands_pending.rs | 16 ++ .../impl/src/model/message_edits_queue.rs | 33 --- .../impl/src/model/messages_pending.rs | 32 +++ .../exchange_bot/impl/src/model/mod.rs | 2 +- .../exchange_bot/impl/src/transfer_to_user.rs | 92 +++++++++ backend/libraries/ledger_utils/src/lib.rs | 2 + 17 files changed, 525 insertions(+), 146 deletions(-) create mode 100644 backend/canisters/exchange_bot/impl/src/commands/swap.rs delete mode 100644 backend/canisters/exchange_bot/impl/src/jobs/edit_messages.rs create mode 100644 backend/canisters/exchange_bot/impl/src/jobs/process_commands.rs create mode 100644 backend/canisters/exchange_bot/impl/src/jobs/process_messages.rs delete mode 100644 backend/canisters/exchange_bot/impl/src/model/message_edits_queue.rs create mode 100644 backend/canisters/exchange_bot/impl/src/model/messages_pending.rs create mode 100644 backend/canisters/exchange_bot/impl/src/transfer_to_user.rs diff --git a/Cargo.lock b/Cargo.lock index 920e1bfce0..d5cd79c705 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2114,6 +2114,7 @@ dependencies = [ "ic-cdk", "ic-cdk-macros", "ic-cdk-timers", + "ic-ledger-types", "ic-stable-structures", "icpswap_client", "icrc1_ledger_canister_c2c_client", diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml index 157b0b33cb..db1f5e7e20 100644 --- a/backend/canisters/exchange_bot/impl/Cargo.toml +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -23,6 +23,7 @@ human_readable = { path = "../../../libraries/human_readable" } ic-cdk = { workspace = true } ic-cdk-macros = { workspace = true } ic-cdk-timers = { workspace = true } +ic-ledger-types = { workspace = true } ic-stable-structures = { workspace = true } icpswap_client = { path = "../../../libraries/icpswap_client" } icrc1_ledger_canister_c2c_client = { path = "../../../external_canisters/icrc1_ledger/c2c_client" } diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index b2ae1c280a..5f2bb4fcdb 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -54,6 +54,13 @@ impl Command { }), } } + + pub fn in_progress(&self) -> bool { + match self { + Command::Quote(q) => q.in_progress.is_some(), + Command::Withdraw(w) => w.in_progress.is_some(), + } + } } #[allow(clippy::large_enum_variant)] diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index ed456425a0..44b7c20e53 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -185,8 +185,8 @@ async fn quote_single( command.set_status(client.exchange_id(), status); let is_finished = command.is_finished(); - let text = command.build_message_text(); - state.enqueue_message_edit(user_id, message_id, text, false); + let message_text = command.build_message_text(); + state.enqueue_message_edit(user_id, message_id, message_text); if is_finished { state.data.commands_pending.remove(user_id, message_id); diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs new file mode 100644 index 0000000000..585ac5ab1a --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -0,0 +1,189 @@ +use crate::commands::common_errors::CommonErrors; +use crate::commands::{Command, CommandParser, ParseMessageResult}; +use crate::swap_client::SwapClient; +use crate::{mutate_state, Data, RuntimeState}; +use exchange_bot_canister::ExchangeId; +use itertools::Itertools; +use lazy_static::lazy_static; +use ledger_utils::format_crypto_amount; +use rand::Rng; +use regex::{Regex, RegexBuilder}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; + +lazy_static! { + static ref REGEX: Regex = RegexBuilder::new(r"swap\s+(?\S+)\s+(?\S+)(\s+(?[\d.,]+))?") + .case_insensitive(true) + .build() + .unwrap(); +} + +pub struct SwapCommandParser; + +impl CommandParser for SwapCommandParser { + fn help_text() -> &'static str { + "**SWAP** + +format: 'quote $InputToken $OutputToken $Amount' + +eg. 'swap ICP CHAT 100' + +If $'Amount' is not provided, the full balance of $InputTokens will be swapped." + } + + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { + let text = message.text().unwrap_or_default(); + + if !REGEX.is_match(text) { + return ParseMessageResult::DoesNotMatch; + } + + let matches = REGEX.captures_iter(text).next().unwrap(); + let input_token = &matches["input_token"]; + let output_token = &matches["output_token"]; + let amount_decimal = matches.name("amount").map(|m| f64::from_str(m.as_str()).unwrap()); + + let (input_token, output_token) = match state.data.get_token_pair(input_token, output_token) { + Ok((i, o)) => (i, o), + Err(tokens) => { + let error = CommonErrors::UnsupportedTokens(tokens); + return build_error_response(error, &state.data); + } + }; + + let amount = (amount_decimal * 10u128.pow(input_token.decimals as u32) as f64) as u128; + + match SwapCommand::build(input_token, output_token, amount, state) { + Ok(command) => ParseMessageResult::Success(Command::Swap(command)), + Err(error) => build_error_response(error, &state.data), + } + } +} + +pub enum SwapTasks { + QueryTokenBalance, + GetQuotes, + TransferToSwapCanister, + NotifySwapCanister, + PerformSwap, + WithdrawFromSwapCanister, +} + +#[derive(Serialize, Deserialize)] +pub struct SwapCommand { + pub created: TimestampMillis, + pub user_id: UserId, + pub input_token: TokenInfo, + pub output_token: TokenInfo, + pub amount: Option, + pub exchange_ids: Vec, + pub message_id: MessageId, + pub quote_statuses: Vec<(ExchangeId, QuoteStatus)>, + pub in_progress: Option, // The time it started being processed +} + +impl SwapCommand { + pub(crate) fn build( + input_token: TokenInfo, + output_token: TokenInfo, + amount: Option, + state: &mut RuntimeState, + ) -> Result { + let clients = state.get_all_swap_clients(input_token.clone(), output_token.clone()); + + if !clients.is_empty() { + let quote_statuses = clients.iter().map(|c| (c.exchange_id(), QuoteStatus::Pending)).collect(); + + Ok(SwapCommand { + created: state.env.now(), + user_id: state.env.caller().into(), + input_token, + output_token, + amount, + exchange_ids: clients.iter().map(|c| c.exchange_id()).collect(), + message_id: state.env.rng().gen(), + quote_statuses, + in_progress: None, + }) + } else { + Err(CommonErrors::PairNotSupported) + } + } + + pub(crate) fn process(mut self, state: &mut RuntimeState) { + self.in_progress = Some(state.env.now()); + + let futures: Vec<_> = self + .exchange_ids + .iter() + .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) + .map(|c| quote_single(c, self.user_id, self.message_id, self.amount, self.output_token.decimals)) + .collect(); + + state.enqueue_command(Command::Quote(self)); + + ic_cdk::spawn(async { + futures::future::join_all(futures).await; + }); + } + + pub fn build_message_text(&self) -> String { + let mut text = "Quotes:".to_string(); + for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { + let exchange_name = exchange_id.to_string(); + let status_text = status.to_string(); + text.push_str(&format!("\n{exchange_name}: {status_text}")); + } + text + } + + fn set_status(&mut self, exchange_id: ExchangeId, new_status: QuoteStatus) { + if let Some(status) = self + .quote_statuses + .iter_mut() + .find(|(e, _)| *e == exchange_id) + .map(|(_, s)| s) + { + *status = new_status; + } + } + + fn is_finished(&self) -> bool { + !self.quote_statuses.iter().any(|(_, s)| matches!(s, QuoteStatus::Pending)) + } +} + +async fn quote_single( + client: Box, + user_id: UserId, + message_id: MessageId, + amount: u128, + output_token_decimals: u8, +) { + let result = client.quote(amount).await; + + mutate_state(|state| { + if let Some(Command::Quote(command)) = state.data.commands_pending.get_mut(user_id, message_id) { + let status = match result { + Ok(amount_out) => QuoteStatus::Success(amount_out, format_crypto_amount(amount_out, output_token_decimals)), + Err(error) => QuoteStatus::Failed(format!("{error:?}")), + }; + command.set_status(client.exchange_id(), status); + let is_finished = command.is_finished(); + + let text = command.build_message_text(); + state.enqueue_message_edit(user_id, message_id, text, false); + + if is_finished { + state.data.commands_pending.remove(user_id, message_id); + } + } + }) +} + +fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { + let response_message = error.build_response_message(data); + ParseMessageResult::Error(data.build_text_response(response_message, None)) +} diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index a3c7edc2fb..d255a027d4 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -2,16 +2,16 @@ use crate::commands::common_errors::CommonErrors; use crate::commands::{ build_error_response, Command, CommandParser, CommandSubTaskResult, OptionalCommandSubTaskResult, ParseMessageResult, }; +use crate::transfer_to_user::transfer_to_user; use crate::{mutate_state, RuntimeState}; -use candid::Principal; use lazy_static::lazy_static; use ledger_utils::{convert_to_subaccount, format_crypto_amount}; use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use types::icrc1::{Account, BlockIndex, TransferArg}; -use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; +use types::icrc1::{Account, BlockIndex}; +use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TimestampNanos, TokenInfo, UserId}; lazy_static! { static ref REGEX: Regex = RegexBuilder::new(r"^withdraw\s+(?\S+)(\s+(?[\d.,]+))?$") @@ -107,7 +107,13 @@ impl WithdrawCommand { self.token.clone(), )); } else if let Some(amount) = self.amount() { - ic_cdk::spawn(withdraw(self.user_id, self.message_id, self.token.clone(), amount)); + ic_cdk::spawn(withdraw( + self.user_id, + self.message_id, + self.token.clone(), + amount, + state.env.now_nanos(), + )); } state.enqueue_command(Command::Withdraw(self)); } @@ -164,26 +170,14 @@ async fn check_user_balance(this_canister_id: CanisterId, user_id: UserId, messa if let Some(Command::Withdraw(command)) = state.data.commands_pending.get_mut(user_id, message_id) { command.sub_tasks.check_user_balance = status; command.in_progress = None; + let message_text = command.build_message_text(); + state.enqueue_message_edit(user_id, message_id, message_text); } }); } -async fn withdraw(user_id: UserId, message_id: MessageId, token: TokenInfo, amount: u128) { - let subaccount = convert_to_subaccount(&user_id.into()).0; - - let status = match icrc1_ledger_canister_c2c_client::icrc1_transfer( - token.ledger, - &TransferArg { - from_subaccount: Some(subaccount), - to: Account::from(Principal::from(user_id)), - fee: None, - created_at_time: None, - memo: None, - amount: amount.into(), - }, - ) - .await - { +async fn withdraw(user_id: UserId, message_id: MessageId, token: TokenInfo, amount: u128, now_nanos: TimestampNanos) { + let status = match transfer_to_user(user_id, &token, amount, now_nanos).await { Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), @@ -192,6 +186,8 @@ async fn withdraw(user_id: UserId, message_id: MessageId, token: TokenInfo, amou if let Some(Command::Withdraw(command)) = state.data.commands_pending.get_mut(user_id, message_id) { command.sub_tasks.withdraw = status; command.in_progress = None; + let message_text = command.build_message_text(); + state.enqueue_message_edit(user_id, message_id, message_text); } }); } diff --git a/backend/canisters/exchange_bot/impl/src/jobs/edit_messages.rs b/backend/canisters/exchange_bot/impl/src/jobs/edit_messages.rs deleted file mode 100644 index 1dce7b65bc..0000000000 --- a/backend/canisters/exchange_bot/impl/src/jobs/edit_messages.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{mutate_state, RuntimeState}; -use ic_cdk_timers::TimerId; -use std::cell::Cell; -use std::time::Duration; -use tracing::trace; -use types::{MessageContent, MessageId, TextContent, UserId}; - -const MAX_BATCH_SIZE: usize = 10; - -thread_local! { - static TIMER_ID: Cell> = Cell::default(); -} - -pub(crate) fn start_job_if_required(state: &RuntimeState) -> bool { - if TIMER_ID.with(|t| t.get().is_none()) && !state.data.message_edits_queue.is_empty() { - let timer_id = ic_cdk_timers::set_timer_interval(Duration::ZERO, run); - TIMER_ID.with(|t| t.set(Some(timer_id))); - trace!("'edit_messages' job started"); - true - } else { - false - } -} - -fn run() { - match mutate_state(next_batch) { - Some(batch) => ic_cdk::spawn(process_batch(batch)), - None => { - if let Some(timer_id) = TIMER_ID.with(|t| t.take()) { - ic_cdk_timers::clear_timer(timer_id); - trace!("'edit_messages' job stopped"); - } - } - } -} - -fn next_batch(state: &mut RuntimeState) -> Option> { - let mut batch = Vec::new(); - while let Some(next) = state.data.message_edits_queue.pop() { - batch.push(next); - if batch.len() == MAX_BATCH_SIZE { - break; - } - } - if !batch.is_empty() { - Some(batch) - } else { - None - } -} - -async fn process_batch(batch: Vec<(UserId, MessageId, String)>) { - let futures: Vec<_> = batch - .into_iter() - .map(|(user_id, message_id, text)| process_single(user_id, message_id, text)) - .collect(); - - futures::future::join_all(futures).await; -} - -async fn process_single(user_id: UserId, message_id: MessageId, text: String) { - let args = user_canister::c2c_edit_message::Args { - message_id, - content: MessageContent::Text(TextContent { text: text.clone() }), - correlation_id: 0, - }; - if user_canister_c2c_client::c2c_edit_message(user_id.into(), &args) - .await - .is_err() - { - mutate_state(|state| { - state.enqueue_message_edit(user_id, message_id, text, false); - }); - } -} diff --git a/backend/canisters/exchange_bot/impl/src/jobs/mod.rs b/backend/canisters/exchange_bot/impl/src/jobs/mod.rs index e508906341..03345cc84b 100644 --- a/backend/canisters/exchange_bot/impl/src/jobs/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/jobs/mod.rs @@ -1,7 +1,9 @@ use crate::RuntimeState; -pub mod edit_messages; +pub mod process_commands; +pub mod process_messages; pub(crate) fn start(state: &RuntimeState) { - edit_messages::start_job_if_required(state); + process_messages::start_job_if_required(state); + process_commands::start_job_if_required(state); } diff --git a/backend/canisters/exchange_bot/impl/src/jobs/process_commands.rs b/backend/canisters/exchange_bot/impl/src/jobs/process_commands.rs new file mode 100644 index 0000000000..c628f9318f --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/jobs/process_commands.rs @@ -0,0 +1,43 @@ +use crate::{mutate_state, RuntimeState}; +use ic_cdk_timers::TimerId; +use std::cell::Cell; +use std::time::Duration; +use tracing::trace; + +const MAX_BATCH_SIZE: usize = 10; + +thread_local! { + static TIMER_ID: Cell> = Cell::default(); +} + +pub(crate) fn start_job_if_required(state: &RuntimeState) -> bool { + if TIMER_ID.with(|t| t.get().is_none()) && !state.data.commands_pending.is_empty() { + let timer_id = ic_cdk_timers::set_timer_interval(Duration::ZERO, run); + TIMER_ID.with(|t| t.set(Some(timer_id))); + trace!("'process_commands' job started"); + true + } else { + false + } +} + +fn run() { + if mutate_state(process_next_batch) == 0 { + if let Some(timer_id) = TIMER_ID.with(|t| t.take()) { + ic_cdk_timers::clear_timer(timer_id); + trace!("'process_commands' job stopped"); + } + } +} + +fn process_next_batch(state: &mut RuntimeState) -> usize { + let mut count = 0; + while let Some(next) = state.data.commands_pending.pop_next_for_processing() { + next.process(state); + count += 1; + if count == MAX_BATCH_SIZE { + break; + } + } + count +} diff --git a/backend/canisters/exchange_bot/impl/src/jobs/process_messages.rs b/backend/canisters/exchange_bot/impl/src/jobs/process_messages.rs new file mode 100644 index 0000000000..09edb6ba8e --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/jobs/process_messages.rs @@ -0,0 +1,93 @@ +use crate::model::messages_pending::MessagePending; +use crate::{mutate_state, read_state, RuntimeState}; +use ic_cdk_timers::TimerId; +use std::cell::Cell; +use std::time::Duration; +use tracing::trace; +use types::{BotMessage, MessageId, UserId}; + +const MAX_BATCH_SIZE: usize = 10; + +thread_local! { + static TIMER_ID: Cell> = Cell::default(); +} + +pub(crate) fn start_job_if_required(state: &RuntimeState) -> bool { + if TIMER_ID.with(|t| t.get().is_none()) && !state.data.messages_pending.is_empty() { + let timer_id = ic_cdk_timers::set_timer_interval(Duration::ZERO, run); + TIMER_ID.with(|t| t.set(Some(timer_id))); + trace!("'process_messages' job started"); + true + } else { + false + } +} + +fn run() { + match mutate_state(next_batch) { + Some(batch) => ic_cdk::spawn(process_batch(batch)), + None => { + if let Some(timer_id) = TIMER_ID.with(|t| t.take()) { + ic_cdk_timers::clear_timer(timer_id); + trace!("'process_messages' job stopped"); + } + } + } +} + +fn next_batch(state: &mut RuntimeState) -> Option> { + let mut batch = Vec::new(); + while let Some(next) = state.data.messages_pending.pop() { + batch.push(next); + if batch.len() == MAX_BATCH_SIZE { + break; + } + } + if !batch.is_empty() { + Some(batch) + } else { + None + } +} + +async fn process_batch(batch: Vec<(UserId, MessageId, MessagePending)>) { + let futures: Vec<_> = batch + .into_iter() + .map(|(user_id, message_id, text)| process_single(user_id, message_id, text)) + .collect(); + + futures::future::join_all(futures).await; +} + +async fn process_single(user_id: UserId, message_id: MessageId, message: MessagePending) { + let is_error = match message.clone() { + MessagePending::Send(content) => { + let args = user_canister::c2c_handle_bot_messages::Args { + bot_name: read_state(|state| state.data.username.clone()), + messages: vec![BotMessage { + content, + message_id: Some(message_id), + }], + }; + user_canister_c2c_client::c2c_handle_bot_messages(user_id.into(), &args) + .await + .is_err() + } + MessagePending::Edit(content) => { + let args = user_canister::c2c_edit_message::Args { + message_id, + content, + correlation_id: 0, + }; + user_canister_c2c_client::c2c_edit_message(user_id.into(), &args) + .await + .is_err() + } + }; + + if is_error { + mutate_state(|state| { + state.enqueue_message(user_id, message_id, message, true); + }); + } +} diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index cae4df1fff..2e8746e104 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -1,7 +1,7 @@ use crate::commands::Command; use crate::icpswap::ICPSwapClientFactory; use crate::model::commands_pending::CommandsPending; -use crate::model::message_edits_queue::MessageEditsQueue; +use crate::model::messages_pending::{MessagePending, MessagesPending}; use crate::swap_client::{SwapClient, SwapClientFactory}; use candid::Principal; use canister_state_macros::canister_state; @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use types::{ - BotMessage, BuildVersion, CanisterId, Cryptocurrency, Cycles, MessageContentInitial, MessageId, TextContent, - TimestampMillis, Timestamped, TokenInfo, UserId, + BotMessage, BuildVersion, CanisterId, Cryptocurrency, Cycles, MessageContent, MessageContentInitial, MessageId, + TextContent, TimestampMillis, Timestamped, TokenInfo, UserId, }; use utils::env::Environment; @@ -25,6 +25,7 @@ mod memory; mod model; mod queries; mod swap_client; +mod transfer_to_user; mod updates; thread_local! { @@ -72,16 +73,28 @@ impl RuntimeState { pub fn enqueue_command(&mut self, command: Command) { self.data.commands_pending.push(command); - // start job + jobs::process_commands::start_job_if_required(self); } - pub fn enqueue_message_edit(&mut self, user_id: UserId, message_id: MessageId, text: String, overwrite_existing: bool) { - if self - .data - .message_edits_queue - .push(user_id, message_id, text, overwrite_existing) - { - jobs::edit_messages::start_job_if_required(self); + pub fn enqueue_message_edit(&mut self, user_id: UserId, message_id: MessageId, text: String) { + self.enqueue_message( + user_id, + message_id, + MessagePending::Edit(MessageContent::Text(TextContent { text })), + false, + ); + } + + pub fn enqueue_message( + &mut self, + user_id: UserId, + message_id: MessageId, + message: MessagePending, + skip_if_already_queued: bool, + ) { + if !skip_if_already_queued || !self.data.messages_pending.contains(user_id, message_id) { + self.data.messages_pending.push(user_id, message_id, message); + jobs::process_messages::start_job_if_required(self); } } @@ -110,7 +123,7 @@ struct Data { token_info: Vec, known_callers: HashMap, commands_pending: CommandsPending, - message_edits_queue: MessageEditsQueue, + messages_pending: MessagesPending, username: String, display_name: Option, is_registered: bool, @@ -133,7 +146,7 @@ impl Data { token_info: build_token_info(), known_callers: HashMap::new(), commands_pending: CommandsPending::default(), - message_edits_queue: MessageEditsQueue::default(), + messages_pending: MessagesPending::default(), username: "".to_string(), display_name: None, is_registered: false, diff --git a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs index 1cd1e1ad30..99e4da4f3b 100644 --- a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs +++ b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs @@ -13,6 +13,18 @@ impl CommandsPending { self.commands.push(command); } + pub fn pop_next_for_processing(&mut self) -> Option { + let next_index = self + .commands + .iter() + .enumerate() + .filter(|(_, c)| !c.in_progress()) + .map(|(i, _)| i) + .next()?; + + Some(self.commands.swap_remove(next_index)) + } + pub fn get(&self, user_id: UserId, message_id: MessageId) -> Option<&Command> { self.commands .iter() @@ -32,4 +44,8 @@ impl CommandsPending { .map(|(i, _)| i) .map(|i| self.commands.remove(i)) } + + pub fn is_empty(&self) -> bool { + self.commands.is_empty() + } } diff --git a/backend/canisters/exchange_bot/impl/src/model/message_edits_queue.rs b/backend/canisters/exchange_bot/impl/src/model/message_edits_queue.rs deleted file mode 100644 index 4ddbfdab75..0000000000 --- a/backend/canisters/exchange_bot/impl/src/model/message_edits_queue.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::btree_map::Entry::{Occupied, Vacant}; -use std::collections::BTreeMap; -use types::{MessageId, UserId}; - -#[derive(Serialize, Deserialize, Default)] -pub struct MessageEditsQueue { - messages: BTreeMap<(UserId, MessageId), String>, -} - -impl MessageEditsQueue { - pub fn push(&mut self, user_id: UserId, message_id: MessageId, text: String, overwrite_existing: bool) -> bool { - match self.messages.entry((user_id, message_id)) { - Vacant(e) => { - e.insert(text); - true - } - Occupied(mut e) if overwrite_existing => { - e.insert(text); - true - } - _ => false, - } - } - - pub fn pop(&mut self) -> Option<(UserId, MessageId, String)> { - self.messages.pop_first().map(|((u, m), t)| (u, m, t)) - } - - pub fn is_empty(&self) -> bool { - self.messages.is_empty() - } -} diff --git a/backend/canisters/exchange_bot/impl/src/model/messages_pending.rs b/backend/canisters/exchange_bot/impl/src/model/messages_pending.rs new file mode 100644 index 0000000000..da0351b2bf --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/model/messages_pending.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use types::{MessageContent, MessageContentInitial, MessageId, UserId}; + +#[derive(Serialize, Deserialize, Default)] +pub struct MessagesPending { + messages: BTreeMap<(UserId, MessageId), MessagePending>, +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum MessagePending { + Send(MessageContentInitial), + Edit(MessageContent), +} + +impl MessagesPending { + pub fn push(&mut self, user_id: UserId, message_id: MessageId, message: MessagePending) { + self.messages.insert((user_id, message_id), message); + } + + pub fn pop(&mut self) -> Option<(UserId, MessageId, MessagePending)> { + self.messages.pop_first().map(|((u, id), m)| (u, id, m)) + } + + pub fn contains(&self, user_id: UserId, message_id: MessageId) -> bool { + self.messages.contains_key(&(user_id, message_id)) + } + + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } +} diff --git a/backend/canisters/exchange_bot/impl/src/model/mod.rs b/backend/canisters/exchange_bot/impl/src/model/mod.rs index 414569b755..db15f6df4a 100644 --- a/backend/canisters/exchange_bot/impl/src/model/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/model/mod.rs @@ -1,2 +1,2 @@ pub mod commands_pending; -pub mod message_edits_queue; +pub mod messages_pending; diff --git a/backend/canisters/exchange_bot/impl/src/transfer_to_user.rs b/backend/canisters/exchange_bot/impl/src/transfer_to_user.rs new file mode 100644 index 0000000000..630b6bceb3 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/transfer_to_user.rs @@ -0,0 +1,92 @@ +use crate::model::messages_pending::MessagePending; +use crate::mutate_state; +use candid::{Nat, Principal}; +use ic_cdk::api::call::CallResult; +use ic_ledger_types::{AccountIdentifier, Memo, Subaccount, Timestamp, Tokens, TransferArgs}; +use ledger_utils::{calculate_transaction_hash, convert_to_subaccount, default_ledger_account}; +use rand::Rng; +use types::icrc1::{Account, CryptoAccount, TransferArg, TransferError}; +use types::{ + icrc1, nns, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, MessageContentInitial, + TimestampNanos, TokenInfo, UserId, +}; + +pub async fn transfer_to_user( + user_id: UserId, + token: &TokenInfo, + amount: u128, + now_nanos: TimestampNanos, +) -> CallResult> { + let subaccount = convert_to_subaccount(&user_id.into()); + let response = icrc1_ledger_canister_c2c_client::icrc1_transfer( + token.ledger, + &TransferArg { + from_subaccount: Some(subaccount.0), + to: Account::from(Principal::from(user_id)), + fee: Some(token.fee.into()), + created_at_time: Some(now_nanos), + memo: None, + amount: amount.into(), + }, + ) + .await; + + if let Ok(Ok(block_index)) = &response { + mutate_state(|state| { + let this_canister_id = state.env.canister_id(); + let transaction = if matches!(token.token, Cryptocurrency::InternetComputer) { + let transfer_args = TransferArgs { + memo: Memo(0), + amount: Tokens::from_e8s(amount.try_into().unwrap()), + fee: Tokens::from_e8s(Cryptocurrency::InternetComputer.fee().unwrap().try_into().unwrap()), + from_subaccount: Some(Subaccount::from(Principal::from(user_id))), + to: default_ledger_account(user_id.into()), + created_at_time: Some(Timestamp { + timestamp_nanos: now_nanos, + }), + }; + let transaction_hash = calculate_transaction_hash(this_canister_id, &transfer_args); + CompletedCryptoTransaction::NNS(nns::CompletedCryptoTransaction { + ledger: token.ledger, + token: token.token.clone(), + amount: Tokens::from_e8s(amount.try_into().unwrap()), + fee: Tokens::from_e8s(token.fee.try_into().unwrap()), + from: nns::CryptoAccount::Account(AccountIdentifier::new(&this_canister_id, &subaccount)), + to: nns::CryptoAccount::Account(default_ledger_account(user_id.into())), + memo: Memo(0), + created: now_nanos, + transaction_hash, + block_index: block_index.0.clone().try_into().unwrap(), + }) + } else { + CompletedCryptoTransaction::ICRC1(icrc1::CompletedCryptoTransaction { + ledger: token.ledger, + token: token.token.clone(), + amount, + from: CryptoAccount::Account(Account { + owner: this_canister_id, + subaccount: Some(subaccount.0), + }), + to: CryptoAccount::Account(Account::from(Principal::from(user_id))), + fee: token.fee, + memo: None, + created: now_nanos, + block_index: block_index.0.clone().try_into().unwrap(), + }) + }; + let message_id = state.env.rng().gen(); + state.enqueue_message( + user_id, + message_id, + MessagePending::Send(MessageContentInitial::Crypto(CryptoContent { + recipient: user_id, + transfer: CryptoTransaction::Completed(transaction), + caption: None, + })), + false, + ); + }); + } + + response +} diff --git a/backend/libraries/ledger_utils/src/lib.rs b/backend/libraries/ledger_utils/src/lib.rs index 319995b4cb..4db83e531a 100644 --- a/backend/libraries/ledger_utils/src/lib.rs +++ b/backend/libraries/ledger_utils/src/lib.rs @@ -85,6 +85,8 @@ pub fn format_crypto_amount(units: u128, decimals: u8) -> String { let subdividable_by = 10u128.pow(decimals as u32); format!("{}.{:0}", units / subdividable_by, units % subdividable_by) + .trim_end_matches("0") + .to_string() } /// An operation which modifies account balances From 81c82e01f53a113490424c12af59e4e7258af088 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 18 Sep 2023 15:46:15 +0100 Subject: [PATCH 19/39] More --- .../exchange_bot/impl/src/commands/mod.rs | 35 ++-------- .../exchange_bot/impl/src/commands/quote.rs | 2 - .../impl/src/commands/withdraw.rs | 67 ++++++++++--------- .../impl/src/updates/handle_direct_message.rs | 1 + 4 files changed, 41 insertions(+), 64 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 5f2bb4fcdb..7267597656 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -77,36 +77,13 @@ fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult #[derive(Serialize, Deserialize)] pub enum CommandSubTaskResult { - Pending, - Complete(T, Option), - Failed(String), -} - -impl CommandSubTaskResult { - pub fn is_pending(&self) -> bool { - matches!(self, Self::Pending) - } -} - -impl Display for CommandSubTaskResult { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - CommandSubTaskResult::Pending => f.write_str("pending"), - CommandSubTaskResult::Complete(_, s) => f.write_str(s.as_deref().unwrap_or("completed")), - CommandSubTaskResult::Failed(e) => write!(f, "failed ({e})"), - } - } -} - -#[derive(Serialize, Deserialize)] -pub enum OptionalCommandSubTaskResult { NotRequired, Pending, Complete(T, Option), Failed(String), } -impl OptionalCommandSubTaskResult { +impl CommandSubTaskResult { pub fn is_required(&self) -> bool { !matches!(self, Self::NotRequired) } @@ -116,13 +93,13 @@ impl OptionalCommandSubTaskResult { } } -impl Display for OptionalCommandSubTaskResult { +impl Display for CommandSubTaskResult { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - OptionalCommandSubTaskResult::NotRequired => f.write_str("not required"), - OptionalCommandSubTaskResult::Pending => f.write_str("pending"), - OptionalCommandSubTaskResult::Complete(_, s) => f.write_str(s.as_deref().unwrap_or("completed")), - OptionalCommandSubTaskResult::Failed(e) => write!(f, "failed ({e})"), + CommandSubTaskResult::NotRequired => f.write_str("not required"), + CommandSubTaskResult::Pending => f.write_str("pending"), + CommandSubTaskResult::Complete(_, s) => f.write_str(s.as_deref().unwrap_or("completed")), + CommandSubTaskResult::Failed(e) => write!(f, "failed ({e})"), } } } diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 44b7c20e53..fe980ae006 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -28,9 +28,7 @@ impl CommandParser for QuoteCommandParser { "**QUOTE** format: 'quote $InputToken $OutputToken $Amount' - eg. 'quote ICP CHAT 100' - '$Amount' will default to 1 if not provided." } diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index d255a027d4..b14456487f 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -1,7 +1,5 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::{ - build_error_response, Command, CommandParser, CommandSubTaskResult, OptionalCommandSubTaskResult, ParseMessageResult, -}; +use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::transfer_to_user::transfer_to_user; use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; @@ -27,10 +25,8 @@ impl CommandParser for WithdrawCommandParser { "**WITHDRAW** format: 'withdraw $Token $Amount' - eg. 'withdraw CHAT 50' - -If '$Amount' is not provided, your ledger balance will be checked and then withdrawn" +If '$Amount' is not provided, your total balance will be withdrawn" } fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { @@ -71,7 +67,7 @@ pub struct WithdrawCommand { #[derive(Serialize, Deserialize)] pub struct WithdrawCommandSubTasks { - pub check_user_balance: OptionalCommandSubTaskResult, + pub check_user_balance: CommandSubTaskResult, pub withdraw: CommandSubTaskResult, } @@ -86,9 +82,9 @@ impl WithdrawCommand { in_progress: None, sub_tasks: WithdrawCommandSubTasks { check_user_balance: if amount.is_some() { - OptionalCommandSubTaskResult::NotRequired + CommandSubTaskResult::NotRequired } else { - OptionalCommandSubTaskResult::Pending + CommandSubTaskResult::Pending }, withdraw: CommandSubTaskResult::Pending, }, @@ -142,8 +138,8 @@ impl WithdrawCommand { fn amount(&self) -> Option { if let Some(a) = self.amount_provided { Some(a) - } else if let OptionalCommandSubTaskResult::Complete(a, _) = self.sub_tasks.check_user_balance { - Some(a - self.token.fee) + } else if let CommandSubTaskResult::Complete(a, _) = self.sub_tasks.check_user_balance { + Some(a.saturating_sub(self.token.fee)) } else { None } @@ -163,31 +159,36 @@ async fn check_user_balance(this_canister_id: CanisterId, user_id: UserId, messa .await .map(|a| u128::try_from(a.0).unwrap()) { - Ok(amount) => OptionalCommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, token.decimals))), - Err(error) => OptionalCommandSubTaskResult::Failed(format!("{error:?}")), + Ok(amount) => CommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, token.decimals))), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), }; - mutate_state(|state| { - if let Some(Command::Withdraw(command)) = state.data.commands_pending.get_mut(user_id, message_id) { - command.sub_tasks.check_user_balance = status; - command.in_progress = None; - let message_text = command.build_message_text(); - state.enqueue_message_edit(user_id, message_id, message_text); - } - }); + mutate_state(|state| update_command(user_id, message_id, |c| c.sub_tasks.check_user_balance = status, state)); } async fn withdraw(user_id: UserId, message_id: MessageId, token: TokenInfo, amount: u128, now_nanos: TimestampNanos) { - let status = match transfer_to_user(user_id, &token, amount, now_nanos).await { - Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), - Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - }; - mutate_state(|state| { - if let Some(Command::Withdraw(command)) = state.data.commands_pending.get_mut(user_id, message_id) { - command.sub_tasks.withdraw = status; - command.in_progress = None; - let message_text = command.build_message_text(); - state.enqueue_message_edit(user_id, message_id, message_text); + let status = if amount <= token.fee { + CommandSubTaskResult::NotRequired + } else { + match transfer_to_user(user_id, &token, amount, now_nanos).await { + Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), + Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), } - }); + }; + mutate_state(|state| update_command(user_id, message_id, |c| c.sub_tasks.withdraw = status, state)); +} + +fn update_command( + user_id: UserId, + message_id: MessageId, + update_fn: F, + state: &mut RuntimeState, +) { + if let Some(Command::Withdraw(command)) = state.data.commands_pending.get_mut(user_id, message_id) { + update_fn(command); + command.in_progress = None; + let message_text = command.build_message_text(); + state.enqueue_message_edit(user_id, message_id, message_text); + crate::jobs::process_commands::start_job_if_required(state); + } } diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index acdee1786f..7ad865869c 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -43,6 +43,7 @@ fn try_parse_message(message: MessageContent, state: &mut RuntimeState) -> Resul let mut text = "This bot currently supports the following message formats:\n\n".to_string(); text.push_str(QuoteCommandParser::help_text()); + text.push_str("\n\n"); text.push_str(WithdrawCommandParser::help_text()); Err(state.data.build_text_response(text, None)) } From 260f60b7e2cfcd3a5637e53282f5c1a74727375a Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 18 Sep 2023 17:38:37 +0100 Subject: [PATCH 20/39] More --- .../exchange_bot/impl/src/commands/mod.rs | 16 +-- .../exchange_bot/impl/src/commands/quote.rs | 64 ++++------ .../impl/src/commands/withdraw.rs | 120 ++++++++---------- .../impl/src/jobs/process_commands.rs | 2 +- .../impl/src/model/commands_pending.rs | 34 +---- backend/libraries/ledger_utils/src/lib.rs | 9 +- 6 files changed, 90 insertions(+), 155 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 7267597656..36d6158c37 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -4,7 +4,7 @@ use crate::commands::withdraw::WithdrawCommand; use crate::{Data, RuntimeState}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; -use types::{MessageContent, MessageContentInitial, MessageId, TextContent, UserId}; +use types::{MessageContent, MessageContentInitial, MessageId, TextContent}; pub mod common_errors; pub mod quote; @@ -23,13 +23,6 @@ pub enum Command { } impl Command { - pub fn user_id(&self) -> UserId { - match self { - Command::Quote(q) => q.user_id, - Command::Withdraw(w) => w.user_id, - } - } - pub fn message_id(&self) -> MessageId { match self { Command::Quote(q) => q.message_id, @@ -54,13 +47,6 @@ impl Command { }), } } - - pub fn in_progress(&self) -> bool { - match self { - Command::Quote(q) => q.in_progress.is_some(), - Command::Withdraw(w) => w.in_progress.is_some(), - } - } } #[allow(clippy::large_enum_variant)] diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index fe980ae006..0564e4985c 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -11,6 +11,7 @@ use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::str::FromStr; +use std::sync::{Arc, Mutex}; use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; lazy_static! { @@ -29,7 +30,7 @@ impl CommandParser for QuoteCommandParser { format: 'quote $InputToken $OutputToken $Amount' eg. 'quote ICP CHAT 100' -'$Amount' will default to 1 if not provided." +$Amount will default to 1 if not provided." } fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { @@ -74,7 +75,6 @@ pub struct QuoteCommand { pub exchange_ids: Vec, pub message_id: MessageId, pub quote_statuses: Vec<(ExchangeId, QuoteStatus)>, - pub in_progress: Option, // The time it started being processed } #[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] @@ -115,24 +115,26 @@ impl QuoteCommand { exchange_ids: clients.iter().map(|c| c.exchange_id()).collect(), message_id: state.env.rng().gen(), quote_statuses, - in_progress: None, }) } else { Err(CommonErrors::PairNotSupported) } } - pub(crate) fn process(mut self, state: &mut RuntimeState) { - self.in_progress = Some(state.env.now()); - - let futures: Vec<_> = self + pub(crate) fn process(self, state: &mut RuntimeState) { + let amount = self.amount; + let clients: Vec<_> = self .exchange_ids .iter() .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) - .map(|c| quote_single(c, self.user_id, self.message_id, self.amount, self.output_token.decimals)) .collect(); - state.enqueue_command(Command::Quote(self)); + let command = Arc::new(Mutex::new(self)); + + let futures: Vec<_> = clients + .into_iter() + .map(|c| quote_single(amount, c, command.clone())) + .collect(); ic_cdk::spawn(async { futures::future::join_all(futures).await; @@ -140,7 +142,12 @@ impl QuoteCommand { } pub fn build_message_text(&self) -> String { - let mut text = "Quotes:".to_string(); + let mut text = format!( + "Quotes ({} {} to {}):", + format_crypto_amount(self.amount, self.input_token.decimals), + self.input_token.token.token_symbol(), + self.output_token.token.token_symbol() + ); for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { let exchange_name = exchange_id.to_string(); let status_text = status.to_string(); @@ -159,36 +166,21 @@ impl QuoteCommand { *status = new_status; } } - - fn is_finished(&self) -> bool { - !self.quote_statuses.iter().any(|(_, s)| matches!(s, QuoteStatus::Pending)) - } } -async fn quote_single( - client: Box, - user_id: UserId, - message_id: MessageId, - amount: u128, - output_token_decimals: u8, -) { +async fn quote_single(amount: u128, client: Box, wrapped_command: Arc>) { let result = client.quote(amount).await; + let mut command = wrapped_command.lock().unwrap(); + let status = match result { + Ok(amount_out) => QuoteStatus::Success(amount_out, format_crypto_amount(amount_out, command.output_token.decimals)), + Err(error) => QuoteStatus::Failed(format!("{error:?}")), + }; + command.set_status(client.exchange_id(), status); + + let message_text = command.build_message_text(); + mutate_state(|state| { - if let Some(Command::Quote(command)) = state.data.commands_pending.get_mut(user_id, message_id) { - let status = match result { - Ok(amount_out) => QuoteStatus::Success(amount_out, format_crypto_amount(amount_out, output_token_decimals)), - Err(error) => QuoteStatus::Failed(format!("{error:?}")), - }; - command.set_status(client.exchange_id(), status); - let is_finished = command.is_finished(); - - let message_text = command.build_message_text(); - state.enqueue_message_edit(user_id, message_id, message_text); - - if is_finished { - state.data.commands_pending.remove(user_id, message_id); - } - } + state.enqueue_message_edit(command.user_id, command.message_id, message_text); }) } diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index b14456487f..c9dd25f177 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -26,7 +26,7 @@ impl CommandParser for WithdrawCommandParser { format: 'withdraw $Token $Amount' eg. 'withdraw CHAT 50' -If '$Amount' is not provided, your total balance will be withdrawn" +If $Amount is not provided, your total balance will be withdrawn" } fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { @@ -61,7 +61,6 @@ pub struct WithdrawCommand { pub token: TokenInfo, pub amount_provided: Option, pub message_id: MessageId, - pub in_progress: Option, // The time it started being processed pub sub_tasks: WithdrawCommandSubTasks, } @@ -79,7 +78,6 @@ impl WithdrawCommand { token, amount_provided: amount, message_id: state.env.rng().gen(), - in_progress: None, sub_tasks: WithdrawCommandSubTasks { check_user_balance: if amount.is_some() { CommandSubTaskResult::NotRequired @@ -91,27 +89,11 @@ impl WithdrawCommand { } } - pub(crate) fn process(mut self, state: &mut RuntimeState) { - self.in_progress = Some(state.env.now()); - - if !self.is_finished() { - if self.sub_tasks.check_user_balance.is_pending() { - ic_cdk::spawn(check_user_balance( - state.env.canister_id(), - self.user_id, - self.message_id, - self.token.clone(), - )); - } else if let Some(amount) = self.amount() { - ic_cdk::spawn(withdraw( - self.user_id, - self.message_id, - self.token.clone(), - amount, - state.env.now_nanos(), - )); - } - state.enqueue_command(Command::Withdraw(self)); + pub(crate) fn process(self, state: &mut RuntimeState) { + if self.sub_tasks.check_user_balance.is_pending() { + ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); + } else if let Some(amount) = self.amount() { + ic_cdk::spawn(self.withdraw(amount, state.env.now_nanos())); } } @@ -135,6 +117,53 @@ impl WithdrawCommand { messages.join("\n") } + async fn check_user_balance(mut self, this_canister_id: CanisterId) { + let account = Account { + owner: this_canister_id, + subaccount: Some(convert_to_subaccount(&self.user_id.into()).0), + }; + self.sub_tasks.check_user_balance = + match icrc1_ledger_canister_c2c_client::icrc1_balance_of(self.token.ledger, &account) + .await + .map(|a| u128::try_from(a.0).unwrap()) + { + Ok(amount) => CommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, self.token.decimals))), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + }; + + if let Some(amount) = self.amount() { + if amount <= self.token.fee { + self.sub_tasks.withdraw = CommandSubTaskResult::NotRequired; + } + } + + mutate_state(|state| self.on_updated(state)); + } + + async fn withdraw(mut self, amount: u128, now_nanos: TimestampNanos) { + self.sub_tasks.withdraw = if amount <= self.token.fee { + CommandSubTaskResult::NotRequired + } else { + match transfer_to_user(self.user_id, &self.token, amount, now_nanos).await { + Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), + Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + } + }; + mutate_state(|state| self.on_updated(state)); + } + + fn on_updated(self, state: &mut RuntimeState) { + let is_finished = self.is_finished(); + + let message_text = self.build_message_text(); + state.enqueue_message_edit(self.user_id, self.message_id, message_text); + + if !is_finished { + state.enqueue_command(Command::Withdraw(self)); + } + } + fn amount(&self) -> Option { if let Some(a) = self.amount_provided { Some(a) @@ -149,46 +178,3 @@ impl WithdrawCommand { !matches!(self.sub_tasks.withdraw, CommandSubTaskResult::Pending) } } - -async fn check_user_balance(this_canister_id: CanisterId, user_id: UserId, message_id: MessageId, token: TokenInfo) { - let account = Account { - owner: this_canister_id, - subaccount: Some(convert_to_subaccount(&user_id.into()).0), - }; - let status = match icrc1_ledger_canister_c2c_client::icrc1_balance_of(token.ledger, &account) - .await - .map(|a| u128::try_from(a.0).unwrap()) - { - Ok(amount) => CommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, token.decimals))), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - }; - mutate_state(|state| update_command(user_id, message_id, |c| c.sub_tasks.check_user_balance = status, state)); -} - -async fn withdraw(user_id: UserId, message_id: MessageId, token: TokenInfo, amount: u128, now_nanos: TimestampNanos) { - let status = if amount <= token.fee { - CommandSubTaskResult::NotRequired - } else { - match transfer_to_user(user_id, &token, amount, now_nanos).await { - Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), - Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - } - }; - mutate_state(|state| update_command(user_id, message_id, |c| c.sub_tasks.withdraw = status, state)); -} - -fn update_command( - user_id: UserId, - message_id: MessageId, - update_fn: F, - state: &mut RuntimeState, -) { - if let Some(Command::Withdraw(command)) = state.data.commands_pending.get_mut(user_id, message_id) { - update_fn(command); - command.in_progress = None; - let message_text = command.build_message_text(); - state.enqueue_message_edit(user_id, message_id, message_text); - crate::jobs::process_commands::start_job_if_required(state); - } -} diff --git a/backend/canisters/exchange_bot/impl/src/jobs/process_commands.rs b/backend/canisters/exchange_bot/impl/src/jobs/process_commands.rs index c628f9318f..dc8d7250b8 100644 --- a/backend/canisters/exchange_bot/impl/src/jobs/process_commands.rs +++ b/backend/canisters/exchange_bot/impl/src/jobs/process_commands.rs @@ -32,7 +32,7 @@ fn run() { fn process_next_batch(state: &mut RuntimeState) -> usize { let mut count = 0; - while let Some(next) = state.data.commands_pending.pop_next_for_processing() { + while let Some(next) = state.data.commands_pending.pop() { next.process(state); count += 1; if count == MAX_BATCH_SIZE { diff --git a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs index 99e4da4f3b..e536f1b0ed 100644 --- a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs +++ b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs @@ -1,7 +1,5 @@ use crate::commands::Command; -use itertools::Itertools; use serde::{Deserialize, Serialize}; -use types::{MessageId, UserId}; #[derive(Serialize, Deserialize, Default)] pub struct CommandsPending { @@ -13,36 +11,8 @@ impl CommandsPending { self.commands.push(command); } - pub fn pop_next_for_processing(&mut self) -> Option { - let next_index = self - .commands - .iter() - .enumerate() - .filter(|(_, c)| !c.in_progress()) - .map(|(i, _)| i) - .next()?; - - Some(self.commands.swap_remove(next_index)) - } - - pub fn get(&self, user_id: UserId, message_id: MessageId) -> Option<&Command> { - self.commands - .iter() - .find(|c| c.user_id() == user_id && c.message_id() == message_id) - } - - pub fn get_mut(&mut self, user_id: UserId, message_id: MessageId) -> Option<&mut Command> { - self.commands - .iter_mut() - .find(|c| c.user_id() == user_id && c.message_id() == message_id) - } - - pub fn remove(&mut self, user_id: UserId, message_id: MessageId) -> Option { - self.commands - .iter() - .find_position(|c| c.user_id() == user_id && c.message_id() == message_id) - .map(|(i, _)| i) - .map(|i| self.commands.remove(i)) + pub fn pop(&mut self) -> Option { + self.commands.pop() } pub fn is_empty(&self) -> bool { diff --git a/backend/libraries/ledger_utils/src/lib.rs b/backend/libraries/ledger_utils/src/lib.rs index 4db83e531a..20ea984ad4 100644 --- a/backend/libraries/ledger_utils/src/lib.rs +++ b/backend/libraries/ledger_utils/src/lib.rs @@ -1,6 +1,3 @@ -pub mod icrc1; -pub mod nns; - use candid::{CandidType, Principal}; use ic_ledger_types::{AccountIdentifier, Memo, Subaccount, Timestamp, Tokens, TransferArgs, DEFAULT_SUBACCOUNT}; use serde::{Deserialize, Serialize}; @@ -11,6 +8,9 @@ use types::{ PendingCryptoTransaction, TimestampNanos, TransactionHash, UserId, }; +pub mod icrc1; +pub mod nns; + pub fn create_pending_transaction( token: Cryptocurrency, ledger: CanisterId, @@ -64,7 +64,7 @@ pub fn convert_to_subaccount(principal: &Principal) -> Subaccount { } pub fn calculate_transaction_hash(sender: CanisterId, args: &TransferArgs) -> TransactionHash { - let from = default_ledger_account(sender); + let from = AccountIdentifier::new(&sender, &args.from_subaccount.unwrap_or(DEFAULT_SUBACCOUNT)); let transaction = Transaction { operation: Operation::Transfer { @@ -86,6 +86,7 @@ pub fn format_crypto_amount(units: u128, decimals: u8) -> String { format!("{}.{:0}", units / subdividable_by, units % subdividable_by) .trim_end_matches("0") + .trim_end_matches(".") .to_string() } From 9fc8c58db56983e1eb7bed6b9d166989fed6874f Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 18 Sep 2023 18:42:48 +0100 Subject: [PATCH 21/39] More --- .../exchange_bot/impl/src/commands/quote.rs | 49 +++++++------------ .../impl/src/commands/withdraw.rs | 12 ++--- .../canisters/exchange_bot/impl/src/lib.rs | 4 ++ .../impl/src/model/commands_pending.rs | 11 +++-- .../impl/src/model/messages_pending.rs | 4 ++ 5 files changed, 38 insertions(+), 42 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 0564e4985c..fadce7cc31 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -1,15 +1,13 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::{build_error_response, Command, CommandParser, ParseMessageResult}; +use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; use crate::{mutate_state, RuntimeState}; use exchange_bot_canister::ExchangeId; -use itertools::Itertools; use lazy_static::lazy_static; use ledger_utils::format_crypto_amount; use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; use std::str::FromStr; use std::sync::{Arc, Mutex}; use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; @@ -74,24 +72,7 @@ pub struct QuoteCommand { pub amount: u128, pub exchange_ids: Vec, pub message_id: MessageId, - pub quote_statuses: Vec<(ExchangeId, QuoteStatus)>, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] -pub enum QuoteStatus { - Success(u128, String), - Failed(String), - Pending, -} - -impl Display for QuoteStatus { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - QuoteStatus::Success(_, text) => f.write_str(text), - QuoteStatus::Failed(_) => f.write_str("Failed"), - QuoteStatus::Pending => f.write_str("Pending"), - } - } + pub quote_statuses: Vec<(ExchangeId, CommandSubTaskResult)>, } impl QuoteCommand { @@ -104,7 +85,10 @@ impl QuoteCommand { let clients = state.get_all_swap_clients(input_token.clone(), output_token.clone()); if !clients.is_empty() { - let quote_statuses = clients.iter().map(|c| (c.exchange_id(), QuoteStatus::Pending)).collect(); + let quote_statuses = clients + .iter() + .map(|c| (c.exchange_id(), CommandSubTaskResult::Pending)) + .collect(); Ok(QuoteCommand { created: state.env.now(), @@ -148,7 +132,7 @@ impl QuoteCommand { self.input_token.token.token_symbol(), self.output_token.token.token_symbol() ); - for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { + for (exchange_id, status) in self.quote_statuses.iter() { let exchange_name = exchange_id.to_string(); let status_text = status.to_string(); text.push_str(&format!("\n{exchange_name}: {status_text}")); @@ -156,27 +140,30 @@ impl QuoteCommand { text } - fn set_status(&mut self, exchange_id: ExchangeId, new_status: QuoteStatus) { - if let Some(status) = self + fn set_quote_result(&mut self, exchange_id: ExchangeId, result: CommandSubTaskResult) { + if let Some(r) = self .quote_statuses .iter_mut() .find(|(e, _)| *e == exchange_id) .map(|(_, s)| s) { - *status = new_status; + *r = result; } } } async fn quote_single(amount: u128, client: Box, wrapped_command: Arc>) { - let result = client.quote(amount).await; + let response = client.quote(amount).await; let mut command = wrapped_command.lock().unwrap(); - let status = match result { - Ok(amount_out) => QuoteStatus::Success(amount_out, format_crypto_amount(amount_out, command.output_token.decimals)), - Err(error) => QuoteStatus::Failed(format!("{error:?}")), + let result = match response { + Ok(amount_out) => CommandSubTaskResult::Complete( + amount_out, + Some(format_crypto_amount(amount_out, command.output_token.decimals)), + ), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), }; - command.set_status(client.exchange_id(), status); + command.set_quote_result(client.exchange_id(), result); let message_text = command.build_message_text(); diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index c9dd25f177..6701040ca4 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -141,14 +141,10 @@ impl WithdrawCommand { } async fn withdraw(mut self, amount: u128, now_nanos: TimestampNanos) { - self.sub_tasks.withdraw = if amount <= self.token.fee { - CommandSubTaskResult::NotRequired - } else { - match transfer_to_user(self.user_id, &self.token, amount, now_nanos).await { - Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), - Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - } + self.sub_tasks.withdraw = match transfer_to_user(self.user_id, &self.token, amount, now_nanos).await { + Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), + Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), }; mutate_state(|state| self.on_updated(state)); } diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index 2e8746e104..4d280d9736 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -106,6 +106,8 @@ impl RuntimeState { wasm_version: WASM_VERSION.with(|v| **v.borrow()), git_commit_id: utils::git::git_commit_id().to_string(), governance_principals: self.data.governance_principals.iter().copied().collect(), + queued_commands: self.data.commands_pending.len() as u32, + queued_messages: self.data.messages_pending.len() as u32, canister_ids: CanisterIds { local_user_index: self.data.local_user_index_canister_id, cycles_dispenser: self.data.cycles_dispenser_canister_id, @@ -233,6 +235,8 @@ pub struct Metrics { pub wasm_version: BuildVersion, pub git_commit_id: String, pub governance_principals: Vec, + pub queued_commands: u32, + pub queued_messages: u32, pub canister_ids: CanisterIds, } diff --git a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs index e536f1b0ed..36b7016e35 100644 --- a/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs +++ b/backend/canisters/exchange_bot/impl/src/model/commands_pending.rs @@ -1,18 +1,23 @@ use crate::commands::Command; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; #[derive(Serialize, Deserialize, Default)] pub struct CommandsPending { - commands: Vec, + commands: VecDeque, } impl CommandsPending { pub fn push(&mut self, command: Command) { - self.commands.push(command); + self.commands.push_back(command); } pub fn pop(&mut self) -> Option { - self.commands.pop() + self.commands.pop_front() + } + + pub fn len(&self) -> usize { + self.commands.len() } pub fn is_empty(&self) -> bool { diff --git a/backend/canisters/exchange_bot/impl/src/model/messages_pending.rs b/backend/canisters/exchange_bot/impl/src/model/messages_pending.rs index da0351b2bf..28c1049bed 100644 --- a/backend/canisters/exchange_bot/impl/src/model/messages_pending.rs +++ b/backend/canisters/exchange_bot/impl/src/model/messages_pending.rs @@ -26,6 +26,10 @@ impl MessagesPending { self.messages.contains_key(&(user_id, message_id)) } + pub fn len(&self) -> usize { + self.messages.len() + } + pub fn is_empty(&self) -> bool { self.messages.is_empty() } From 5dbfc3ab6fbcdf909eceb9b4c81b3d07560e3912 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 18 Sep 2023 23:39:37 +0100 Subject: [PATCH 22/39] Add `balance` command --- .../exchange_bot/impl/src/commands/balance.rs | 108 ++++++++++++++++++ .../exchange_bot/impl/src/commands/mod.rs | 12 +- .../impl/src/commands/withdraw.rs | 18 +-- .../impl/src/updates/handle_direct_message.rs | 9 ++ 4 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 backend/canisters/exchange_bot/impl/src/commands/balance.rs diff --git a/backend/canisters/exchange_bot/impl/src/commands/balance.rs b/backend/canisters/exchange_bot/impl/src/commands/balance.rs new file mode 100644 index 0000000000..555465623c --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/balance.rs @@ -0,0 +1,108 @@ +use crate::commands::common_errors::CommonErrors; +use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; +use crate::{mutate_state, RuntimeState}; +use lazy_static::lazy_static; +use ledger_utils::{convert_to_subaccount, format_crypto_amount}; +use rand::Rng; +use regex::{Regex, RegexBuilder}; +use serde::{Deserialize, Serialize}; +use types::icrc1::Account; +use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; + +lazy_static! { + static ref REGEX: Regex = RegexBuilder::new(r"^balance\s+(?\S+)$") + .case_insensitive(true) + .build() + .unwrap(); +} + +pub struct BalanceCommandParser; + +impl CommandParser for BalanceCommandParser { + fn help_text() -> &'static str { + "**BALANCE** + +format: 'balance $Token' +eg. 'balance CHAT'" + } + + fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { + let text = message.text().unwrap_or_default(); + + if !REGEX.is_match(text) { + return ParseMessageResult::DoesNotMatch; + } + + let matches = REGEX.captures_iter(text).next().unwrap(); + let token = &matches["token"]; + + let token = if let Some(t) = state.data.get_token(token) { + t + } else { + let error = CommonErrors::UnsupportedTokens(vec![token.to_string()]); + return build_error_response(error, &state.data); + }; + + let command = BalanceCommand::build(token, state); + ParseMessageResult::Success(Command::Balance(command)) + } +} + +#[derive(Serialize, Deserialize)] +pub struct BalanceCommand { + pub created: TimestampMillis, + pub user_id: UserId, + pub token: TokenInfo, + pub message_id: MessageId, + pub result: CommandSubTaskResult, +} + +impl BalanceCommand { + pub(crate) fn build(token: TokenInfo, state: &mut RuntimeState) -> BalanceCommand { + BalanceCommand { + created: state.env.now(), + user_id: state.env.caller().into(), + token, + message_id: state.env.rng().gen(), + result: CommandSubTaskResult::Pending, + } + } + + pub(crate) fn process(self, state: &mut RuntimeState) { + ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); + } + + pub fn build_message_text(&self) -> String { + let symbol = self.token.token.token_symbol(); + let status = self.result.to_string(); + format!("Checking {symbol} balance: {status}") + } + + async fn check_user_balance(mut self, this_canister_id: CanisterId) { + self.result = check_user_balance(self.user_id, &self.token, this_canister_id).await; + + mutate_state(|state| { + let message_text = self.build_message_text(); + state.enqueue_message_edit(self.user_id, self.message_id, message_text); + }); + } +} + +pub(crate) async fn check_user_balance( + user_id: UserId, + token: &TokenInfo, + this_canister_id: CanisterId, +) -> CommandSubTaskResult { + let account = Account { + owner: this_canister_id, + subaccount: Some(convert_to_subaccount(&user_id.into()).0), + }; + + match icrc1_ledger_canister_c2c_client::icrc1_balance_of(token.ledger, &account) + .await + .map(|a| u128::try_from(a.0).unwrap()) + { + Ok(amount) => CommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, token.decimals))), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + } +} diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 36d6158c37..987824363d 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -1,3 +1,4 @@ +use crate::commands::balance::BalanceCommand; use crate::commands::common_errors::CommonErrors; use crate::commands::quote::QuoteCommand; use crate::commands::withdraw::WithdrawCommand; @@ -6,6 +7,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use types::{MessageContent, MessageContentInitial, MessageId, TextContent}; +pub mod balance; pub mod common_errors; pub mod quote; pub mod withdraw; @@ -18,6 +20,7 @@ pub(crate) trait CommandParser { #[derive(Serialize, Deserialize)] pub enum Command { + Balance(BalanceCommand), Quote(QuoteCommand), Withdraw(WithdrawCommand), } @@ -25,6 +28,7 @@ pub enum Command { impl Command { pub fn message_id(&self) -> MessageId { match self { + Command::Balance(b) => b.message_id, Command::Quote(q) => q.message_id, Command::Withdraw(w) => w.message_id, } @@ -32,6 +36,7 @@ impl Command { pub(crate) fn process(self, state: &mut RuntimeState) { match self { + Command::Balance(b) => b.process(state), Command::Quote(q) => q.process(state), Command::Withdraw(w) => w.process(state), } @@ -39,6 +44,9 @@ impl Command { pub fn build_message(&self) -> MessageContentInitial { match self { + Command::Balance(b) => MessageContentInitial::Text(TextContent { + text: b.build_message_text(), + }), Command::Quote(q) => MessageContentInitial::Text(TextContent { text: q.build_message_text(), }), @@ -70,10 +78,6 @@ pub enum CommandSubTaskResult { } impl CommandSubTaskResult { - pub fn is_required(&self) -> bool { - !matches!(self, Self::NotRequired) - } - pub fn is_pending(&self) -> bool { matches!(self, Self::Pending) } diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index 6701040ca4..d7b9a847f6 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -1,14 +1,15 @@ +use crate::commands::balance::check_user_balance; use crate::commands::common_errors::CommonErrors; use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::transfer_to_user::transfer_to_user; use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; -use ledger_utils::{convert_to_subaccount, format_crypto_amount}; +use ledger_utils::format_crypto_amount; use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use types::icrc1::{Account, BlockIndex}; +use types::icrc1::BlockIndex; use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TimestampNanos, TokenInfo, UserId}; lazy_static! { @@ -118,18 +119,7 @@ impl WithdrawCommand { } async fn check_user_balance(mut self, this_canister_id: CanisterId) { - let account = Account { - owner: this_canister_id, - subaccount: Some(convert_to_subaccount(&self.user_id.into()).0), - }; - self.sub_tasks.check_user_balance = - match icrc1_ledger_canister_c2c_client::icrc1_balance_of(self.token.ledger, &account) - .await - .map(|a| u128::try_from(a.0).unwrap()) - { - Ok(amount) => CommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, self.token.decimals))), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - }; + self.sub_tasks.check_user_balance = check_user_balance(self.user_id, &self.token, this_canister_id).await; if let Some(amount) = self.amount() { if amount <= self.token.fee { diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 7ad865869c..ae639393f3 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -1,3 +1,4 @@ +use crate::commands::balance::BalanceCommandParser; use crate::commands::quote::QuoteCommandParser; use crate::commands::withdraw::WithdrawCommandParser; use crate::commands::{Command, CommandParser, ParseMessageResult}; @@ -29,6 +30,12 @@ async fn handle_direct_message(args: Args) -> Response { } fn try_parse_message(message: MessageContent, state: &mut RuntimeState) -> Result { + match BalanceCommandParser::try_parse(&message, state) { + ParseMessageResult::Success(c) => return Ok(c), + ParseMessageResult::Error(response) => return Err(response), + ParseMessageResult::DoesNotMatch => {} + }; + match QuoteCommandParser::try_parse(&message, state) { ParseMessageResult::Success(c) => return Ok(c), ParseMessageResult::Error(response) => return Err(response), @@ -44,6 +51,8 @@ fn try_parse_message(message: MessageContent, state: &mut RuntimeState) -> Resul let mut text = "This bot currently supports the following message formats:\n\n".to_string(); text.push_str(QuoteCommandParser::help_text()); text.push_str("\n\n"); + text.push_str(BalanceCommandParser::help_text()); + text.push_str("\n\n"); text.push_str(WithdrawCommandParser::help_text()); Err(state.data.build_text_response(text, None)) } From 45d9e38ca16c355a7bda6b480627fd8fa4456058 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 18 Sep 2023 23:58:31 +0100 Subject: [PATCH 23/39] More --- .../exchange_bot/impl/src/commands/balance.rs | 28 +-- .../exchange_bot/impl/src/commands/mod.rs | 20 +- .../exchange_bot/impl/src/commands/quote.rs | 33 ++-- .../src/commands/sub_tasks/check_balance.rs | 23 +++ .../impl/src/commands/sub_tasks/get_quotes.rs | 14 ++ .../impl/src/commands/sub_tasks/mod.rs | 2 + .../exchange_bot/impl/src/commands/swap.rs | 187 +++++++++++------- .../impl/src/commands/withdraw.rs | 6 +- 8 files changed, 196 insertions(+), 117 deletions(-) create mode 100644 backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_balance.rs create mode 100644 backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs create mode 100644 backend/canisters/exchange_bot/impl/src/commands/sub_tasks/mod.rs diff --git a/backend/canisters/exchange_bot/impl/src/commands/balance.rs b/backend/canisters/exchange_bot/impl/src/commands/balance.rs index 555465623c..625d8a7df1 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/balance.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/balance.rs @@ -1,12 +1,11 @@ use crate::commands::common_errors::CommonErrors; +use crate::commands::sub_tasks::check_balance::check_balance; use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; -use ledger_utils::{convert_to_subaccount, format_crypto_amount}; use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; -use types::icrc1::Account; use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; lazy_static! { @@ -69,7 +68,7 @@ impl BalanceCommand { } pub(crate) fn process(self, state: &mut RuntimeState) { - ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); + ic_cdk::spawn(self.check_balance(state.env.canister_id())); } pub fn build_message_text(&self) -> String { @@ -78,8 +77,8 @@ impl BalanceCommand { format!("Checking {symbol} balance: {status}") } - async fn check_user_balance(mut self, this_canister_id: CanisterId) { - self.result = check_user_balance(self.user_id, &self.token, this_canister_id).await; + async fn check_balance(mut self, this_canister_id: CanisterId) { + self.result = check_balance(self.user_id, &self.token, this_canister_id).await; mutate_state(|state| { let message_text = self.build_message_text(); @@ -87,22 +86,3 @@ impl BalanceCommand { }); } } - -pub(crate) async fn check_user_balance( - user_id: UserId, - token: &TokenInfo, - this_canister_id: CanisterId, -) -> CommandSubTaskResult { - let account = Account { - owner: this_canister_id, - subaccount: Some(convert_to_subaccount(&user_id.into()).0), - }; - - match icrc1_ledger_canister_c2c_client::icrc1_balance_of(token.ledger, &account) - .await - .map(|a| u128::try_from(a.0).unwrap()) - { - Ok(amount) => CommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, token.decimals))), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - } -} diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 987824363d..ce9cefb753 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -1,6 +1,7 @@ use crate::commands::balance::BalanceCommand; use crate::commands::common_errors::CommonErrors; use crate::commands::quote::QuoteCommand; +use crate::commands::swap::SwapCommand; use crate::commands::withdraw::WithdrawCommand; use crate::{Data, RuntimeState}; use serde::{Deserialize, Serialize}; @@ -10,6 +11,8 @@ use types::{MessageContent, MessageContentInitial, MessageId, TextContent}; pub mod balance; pub mod common_errors; pub mod quote; +mod sub_tasks; +pub mod swap; pub mod withdraw; pub(crate) trait CommandParser { @@ -22,6 +25,7 @@ pub(crate) trait CommandParser { pub enum Command { Balance(BalanceCommand), Quote(QuoteCommand), + Swap(SwapCommand), Withdraw(WithdrawCommand), } @@ -30,6 +34,7 @@ impl Command { match self { Command::Balance(b) => b.message_id, Command::Quote(q) => q.message_id, + Command::Swap(s) => s.message_id, Command::Withdraw(w) => w.message_id, } } @@ -38,6 +43,7 @@ impl Command { match self { Command::Balance(b) => b.process(state), Command::Quote(q) => q.process(state), + Command::Swap(s) => s.process(state), Command::Withdraw(w) => w.process(state), } } @@ -50,6 +56,9 @@ impl Command { Command::Quote(q) => MessageContentInitial::Text(TextContent { text: q.build_message_text(), }), + Command::Swap(s) => MessageContentInitial::Text(TextContent { + text: s.build_message_text(), + }), Command::Withdraw(w) => MessageContentInitial::Text(TextContent { text: w.build_message_text(), }), @@ -69,9 +78,10 @@ fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult ParseMessageResult::Error(data.build_text_response(response_message, None)) } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Default)] pub enum CommandSubTaskResult { NotRequired, + #[default] Pending, Complete(T, Option), Failed(String), @@ -81,6 +91,14 @@ impl CommandSubTaskResult { pub fn is_pending(&self) -> bool { matches!(self, Self::Pending) } + + pub fn value(&self) -> Option<&T> { + if let CommandSubTaskResult::Complete(v, _) = self { + Some(v) + } else { + None + } + } } impl Display for CommandSubTaskResult { diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index fadce7cc31..a77e12d087 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -1,4 +1,5 @@ use crate::commands::common_errors::CommonErrors; +use crate::commands::sub_tasks::get_quotes::get_quote; use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; use crate::{mutate_state, RuntimeState}; @@ -72,7 +73,7 @@ pub struct QuoteCommand { pub amount: u128, pub exchange_ids: Vec, pub message_id: MessageId, - pub quote_statuses: Vec<(ExchangeId, CommandSubTaskResult)>, + pub results: Vec<(ExchangeId, CommandSubTaskResult)>, } impl QuoteCommand { @@ -98,7 +99,7 @@ impl QuoteCommand { amount, exchange_ids: clients.iter().map(|c| c.exchange_id()).collect(), message_id: state.env.rng().gen(), - quote_statuses, + results: quote_statuses, }) } else { Err(CommonErrors::PairNotSupported) @@ -107,6 +108,7 @@ impl QuoteCommand { pub(crate) fn process(self, state: &mut RuntimeState) { let amount = self.amount; + let output_token_decimals = self.output_token.decimals; let clients: Vec<_> = self .exchange_ids .iter() @@ -117,7 +119,7 @@ impl QuoteCommand { let futures: Vec<_> = clients .into_iter() - .map(|c| quote_single(amount, c, command.clone())) + .map(|c| quote_single(c, amount, output_token_decimals, command.clone())) .collect(); ic_cdk::spawn(async { @@ -132,7 +134,7 @@ impl QuoteCommand { self.input_token.token.token_symbol(), self.output_token.token.token_symbol() ); - for (exchange_id, status) in self.quote_statuses.iter() { + for (exchange_id, status) in self.results.iter() { let exchange_name = exchange_id.to_string(); let status_text = status.to_string(); text.push_str(&format!("\n{exchange_name}: {status_text}")); @@ -141,28 +143,21 @@ impl QuoteCommand { } fn set_quote_result(&mut self, exchange_id: ExchangeId, result: CommandSubTaskResult) { - if let Some(r) = self - .quote_statuses - .iter_mut() - .find(|(e, _)| *e == exchange_id) - .map(|(_, s)| s) - { + if let Some(r) = self.results.iter_mut().find(|(e, _)| *e == exchange_id).map(|(_, s)| s) { *r = result; } } } -async fn quote_single(amount: u128, client: Box, wrapped_command: Arc>) { - let response = client.quote(amount).await; +async fn quote_single( + client: Box, + amount: u128, + output_token_decimals: u8, + wrapped_command: Arc>, +) { + let result = get_quote(client.as_ref(), amount, output_token_decimals).await; let mut command = wrapped_command.lock().unwrap(); - let result = match response { - Ok(amount_out) => CommandSubTaskResult::Complete( - amount_out, - Some(format_crypto_amount(amount_out, command.output_token.decimals)), - ), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - }; command.set_quote_result(client.exchange_id(), result); let message_text = command.build_message_text(); diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_balance.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_balance.rs new file mode 100644 index 0000000000..ee9e0f78fd --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_balance.rs @@ -0,0 +1,23 @@ +use crate::commands::CommandSubTaskResult; +use ledger_utils::{convert_to_subaccount, format_crypto_amount}; +use types::icrc1::Account; +use types::{CanisterId, TokenInfo, UserId}; + +pub(crate) async fn check_balance( + user_id: UserId, + token: &TokenInfo, + this_canister_id: CanisterId, +) -> CommandSubTaskResult { + let account = Account { + owner: this_canister_id, + subaccount: Some(convert_to_subaccount(&user_id.into()).0), + }; + + match icrc1_ledger_canister_c2c_client::icrc1_balance_of(token.ledger, &account) + .await + .map(|a| u128::try_from(a.0).unwrap()) + { + Ok(amount) => CommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, token.decimals))), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + } +} diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs new file mode 100644 index 0000000000..2b510a5204 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs @@ -0,0 +1,14 @@ +use crate::commands::CommandSubTaskResult; +use crate::swap_client::SwapClient; +use ledger_utils::format_crypto_amount; + +pub(crate) async fn get_quote(client: &dyn SwapClient, amount: u128, output_token_decimals: u8) -> CommandSubTaskResult { + let response = client.quote(amount).await; + + match response { + Ok(amount_out) => { + CommandSubTaskResult::Complete(amount_out, Some(format_crypto_amount(amount_out, output_token_decimals))) + } + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + } +} diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/mod.rs new file mode 100644 index 0000000000..740ec05dc9 --- /dev/null +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/mod.rs @@ -0,0 +1,2 @@ +pub mod check_balance; +pub mod get_quotes; diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 585ac5ab1a..95b09e1c6a 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -1,17 +1,18 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::{Command, CommandParser, ParseMessageResult}; +use crate::commands::sub_tasks::check_balance::check_balance; +use crate::commands::sub_tasks::get_quotes::get_quote; +use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; use crate::{mutate_state, Data, RuntimeState}; use exchange_bot_canister::ExchangeId; -use itertools::Itertools; use lazy_static::lazy_static; -use ledger_utils::format_crypto_amount; use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; use std::str::FromStr; -use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; +use std::sync::{Arc, Mutex}; +use types::icrc1::BlockIndex; +use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; lazy_static! { static ref REGEX: Regex = RegexBuilder::new(r"swap\s+(?\S+)\s+(?\S+)(\s+(?[\d.,]+))?") @@ -53,7 +54,7 @@ If $'Amount' is not provided, the full balance of $InputTokens will be swapped." } }; - let amount = (amount_decimal * 10u128.pow(input_token.decimals as u32) as f64) as u128; + let amount = amount_decimal.map(|a| (a * 10u128.pow(input_token.decimals as u32) as f64) as u128); match SwapCommand::build(input_token, output_token, amount, state) { Ok(command) => ParseMessageResult::Success(Command::Swap(command)), @@ -62,26 +63,27 @@ If $'Amount' is not provided, the full balance of $InputTokens will be swapped." } } -pub enum SwapTasks { - QueryTokenBalance, - GetQuotes, - TransferToSwapCanister, - NotifySwapCanister, - PerformSwap, - WithdrawFromSwapCanister, -} - #[derive(Serialize, Deserialize)] pub struct SwapCommand { pub created: TimestampMillis, pub user_id: UserId, pub input_token: TokenInfo, pub output_token: TokenInfo, - pub amount: Option, - pub exchange_ids: Vec, + pub amount_provided: Option, pub message_id: MessageId, - pub quote_statuses: Vec<(ExchangeId, QuoteStatus)>, - pub in_progress: Option, // The time it started being processed + pub exchange_ids: Vec, + pub quotes: Vec<(ExchangeId, CommandSubTaskResult)>, + pub sub_tasks: SwapCommandSubTasks, +} + +#[derive(Serialize, Deserialize, Default)] +pub struct SwapCommandSubTasks { + pub check_user_balance: CommandSubTaskResult, + pub quotes: CommandSubTaskResult, + pub transfer_to_dex: CommandSubTaskResult, + pub notify_dex: CommandSubTaskResult, + pub swap: CommandSubTaskResult, + pub withdraw: CommandSubTaskResult, } impl SwapCommand { @@ -94,92 +96,137 @@ impl SwapCommand { let clients = state.get_all_swap_clients(input_token.clone(), output_token.clone()); if !clients.is_empty() { - let quote_statuses = clients.iter().map(|c| (c.exchange_id(), QuoteStatus::Pending)).collect(); + let quotes = clients + .iter() + .map(|c| (c.exchange_id(), CommandSubTaskResult::Pending)) + .collect(); Ok(SwapCommand { created: state.env.now(), user_id: state.env.caller().into(), input_token, output_token, - amount, - exchange_ids: clients.iter().map(|c| c.exchange_id()).collect(), + amount_provided: amount, message_id: state.env.rng().gen(), - quote_statuses, - in_progress: None, + exchange_ids: clients.iter().map(|c| c.exchange_id()).collect(), + quotes, + sub_tasks: SwapCommandSubTasks { + check_user_balance: if amount.is_some() { + CommandSubTaskResult::Pending + } else { + CommandSubTaskResult::NotRequired + }, + ..Default::default() + }, }) } else { Err(CommonErrors::PairNotSupported) } } - pub(crate) fn process(mut self, state: &mut RuntimeState) { - self.in_progress = Some(state.env.now()); + pub(crate) fn process(self, state: &mut RuntimeState) { + if self.sub_tasks.check_user_balance.is_pending() { + ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); + } else if let Some(amount) = self.sub_tasks.quotes.is_pending().then_some(self.amount()).flatten() { + let clients: Vec<_> = self + .exchange_ids + .iter() + .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) + .collect(); + + ic_cdk::spawn(get_quotes(self, clients, amount)); + } + } - let futures: Vec<_> = self - .exchange_ids - .iter() - .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) - .map(|c| quote_single(c, self.user_id, self.message_id, self.amount, self.output_token.decimals)) - .collect(); + pub fn build_message_text(&self) -> String { + let text = "Quotes:".to_string(); + // for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { + // let exchange_name = exchange_id.to_string(); + // let status_text = status.to_string(); + // text.push_str(&format!("\n{exchange_name}: {status_text}")); + // } + text + } - state.enqueue_command(Command::Quote(self)); + async fn check_user_balance(mut self, this_canister_id: CanisterId) { + self.sub_tasks.check_user_balance = check_balance(self.user_id, &self.input_token, this_canister_id).await; - ic_cdk::spawn(async { - futures::future::join_all(futures).await; - }); + mutate_state(|state| self.on_updated(state)); } - pub fn build_message_text(&self) -> String { - let mut text = "Quotes:".to_string(); - for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { - let exchange_name = exchange_id.to_string(); - let status_text = status.to_string(); - text.push_str(&format!("\n{exchange_name}: {status_text}")); + fn on_updated(self, state: &mut RuntimeState) { + let is_finished = self.is_finished(); + + let message_text = self.build_message_text(); + state.enqueue_message_edit(self.user_id, self.message_id, message_text); + + if !is_finished { + state.enqueue_command(Command::Swap(self)); } - text } - fn set_status(&mut self, exchange_id: ExchangeId, new_status: QuoteStatus) { - if let Some(status) = self - .quote_statuses - .iter_mut() - .find(|(e, _)| *e == exchange_id) - .map(|(_, s)| s) - { - *status = new_status; + fn set_quote_result(&mut self, exchange_id: ExchangeId, result: CommandSubTaskResult) { + if let Some(r) = self.quotes.iter_mut().find(|(e, _)| *e == exchange_id).map(|(_, s)| s) { + *r = result; + } + } + + fn amount(&self) -> Option { + if let Some(a) = self.amount_provided { + Some(a) + } else if let CommandSubTaskResult::Complete(a, _) = self.sub_tasks.check_user_balance { + Some(a.saturating_sub(self.input_token.fee)) + } else { + None } } fn is_finished(&self) -> bool { - !self.quote_statuses.iter().any(|(_, s)| matches!(s, QuoteStatus::Pending)) + !self.sub_tasks.withdraw.is_pending() } } +async fn get_quotes(command: SwapCommand, clients: Vec>, amount: u128) { + let output_token_decimals = command.output_token.decimals; + let wrapped_command = Arc::new(Mutex::new(command)); + + let futures: Vec<_> = clients + .into_iter() + .map(|c| quote_single(c, amount, output_token_decimals, wrapped_command.clone())) + .collect(); + + futures::future::join_all(futures).await; + + let mut command = Arc::try_unwrap(wrapped_command) + .map_err(|_| ()) + .unwrap() + .into_inner() + .unwrap(); + + if let Some((exchange_id, CommandSubTaskResult::Complete(..))) = command.quotes.iter().max_by_key(|(_, r)| r.value()) { + command.sub_tasks.quotes = CommandSubTaskResult::Complete(*exchange_id, Some(exchange_id.to_string())); + } else { + command.sub_tasks.quotes = CommandSubTaskResult::Failed("Failed to get any valid quotes".to_string()); + } + + mutate_state(|state| command.on_updated(state)); +} + async fn quote_single( client: Box, - user_id: UserId, - message_id: MessageId, amount: u128, output_token_decimals: u8, + wrapped_command: Arc>, ) { - let result = client.quote(amount).await; + let result = get_quote(client.as_ref(), amount, output_token_decimals).await; + + let mut command = wrapped_command.lock().unwrap(); + command.set_quote_result(client.exchange_id(), result); + + let message_text = command.build_message_text(); mutate_state(|state| { - if let Some(Command::Quote(command)) = state.data.commands_pending.get_mut(user_id, message_id) { - let status = match result { - Ok(amount_out) => QuoteStatus::Success(amount_out, format_crypto_amount(amount_out, output_token_decimals)), - Err(error) => QuoteStatus::Failed(format!("{error:?}")), - }; - command.set_status(client.exchange_id(), status); - let is_finished = command.is_finished(); - - let text = command.build_message_text(); - state.enqueue_message_edit(user_id, message_id, text, false); - - if is_finished { - state.data.commands_pending.remove(user_id, message_id); - } - } + state.enqueue_message_edit(command.user_id, command.message_id, message_text); }) } diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index d7b9a847f6..79953119c0 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -1,5 +1,5 @@ -use crate::commands::balance::check_user_balance; use crate::commands::common_errors::CommonErrors; +use crate::commands::sub_tasks::check_balance::check_balance; use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::transfer_to_user::transfer_to_user; use crate::{mutate_state, RuntimeState}; @@ -119,7 +119,7 @@ impl WithdrawCommand { } async fn check_user_balance(mut self, this_canister_id: CanisterId) { - self.sub_tasks.check_user_balance = check_user_balance(self.user_id, &self.token, this_canister_id).await; + self.sub_tasks.check_user_balance = check_balance(self.user_id, &self.token, this_canister_id).await; if let Some(amount) = self.amount() { if amount <= self.token.fee { @@ -161,6 +161,6 @@ impl WithdrawCommand { } fn is_finished(&self) -> bool { - !matches!(self.sub_tasks.withdraw, CommandSubTaskResult::Pending) + !self.sub_tasks.withdraw.is_pending() } } From 16fd3dc1091eb2170507ce7faabd4c983680bed7 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Tue, 19 Sep 2023 15:50:33 +0100 Subject: [PATCH 24/39] Add `withdraw` sub task --- .../exchange_bot/impl/src/commands/balance.rs | 8 +++---- ...check_balance.rs => check_user_balance.rs} | 2 +- .../impl/src/commands/sub_tasks/mod.rs | 3 ++- .../sub_tasks/withdraw.rs} | 22 +++++++++++++++---- .../exchange_bot/impl/src/commands/swap.rs | 4 ++-- .../impl/src/commands/withdraw.rs | 13 +++++------ .../canisters/exchange_bot/impl/src/lib.rs | 1 - 7 files changed, 32 insertions(+), 21 deletions(-) rename backend/canisters/exchange_bot/impl/src/commands/sub_tasks/{check_balance.rs => check_user_balance.rs} (95%) rename backend/canisters/exchange_bot/impl/src/{transfer_to_user.rs => commands/sub_tasks/withdraw.rs} (83%) diff --git a/backend/canisters/exchange_bot/impl/src/commands/balance.rs b/backend/canisters/exchange_bot/impl/src/commands/balance.rs index 625d8a7df1..609ddf6585 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/balance.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/balance.rs @@ -1,5 +1,5 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::sub_tasks::check_balance::check_balance; +use crate::commands::sub_tasks::check_user_balance::check_user_balance; use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; @@ -68,7 +68,7 @@ impl BalanceCommand { } pub(crate) fn process(self, state: &mut RuntimeState) { - ic_cdk::spawn(self.check_balance(state.env.canister_id())); + ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); } pub fn build_message_text(&self) -> String { @@ -77,8 +77,8 @@ impl BalanceCommand { format!("Checking {symbol} balance: {status}") } - async fn check_balance(mut self, this_canister_id: CanisterId) { - self.result = check_balance(self.user_id, &self.token, this_canister_id).await; + async fn check_user_balance(mut self, this_canister_id: CanisterId) { + self.result = check_user_balance(self.user_id, &self.token, this_canister_id).await; mutate_state(|state| { let message_text = self.build_message_text(); diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_balance.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs similarity index 95% rename from backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_balance.rs rename to backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs index ee9e0f78fd..b4ca9c61d1 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_balance.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs @@ -3,7 +3,7 @@ use ledger_utils::{convert_to_subaccount, format_crypto_amount}; use types::icrc1::Account; use types::{CanisterId, TokenInfo, UserId}; -pub(crate) async fn check_balance( +pub(crate) async fn check_user_balance( user_id: UserId, token: &TokenInfo, this_canister_id: CanisterId, diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/mod.rs index 740ec05dc9..c869384d3a 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/mod.rs @@ -1,2 +1,3 @@ -pub mod check_balance; +pub mod check_user_balance; pub mod get_quotes; +pub mod withdraw; diff --git a/backend/canisters/exchange_bot/impl/src/transfer_to_user.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs similarity index 83% rename from backend/canisters/exchange_bot/impl/src/transfer_to_user.rs rename to backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs index 630b6bceb3..ef1a73885f 100644 --- a/backend/canisters/exchange_bot/impl/src/transfer_to_user.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs @@ -1,17 +1,31 @@ +use crate::commands::CommandSubTaskResult; use crate::model::messages_pending::MessagePending; use crate::mutate_state; use candid::{Nat, Principal}; use ic_cdk::api::call::CallResult; -use ic_ledger_types::{AccountIdentifier, Memo, Subaccount, Timestamp, Tokens, TransferArgs}; +use ic_ledger_types::{AccountIdentifier, Memo, Timestamp, Tokens, TransferArgs}; use ledger_utils::{calculate_transaction_hash, convert_to_subaccount, default_ledger_account}; use rand::Rng; -use types::icrc1::{Account, CryptoAccount, TransferArg, TransferError}; +use types::icrc1::{Account, BlockIndex, CryptoAccount, TransferArg, TransferError}; use types::{ icrc1, nns, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, MessageContentInitial, TimestampNanos, TokenInfo, UserId, }; -pub async fn transfer_to_user( +pub async fn withdraw( + user_id: UserId, + token: &TokenInfo, + amount: u128, + now_nanos: TimestampNanos, +) -> CommandSubTaskResult { + match transfer_to_user(user_id, &token, amount, now_nanos).await { + Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), + Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + } +} + +async fn transfer_to_user( user_id: UserId, token: &TokenInfo, amount: u128, @@ -39,7 +53,7 @@ pub async fn transfer_to_user( memo: Memo(0), amount: Tokens::from_e8s(amount.try_into().unwrap()), fee: Tokens::from_e8s(Cryptocurrency::InternetComputer.fee().unwrap().try_into().unwrap()), - from_subaccount: Some(Subaccount::from(Principal::from(user_id))), + from_subaccount: Some(subaccount), to: default_ledger_account(user_id.into()), created_at_time: Some(Timestamp { timestamp_nanos: now_nanos, diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 95b09e1c6a..92674bad37 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -1,5 +1,5 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::sub_tasks::check_balance::check_balance; +use crate::commands::sub_tasks::check_user_balance::check_user_balance; use crate::commands::sub_tasks::get_quotes::get_quote; use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; @@ -149,7 +149,7 @@ impl SwapCommand { } async fn check_user_balance(mut self, this_canister_id: CanisterId) { - self.sub_tasks.check_user_balance = check_balance(self.user_id, &self.input_token, this_canister_id).await; + self.sub_tasks.check_user_balance = check_user_balance(self.user_id, &self.input_token, this_canister_id).await; mutate_state(|state| self.on_updated(state)); } diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index 79953119c0..202558e492 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -1,7 +1,7 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::sub_tasks::check_balance::check_balance; +use crate::commands::sub_tasks::check_user_balance::check_user_balance; +use crate::commands::sub_tasks::withdraw::withdraw; use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; -use crate::transfer_to_user::transfer_to_user; use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; use ledger_utils::format_crypto_amount; @@ -119,7 +119,7 @@ impl WithdrawCommand { } async fn check_user_balance(mut self, this_canister_id: CanisterId) { - self.sub_tasks.check_user_balance = check_balance(self.user_id, &self.token, this_canister_id).await; + self.sub_tasks.check_user_balance = check_user_balance(self.user_id, &self.token, this_canister_id).await; if let Some(amount) = self.amount() { if amount <= self.token.fee { @@ -131,11 +131,8 @@ impl WithdrawCommand { } async fn withdraw(mut self, amount: u128, now_nanos: TimestampNanos) { - self.sub_tasks.withdraw = match transfer_to_user(self.user_id, &self.token, amount, now_nanos).await { - Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), - Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - }; + self.sub_tasks.withdraw = withdraw(self.user_id, &self.token, amount, now_nanos).await; + mutate_state(|state| self.on_updated(state)); } diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index 4d280d9736..c7c07f0159 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -25,7 +25,6 @@ mod memory; mod model; mod queries; mod swap_client; -mod transfer_to_user; mod updates; thread_local! { From 579c5655be085763bd974cdea4c4c8eb21dad4c7 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Tue, 19 Sep 2023 16:29:48 +0100 Subject: [PATCH 25/39] Consolidate getting quotes --- .../exchange_bot/impl/src/commands/quote.rs | 44 +++------- .../impl/src/commands/sub_tasks/get_quotes.rs | 36 +++++++- .../exchange_bot/impl/src/commands/swap.rs | 85 +++++++------------ 3 files changed, 77 insertions(+), 88 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index a77e12d087..98d23c54e6 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -1,5 +1,5 @@ use crate::commands::common_errors::CommonErrors; -use crate::commands::sub_tasks::get_quotes::get_quote; +use crate::commands::sub_tasks::get_quotes::get_quotes; use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; use crate::{mutate_state, RuntimeState}; @@ -10,7 +10,6 @@ use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use std::sync::{Arc, Mutex}; use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; lazy_static! { @@ -108,23 +107,13 @@ impl QuoteCommand { pub(crate) fn process(self, state: &mut RuntimeState) { let amount = self.amount; - let output_token_decimals = self.output_token.decimals; let clients: Vec<_> = self .exchange_ids .iter() .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) .collect(); - let command = Arc::new(Mutex::new(self)); - - let futures: Vec<_> = clients - .into_iter() - .map(|c| quote_single(c, amount, output_token_decimals, command.clone())) - .collect(); - - ic_cdk::spawn(async { - futures::future::join_all(futures).await; - }); + ic_cdk::spawn(self.get_quotes(clients, amount)); } pub fn build_message_text(&self) -> String { @@ -142,27 +131,20 @@ impl QuoteCommand { text } + async fn get_quotes(mut self, clients: Vec>, amount: u128) { + get_quotes(clients, amount, self.output_token.decimals, |exchange_id, result| { + self.set_quote_result(exchange_id, result); + let message_text = self.build_message_text(); + mutate_state(|state| { + state.enqueue_message_edit(self.user_id, self.message_id, message_text); + }); + }) + .await + } + fn set_quote_result(&mut self, exchange_id: ExchangeId, result: CommandSubTaskResult) { if let Some(r) = self.results.iter_mut().find(|(e, _)| *e == exchange_id).map(|(_, s)| s) { *r = result; } } } - -async fn quote_single( - client: Box, - amount: u128, - output_token_decimals: u8, - wrapped_command: Arc>, -) { - let result = get_quote(client.as_ref(), amount, output_token_decimals).await; - - let mut command = wrapped_command.lock().unwrap(); - command.set_quote_result(client.exchange_id(), result); - - let message_text = command.build_message_text(); - - mutate_state(|state| { - state.enqueue_message_edit(command.user_id, command.message_id, message_text); - }) -} diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs index 2b510a5204..f377f64cd4 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs @@ -1,14 +1,44 @@ use crate::commands::CommandSubTaskResult; use crate::swap_client::SwapClient; +use exchange_bot_canister::ExchangeId; +use futures::stream::FuturesUnordered; +use futures::StreamExt; use ledger_utils::format_crypto_amount; +use std::future::ready; -pub(crate) async fn get_quote(client: &dyn SwapClient, amount: u128, output_token_decimals: u8) -> CommandSubTaskResult { +pub(crate) async fn get_quotes)>( + clients: Vec>, + amount: u128, + output_token_decimals: u8, + mut callback: C, +) { + let futures = FuturesUnordered::new(); + for client in clients { + futures.push(get_quote(client, amount, output_token_decimals)); + } + + futures + .for_each(|(exchange_id, result)| { + callback(exchange_id, result); + ready(()) + }) + .await; +} + +async fn get_quote( + client: Box, + amount: u128, + output_token_decimals: u8, +) -> (ExchangeId, CommandSubTaskResult) { + let exchange_id = client.exchange_id(); let response = client.quote(amount).await; - match response { + let result = match response { Ok(amount_out) => { CommandSubTaskResult::Complete(amount_out, Some(format_crypto_amount(amount_out, output_token_decimals))) } Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), - } + }; + + (exchange_id, result) } diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 92674bad37..61029acbdd 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -1,6 +1,6 @@ use crate::commands::common_errors::CommonErrors; use crate::commands::sub_tasks::check_user_balance::check_user_balance; -use crate::commands::sub_tasks::get_quotes::get_quote; +use crate::commands::sub_tasks::get_quotes::get_quotes; use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; use crate::{mutate_state, Data, RuntimeState}; @@ -10,7 +10,6 @@ use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use std::sync::{Arc, Mutex}; use types::icrc1::BlockIndex; use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; @@ -127,14 +126,17 @@ impl SwapCommand { pub(crate) fn process(self, state: &mut RuntimeState) { if self.sub_tasks.check_user_balance.is_pending() { ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); - } else if let Some(amount) = self.sub_tasks.quotes.is_pending().then_some(self.amount()).flatten() { - let clients: Vec<_> = self - .exchange_ids - .iter() - .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) - .collect(); - - ic_cdk::spawn(get_quotes(self, clients, amount)); + } else if let Some(amount) = self.amount() { + if self.sub_tasks.quotes.is_pending() { + let clients: Vec<_> = self + .exchange_ids + .iter() + .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) + .collect(); + + ic_cdk::spawn(self.get_quotes(clients, amount)); + } else if self.sub_tasks.transfer_to_dex.is_pending() { + } } } @@ -154,6 +156,25 @@ impl SwapCommand { mutate_state(|state| self.on_updated(state)); } + async fn get_quotes(mut self, clients: Vec>, amount: u128) { + get_quotes(clients, amount, self.output_token.decimals, |exchange_id, result| { + self.set_quote_result(exchange_id, result); + let message_text = self.build_message_text(); + mutate_state(|state| { + state.enqueue_message_edit(self.user_id, self.message_id, message_text); + }); + }) + .await; + + if let Some((exchange_id, CommandSubTaskResult::Complete(..))) = self.quotes.iter().max_by_key(|(_, r)| r.value()) { + self.sub_tasks.quotes = CommandSubTaskResult::Complete(*exchange_id, Some(exchange_id.to_string())); + } else { + self.sub_tasks.quotes = CommandSubTaskResult::Failed("Failed to get any valid quotes".to_string()); + } + + mutate_state(|state| self.on_updated(state)); + } + fn on_updated(self, state: &mut RuntimeState) { let is_finished = self.is_finished(); @@ -186,50 +207,6 @@ impl SwapCommand { } } -async fn get_quotes(command: SwapCommand, clients: Vec>, amount: u128) { - let output_token_decimals = command.output_token.decimals; - let wrapped_command = Arc::new(Mutex::new(command)); - - let futures: Vec<_> = clients - .into_iter() - .map(|c| quote_single(c, amount, output_token_decimals, wrapped_command.clone())) - .collect(); - - futures::future::join_all(futures).await; - - let mut command = Arc::try_unwrap(wrapped_command) - .map_err(|_| ()) - .unwrap() - .into_inner() - .unwrap(); - - if let Some((exchange_id, CommandSubTaskResult::Complete(..))) = command.quotes.iter().max_by_key(|(_, r)| r.value()) { - command.sub_tasks.quotes = CommandSubTaskResult::Complete(*exchange_id, Some(exchange_id.to_string())); - } else { - command.sub_tasks.quotes = CommandSubTaskResult::Failed("Failed to get any valid quotes".to_string()); - } - - mutate_state(|state| command.on_updated(state)); -} - -async fn quote_single( - client: Box, - amount: u128, - output_token_decimals: u8, - wrapped_command: Arc>, -) { - let result = get_quote(client.as_ref(), amount, output_token_decimals).await; - - let mut command = wrapped_command.lock().unwrap(); - command.set_quote_result(client.exchange_id(), result); - - let message_text = command.build_message_text(); - - mutate_state(|state| { - state.enqueue_message_edit(command.user_id, command.message_id, message_text); - }) -} - fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { let response_message = error.build_response_message(data); ParseMessageResult::Error(data.build_text_response(response_message, None)) From 16bd85c613d9d4f26571e7baffcffbd5d08df5a2 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Tue, 19 Sep 2023 18:31:21 +0100 Subject: [PATCH 26/39] Nearly done implementing `swap` --- .../impl/src/lifecycle/heartbeat.rs | 6 +- .../impl/src/jobs/process_pending_actions.rs | 1 + .../exchange_bot/api/src/updates/mod.rs | 1 - .../exchange_bot/api/src/updates/swap.rs | 19 --- .../exchange_bot/impl/src/commands/mod.rs | 4 + .../commands/sub_tasks/check_user_balance.rs | 9 +- .../exchange_bot/impl/src/commands/swap.rs | 144 +++++++++++++++--- .../exchange_bot/impl/src/icpswap/mod.rs | 4 +- .../impl/src/jobs/process_messages.rs | 4 +- .../impl/src/lifecycle/inspect_message.rs | 2 +- .../exchange_bot/impl/src/swap_client.rs | 2 +- .../impl/src/updates/handle_direct_message.rs | 9 ++ .../exchange_bot/impl/src/updates/mod.rs | 1 - .../exchange_bot/impl/src/updates/swap.rs | 78 ---------- .../src/updates/c2c_handle_bot_messages.rs | 2 + .../impl/src/updates/c2c_send_messages.rs | 2 +- 16 files changed, 164 insertions(+), 124 deletions(-) delete mode 100644 backend/canisters/exchange_bot/api/src/updates/swap.rs delete mode 100644 backend/canisters/exchange_bot/impl/src/updates/swap.rs diff --git a/backend/bots/examples/icp_dispenser/impl/src/lifecycle/heartbeat.rs b/backend/bots/examples/icp_dispenser/impl/src/lifecycle/heartbeat.rs index 4e8c013a66..c7c8ba7312 100644 --- a/backend/bots/examples/icp_dispenser/impl/src/lifecycle/heartbeat.rs +++ b/backend/bots/examples/icp_dispenser/impl/src/lifecycle/heartbeat.rs @@ -68,7 +68,11 @@ mod process_pending_actions { async fn send_messages(recipient: UserId, messages: Vec) { let bot_name = read_state(|state| state.data.bot_name.clone()); - let args = user_canister::c2c_handle_bot_messages::Args { bot_name, messages }; + let args = user_canister::c2c_handle_bot_messages::Args { + bot_name, + bot_display_name: None, + messages, + }; let response = user_canister_c2c_client::c2c_handle_bot_messages(recipient.into(), &args).await; if response.is_err() { diff --git a/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs b/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs index 36cac68a83..f839bfaaff 100644 --- a/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs +++ b/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs @@ -57,6 +57,7 @@ async fn process_action(action: Action) { CanisterId::from(user_id), &user_canister::c2c_handle_bot_messages::Args { bot_name: read_state(|state| state.data.username.clone()), + bot_display_name: None, messages: messages .into_iter() .map(|m| BotMessage { diff --git a/backend/canisters/exchange_bot/api/src/updates/mod.rs b/backend/canisters/exchange_bot/api/src/updates/mod.rs index 74a4a68810..0689567506 100644 --- a/backend/canisters/exchange_bot/api/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/api/src/updates/mod.rs @@ -1,3 +1,2 @@ pub mod handle_direct_message; pub mod register_bot; -pub mod swap; diff --git a/backend/canisters/exchange_bot/api/src/updates/swap.rs b/backend/canisters/exchange_bot/api/src/updates/swap.rs deleted file mode 100644 index 72bbe72773..0000000000 --- a/backend/canisters/exchange_bot/api/src/updates/swap.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::ExchangeId; -use candid::CandidType; -use serde::{Deserialize, Serialize}; - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct Args { - pub exchange_id: ExchangeId, - pub input_token: String, - pub output_token: String, - pub amount: u128, -} - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub enum Response { - Success(u128), - UnsupportedTokens(Vec), - PairNotSupportedByExchange, - InternalError(String), -} diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index ce9cefb753..dc720a4209 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -92,6 +92,10 @@ impl CommandSubTaskResult { matches!(self, Self::Pending) } + pub fn is_failed(&self) -> bool { + matches!(self, Self::Failed(_)) + } + pub fn value(&self) -> Option<&T> { if let CommandSubTaskResult::Complete(v, _) = self { Some(v) diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs index b4ca9c61d1..fde32da302 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/check_user_balance.rs @@ -17,7 +17,14 @@ pub(crate) async fn check_user_balance( .await .map(|a| u128::try_from(a.0).unwrap()) { - Ok(amount) => CommandSubTaskResult::Complete(amount, Some(format_crypto_amount(amount, token.decimals))), + Ok(amount) => { + let text = format!( + "{} {}", + format_crypto_amount(amount, token.decimals), + token.token.token_symbol() + ); + CommandSubTaskResult::Complete(amount, Some(text)) + } Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), } } diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 61029acbdd..998349dc48 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -1,17 +1,21 @@ use crate::commands::common_errors::CommonErrors; use crate::commands::sub_tasks::check_user_balance::check_user_balance; use crate::commands::sub_tasks::get_quotes::get_quotes; +use crate::commands::sub_tasks::withdraw::withdraw; use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; use crate::{mutate_state, Data, RuntimeState}; +use candid::Principal; use exchange_bot_canister::ExchangeId; use lazy_static::lazy_static; +use ledger_utils::{convert_to_subaccount, format_crypto_amount}; use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use types::icrc1::BlockIndex; -use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; +use tracing::trace; +use types::icrc1::{BlockIndex, TransferArg}; +use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TimestampNanos, TokenInfo, UserId}; lazy_static! { static ref REGEX: Regex = RegexBuilder::new(r"swap\s+(?\S+)\s+(?\S+)(\s+(?[\d.,]+))?") @@ -26,11 +30,11 @@ impl CommandParser for SwapCommandParser { fn help_text() -> &'static str { "**SWAP** -format: 'quote $InputToken $OutputToken $Amount' +format: 'swap $InputToken $OutputToken $Amount' eg. 'swap ICP CHAT 100' -If $'Amount' is not provided, the full balance of $InputTokens will be swapped." +If $Amount is not provided, the full balance of $InputTokens will be swapped." } fn try_parse(message: &MessageContent, state: &mut RuntimeState) -> ParseMessageResult { @@ -80,7 +84,7 @@ pub struct SwapCommandSubTasks { pub check_user_balance: CommandSubTaskResult, pub quotes: CommandSubTaskResult, pub transfer_to_dex: CommandSubTaskResult, - pub notify_dex: CommandSubTaskResult, + pub notify_dex: CommandSubTaskResult<()>, pub swap: CommandSubTaskResult, pub withdraw: CommandSubTaskResult, } @@ -111,9 +115,9 @@ impl SwapCommand { quotes, sub_tasks: SwapCommandSubTasks { check_user_balance: if amount.is_some() { - CommandSubTaskResult::Pending - } else { CommandSubTaskResult::NotRequired + } else { + CommandSubTaskResult::Pending }, ..Default::default() }, @@ -124,18 +128,60 @@ impl SwapCommand { } pub(crate) fn process(self, state: &mut RuntimeState) { + let message_id = self.message_id; + + if self.is_finished() { + trace!(%message_id, "Finished"); + return; + } + if self.sub_tasks.check_user_balance.is_pending() { + trace!(%message_id, "Checking user balance"); ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); } else if let Some(amount) = self.amount() { - if self.sub_tasks.quotes.is_pending() { - let clients: Vec<_> = self - .exchange_ids - .iter() - .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) - .collect(); - - ic_cdk::spawn(self.get_quotes(clients, amount)); - } else if self.sub_tasks.transfer_to_dex.is_pending() { + match self.sub_tasks.quotes { + CommandSubTaskResult::Pending => { + let clients: Vec<_> = self + .exchange_ids + .iter() + .filter_map(|e| state.get_swap_client(*e, self.input_token.clone(), self.output_token.clone())) + .collect(); + + trace!(%message_id, "Getting quotes"); + ic_cdk::spawn(self.get_quotes(clients, amount)); + } + CommandSubTaskResult::Complete(exchange_id, _) => { + let amount_to_dex = amount.saturating_sub(self.input_token.fee); + if self.sub_tasks.transfer_to_dex.is_pending() { + if let Some(client) = + state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) + { + trace!(%message_id, "Transferring to dex"); + ic_cdk::spawn(self.transfer_to_dex(client, amount_to_dex)); + } + } else if self.sub_tasks.notify_dex.is_pending() { + if let Some(client) = + state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) + { + trace!(%message_id, "Notifying to dex"); + ic_cdk::spawn(self.notify_dex(client, amount_to_dex)); + } + } else if self.sub_tasks.swap.is_pending() { + if let Some(client) = + state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) + { + trace!(%message_id, "Performing swap"); + ic_cdk::spawn(self.perform_swap(client, amount_to_dex)); + } + } else if self.sub_tasks.withdraw.is_pending() { + if let CommandSubTaskResult::Complete(amount_swapped, _) = self.sub_tasks.swap { + let amount_out = amount_swapped.saturating_sub(self.output_token.fee); + trace!(%message_id, "Withdrawing from dex"); + ic_cdk::spawn(self.withdraw(amount_out, state.env.now_nanos())); + } + } + } + _ => {} } } } @@ -175,6 +221,59 @@ impl SwapCommand { mutate_state(|state| self.on_updated(state)); } + async fn transfer_to_dex(mut self, client: Box, amount: u128) { + self.sub_tasks.transfer_to_dex = match client.deposit_account().await { + Ok((ledger, account)) => { + match icrc1_ledger_canister_c2c_client::icrc1_transfer( + ledger, + &TransferArg { + from_subaccount: Some(convert_to_subaccount(&Principal::from(self.user_id)).0), + to: account, + fee: None, + created_at_time: None, + memo: None, + amount: amount.into(), + }, + ) + .await + { + Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), + Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + } + } + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + }; + + mutate_state(|state| self.on_updated(state)); + } + + async fn notify_dex(mut self, client: Box, amount: u128) { + self.sub_tasks.notify_dex = match client.deposit(amount).await { + Ok(_) => CommandSubTaskResult::Complete((), None), + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + }; + + mutate_state(|state| self.on_updated(state)); + } + + async fn perform_swap(mut self, client: Box, amount: u128) { + self.sub_tasks.swap = match client.swap(amount).await { + Ok(amount_out) => { + CommandSubTaskResult::Complete(amount_out, Some(format_crypto_amount(amount_out, self.output_token.decimals))) + } + Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + }; + + mutate_state(|state| self.on_updated(state)) + } + + async fn withdraw(mut self, amount: u128, now_nanos: TimestampNanos) { + self.sub_tasks.withdraw = withdraw(self.user_id, &self.output_token, amount, now_nanos).await; + + mutate_state(|state| self.on_updated(state)); + } + fn on_updated(self, state: &mut RuntimeState) { let is_finished = self.is_finished(); @@ -203,7 +302,18 @@ impl SwapCommand { } fn is_finished(&self) -> bool { - !self.sub_tasks.withdraw.is_pending() + self.sub_tasks.any_failed() || !self.sub_tasks.withdraw.is_pending() + } +} + +impl SwapCommandSubTasks { + fn any_failed(&self) -> bool { + self.check_user_balance.is_failed() + || self.quotes.is_failed() + || self.transfer_to_dex.is_failed() + || self.notify_dex.is_failed() + || self.swap.is_failed() + || self.withdraw.is_failed() } } diff --git a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs index 54a573bfaa..6167bf9e17 100644 --- a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs @@ -66,8 +66,8 @@ impl SwapClient for ICPSwapClient { Ok(self.deposit_account()) } - async fn deposit(&self, amount: u128) -> CallResult { - self.deposit(amount).await + async fn deposit(&self, amount: u128) -> CallResult<()> { + self.deposit(amount).await.map(|_| ()) } async fn swap(&self, amount: u128) -> CallResult { diff --git a/backend/canisters/exchange_bot/impl/src/jobs/process_messages.rs b/backend/canisters/exchange_bot/impl/src/jobs/process_messages.rs index 09edb6ba8e..8ca9c6bbc7 100644 --- a/backend/canisters/exchange_bot/impl/src/jobs/process_messages.rs +++ b/backend/canisters/exchange_bot/impl/src/jobs/process_messages.rs @@ -62,8 +62,10 @@ async fn process_batch(batch: Vec<(UserId, MessageId, MessagePending)>) { async fn process_single(user_id: UserId, message_id: MessageId, message: MessagePending) { let is_error = match message.clone() { MessagePending::Send(content) => { + let (username, display_name) = read_state(|state| (state.data.username.clone(), state.data.display_name.clone())); let args = user_canister::c2c_handle_bot_messages::Args { - bot_name: read_state(|state| state.data.username.clone()), + bot_name: username, + bot_display_name: display_name, messages: vec![BotMessage { content, message_id: Some(message_id), diff --git a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs index 8c1922e963..c4d9e188cf 100644 --- a/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs +++ b/backend/canisters/exchange_bot/impl/src/lifecycle/inspect_message.rs @@ -10,7 +10,7 @@ fn accept_if_valid(state: &RuntimeState) { let method_name = ic_cdk::api::call::method_name(); let is_valid = match method_name.as_str() { - "register_bot" | "swap" => state.is_caller_governance_principal(), + "register_bot" => state.is_caller_governance_principal(), _ => false, }; diff --git a/backend/canisters/exchange_bot/impl/src/swap_client.rs b/backend/canisters/exchange_bot/impl/src/swap_client.rs index edef47a7db..181ed29183 100644 --- a/backend/canisters/exchange_bot/impl/src/swap_client.rs +++ b/backend/canisters/exchange_bot/impl/src/swap_client.rs @@ -18,7 +18,7 @@ pub trait SwapClient { fn exchange_id(&self) -> ExchangeId; async fn quote(&self, amount: u128) -> CallResult; async fn deposit_account(&self) -> CallResult<(CanisterId, Account)>; - async fn deposit(&self, amount: u128) -> CallResult; + async fn deposit(&self, amount: u128) -> CallResult<()>; async fn swap(&self, amount: u128) -> CallResult; async fn withdraw(&self, amount: u128) -> CallResult; } diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index ae639393f3..2b7e69cc73 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -1,5 +1,6 @@ use crate::commands::balance::BalanceCommandParser; use crate::commands::quote::QuoteCommandParser; +use crate::commands::swap::SwapCommandParser; use crate::commands::withdraw::WithdrawCommandParser; use crate::commands::{Command, CommandParser, ParseMessageResult}; use crate::{mutate_state, read_state, RuntimeState}; @@ -42,6 +43,12 @@ fn try_parse_message(message: MessageContent, state: &mut RuntimeState) -> Resul ParseMessageResult::DoesNotMatch => {} }; + match SwapCommandParser::try_parse(&message, state) { + ParseMessageResult::Success(c) => return Ok(c), + ParseMessageResult::Error(response) => return Err(response), + ParseMessageResult::DoesNotMatch => {} + }; + match WithdrawCommandParser::try_parse(&message, state) { ParseMessageResult::Success(c) => return Ok(c), ParseMessageResult::Error(response) => return Err(response), @@ -53,6 +60,8 @@ fn try_parse_message(message: MessageContent, state: &mut RuntimeState) -> Resul text.push_str("\n\n"); text.push_str(BalanceCommandParser::help_text()); text.push_str("\n\n"); + text.push_str(SwapCommandParser::help_text()); + text.push_str("\n\n"); text.push_str(WithdrawCommandParser::help_text()); Err(state.data.build_text_response(text, None)) } diff --git a/backend/canisters/exchange_bot/impl/src/updates/mod.rs b/backend/canisters/exchange_bot/impl/src/updates/mod.rs index fa2aaac01b..11eb9ee3ce 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/mod.rs @@ -1,4 +1,3 @@ mod handle_direct_message; mod register_bot; -mod swap; mod wallet_receive; diff --git a/backend/canisters/exchange_bot/impl/src/updates/swap.rs b/backend/canisters/exchange_bot/impl/src/updates/swap.rs deleted file mode 100644 index 551a95c42f..0000000000 --- a/backend/canisters/exchange_bot/impl/src/updates/swap.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::guards::caller_is_governance_principal; -use crate::swap_client::SwapClient; -use crate::{read_state, RuntimeState}; -use canister_tracing_macros::trace; -use exchange_bot_canister::swap::{Response::*, *}; -use ic_cdk::api::call::{CallResult, RejectionCode}; -use ic_cdk_macros::update; -use types::{icrc1, TokenInfo}; - -#[update(guard = "caller_is_governance_principal")] -#[trace] -async fn swap(args: Args) -> Response { - let PrepareResult { - client, - input_token, - output_token, - } = match read_state(|state| prepare(&args, state)) { - Ok(ok) => ok, - Err(response) => return response, - }; - - match swap_impl(client, args.amount, input_token, output_token).await { - Ok(amount_out) => Success(amount_out), - Err(error) => InternalError(format!("{error:?}")), - } -} - -struct PrepareResult { - client: Box, - input_token: TokenInfo, - output_token: TokenInfo, -} - -fn prepare(args: &Args, state: &RuntimeState) -> Result { - match state.data.get_token_pair(&args.input_token, &args.output_token) { - Ok((input_token, output_token)) => { - if let Some(client) = state.get_swap_client(args.exchange_id, input_token.clone(), output_token.clone()) { - Ok(PrepareResult { - client, - input_token, - output_token, - }) - } else { - Err(PairNotSupportedByExchange) - } - } - Err(tokens) => Err(UnsupportedTokens(tokens)), - } -} - -async fn swap_impl( - client: Box, - amount: u128, - input_token: TokenInfo, - output_token: TokenInfo, -) -> CallResult { - let (ledger_canister_id, deposit_account) = client.deposit_account().await?; - - let transfer_args = icrc1::TransferArg { - from_subaccount: None, - to: deposit_account, - fee: Some(input_token.fee.into()), - created_at_time: None, - memo: None, - amount: amount.into(), - }; - if let Err(error) = icrc1_ledger_canister_c2c_client::icrc1_transfer(ledger_canister_id, &transfer_args).await? { - return Err((RejectionCode::Unknown, format!("{error:?}"))); - } - - let amount_deposited = client.deposit(amount.saturating_sub(input_token.fee)).await?; - - let amount_out = client.swap(amount_deposited).await?; - - let amount_withdrawn = client.withdraw(amount_out.saturating_sub(output_token.fee)).await?; - - Ok(amount_withdrawn) -} diff --git a/backend/canisters/user/api/src/updates/c2c_handle_bot_messages.rs b/backend/canisters/user/api/src/updates/c2c_handle_bot_messages.rs index 01fb445cc7..87c4878826 100644 --- a/backend/canisters/user/api/src/updates/c2c_handle_bot_messages.rs +++ b/backend/canisters/user/api/src/updates/c2c_handle_bot_messages.rs @@ -5,6 +5,8 @@ use types::{BotMessage, ContentValidationError}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub bot_name: String, + #[serde(default)] + pub bot_display_name: Option, pub messages: Vec, } diff --git a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs index 472691d536..b78363531d 100644 --- a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs +++ b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs @@ -105,7 +105,7 @@ async fn c2c_handle_bot_messages( message_id: None, sender_message_index: None, sender_name: args.bot_name.clone(), - sender_display_name: None, + sender_display_name: args.bot_display_name.clone(), content: message.content.into(), replies_to: None, forwarding: false, From fc2a4ad9226cf96574a741d5a8429fe3f04cd573 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Tue, 19 Sep 2023 18:52:21 +0100 Subject: [PATCH 27/39] Build swap messages --- .../exchange_bot/impl/src/commands/mod.rs | 4 ++ .../exchange_bot/impl/src/commands/swap.rs | 42 +++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index dc720a4209..a786dfbe1f 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -92,6 +92,10 @@ impl CommandSubTaskResult { matches!(self, Self::Pending) } + pub fn is_completed(&self) -> bool { + matches!(self, Self::Complete(..)) + } + pub fn is_failed(&self) -> bool { matches!(self, Self::Failed(_)) } diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 998349dc48..b412c9b2dd 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -187,13 +187,41 @@ impl SwapCommand { } pub fn build_message_text(&self) -> String { - let text = "Quotes:".to_string(); - // for (exchange_id, status) in self.quote_statuses.iter().sorted_unstable_by_key(|(_, s)| s) { - // let exchange_name = exchange_id.to_string(); - // let status_text = status.to_string(); - // text.push_str(&format!("\n{exchange_name}: {status_text}")); - // } - text + let input_token = self.input_token.token.token_symbol(); + let output_token = self.output_token.token.token_symbol(); + + let mut messages = vec!["Performing Swap:".to_string()]; + if !matches!(self.sub_tasks.check_user_balance, CommandSubTaskResult::NotRequired) { + messages.push(format!( + "Checking {input_token} balance: {}", + self.sub_tasks.check_user_balance.to_string() + )); + } + if self.sub_tasks.check_user_balance.is_completed() { + messages.push(format!("Getting quotes: {}", self.sub_tasks.quotes.to_string())); + } + if let Some(exchange_id) = self.sub_tasks.quotes.value() { + messages.push(format!( + "Transferring {input_token} to {exchange_id}: {}", + self.sub_tasks.transfer_to_dex.to_string() + )); + if self.sub_tasks.transfer_to_dex.is_completed() { + messages.push(format!( + "Notifying {exchange_id} of transfer: {}", + self.sub_tasks.notify_dex.to_string() + )); + } + if self.sub_tasks.notify_dex.is_completed() { + messages.push(format!( + "Swapping {input_token} for {output_token}: {}", + self.sub_tasks.swap.to_string() + )); + } + if self.sub_tasks.swap.is_completed() { + messages.push(format!("Withdrawing {output_token}: {}", self.sub_tasks.withdraw.to_string())); + } + } + messages.join("\n") } async fn check_user_balance(mut self, this_canister_id: CanisterId) { From c26e14256422b49249619c8c7ada8507c36d8e5a Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Tue, 19 Sep 2023 19:07:47 +0100 Subject: [PATCH 28/39] More --- backend/canisters/exchange_bot/impl/src/commands/swap.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index b412c9b2dd..c9c10463cb 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -171,7 +171,8 @@ impl SwapCommand { state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) { trace!(%message_id, "Performing swap"); - ic_cdk::spawn(self.perform_swap(client, amount_to_dex)); + let amount_to_swap = amount_to_dex.saturating_sub(self.input_token.fee); + ic_cdk::spawn(self.perform_swap(client, amount_to_swap)); } } else if self.sub_tasks.withdraw.is_pending() { if let CommandSubTaskResult::Complete(amount_swapped, _) = self.sub_tasks.swap { @@ -241,7 +242,7 @@ impl SwapCommand { .await; if let Some((exchange_id, CommandSubTaskResult::Complete(..))) = self.quotes.iter().max_by_key(|(_, r)| r.value()) { - self.sub_tasks.quotes = CommandSubTaskResult::Complete(*exchange_id, Some(exchange_id.to_string())); + self.sub_tasks.quotes = CommandSubTaskResult::Complete(*exchange_id, Some(format!("{exchange_id} is best"))); } else { self.sub_tasks.quotes = CommandSubTaskResult::Failed("Failed to get any valid quotes".to_string()); } From 83ba7f0e4a9acee481b584842810a3e3d5e165e0 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Tue, 19 Sep 2023 19:33:26 +0100 Subject: [PATCH 29/39] clippy --- .../exchange_bot/impl/src/commands/balance.rs | 2 +- .../exchange_bot/impl/src/commands/mod.rs | 8 +++---- .../exchange_bot/impl/src/commands/quote.rs | 2 +- .../impl/src/commands/sub_tasks/withdraw.rs | 2 +- .../exchange_bot/impl/src/commands/swap.rs | 22 +++++++------------ .../impl/src/commands/withdraw.rs | 4 ++-- backend/libraries/ledger_utils/src/lib.rs | 4 ++-- 7 files changed, 19 insertions(+), 25 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/balance.rs b/backend/canisters/exchange_bot/impl/src/commands/balance.rs index 609ddf6585..1fd3501d11 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/balance.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/balance.rs @@ -43,7 +43,7 @@ eg. 'balance CHAT'" }; let command = BalanceCommand::build(token, state); - ParseMessageResult::Success(Command::Balance(command)) + ParseMessageResult::Success(Command::Balance(Box::new(command))) } } diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index a786dfbe1f..5804542fea 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -23,10 +23,10 @@ pub(crate) trait CommandParser { #[derive(Serialize, Deserialize)] pub enum Command { - Balance(BalanceCommand), - Quote(QuoteCommand), - Swap(SwapCommand), - Withdraw(WithdrawCommand), + Balance(Box), + Quote(Box), + Swap(Box), + Withdraw(Box), } impl Command { diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 98d23c54e6..8671fb78c7 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -57,7 +57,7 @@ $Amount will default to 1 if not provided." let amount = (amount_decimal * 10u128.pow(input_token.decimals as u32) as f64) as u128; match QuoteCommand::build(input_token, output_token, amount, state) { - Ok(command) => ParseMessageResult::Success(Command::Quote(command)), + Ok(command) => ParseMessageResult::Success(Command::Quote(Box::new(command))), Err(error) => build_error_response(error, &state.data), } } diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs index ef1a73885f..b683c2d16f 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs @@ -18,7 +18,7 @@ pub async fn withdraw( amount: u128, now_nanos: TimestampNanos, ) -> CommandSubTaskResult { - match transfer_to_user(user_id, &token, amount, now_nanos).await { + match transfer_to_user(user_id, token, amount, now_nanos).await { Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index c9c10463cb..d7fdb8d6d4 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -60,7 +60,7 @@ If $Amount is not provided, the full balance of $InputTokens will be swapped." let amount = amount_decimal.map(|a| (a * 10u128.pow(input_token.decimals as u32) as f64) as u128); match SwapCommand::build(input_token, output_token, amount, state) { - Ok(command) => ParseMessageResult::Success(Command::Swap(command)), + Ok(command) => ParseMessageResult::Success(Command::Swap(Box::new(command))), Err(error) => build_error_response(error, &state.data), } } @@ -195,31 +195,25 @@ impl SwapCommand { if !matches!(self.sub_tasks.check_user_balance, CommandSubTaskResult::NotRequired) { messages.push(format!( "Checking {input_token} balance: {}", - self.sub_tasks.check_user_balance.to_string() + self.sub_tasks.check_user_balance )); } if self.sub_tasks.check_user_balance.is_completed() { - messages.push(format!("Getting quotes: {}", self.sub_tasks.quotes.to_string())); + messages.push(format!("Getting quotes: {}", self.sub_tasks.quotes)); } if let Some(exchange_id) = self.sub_tasks.quotes.value() { messages.push(format!( "Transferring {input_token} to {exchange_id}: {}", - self.sub_tasks.transfer_to_dex.to_string() + self.sub_tasks.transfer_to_dex )); if self.sub_tasks.transfer_to_dex.is_completed() { - messages.push(format!( - "Notifying {exchange_id} of transfer: {}", - self.sub_tasks.notify_dex.to_string() - )); + messages.push(format!("Notifying {exchange_id} of transfer: {}", self.sub_tasks.notify_dex)); } if self.sub_tasks.notify_dex.is_completed() { - messages.push(format!( - "Swapping {input_token} for {output_token}: {}", - self.sub_tasks.swap.to_string() - )); + messages.push(format!("Swapping {input_token} for {output_token}: {}", self.sub_tasks.swap)); } if self.sub_tasks.swap.is_completed() { - messages.push(format!("Withdrawing {output_token}: {}", self.sub_tasks.withdraw.to_string())); + messages.push(format!("Withdrawing {output_token}: {}", self.sub_tasks.withdraw)); } } messages.join("\n") @@ -310,7 +304,7 @@ impl SwapCommand { state.enqueue_message_edit(self.user_id, self.message_id, message_text); if !is_finished { - state.enqueue_command(Command::Swap(self)); + state.enqueue_command(Command::Swap(Box::new(self))); } } diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index 202558e492..49968c28d6 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -51,7 +51,7 @@ If $Amount is not provided, your total balance will be withdrawn" let amount = amount_decimal.map(|a| (a * 10u128.pow(token.decimals as u32) as f64) as u128); let command = WithdrawCommand::build(token, amount, state); - ParseMessageResult::Success(Command::Withdraw(command)) + ParseMessageResult::Success(Command::Withdraw(Box::new(command))) } } @@ -143,7 +143,7 @@ impl WithdrawCommand { state.enqueue_message_edit(self.user_id, self.message_id, message_text); if !is_finished { - state.enqueue_command(Command::Withdraw(self)); + state.enqueue_command(Command::Withdraw(Box::new(self))); } } diff --git a/backend/libraries/ledger_utils/src/lib.rs b/backend/libraries/ledger_utils/src/lib.rs index 20ea984ad4..fae4cac96b 100644 --- a/backend/libraries/ledger_utils/src/lib.rs +++ b/backend/libraries/ledger_utils/src/lib.rs @@ -85,8 +85,8 @@ pub fn format_crypto_amount(units: u128, decimals: u8) -> String { let subdividable_by = 10u128.pow(decimals as u32); format!("{}.{:0}", units / subdividable_by, units % subdividable_by) - .trim_end_matches("0") - .trim_end_matches(".") + .trim_end_matches('0') + .trim_end_matches('.') .to_string() } From da22770fe67bcd395881ca70b8637ae451b3c12b Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Tue, 19 Sep 2023 23:42:28 +0100 Subject: [PATCH 30/39] Fix `format_crypto_amount` --- Cargo.lock | 1 + backend/libraries/ledger_utils/Cargo.toml | 3 +++ backend/libraries/ledger_utils/src/lib.rs | 18 +++++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d5cd79c705..298c167bce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3585,6 +3585,7 @@ dependencies = [ "serde_cbor", "sha2 0.10.7", "sha256", + "test-case", "types", ] diff --git a/backend/libraries/ledger_utils/Cargo.toml b/backend/libraries/ledger_utils/Cargo.toml index c9b198bf6f..d72d6abe2e 100644 --- a/backend/libraries/ledger_utils/Cargo.toml +++ b/backend/libraries/ledger_utils/Cargo.toml @@ -15,3 +15,6 @@ serde_cbor = { workspace = true } sha2 = { workspace = true } sha256 = { path = "../sha256" } types = { path = "../types" } + +[dev-dependencies] +test-case = { workspace = true } diff --git a/backend/libraries/ledger_utils/src/lib.rs b/backend/libraries/ledger_utils/src/lib.rs index fae4cac96b..3512f0a261 100644 --- a/backend/libraries/ledger_utils/src/lib.rs +++ b/backend/libraries/ledger_utils/src/lib.rs @@ -83,8 +83,10 @@ pub fn calculate_transaction_hash(sender: CanisterId, args: &TransferArgs) -> Tr pub fn format_crypto_amount(units: u128, decimals: u8) -> String { let subdividable_by = 10u128.pow(decimals as u32); + let whole_units = units / subdividable_by; + let fractional = units % subdividable_by; - format!("{}.{:0}", units / subdividable_by, units % subdividable_by) + format!("{whole_units}.{fractional:0decimals$}", decimals = decimals as usize) .trim_end_matches('0') .trim_end_matches('.') .to_string() @@ -125,3 +127,17 @@ impl Transaction { sha256(&bytes) } } + +#[cfg(test)] +mod tests { + use test_case::test_case; + + #[test_case(1000000, 8, "0.01")] + #[test_case(321000000, 8, "3.21")] + #[test_case(9876543210, 6, "9876.54321")] + #[test_case(123456789, 8, "1.23456789")] + fn format(units: u128, decimals: u8, expected: &str) { + let formatted = super::format_crypto_amount(units, decimals); + assert_eq!(formatted, expected); + } +} From fbdfdd1e8babbe8ed22ea852ea5d9051c72cf570 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 20 Sep 2023 08:05:32 +0100 Subject: [PATCH 31/39] Retry failed notifications + withdrawals --- .../exchange_bot/impl/src/commands/swap.rs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index d7fdb8d6d4..04daa507dd 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -13,7 +13,7 @@ use rand::Rng; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use tracing::trace; +use tracing::{error, trace}; use types::icrc1::{BlockIndex, TransferArg}; use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TimestampNanos, TokenInfo, UserId}; @@ -274,7 +274,17 @@ impl SwapCommand { async fn notify_dex(mut self, client: Box, amount: u128) { self.sub_tasks.notify_dex = match client.deposit(amount).await { Ok(_) => CommandSubTaskResult::Complete((), None), - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + Err(error) => { + error!( + error = format!("{error:?}").as_str(), + message_id = %self.message_id, + exchange_id = %client.exchange_id(), + token = self.input_token.token.token_symbol(), + amount, + "Failed to notify dex, retrying" + ); + CommandSubTaskResult::Pending + } }; mutate_state(|state| self.on_updated(state)); @@ -292,7 +302,19 @@ impl SwapCommand { } async fn withdraw(mut self, amount: u128, now_nanos: TimestampNanos) { - self.sub_tasks.withdraw = withdraw(self.user_id, &self.output_token, amount, now_nanos).await; + self.sub_tasks.withdraw = match withdraw(self.user_id, &self.output_token, amount, now_nanos).await { + CommandSubTaskResult::Failed(error) => { + error!( + error = format!("{error:?}").as_str(), + message_id = %self.message_id, + token = self.output_token.token.token_symbol(), + amount, + "Failed to withdraw, retrying" + ); + CommandSubTaskResult::Pending + } + result => result, + }; mutate_state(|state| self.on_updated(state)); } From 1919aaa1b7dd7be93559e963c883fbd2907d4847 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 20 Sep 2023 08:13:39 +0100 Subject: [PATCH 32/39] Fix token0 and token1 order --- backend/canisters/exchange_bot/impl/src/icpswap/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs index 6167bf9e17..cb933d6420 100644 --- a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs @@ -15,7 +15,7 @@ impl ICPSwapClientFactory { fn lookup_swap_canister_id(&self, token0: &TokenInfo, token1: &TokenInfo) -> Option { match (token0.token.clone(), token1.token.clone()) { - (Cryptocurrency::InternetComputer, Cryptocurrency::CHAT) => { + (Cryptocurrency::CHAT, Cryptocurrency::InternetComputer) => { Some(CanisterId::from_text("ne2vj-6yaaa-aaaag-qb3ia-cai").unwrap()) } _ => None, From 6f1af563b5f603259f4d4053df1931d63f550962 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 20 Sep 2023 08:39:50 +0100 Subject: [PATCH 33/39] Withdraw from dex before transferring back to user --- .../exchange_bot/impl/src/commands/swap.rs | 107 ++++++++++++------ 1 file changed, 74 insertions(+), 33 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 04daa507dd..5596cd4ecc 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -86,7 +86,8 @@ pub struct SwapCommandSubTasks { pub transfer_to_dex: CommandSubTaskResult, pub notify_dex: CommandSubTaskResult<()>, pub swap: CommandSubTaskResult, - pub withdraw: CommandSubTaskResult, + pub withdraw_from_dex: CommandSubTaskResult, + pub transfer_to_user: CommandSubTaskResult, } impl SwapCommand { @@ -138,7 +139,7 @@ impl SwapCommand { if self.sub_tasks.check_user_balance.is_pending() { trace!(%message_id, "Checking user balance"); ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); - } else if let Some(amount) = self.amount() { + } else if let Some(quote_amount) = self.amount() { match self.sub_tasks.quotes { CommandSubTaskResult::Pending => { let clients: Vec<_> = self @@ -148,37 +149,35 @@ impl SwapCommand { .collect(); trace!(%message_id, "Getting quotes"); - ic_cdk::spawn(self.get_quotes(clients, amount)); + ic_cdk::spawn(self.get_quotes(clients, quote_amount)); } CommandSubTaskResult::Complete(exchange_id, _) => { - let amount_to_dex = amount.saturating_sub(self.input_token.fee); - if self.sub_tasks.transfer_to_dex.is_pending() { - if let Some(client) = - state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) - { + if let Some(client) = + state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) + { + let amount_to_dex = quote_amount.saturating_sub(self.input_token.fee); + if self.sub_tasks.transfer_to_dex.is_pending() { trace!(%message_id, "Transferring to dex"); ic_cdk::spawn(self.transfer_to_dex(client, amount_to_dex)); - } - } else if self.sub_tasks.notify_dex.is_pending() { - if let Some(client) = - state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) - { + } else if self.sub_tasks.notify_dex.is_pending() { trace!(%message_id, "Notifying to dex"); ic_cdk::spawn(self.notify_dex(client, amount_to_dex)); - } - } else if self.sub_tasks.swap.is_pending() { - if let Some(client) = - state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) - { + } else if self.sub_tasks.swap.is_pending() { trace!(%message_id, "Performing swap"); let amount_to_swap = amount_to_dex.saturating_sub(self.input_token.fee); ic_cdk::spawn(self.perform_swap(client, amount_to_swap)); - } - } else if self.sub_tasks.withdraw.is_pending() { - if let CommandSubTaskResult::Complete(amount_swapped, _) = self.sub_tasks.swap { - let amount_out = amount_swapped.saturating_sub(self.output_token.fee); - trace!(%message_id, "Withdrawing from dex"); - ic_cdk::spawn(self.withdraw(amount_out, state.env.now_nanos())); + } else if self.sub_tasks.withdraw_from_dex.is_pending() { + if let Some(&amount_swapped) = self.sub_tasks.swap.value() { + let amount_out = amount_swapped.saturating_sub(self.output_token.fee); + trace!(%message_id, "Withdrawing from dex"); + ic_cdk::spawn(self.withdraw_from_dex(client, amount_out)); + } + } else if self.sub_tasks.transfer_to_user.is_pending() { + if let Some(&amount_withdrawn_from_dex) = self.sub_tasks.withdraw_from_dex.value() { + let amount_to_user = amount_withdrawn_from_dex.saturating_sub(self.output_token.fee); + trace!(%message_id, "Transferring funds to user"); + ic_cdk::spawn(self.transfer_funds_to_user(amount_to_user, state.env.now_nanos())); + } } } } @@ -213,7 +212,16 @@ impl SwapCommand { messages.push(format!("Swapping {input_token} for {output_token}: {}", self.sub_tasks.swap)); } if self.sub_tasks.swap.is_completed() { - messages.push(format!("Withdrawing {output_token}: {}", self.sub_tasks.withdraw)); + messages.push(format!( + "Withdrawing {output_token} from {exchange_id}: {}", + self.sub_tasks.withdraw_from_dex + )); + } + if self.sub_tasks.withdraw_from_dex.is_completed() { + messages.push(format!( + "Transferring {output_token} to user: {}", + self.sub_tasks.transfer_to_user + )); } } messages.join("\n") @@ -278,7 +286,7 @@ impl SwapCommand { error!( error = format!("{error:?}").as_str(), message_id = %self.message_id, - exchange_id = %client.exchange_id(), + exchange = %client.exchange_id(), token = self.input_token.token.token_symbol(), amount, "Failed to notify dex, retrying" @@ -295,28 +303,60 @@ impl SwapCommand { Ok(amount_out) => { CommandSubTaskResult::Complete(amount_out, Some(format_crypto_amount(amount_out, self.output_token.decimals))) } - Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), + Err(error) => { + error!( + error = format!("{error:?}").as_str(), + message_id = %self.message_id, + exchange = %client.exchange_id(), + input_token = self.input_token.token.token_symbol(), + output_token = self.output_token.token.token_symbol(), + amount, + "Failed to perform swap, retrying" + ); + CommandSubTaskResult::Pending + } + }; + + mutate_state(|state| self.on_updated(state)) + } + + async fn withdraw_from_dex(mut self, client: Box, amount: u128) { + self.sub_tasks.withdraw_from_dex = match client.withdraw(amount).await { + Ok(amount_out) => { + CommandSubTaskResult::Complete(amount_out, Some(format_crypto_amount(amount_out, self.output_token.decimals))) + } + Err(error) => { + error!( + error = format!("{error:?}").as_str(), + message_id = %self.message_id, + exchange = %client.exchange_id(), + token = self.output_token.token.token_symbol(), + amount, + "Failed to withdraw from dex, retrying" + ); + CommandSubTaskResult::Pending + } }; mutate_state(|state| self.on_updated(state)) } - async fn withdraw(mut self, amount: u128, now_nanos: TimestampNanos) { - self.sub_tasks.withdraw = match withdraw(self.user_id, &self.output_token, amount, now_nanos).await { + async fn transfer_funds_to_user(mut self, amount: u128, now_nanos: TimestampNanos) { + self.sub_tasks.transfer_to_user = match withdraw(self.user_id, &self.output_token, amount, now_nanos).await { CommandSubTaskResult::Failed(error) => { error!( error = format!("{error:?}").as_str(), message_id = %self.message_id, token = self.output_token.token.token_symbol(), amount, - "Failed to withdraw, retrying" + "Failed to transfer funds to user, retrying" ); CommandSubTaskResult::Pending } result => result, }; - mutate_state(|state| self.on_updated(state)); + mutate_state(|state| self.on_updated(state)) } fn on_updated(self, state: &mut RuntimeState) { @@ -347,7 +387,7 @@ impl SwapCommand { } fn is_finished(&self) -> bool { - self.sub_tasks.any_failed() || !self.sub_tasks.withdraw.is_pending() + self.sub_tasks.any_failed() || !self.sub_tasks.transfer_to_user.is_pending() } } @@ -358,7 +398,8 @@ impl SwapCommandSubTasks { || self.transfer_to_dex.is_failed() || self.notify_dex.is_failed() || self.swap.is_failed() - || self.withdraw.is_failed() + || self.withdraw_from_dex.is_failed() + || self.transfer_to_user.is_failed() } } From dd391d0a7a32e244c5a4c3d6f6f58ad37747fa60 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 20 Sep 2023 09:58:36 +0100 Subject: [PATCH 34/39] Support withdrawing from default subaccount --- .../exchange_bot/impl/src/commands/sub_tasks/withdraw.rs | 8 +++++--- backend/canisters/exchange_bot/impl/src/commands/swap.rs | 2 +- .../canisters/exchange_bot/impl/src/commands/withdraw.rs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs index b683c2d16f..cb999680bc 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/withdraw.rs @@ -3,7 +3,7 @@ use crate::model::messages_pending::MessagePending; use crate::mutate_state; use candid::{Nat, Principal}; use ic_cdk::api::call::CallResult; -use ic_ledger_types::{AccountIdentifier, Memo, Timestamp, Tokens, TransferArgs}; +use ic_ledger_types::{AccountIdentifier, Memo, Timestamp, Tokens, TransferArgs, DEFAULT_SUBACCOUNT}; use ledger_utils::{calculate_transaction_hash, convert_to_subaccount, default_ledger_account}; use rand::Rng; use types::icrc1::{Account, BlockIndex, CryptoAccount, TransferArg, TransferError}; @@ -16,9 +16,10 @@ pub async fn withdraw( user_id: UserId, token: &TokenInfo, amount: u128, + default_subaccount: bool, now_nanos: TimestampNanos, ) -> CommandSubTaskResult { - match transfer_to_user(user_id, token, amount, now_nanos).await { + match transfer_to_user(user_id, token, amount, default_subaccount, now_nanos).await { Ok(Ok(block_index)) => CommandSubTaskResult::Complete(block_index, None), Ok(Err(error)) => CommandSubTaskResult::Failed(format!("{error:?}")), Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), @@ -29,9 +30,10 @@ async fn transfer_to_user( user_id: UserId, token: &TokenInfo, amount: u128, + default_subaccount: bool, now_nanos: TimestampNanos, ) -> CallResult> { - let subaccount = convert_to_subaccount(&user_id.into()); + let subaccount = if default_subaccount { DEFAULT_SUBACCOUNT } else { convert_to_subaccount(&user_id.into()) }; let response = icrc1_ledger_canister_c2c_client::icrc1_transfer( token.ledger, &TransferArg { diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 5596cd4ecc..8e24e5c98c 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -342,7 +342,7 @@ impl SwapCommand { } async fn transfer_funds_to_user(mut self, amount: u128, now_nanos: TimestampNanos) { - self.sub_tasks.transfer_to_user = match withdraw(self.user_id, &self.output_token, amount, now_nanos).await { + self.sub_tasks.transfer_to_user = match withdraw(self.user_id, &self.output_token, amount, true, now_nanos).await { CommandSubTaskResult::Failed(error) => { error!( error = format!("{error:?}").as_str(), diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index 49968c28d6..c226cfc389 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -131,7 +131,7 @@ impl WithdrawCommand { } async fn withdraw(mut self, amount: u128, now_nanos: TimestampNanos) { - self.sub_tasks.withdraw = withdraw(self.user_id, &self.token, amount, now_nanos).await; + self.sub_tasks.withdraw = withdraw(self.user_id, &self.token, amount, false, now_nanos).await; mutate_state(|state| self.on_updated(state)); } From 2edd838600b429b6c9826e538f2f48dc2d11a640 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 20 Sep 2023 10:53:53 +0100 Subject: [PATCH 35/39] Fix amount sent to dex which was off by 1 transfer fee --- backend/canisters/exchange_bot/impl/src/commands/swap.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 8e24e5c98c..1ad7b19eec 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -139,7 +139,7 @@ impl SwapCommand { if self.sub_tasks.check_user_balance.is_pending() { trace!(%message_id, "Checking user balance"); ic_cdk::spawn(self.check_user_balance(state.env.canister_id())); - } else if let Some(quote_amount) = self.amount() { + } else if let Some(amount_to_dex) = self.amount() { match self.sub_tasks.quotes { CommandSubTaskResult::Pending => { let clients: Vec<_> = self @@ -149,13 +149,12 @@ impl SwapCommand { .collect(); trace!(%message_id, "Getting quotes"); - ic_cdk::spawn(self.get_quotes(clients, quote_amount)); + ic_cdk::spawn(self.get_quotes(clients, amount_to_dex)); } CommandSubTaskResult::Complete(exchange_id, _) => { if let Some(client) = state.get_swap_client(exchange_id, self.input_token.clone(), self.output_token.clone()) { - let amount_to_dex = quote_amount.saturating_sub(self.input_token.fee); if self.sub_tasks.transfer_to_dex.is_pending() { trace!(%message_id, "Transferring to dex"); ic_cdk::spawn(self.transfer_to_dex(client, amount_to_dex)); From 67aec94d27f7bf580824e27087087bc835b99bd3 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 20 Sep 2023 12:01:55 +0100 Subject: [PATCH 36/39] Simplify --- .../exchange_bot/impl/src/commands/balance.rs | 4 +- .../exchange_bot/impl/src/commands/mod.rs | 30 ++--- .../exchange_bot/impl/src/commands/quote.rs | 6 +- .../exchange_bot/impl/src/commands/swap.rs | 11 +- .../impl/src/commands/withdraw.rs | 4 +- .../canisters/exchange_bot/impl/src/lib.rs | 31 +---- .../impl/src/updates/handle_direct_message.rs | 122 ++++++++++++------ .../libraries/types/src/message_content.rs | 6 + 8 files changed, 106 insertions(+), 108 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/balance.rs b/backend/canisters/exchange_bot/impl/src/commands/balance.rs index 1fd3501d11..7ad468267c 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/balance.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/balance.rs @@ -1,6 +1,6 @@ use crate::commands::common_errors::CommonErrors; use crate::commands::sub_tasks::check_user_balance::check_user_balance; -use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; +use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; use rand::Rng; @@ -39,7 +39,7 @@ eg. 'balance CHAT'" t } else { let error = CommonErrors::UnsupportedTokens(vec![token.to_string()]); - return build_error_response(error, &state.data); + return ParseMessageResult::Error(error.build_response_message(&state.data)); }; let command = BalanceCommand::build(token, state); diff --git a/backend/canisters/exchange_bot/impl/src/commands/mod.rs b/backend/canisters/exchange_bot/impl/src/commands/mod.rs index 5804542fea..afe8c144d9 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/mod.rs @@ -1,12 +1,11 @@ use crate::commands::balance::BalanceCommand; -use crate::commands::common_errors::CommonErrors; use crate::commands::quote::QuoteCommand; use crate::commands::swap::SwapCommand; use crate::commands::withdraw::WithdrawCommand; -use crate::{Data, RuntimeState}; +use crate::RuntimeState; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; -use types::{MessageContent, MessageContentInitial, MessageId, TextContent}; +use types::{MessageContent, MessageId}; pub mod balance; pub mod common_errors; @@ -48,20 +47,12 @@ impl Command { } } - pub fn build_message(&self) -> MessageContentInitial { + pub fn build_message_text(&self) -> String { match self { - Command::Balance(b) => MessageContentInitial::Text(TextContent { - text: b.build_message_text(), - }), - Command::Quote(q) => MessageContentInitial::Text(TextContent { - text: q.build_message_text(), - }), - Command::Swap(s) => MessageContentInitial::Text(TextContent { - text: s.build_message_text(), - }), - Command::Withdraw(w) => MessageContentInitial::Text(TextContent { - text: w.build_message_text(), - }), + Command::Balance(b) => b.build_message_text(), + Command::Quote(q) => q.build_message_text(), + Command::Swap(s) => s.build_message_text(), + Command::Withdraw(w) => w.build_message_text(), } } } @@ -69,15 +60,10 @@ impl Command { #[allow(clippy::large_enum_variant)] pub enum ParseMessageResult { Success(Command), - Error(exchange_bot_canister::handle_direct_message::Response), + Error(String), DoesNotMatch, } -fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { - let response_message = error.build_response_message(data); - ParseMessageResult::Error(data.build_text_response(response_message, None)) -} - #[derive(Serialize, Deserialize, Default)] pub enum CommandSubTaskResult { NotRequired, diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 8671fb78c7..09da266a5d 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -1,6 +1,6 @@ use crate::commands::common_errors::CommonErrors; use crate::commands::sub_tasks::get_quotes::get_quotes; -use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; +use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; use crate::{mutate_state, RuntimeState}; use exchange_bot_canister::ExchangeId; @@ -50,7 +50,7 @@ $Amount will default to 1 if not provided." Ok((i, o)) => (i, o), Err(tokens) => { let error = CommonErrors::UnsupportedTokens(tokens); - return build_error_response(error, &state.data); + return ParseMessageResult::Error(error.build_response_message(&state.data)); } }; @@ -58,7 +58,7 @@ $Amount will default to 1 if not provided." match QuoteCommand::build(input_token, output_token, amount, state) { Ok(command) => ParseMessageResult::Success(Command::Quote(Box::new(command))), - Err(error) => build_error_response(error, &state.data), + Err(error) => ParseMessageResult::Error(error.build_response_message(&state.data)), } } } diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index 1ad7b19eec..bcecef6b7f 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -4,7 +4,7 @@ use crate::commands::sub_tasks::get_quotes::get_quotes; use crate::commands::sub_tasks::withdraw::withdraw; use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::swap_client::SwapClient; -use crate::{mutate_state, Data, RuntimeState}; +use crate::{mutate_state, RuntimeState}; use candid::Principal; use exchange_bot_canister::ExchangeId; use lazy_static::lazy_static; @@ -53,7 +53,7 @@ If $Amount is not provided, the full balance of $InputTokens will be swapped." Ok((i, o)) => (i, o), Err(tokens) => { let error = CommonErrors::UnsupportedTokens(tokens); - return build_error_response(error, &state.data); + return ParseMessageResult::Error(error.build_response_message(&state.data)); } }; @@ -61,7 +61,7 @@ If $Amount is not provided, the full balance of $InputTokens will be swapped." match SwapCommand::build(input_token, output_token, amount, state) { Ok(command) => ParseMessageResult::Success(Command::Swap(Box::new(command))), - Err(error) => build_error_response(error, &state.data), + Err(error) => ParseMessageResult::Error(error.build_response_message(&state.data)), } } } @@ -401,8 +401,3 @@ impl SwapCommandSubTasks { || self.transfer_to_user.is_failed() } } - -fn build_error_response(error: CommonErrors, data: &Data) -> ParseMessageResult { - let response_message = error.build_response_message(data); - ParseMessageResult::Error(data.build_text_response(response_message, None)) -} diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index c226cfc389..d2e6a2172d 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -1,7 +1,7 @@ use crate::commands::common_errors::CommonErrors; use crate::commands::sub_tasks::check_user_balance::check_user_balance; use crate::commands::sub_tasks::withdraw::withdraw; -use crate::commands::{build_error_response, Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; +use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessageResult}; use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; use ledger_utils::format_crypto_amount; @@ -45,7 +45,7 @@ If $Amount is not provided, your total balance will be withdrawn" t } else { let error = CommonErrors::UnsupportedTokens(vec![token.to_string()]); - return build_error_response(error, &state.data); + return ParseMessageResult::Error(error.build_response_message(&state.data)); }; let amount = amount_decimal.map(|a| (a * 10u128.pow(token.decimals as u32) as f64) as u128); diff --git a/backend/canisters/exchange_bot/impl/src/lib.rs b/backend/canisters/exchange_bot/impl/src/lib.rs index c7c07f0159..021085677e 100644 --- a/backend/canisters/exchange_bot/impl/src/lib.rs +++ b/backend/canisters/exchange_bot/impl/src/lib.rs @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use types::{ - BotMessage, BuildVersion, CanisterId, Cryptocurrency, Cycles, MessageContent, MessageContentInitial, MessageId, - TextContent, TimestampMillis, Timestamped, TokenInfo, UserId, + BuildVersion, CanisterId, Cryptocurrency, Cycles, MessageContent, MessageId, TextContent, TimestampMillis, Timestamped, + TokenInfo, UserId, }; use utils::env::Environment; @@ -180,33 +180,6 @@ impl Data { .sorted_unstable() .collect() } - - pub fn build_text_response( - &self, - text: String, - message_id: Option, - ) -> exchange_bot_canister::handle_direct_message::Response { - self.build_response(MessageContentInitial::Text(TextContent { text }), message_id) - } - - pub fn build_response( - &self, - message: MessageContentInitial, - message_id: Option, - ) -> exchange_bot_canister::handle_direct_message::Response { - let (username, display_name) = (self.username.clone(), self.display_name.clone()); - - exchange_bot_canister::handle_direct_message::Response::Success( - exchange_bot_canister::handle_direct_message::SuccessResult { - bot_name: username, - bot_display_name: display_name, - messages: vec![BotMessage { - content: message, - message_id, - }], - }, - ) - } } fn build_token_info() -> Vec { diff --git a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs index 2b7e69cc73..8fa1e8ccc7 100644 --- a/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs +++ b/backend/canisters/exchange_bot/impl/src/updates/handle_direct_message.rs @@ -2,68 +2,106 @@ use crate::commands::balance::BalanceCommandParser; use crate::commands::quote::QuoteCommandParser; use crate::commands::swap::SwapCommandParser; use crate::commands::withdraw::WithdrawCommandParser; -use crate::commands::{Command, CommandParser, ParseMessageResult}; -use crate::{mutate_state, read_state, RuntimeState}; +use crate::commands::{CommandParser, ParseMessageResult}; +use crate::{mutate_state, read_state, Data, RuntimeState}; use candid::Principal; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; use exchange_bot_canister::handle_direct_message::*; +use ledger_utils::format_crypto_amount; use local_user_index_canister_c2c_client::LookupUserError; -use types::{MessageContent, UserId}; +use types::{BotMessage, MessageContent, MessageContentInitial, UserId}; #[update_msgpack] #[trace] async fn handle_direct_message(args: Args) -> Response { - if let Err(message) = verify_caller().await { - return read_state(|state| state.data.build_text_response(message, None)); + if let Err(error) = verify_caller().await { + return read_state(|state| build_response(vec![convert_to_message(error)], &state.data)); }; - mutate_state(|state| match try_parse_message(args.content, state) { - Ok(command) => { - let message = command.build_message(); - let message_id = command.message_id(); - let response = state.data.build_response(message, Some(message_id)); - command.process(state); - response - } - Err(response) => response, - }) + mutate_state(|state| handle_direct_message_impl(args.content, state)) } -fn try_parse_message(message: MessageContent, state: &mut RuntimeState) -> Result { +fn handle_direct_message_impl(message: MessageContent, state: &mut RuntimeState) -> Response { + let mut command = None; + let mut response_messages = Vec::new(); + + if let MessageContent::Crypto(c) = &message { + let token = c.transfer.token(); + response_messages.push(convert_to_message(format!( + "{} {} received", + format_crypto_amount(c.transfer.units(), token.decimals().unwrap_or(8)), + token.token_symbol() + ))); + } + match BalanceCommandParser::try_parse(&message, state) { - ParseMessageResult::Success(c) => return Ok(c), - ParseMessageResult::Error(response) => return Err(response), + ParseMessageResult::Success(c) => command = Some(c), + ParseMessageResult::Error(e) => response_messages.push(convert_to_message(e)), ParseMessageResult::DoesNotMatch => {} }; - match QuoteCommandParser::try_parse(&message, state) { - ParseMessageResult::Success(c) => return Ok(c), - ParseMessageResult::Error(response) => return Err(response), - ParseMessageResult::DoesNotMatch => {} - }; + if command.is_none() { + match QuoteCommandParser::try_parse(&message, state) { + ParseMessageResult::Success(c) => command = Some(c), + ParseMessageResult::Error(e) => response_messages.push(convert_to_message(e)), + ParseMessageResult::DoesNotMatch => {} + }; + } - match SwapCommandParser::try_parse(&message, state) { - ParseMessageResult::Success(c) => return Ok(c), - ParseMessageResult::Error(response) => return Err(response), - ParseMessageResult::DoesNotMatch => {} - }; + if command.is_none() { + match SwapCommandParser::try_parse(&message, state) { + ParseMessageResult::Success(c) => command = Some(c), + ParseMessageResult::Error(e) => response_messages.push(convert_to_message(e)), + ParseMessageResult::DoesNotMatch => {} + }; + } - match WithdrawCommandParser::try_parse(&message, state) { - ParseMessageResult::Success(c) => return Ok(c), - ParseMessageResult::Error(response) => return Err(response), - ParseMessageResult::DoesNotMatch => {} - }; + if command.is_none() { + match WithdrawCommandParser::try_parse(&message, state) { + ParseMessageResult::Success(c) => command = Some(c), + ParseMessageResult::Error(e) => response_messages.push(convert_to_message(e)), + ParseMessageResult::DoesNotMatch => {} + }; + } + + if let Some(command) = command { + response_messages.push(BotMessage { + content: MessageContentInitial::Text(command.build_message_text().into()), + message_id: Some(command.message_id()), + }); + command.process(state); + } + + let add_help_text = response_messages.is_empty(); + if add_help_text { + let mut text = "This bot currently supports the following message formats:\n\n".to_string(); + text.push_str(QuoteCommandParser::help_text()); + text.push_str("\n\n"); + text.push_str(BalanceCommandParser::help_text()); + text.push_str("\n\n"); + text.push_str(SwapCommandParser::help_text()); + text.push_str("\n\n"); + text.push_str(WithdrawCommandParser::help_text()); + response_messages.push(convert_to_message(text)); + } - let mut text = "This bot currently supports the following message formats:\n\n".to_string(); - text.push_str(QuoteCommandParser::help_text()); - text.push_str("\n\n"); - text.push_str(BalanceCommandParser::help_text()); - text.push_str("\n\n"); - text.push_str(SwapCommandParser::help_text()); - text.push_str("\n\n"); - text.push_str(WithdrawCommandParser::help_text()); - Err(state.data.build_text_response(text, None)) + build_response(response_messages, &state.data) +} + +fn build_response(messages: Vec, data: &Data) -> Response { + Success(SuccessResult { + bot_name: data.username.clone(), + bot_display_name: data.display_name.clone(), + messages, + }) +} + +fn convert_to_message(text: String) -> BotMessage { + BotMessage { + content: MessageContentInitial::Text(text.into()), + message_id: None, + } } async fn verify_caller() -> Result { diff --git a/backend/libraries/types/src/message_content.rs b/backend/libraries/types/src/message_content.rs index 2707acd21f..4296ca1234 100644 --- a/backend/libraries/types/src/message_content.rs +++ b/backend/libraries/types/src/message_content.rs @@ -385,6 +385,12 @@ pub struct TextContent { pub text: String, } +impl From for TextContent { + fn from(value: String) -> Self { + TextContent { text: value } + } +} + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct ImageContent { pub width: u32, From 1e4e8b7b0792a838704fb5e08881aadfb70154ae Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 20 Sep 2023 12:42:08 +0100 Subject: [PATCH 37/39] Avoid unnecessary message edits --- .../exchange_bot/impl/src/commands/swap.rs | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index bcecef6b7f..a5954e19a5 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -298,9 +298,13 @@ impl SwapCommand { } async fn perform_swap(mut self, client: Box, amount: u128) { - self.sub_tasks.swap = match client.swap(amount).await { + match client.swap(amount).await { Ok(amount_out) => { - CommandSubTaskResult::Complete(amount_out, Some(format_crypto_amount(amount_out, self.output_token.decimals))) + self.sub_tasks.swap = CommandSubTaskResult::Complete( + amount_out, + Some(format_crypto_amount(amount_out, self.output_token.decimals)), + ); + mutate_state(|state| self.on_updated(state)); } Err(error) => { error!( @@ -312,17 +316,19 @@ impl SwapCommand { amount, "Failed to perform swap, retrying" ); - CommandSubTaskResult::Pending + mutate_state(|state| self.enqueue(state)); } - }; - - mutate_state(|state| self.on_updated(state)) + } } async fn withdraw_from_dex(mut self, client: Box, amount: u128) { - self.sub_tasks.withdraw_from_dex = match client.withdraw(amount).await { + match client.withdraw(amount).await { Ok(amount_out) => { - CommandSubTaskResult::Complete(amount_out, Some(format_crypto_amount(amount_out, self.output_token.decimals))) + self.sub_tasks.withdraw_from_dex = CommandSubTaskResult::Complete( + amount_out, + Some(format_crypto_amount(amount_out, self.output_token.decimals)), + ); + mutate_state(|state| self.on_updated(state)) } Err(error) => { error!( @@ -333,15 +339,13 @@ impl SwapCommand { amount, "Failed to withdraw from dex, retrying" ); - CommandSubTaskResult::Pending + mutate_state(|state| self.enqueue(state)); } }; - - mutate_state(|state| self.on_updated(state)) } async fn transfer_funds_to_user(mut self, amount: u128, now_nanos: TimestampNanos) { - self.sub_tasks.transfer_to_user = match withdraw(self.user_id, &self.output_token, amount, true, now_nanos).await { + match withdraw(self.user_id, &self.output_token, amount, true, now_nanos).await { CommandSubTaskResult::Failed(error) => { error!( error = format!("{error:?}").as_str(), @@ -350,21 +354,23 @@ impl SwapCommand { amount, "Failed to transfer funds to user, retrying" ); - CommandSubTaskResult::Pending + mutate_state(|state| self.enqueue(state)); + } + result => { + self.sub_tasks.transfer_to_user = result; + mutate_state(|state| self.on_updated(state)) } - result => result, }; - - mutate_state(|state| self.on_updated(state)) } fn on_updated(self, state: &mut RuntimeState) { - let is_finished = self.is_finished(); - let message_text = self.build_message_text(); state.enqueue_message_edit(self.user_id, self.message_id, message_text); + self.enqueue(state); + } - if !is_finished { + fn enqueue(self, state: &mut RuntimeState) { + if !self.is_finished() { state.enqueue_command(Command::Swap(Box::new(self))); } } From 1af7039b942f111dede5721aa90c97f42501c4a2 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Wed, 20 Sep 2023 13:16:02 +0100 Subject: [PATCH 38/39] Add token symbol to quotes --- .../exchange_bot/impl/src/commands/quote.rs | 2 +- .../impl/src/commands/sub_tasks/get_quotes.rs | 19 ++++++++++--------- .../exchange_bot/impl/src/commands/swap.rs | 2 +- .../exchange_bot/impl/src/icpswap/mod.rs | 8 ++++++++ .../exchange_bot/impl/src/swap_client.rs | 2 ++ backend/libraries/icpswap_client/src/lib.rs | 16 ++++++++++++++++ 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 09da266a5d..373f835d63 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -132,7 +132,7 @@ impl QuoteCommand { } async fn get_quotes(mut self, clients: Vec>, amount: u128) { - get_quotes(clients, amount, self.output_token.decimals, |exchange_id, result| { + get_quotes(clients, amount, |exchange_id, result| { self.set_quote_result(exchange_id, result); let message_text = self.build_message_text(); mutate_state(|state| { diff --git a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs index f377f64cd4..f26d581da3 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/sub_tasks/get_quotes.rs @@ -9,12 +9,11 @@ use std::future::ready; pub(crate) async fn get_quotes)>( clients: Vec>, amount: u128, - output_token_decimals: u8, mut callback: C, ) { let futures = FuturesUnordered::new(); for client in clients { - futures.push(get_quote(client, amount, output_token_decimals)); + futures.push(get_quote(client, amount)); } futures @@ -25,20 +24,22 @@ pub(crate) async fn get_quotes)> .await; } -async fn get_quote( - client: Box, - amount: u128, - output_token_decimals: u8, -) -> (ExchangeId, CommandSubTaskResult) { - let exchange_id = client.exchange_id(); +async fn get_quote(client: Box, amount: u128) -> (ExchangeId, CommandSubTaskResult) { let response = client.quote(amount).await; let result = match response { Ok(amount_out) => { - CommandSubTaskResult::Complete(amount_out, Some(format_crypto_amount(amount_out, output_token_decimals))) + let output_token = client.output_token(); + let text = format!( + "{} {}", + format_crypto_amount(amount_out, output_token.decimals), + output_token.token.token_symbol() + ); + CommandSubTaskResult::Complete(amount_out, Some(text)) } Err(error) => CommandSubTaskResult::Failed(format!("{error:?}")), }; + let exchange_id = client.exchange_id(); (exchange_id, result) } diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index a5954e19a5..ace549f488 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -233,7 +233,7 @@ impl SwapCommand { } async fn get_quotes(mut self, clients: Vec>, amount: u128) { - get_quotes(clients, amount, self.output_token.decimals, |exchange_id, result| { + get_quotes(clients, amount, |exchange_id, result| { self.set_quote_result(exchange_id, result); let message_text = self.build_message_text(); mutate_state(|state| { diff --git a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs index cb933d6420..9b7ff0cf49 100644 --- a/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs +++ b/backend/canisters/exchange_bot/impl/src/icpswap/mod.rs @@ -58,6 +58,14 @@ impl SwapClient for ICPSwapClient { ExchangeId::ICPSwap } + fn input_token(&self) -> &TokenInfo { + self.input_token() + } + + fn output_token(&self) -> &TokenInfo { + self.output_token() + } + async fn quote(&self, amount: u128) -> CallResult { self.quote(amount).await } diff --git a/backend/canisters/exchange_bot/impl/src/swap_client.rs b/backend/canisters/exchange_bot/impl/src/swap_client.rs index 181ed29183..3e33432f95 100644 --- a/backend/canisters/exchange_bot/impl/src/swap_client.rs +++ b/backend/canisters/exchange_bot/impl/src/swap_client.rs @@ -16,6 +16,8 @@ pub trait SwapClientFactory { #[async_trait] pub trait SwapClient { fn exchange_id(&self) -> ExchangeId; + fn input_token(&self) -> &TokenInfo; + fn output_token(&self) -> &TokenInfo; async fn quote(&self, amount: u128) -> CallResult; async fn deposit_account(&self) -> CallResult<(CanisterId, Account)>; async fn deposit(&self, amount: u128) -> CallResult<()>; diff --git a/backend/libraries/icpswap_client/src/lib.rs b/backend/libraries/icpswap_client/src/lib.rs index 9442c05d1e..3311ae0867 100644 --- a/backend/libraries/icpswap_client/src/lib.rs +++ b/backend/libraries/icpswap_client/src/lib.rs @@ -42,6 +42,22 @@ impl ICPSwapClient { ) } + pub fn input_token(&self) -> &TokenInfo { + if self.zero_for_one { + &self.token0 + } else { + &self.token1 + } + } + + pub fn output_token(&self) -> &TokenInfo { + if self.zero_for_one { + &self.token1 + } else { + &self.token0 + } + } + pub async fn quote(&self, amount: u128) -> CallResult { let args = icpswap_swap_pool_canister::quote::Args { operator: self.this_canister_id, From d4af59e7993d0a16c895c890562fe5cfaac52ef0 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Thu, 21 Sep 2023 22:15:24 +0100 Subject: [PATCH 39/39] Switch to `regex-lite` --- Cargo.lock | 2 +- backend/canisters/exchange_bot/impl/Cargo.toml | 2 +- backend/canisters/exchange_bot/impl/src/commands/balance.rs | 2 +- backend/canisters/exchange_bot/impl/src/commands/quote.rs | 2 +- backend/canisters/exchange_bot/impl/src/commands/swap.rs | 2 +- backend/canisters/exchange_bot/impl/src/commands/withdraw.rs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9da7245e9c..1ece858690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2125,7 +2125,7 @@ dependencies = [ "local_user_index_canister_c2c_client", "msgpack", "rand", - "regex", + "regex-lite", "serde", "serializer", "tracing", diff --git a/backend/canisters/exchange_bot/impl/Cargo.toml b/backend/canisters/exchange_bot/impl/Cargo.toml index db1f5e7e20..94b2b66268 100644 --- a/backend/canisters/exchange_bot/impl/Cargo.toml +++ b/backend/canisters/exchange_bot/impl/Cargo.toml @@ -33,7 +33,7 @@ ledger_utils = { path = "../../../libraries/ledger_utils" } local_user_index_canister_c2c_client = { path = "../../local_user_index/c2c_client" } msgpack = { path = "../../../libraries/msgpack" } rand = { workspace = true } -regex = { workspace = true } +regex-lite = { workspace = true } serde = { workspace = true } serializer = { path = "../../../libraries/serializer" } tracing = { workspace = true } diff --git a/backend/canisters/exchange_bot/impl/src/commands/balance.rs b/backend/canisters/exchange_bot/impl/src/commands/balance.rs index 7ad468267c..9c0c354049 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/balance.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/balance.rs @@ -4,7 +4,7 @@ use crate::commands::{Command, CommandParser, CommandSubTaskResult, ParseMessage use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; use rand::Rng; -use regex::{Regex, RegexBuilder}; +use regex_lite::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use types::{CanisterId, MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; diff --git a/backend/canisters/exchange_bot/impl/src/commands/quote.rs b/backend/canisters/exchange_bot/impl/src/commands/quote.rs index 373f835d63..3a619c6afa 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/quote.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/quote.rs @@ -7,7 +7,7 @@ use exchange_bot_canister::ExchangeId; use lazy_static::lazy_static; use ledger_utils::format_crypto_amount; use rand::Rng; -use regex::{Regex, RegexBuilder}; +use regex_lite::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use types::{MessageContent, MessageId, TimestampMillis, TokenInfo, UserId}; diff --git a/backend/canisters/exchange_bot/impl/src/commands/swap.rs b/backend/canisters/exchange_bot/impl/src/commands/swap.rs index ace549f488..6225112269 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/swap.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/swap.rs @@ -10,7 +10,7 @@ use exchange_bot_canister::ExchangeId; use lazy_static::lazy_static; use ledger_utils::{convert_to_subaccount, format_crypto_amount}; use rand::Rng; -use regex::{Regex, RegexBuilder}; +use regex_lite::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use tracing::{error, trace}; diff --git a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs index d2e6a2172d..2809a6b41c 100644 --- a/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs +++ b/backend/canisters/exchange_bot/impl/src/commands/withdraw.rs @@ -6,7 +6,7 @@ use crate::{mutate_state, RuntimeState}; use lazy_static::lazy_static; use ledger_utils::format_crypto_amount; use rand::Rng; -use regex::{Regex, RegexBuilder}; +use regex_lite::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use types::icrc1::BlockIndex;