Skip to content

Commit

Permalink
Implement swapping of tokens via external DEXs (#4819)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Nov 21, 2023
1 parent 5c1805e commit 7b71028
Show file tree
Hide file tree
Showing 28 changed files with 565 additions and 106 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/canisters/user/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Support paying in CHAT for Diamond membership ([#4748](https://github.com/open-chat-labs/open-chat/pull/4748))
- Add `approve_transfer` endpoint ([#4767](https://github.com/open-chat-labs/open-chat/pull/4767))
- Support deleting direct chats (only for the current user) ([#4816](https://github.com/open-chat-labs/open-chat/pull/4816))
- Implement swapping of tokens via external DEXs ([#4819](https://github.com/open-chat-labs/open-chat/pull/4819))

### Changed

Expand Down
63 changes: 63 additions & 0 deletions backend/canisters/user/api/can.did
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,67 @@ type ApproveTransferResponse = variant {
InternalError : text;
};

type SwapTokensArgs = record {
swap_id : nat;
input_token : TokenInfo;
output_token : TokenInfo;
input_amount : nat;
exchange_args : variant {
ICPSwap : record {
swap_canister_id : CanisterId;
zero_for_one : bool;
};
};
min_output_amount : nat;
};

type SwapTokensResponse = variant {
Success : record {
amount_out : nat;
};
InternalError : text;
};

type TokenInfo = record {
token : Cryptocurrency;
ledger : CanisterId;
decimals : nat8;
fee: nat;
};

type TokenSwapStatusArgs = record {
swap_id : nat;
};

type TokenSwapStatusResponse = variant {
Success : record {
status : record {
started : TimestampMillis;
deposit_account : opt variant {
Ok : Account;
Err : text;
};
transfer : opt variant {
Ok : nat64;
Err : text;
};
notified_dex : opt variant {
Ok;
Err : text;
};
amount_swapped : opt variant {
Ok : nat;
Err : text;
};
withdrawn_from_dex : opt variant {
Ok;
Err : text;
};
};
};
NotFound;
};

type TipMessageArgs = record {
chat : Chat;
recipient : UserId;
Expand Down Expand Up @@ -969,6 +1030,7 @@ service : {
submit_proposal : (SubmitProposalArgs) -> (SubmitProposalResponse);
report_message : (ReportMessageArgs) -> (ReportMessageResponse);
approve_transfer : (ApproveTransferArgs) -> (ApproveTransferResponse);
swap_tokens : (SwapTokensArgs) -> (SwapTokensResponse);

init_user_principal_migration : (InitUserPrincipalMigrationArgs) -> (InitUserPrincipalMigrationResponse);
migrate_user_principal : (MigrateUserPrincipalArgs) -> (MigrateUserPrincipalResponse);
Expand All @@ -987,4 +1049,5 @@ service : {
public_profile : (PublicProfileArgs) -> (PublicProfileResponse) query;
hot_group_exclusions : (HotGroupExclusionsArgs) -> (HotGroupExclusionsResponse) query;
saved_crypto_accounts : (EmptyArgs) -> (SavedCryptoAccountsResponse) query;
token_swap_status : (TokenSwapStatusArgs) -> (TokenSwapStatusResponse) query;
};
2 changes: 2 additions & 0 deletions backend/canisters/user/api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ fn main() {
generate_candid_method!(user, public_profile, query);
generate_candid_method!(user, search_messages, query);
generate_candid_method!(user, saved_crypto_accounts, query);
generate_candid_method!(user, token_swap_status, query);
generate_candid_method!(user, updates, query);

generate_candid_method!(user, add_hot_group_exclusions, update);
Expand Down Expand Up @@ -49,6 +50,7 @@ fn main() {
generate_candid_method!(user, set_contact, update);
generate_candid_method!(user, set_message_reminder_v2, update);
generate_candid_method!(user, submit_proposal, update);
generate_candid_method!(user, swap_tokens, update);
generate_candid_method!(user, tip_message, update);
generate_candid_method!(user, unblock_user, update);
generate_candid_method!(user, undelete_messages, update);
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/api/src/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ pub mod messages_by_message_index;
pub mod public_profile;
pub mod saved_crypto_accounts;
pub mod search_messages;
pub mod token_swap_status;
pub mod updates;
32 changes: 32 additions & 0 deletions backend/canisters/user/api/src/queries/token_swap_status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
pub use candid::CandidType;
use icrc_ledger_types::icrc1::account::Account;
use serde::{Deserialize, Serialize};
use types::TimestampMillis;

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct Args {
pub swap_id: u128,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub enum Response {
Success(SuccessResult),
NotFound,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct SuccessResult {
pub status: TokenSwapStatus,
}

#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
pub struct TokenSwapStatus {
pub started: TimestampMillis,
pub deposit_account: SwapSubtask<Account>,
pub transfer: SwapSubtask<u64>, // Block Index
pub notified_dex: SwapSubtask<()>,
pub amount_swapped: SwapSubtask<u128>,
pub withdrawn_from_dex: SwapSubtask<()>,
}

type SwapSubtask<T = ()> = Option<Result<T, String>>;
1 change: 1 addition & 0 deletions backend/canisters/user/api/src/updates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub mod set_community_indexes;
pub mod set_contact;
pub mod set_message_reminder_v2;
pub mod submit_proposal;
pub mod swap_tokens;
pub mod tip_message;
pub mod unblock_user;
pub mod undelete_messages;
Expand Down
35 changes: 35 additions & 0 deletions backend/canisters/user/api/src/updates/swap_tokens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use candid::CandidType;
use serde::{Deserialize, Serialize};
use types::{CanisterId, TokenInfo};

#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
pub struct Args {
pub swap_id: u128,
pub input_token: TokenInfo,
pub output_token: TokenInfo,
pub input_amount: u128,
pub exchange_args: ExchangeArgs,
pub min_output_amount: u128,
}

#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
pub enum ExchangeArgs {
ICPSwap(ICPSwapArgs),
}

#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
pub struct ICPSwapArgs {
pub swap_canister_id: CanisterId,
pub zero_for_one: bool,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub enum Response {
Success(SuccessResult),
InternalError(String),
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct SuccessResult {
pub amount_out: u128,
}
3 changes: 3 additions & 0 deletions backend/canisters/user/impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ path = "src/lib.rs"
crate-type = ["cdylib"]

[dependencies]
async-trait = { workspace = true }
bot_api = { path = "../../../bots/api" }
bot_c2c_client = { path = "../../../bots/c2c_client" }
candid = { workspace = true }
Expand All @@ -34,6 +35,7 @@ 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" }
icrc_ledger_canister_c2c_client = { path = "../../../external_canisters/icrc_ledger/c2c_client" }
icrc_ledger_canister = { path = "../../../external_canisters/icrc_ledger/api" }
icrc-ledger-types = { workspace = true }
Expand All @@ -54,6 +56,7 @@ serde_bytes = { workspace = true }
serializer = { path = "../../../libraries/serializer" }
sns_governance_canister = { path = "../../../external_canisters/sns_governance/api" }
sns_governance_canister_c2c_client = { path = "../../../external_canisters/sns_governance/c2c_client" }
sonic_client = { path = "../../../libraries/sonic_client" }
stable_memory = { path = "../../../libraries/stable_memory" }
storage_bucket_client = { path = "../../../libraries/storage_bucket_client" }
tracing = { workspace = true }
Expand Down
5 changes: 5 additions & 0 deletions backend/canisters/user/impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::model::direct_chats::DirectChats;
use crate::model::group_chat::GroupChat;
use crate::model::group_chats::GroupChats;
use crate::model::hot_group_exclusions::HotGroupExclusions;
use crate::model::token_swaps::TokenSwaps;
use crate::timer_job_types::{RemoveExpiredEventsJob, TimerJob};
use candid::Principal;
use canister_state_macros::canister_state;
Expand Down Expand Up @@ -37,6 +38,7 @@ mod openchat_bot;
mod queries;
mod regular_jobs;
mod timer_job_types;
mod token_swaps;
mod updates;

pub const BASIC_GROUP_CREATION_LIMIT: u32 = 5;
Expand Down Expand Up @@ -177,6 +179,8 @@ struct Data {
pub saved_crypto_accounts: Vec<NamedAccount>,
pub next_event_expiry: Option<TimestampMillis>,
#[serde(default)]
pub token_swaps: TokenSwaps,
#[serde(default)]
pub rng_seed: [u8; 32],
}

Expand Down Expand Up @@ -225,6 +229,7 @@ impl Data {
fire_and_forget_handler: FireAndForgetHandler::default(),
saved_crypto_accounts: Vec::new(),
next_event_expiry: None,
token_swaps: TokenSwaps::default(),
rng_seed: [0; 32],
}
}
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/impl/src/model/direct_chats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct DirectChats {
direct_chats: HashMap<ChatId, DirectChat>,
pinned: Timestamped<Vec<ChatId>>,
metrics: ChatMetricsInternal,
#[serde(default)]
chats_removed: BTreeSet<(TimestampMillis, ChatId)>,
}

Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/impl/src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pub mod favourite_chats;
pub mod group_chat;
pub mod group_chats;
pub mod hot_group_exclusions;
pub mod token_swaps;
pub mod unread_message_index_map;
68 changes: 68 additions & 0 deletions backend/canisters/user/impl/src/model/token_swaps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use icrc_ledger_types::icrc1::account::Account;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use types::{TimestampMillis, Timestamped};
use user_canister::token_swap_status::TokenSwapStatus;

#[derive(Serialize, Deserialize, Default)]
pub struct TokenSwaps {
swaps: HashMap<u128, TokenSwap>,
}

impl TokenSwaps {
pub fn push_new(&mut self, args: user_canister::swap_tokens::Args, now: TimestampMillis) -> TokenSwap {
let token_swap = TokenSwap::new(args, now);
self.upsert(token_swap.clone());
token_swap
}

pub fn upsert(&mut self, swap: TokenSwap) {
self.swaps.insert(swap.args.swap_id, swap);
}

pub fn get(&self, swap_id: u128) -> Option<&TokenSwap> {
self.swaps.get(&swap_id)
}
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TokenSwap {
pub args: user_canister::swap_tokens::Args,
pub started: TimestampMillis,
pub deposit_account: SwapSubtask<Account>,
pub transfer: SwapSubtask<u64>, // Block Index
pub notified_dex_at: SwapSubtask,
pub amount_swapped: SwapSubtask<u128>,
pub withdrawn_from_dex_at: SwapSubtask,
pub success: Option<Timestamped<bool>>,
}

type SwapSubtask<T = ()> = Option<Timestamped<Result<T, String>>>;

impl TokenSwap {
pub fn new(args: user_canister::swap_tokens::Args, now: TimestampMillis) -> TokenSwap {
TokenSwap {
args,
started: now,
deposit_account: None,
transfer: None,
notified_dex_at: None,
amount_swapped: None,
withdrawn_from_dex_at: None,
success: None,
}
}
}

impl From<TokenSwap> for TokenSwapStatus {
fn from(value: TokenSwap) -> Self {
TokenSwapStatus {
started: value.started,
deposit_account: value.deposit_account.map(|a| a.value),
transfer: value.transfer.map(|t| t.value),
notified_dex: value.notified_dex_at.map(|t| t.value.map(|_| ())),
amount_swapped: value.amount_swapped.map(|t| t.value),
withdrawn_from_dex: value.withdrawn_from_dex_at.map(|t| t.value.map(|_| ())),
}
}
}
1 change: 1 addition & 0 deletions backend/canisters/user/impl/src/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod messages_by_message_index;
pub mod public_profile;
pub mod saved_crypto_accounts;
pub mod search_messages;
pub mod token_swap_status;
pub mod updates;

fn check_replica_up_to_date(latest_known_update: Option<TimestampMillis>, state: &RuntimeState) -> Result<(), TimestampMillis> {
Expand Down
19 changes: 19 additions & 0 deletions backend/canisters/user/impl/src/queries/token_swap_status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::guards::caller_is_owner;
use crate::{read_state, RuntimeState};
use ic_cdk_macros::query;
use user_canister::token_swap_status::{Response::*, *};

#[query(guard = "caller_is_owner")]
fn token_swap_status(args: Args) -> Response {
read_state(|state| token_swap_status_impl(args, state))
}

fn token_swap_status_impl(args: Args, state: &RuntimeState) -> Response {
if let Some(token_swap) = state.data.token_swaps.get(args.swap_id).cloned() {
Success(SuccessResult {
status: token_swap.into(),
})
} else {
NotFound
}
}
Loading

0 comments on commit 7b71028

Please sign in to comment.