From 01426a3ede03116de65838cbd09532fe7ec4d8ac Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Thu, 6 Jun 2024 09:46:24 +0100 Subject: [PATCH] Integrate with Sonic for token swaps (#5908) --- backend/canisters/user/api/can.did | 11 ++- .../user/api/src/updates/swap_tokens.rs | 4 + .../user/impl/src/token_swaps/mod.rs | 1 + .../user/impl/src/token_swaps/sonic.rs | 24 ++++++ .../user/impl/src/updates/swap_tokens.rs | 11 +++ backend/libraries/sonic_client/src/lib.rs | 22 +++--- .../components/home/profile/SwapCrypto.svelte | 3 + frontend/openchat-agent/codegen.sh | 3 + .../src/services/dexes/index.ts | 51 ++++++++++--- .../services/dexes/sonic/swaps/candid/can.did | 22 ++++++ .../dexes/sonic/swaps/candid/idl.d.ts | 5 ++ .../services/dexes/sonic/swaps/candid/idl.js | 25 +++++++ .../dexes/sonic/swaps/candid/types.d.ts | 24 ++++++ .../src/services/dexes/sonic/swaps/mappers.ts | 32 ++++++++ .../dexes/sonic/swaps/sonic.swaps.client.ts | 67 +++++++++++++++++ .../src/services/openchatAgent.ts | 6 +- .../src/services/user/candid/idl.d.ts | 2 + .../src/services/user/candid/idl.js | 10 ++- .../src/services/user/candid/types.d.ts | 9 ++- .../src/services/user/mappers.ts | 75 +++++++++++++++---- .../src/services/user/user.client.ts | 15 ++-- .../openchat-shared/src/domain/dexes/index.ts | 4 +- 22 files changed, 364 insertions(+), 62 deletions(-) create mode 100644 backend/canisters/user/impl/src/token_swaps/sonic.rs create mode 100644 frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/can.did create mode 100644 frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/idl.d.ts create mode 100644 frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/idl.js create mode 100644 frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/types.d.ts create mode 100644 frontend/openchat-agent/src/services/dexes/sonic/swaps/mappers.ts create mode 100644 frontend/openchat-agent/src/services/dexes/sonic/swaps/sonic.swaps.client.ts diff --git a/backend/canisters/user/api/can.did b/backend/canisters/user/api/can.did index 0a36f359f8..01546de2a1 100644 --- a/backend/canisters/user/api/can.did +++ b/backend/canisters/user/api/can.did @@ -183,15 +183,18 @@ type SwapTokensArgs = record { output_token : TokenInfo; input_amount : nat; exchange_args : variant { - ICPSwap : record { - swap_canister_id : CanisterId; - zero_for_one : bool; - }; + ICPSwap : ExchangeArgs; + Sonic : ExchangeArgs; }; min_output_amount : nat; pin : opt text; }; +type ExchangeArgs = record { + swap_canister_id : CanisterId; + zero_for_one : bool; +}; + type SwapTokensResponse = variant { Success : record { amount_out : nat; diff --git a/backend/canisters/user/api/src/updates/swap_tokens.rs b/backend/canisters/user/api/src/updates/swap_tokens.rs index 2903ef0bdc..a6e31753f8 100644 --- a/backend/canisters/user/api/src/updates/swap_tokens.rs +++ b/backend/canisters/user/api/src/updates/swap_tokens.rs @@ -16,12 +16,14 @@ pub struct Args { #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub enum ExchangeArgs { ICPSwap(ICPSwapArgs), + Sonic(SonicArgs), } impl ExchangeArgs { pub fn exchange_id(&self) -> ExchangeId { match self { ExchangeArgs::ICPSwap(_) => ExchangeId::ICPSwap, + ExchangeArgs::Sonic(_) => ExchangeId::Sonic, } } } @@ -32,6 +34,8 @@ pub struct ICPSwapArgs { pub zero_for_one: bool, } +pub type SonicArgs = ICPSwapArgs; + #[derive(CandidType, Serialize, Deserialize, Debug)] pub enum Response { Success(SuccessResult), diff --git a/backend/canisters/user/impl/src/token_swaps/mod.rs b/backend/canisters/user/impl/src/token_swaps/mod.rs index 93fb25992a..89bca334ce 100644 --- a/backend/canisters/user/impl/src/token_swaps/mod.rs +++ b/backend/canisters/user/impl/src/token_swaps/mod.rs @@ -1,2 +1,3 @@ pub mod icpswap; +pub mod sonic; pub mod swap_client; diff --git a/backend/canisters/user/impl/src/token_swaps/sonic.rs b/backend/canisters/user/impl/src/token_swaps/sonic.rs new file mode 100644 index 0000000000..416c2eb190 --- /dev/null +++ b/backend/canisters/user/impl/src/token_swaps/sonic.rs @@ -0,0 +1,24 @@ +use super::swap_client::SwapClient; +use async_trait::async_trait; +use ic_cdk::api::call::CallResult; +use icrc_ledger_types::icrc1::account::Account; +use sonic_client::SonicClient; + +#[async_trait] +impl SwapClient for SonicClient { + async fn deposit_account(&self) -> CallResult { + self.deposit_account().await + } + + async fn deposit(&self, amount: u128) -> CallResult<()> { + self.deposit(amount).await.map(|_| ()) + } + + async fn swap(&self, amount: u128, min_amount_out: u128) -> CallResult> { + self.swap(amount, min_amount_out).await + } + + async fn withdraw(&self, successful_swap: bool, amount: u128) -> CallResult { + self.withdraw(successful_swap, amount).await + } +} diff --git a/backend/canisters/user/impl/src/updates/swap_tokens.rs b/backend/canisters/user/impl/src/updates/swap_tokens.rs index b9f80cdeb6..d8cc79feed 100644 --- a/backend/canisters/user/impl/src/updates/swap_tokens.rs +++ b/backend/canisters/user/impl/src/updates/swap_tokens.rs @@ -8,6 +8,7 @@ use canister_tracing_macros::trace; use ic_cdk::update; use icpswap_client::ICPSwapClient; use icrc_ledger_types::icrc1::transfer::TransferArg; +use sonic_client::SonicClient; use tracing::error; use types::{TimestampMillis, Timestamped}; use user_canister::swap_tokens::{Response::*, *}; @@ -213,6 +214,16 @@ fn build_swap_client(args: &Args, state: &RuntimeState) -> Box { icpswap.zero_for_one, )) } + ExchangeArgs::Sonic(sonic) => { + let (token0, token1) = if sonic.zero_for_one { (input_token, output_token) } else { (output_token, input_token) }; + Box::new(SonicClient::new( + this_canister_id, + sonic.swap_canister_id, + token0, + token1, + sonic.zero_for_one, + )) + } } } diff --git a/backend/libraries/sonic_client/src/lib.rs b/backend/libraries/sonic_client/src/lib.rs index 1284fc54d2..c9594f8f79 100644 --- a/backend/libraries/sonic_client/src/lib.rs +++ b/backend/libraries/sonic_client/src/lib.rs @@ -17,7 +17,6 @@ pub struct SonicClient { token0: TokenInfo, token1: TokenInfo, zero_for_one: bool, - deposit_subaccount: [u8; 32], } impl SonicClient { @@ -27,7 +26,6 @@ impl SonicClient { token0: TokenInfo, token1: TokenInfo, zero_for_one: bool, - deposit_subaccount: [u8; 32], ) -> Self { SonicClient { this_canister_id, @@ -35,7 +33,6 @@ impl SonicClient { token0, token1, zero_for_one, - deposit_subaccount, } } @@ -47,17 +44,18 @@ impl SonicClient { } pub async fn deposit(&self, amount: u128) -> CallResult { - let args = (self.input_token().ledger, amount.into()); + let token = self.input_token(); + let args = (token.ledger, amount.saturating_sub(token.fee).into()); match sonic_canister_c2c_client::deposit(self.sonic_canister_id, args).await?.0 { SonicResult::Ok(amount_deposited) => Ok(nat_to_u128(amount_deposited)), SonicResult::Err(error) => Err(convert_error(error)), } } - pub async fn swap(&self, amount: u128) -> CallResult { + pub async fn swap(&self, amount: u128, min_amount_out: u128) -> CallResult> { let args = ( Nat::from(amount), - Nat::from(0u32), + Nat::from(min_amount_out), vec![self.input_token().ledger.to_string(), self.output_token().ledger.to_string()], self.this_canister_id, Int::from(u64::MAX), @@ -66,15 +64,15 @@ impl SonicClient { .await? .0 { - SonicResult::Ok(_tx_id) => { - unimplemented!() - } - SonicResult::Err(error) => Err(convert_error(error)), + SonicResult::Ok(amount_out) => Ok(Ok(nat_to_u128(amount_out))), + SonicResult::Err(error) => Ok(Err(error)), } } - pub async fn withdraw(&self, amount: u128) -> CallResult { - let args = (self.output_token().ledger, amount.into()); + pub async fn withdraw(&self, successful_swap: bool, amount: u128) -> CallResult { + let token = if successful_swap { self.output_token() } else { self.input_token() }; + let amount = if successful_swap { amount } else { amount.saturating_sub(token.fee) }; + let args = (token.ledger, amount.into()); match sonic_canister_c2c_client::withdraw(self.sonic_canister_id, args).await?.0 { SonicResult::Ok(amount_withdrawn) => Ok(nat_to_u128(amount_withdrawn)), SonicResult::Err(error) => Err(convert_error(error)), diff --git a/frontend/app/src/components/home/profile/SwapCrypto.svelte b/frontend/app/src/components/home/profile/SwapCrypto.svelte index ef6620bff0..8d22cfbb3f 100644 --- a/frontend/app/src/components/home/profile/SwapCrypto.svelte +++ b/frontend/app/src/components/home/profile/SwapCrypto.svelte @@ -156,6 +156,9 @@ switch (dex) { case "icpswap": return "ICPSwap"; + + case "sonic": + return "Sonic"; } } diff --git a/frontend/openchat-agent/codegen.sh b/frontend/openchat-agent/codegen.sh index 29c279af2b..81723bb336 100755 --- a/frontend/openchat-agent/codegen.sh +++ b/frontend/openchat-agent/codegen.sh @@ -53,6 +53,9 @@ didc bind ./src/services/dexes/icpSwap/index/candid/can.did -t js > ./src/servic didc bind ./src/services/dexes/icpSwap/pool/candid/can.did -t ts > ./src/services/dexes/icpSwap/pool/candid/types.d.ts didc bind ./src/services/dexes/icpSwap/pool/candid/can.did -t js > ./src/services/dexes/icpSwap/pool/candid/idl.js +didc bind ./src/services/dexes/sonic/swaps/candid/can.did -t ts > ./src/services/dexes/sonic/swaps/candid/types.d.ts +didc bind ./src/services/dexes/sonic/swaps/candid/can.did -t js > ./src/services/dexes/sonic/swaps/candid/idl.js + didc bind ./src/services/icpcoins/candid/can.did -t ts > ./src/services/icpcoins/candid/types.d.ts didc bind ./src/services/icpcoins/candid/can.did -t js > ./src/services/icpcoins/candid/idl.js diff --git a/frontend/openchat-agent/src/services/dexes/index.ts b/frontend/openchat-agent/src/services/dexes/index.ts index 5f72acecda..8ccaa8375c 100644 --- a/frontend/openchat-agent/src/services/dexes/index.ts +++ b/frontend/openchat-agent/src/services/dexes/index.ts @@ -3,18 +3,21 @@ import type { DexId, TokenSwapPool } from "openchat-shared"; import type { AgentConfig } from "../../config"; import { IcpSwapIndexClient } from "./icpSwap/index/icpSwap.index.client"; import { IcpSwapPoolClient } from "./icpSwap/pool/icpSwap.pool.client"; +import { SonicSwapsClient } from "./sonic/swaps/sonic.swaps.client"; export class DexesAgent { private _identity: Identity; private _icpSwapIndexClient: IcpSwapIndexClient; + private _sonicSwapsClient: SonicSwapsClient; constructor(private config: AgentConfig) { this._identity = new AnonymousIdentity(); this._icpSwapIndexClient = IcpSwapIndexClient.create(this._identity, config); + this._sonicSwapsClient = SonicSwapsClient.create(this._identity, config); } async getSwapPools(inputToken: string, outputTokens: Set): Promise { - const allPools = await this._icpSwapIndexClient.getPools(); + const allPools = await this.getSwapPoolsUnfiltered(); return allPools.filter( (p) => @@ -24,7 +27,7 @@ export class DexesAgent { } async canSwap(tokens: Set): Promise> { - const allPools = await this._icpSwapIndexClient.getPools(); + const allPools = await this.getSwapPoolsUnfiltered(); const available = new Set(); @@ -47,16 +50,42 @@ export class DexesAgent { return await Promise.all( pools.map((p) => - IcpSwapPoolClient.create( - this._identity, - this.config, - p.canisterId, - p.token0, - p.token1, - ) - .quote(inputToken, outputToken, amountIn) - .then((quote) => [p.dex, quote] as [DexId, bigint]), + this.quoteSingle(p, inputToken, outputToken, amountIn).then( + (quote) => [p.dex, quote] as [DexId, bigint], + ), ), ); } + + private async getSwapPoolsUnfiltered(): Promise { + const [icpSwap, sonic] = await Promise.all([ + this._icpSwapIndexClient.getPools(), + this._sonicSwapsClient.getPools(), + ]); + + return icpSwap.concat(sonic); + } + + private quoteSingle( + pool: TokenSwapPool, + inputToken: string, + outputToken: string, + amountIn: bigint, + ): Promise { + if (pool.dex === "icpswap") { + const client = IcpSwapPoolClient.create( + this._identity, + this.config, + pool.canisterId, + pool.token0, + pool.token1, + ); + return client.quote(inputToken, outputToken, amountIn); + } else if (pool.dex === "sonic") { + const client = SonicSwapsClient.create(this._identity, this.config); + return client.quote(inputToken, outputToken, amountIn); + } else { + return Promise.resolve(BigInt(0)); + } + } } diff --git a/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/can.did b/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/can.did new file mode 100644 index 0000000000..489fe8b58f --- /dev/null +++ b/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/can.did @@ -0,0 +1,22 @@ +// This is a trimmed down version of the full candid file which can be found here - +// https://dashboard.internetcomputer.org/canister/3xwpq-ziaaa-aaaah-qcn4a-cai + +type PairInfoExt = + record { + blockTimestampLast: int; + creator: principal; + id: text; + kLast: nat; + lptoken: text; + price0CumulativeLast: nat; + price1CumulativeLast: nat; + reserve0: nat; + reserve1: nat; + token0: text; + token1: text; + totalSupply: nat; + }; +service : { + getAllPairs: () -> (vec PairInfoExt) query; + getPair: (principal, principal) -> (opt PairInfoExt) query; +} diff --git a/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/idl.d.ts b/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/idl.d.ts new file mode 100644 index 0000000000..52761aad1f --- /dev/null +++ b/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/idl.d.ts @@ -0,0 +1,5 @@ +import type { IDL } from "@dfinity/candid"; +import { PairInfoExt, _SERVICE } from "./types"; +export { PairInfoExt as ApiPairInfo, _SERVICE as SonicSwapsService }; + +export const idlFactory: IDL.InterfaceFactory; diff --git a/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/idl.js b/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/idl.js new file mode 100644 index 0000000000..1387226338 --- /dev/null +++ b/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/idl.js @@ -0,0 +1,25 @@ +export const idlFactory = ({ IDL }) => { + const PairInfoExt = IDL.Record({ + 'id' : IDL.Text, + 'price0CumulativeLast' : IDL.Nat, + 'creator' : IDL.Principal, + 'reserve0' : IDL.Nat, + 'reserve1' : IDL.Nat, + 'lptoken' : IDL.Text, + 'totalSupply' : IDL.Nat, + 'token0' : IDL.Text, + 'token1' : IDL.Text, + 'price1CumulativeLast' : IDL.Nat, + 'kLast' : IDL.Nat, + 'blockTimestampLast' : IDL.Int, + }); + return IDL.Service({ + 'getAllPairs' : IDL.Func([], [IDL.Vec(PairInfoExt)], ['query']), + 'getPair' : IDL.Func( + [IDL.Principal, IDL.Principal], + [IDL.Opt(PairInfoExt)], + ['query'], + ), + }); +}; +export const init = ({ IDL }) => { return []; }; diff --git a/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/types.d.ts b/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/types.d.ts new file mode 100644 index 0000000000..1a50fac03b --- /dev/null +++ b/frontend/openchat-agent/src/services/dexes/sonic/swaps/candid/types.d.ts @@ -0,0 +1,24 @@ +import type { Principal } from '@dfinity/principal'; +import type { ActorMethod } from '@dfinity/agent'; +import type { IDL } from '@dfinity/candid'; + +export interface PairInfoExt { + 'id' : string, + 'price0CumulativeLast' : bigint, + 'creator' : Principal, + 'reserve0' : bigint, + 'reserve1' : bigint, + 'lptoken' : string, + 'totalSupply' : bigint, + 'token0' : string, + 'token1' : string, + 'price1CumulativeLast' : bigint, + 'kLast' : bigint, + 'blockTimestampLast' : bigint, +} +export interface _SERVICE { + 'getAllPairs' : ActorMethod<[], Array>, + 'getPair' : ActorMethod<[Principal, Principal], [] | [PairInfoExt]>, +} +export declare const idlFactory: IDL.InterfaceFactory; +export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; diff --git a/frontend/openchat-agent/src/services/dexes/sonic/swaps/mappers.ts b/frontend/openchat-agent/src/services/dexes/sonic/swaps/mappers.ts new file mode 100644 index 0000000000..9218d31717 --- /dev/null +++ b/frontend/openchat-agent/src/services/dexes/sonic/swaps/mappers.ts @@ -0,0 +1,32 @@ +import type { ApiPairInfo } from "./candid/idl"; +import type { TokenSwapPool } from "openchat-shared"; +import { optional } from "../../../../utils/mapping"; + +export function getAllPairsResponse(candid: ApiPairInfo[], canisterId: string): TokenSwapPool[] { + return candid.map((p) => ({ + dex: "sonic", + canisterId, + token0: p.token0, + token1: p.token1, + })); +} + +export function getPairResponse(candid: [ApiPairInfo] | []): TokenPair | undefined { + return optional(candid, pair); +} + +function pair(candid: ApiPairInfo): TokenPair { + return { + token0: candid.token0, + reserve0: candid.reserve0, + token1: candid.token1, + reserve1: candid.reserve1, + }; +} + +export type TokenPair = { + token0: string; + reserve0: bigint; + token1: string; + reserve1: bigint; +}; diff --git a/frontend/openchat-agent/src/services/dexes/sonic/swaps/sonic.swaps.client.ts b/frontend/openchat-agent/src/services/dexes/sonic/swaps/sonic.swaps.client.ts new file mode 100644 index 0000000000..d8cb351163 --- /dev/null +++ b/frontend/openchat-agent/src/services/dexes/sonic/swaps/sonic.swaps.client.ts @@ -0,0 +1,67 @@ +import type { Identity } from "@dfinity/agent"; +import { idlFactory, type SonicSwapsService } from "./candid/idl"; +import { CandidService } from "../../../candidService"; +import type { AgentConfig } from "../../../../config"; +import type { TokenSwapPool } from "openchat-shared"; +import { getAllPairsResponse, getPairResponse } from "./mappers"; +import { Principal } from "@dfinity/principal"; + +const SONIC_INDEX_CANISTER_ID = "3xwpq-ziaaa-aaaah-qcn4a-cai"; +const TEN_MINUTES = 10 * 60 * 1000; +const ENABLED: boolean = false; + +export class SonicSwapsClient extends CandidService { + private service: SonicSwapsService; + private pools: TokenSwapPool[] = []; // Cache the pools for 10 minutes + private poolsLastUpdated: number = 0; + + private constructor(identity: Identity, config: AgentConfig) { + super(identity); + + this.service = this.createServiceClient( + idlFactory, + SONIC_INDEX_CANISTER_ID, + config, + ); + } + + static create(identity: Identity, config: AgentConfig): SonicSwapsClient { + return new SonicSwapsClient(identity, config); + } + + async getPools(): Promise { + if (!ENABLED) return Promise.resolve([]); + + const now = Date.now(); + if (this.pools.length > 0 && now - this.poolsLastUpdated < TEN_MINUTES) + return Promise.resolve(this.pools); + + const pools = await this.handleQueryResponse(this.service.getAllPairs, (resp) => + getAllPairsResponse(resp, SONIC_INDEX_CANISTER_ID), + ); + + this.poolsLastUpdated = now; + return (this.pools = pools); + } + + async quote(inputToken: string, outputToken: string, amountIn: bigint): Promise { + const pair = await this.handleQueryResponse( + () => + this.service.getPair( + Principal.fromText(inputToken), + Principal.fromText(outputToken), + ), + getPairResponse, + ); + if (pair === undefined) return BigInt(0); + + const zeroForOne = pair.token0 === inputToken; + const reserveIn = zeroForOne ? pair.reserve0 : pair.reserve1; + const reserveOut = zeroForOne ? pair.reserve1 : pair.reserve0; + + const amountInWithFee = amountIn * BigInt(997); + const numerator = amountInWithFee * reserveOut; + const denominator = reserveIn * BigInt(1000) + amountInWithFee; + return numerator / denominator; + } +} diff --git a/frontend/openchat-agent/src/services/openchatAgent.ts b/frontend/openchat-agent/src/services/openchatAgent.ts index 0f650677e2..d361732d1c 100644 --- a/frontend/openchat-agent/src/services/openchatAgent.ts +++ b/frontend/openchat-agent/src/services/openchatAgent.ts @@ -3137,11 +3137,7 @@ export class OpenChatAgent extends EventTarget { return this._dexesAgent .getSwapPools(inputTokenDetails.ledger, new Set([outputTokenDetails.ledger])) .then((pools) => { - const pool = pools.find( - (p) => - (p.dex === dex && p.token0 === inputTokenDetails.ledger) || - p.token0 === outputTokenDetails.ledger, - ); + const pool = pools.find((p) => p.dex === dex); if (pool === undefined) { return Promise.reject("Cannot find a matching pool"); diff --git a/frontend/openchat-agent/src/services/user/candid/idl.d.ts b/frontend/openchat-agent/src/services/user/candid/idl.d.ts index 18cbcf67a0..01b0124b8b 100644 --- a/frontend/openchat-agent/src/services/user/candid/idl.d.ts +++ b/frontend/openchat-agent/src/services/user/candid/idl.d.ts @@ -58,6 +58,7 @@ import { EventsResponse, EventsSuccessResult, EventsArgs, + ExchangeArgs, SendMessageV2Args, SendMessageResponse, EditMessageResponse, @@ -200,6 +201,7 @@ export { EventsResponse as ApiEventsResponse, EventsSuccessResult as ApiEventsSuccessResult, EventsArgs as ApiEventsArgs, + ExchangeArgs as ApiExchangeArgs, BlockUserResponse as ApiBlockUserResponse, UnblockUserResponse as ApiUnblockUserResponse, LeaveGroupResponse as ApiLeaveGroupResponse, diff --git a/frontend/openchat-agent/src/services/user/candid/idl.js b/frontend/openchat-agent/src/services/user/candid/idl.js index 1c92adf95f..2066159d43 100644 --- a/frontend/openchat-agent/src/services/user/candid/idl.js +++ b/frontend/openchat-agent/src/services/user/candid/idl.js @@ -1681,6 +1681,10 @@ export const idlFactory = ({ IDL }) => { 'TransferFailed' : IDL.Text, 'InternalError' : IDL.Text, }); + const ExchangeArgs = IDL.Record({ + 'zero_for_one' : IDL.Bool, + 'swap_canister_id' : CanisterId, + }); const SwapTokensArgs = IDL.Record({ 'pin' : IDL.Opt(IDL.Text), 'input_amount' : IDL.Nat, @@ -1688,10 +1692,8 @@ export const idlFactory = ({ IDL }) => { 'swap_id' : IDL.Nat, 'input_token' : TokenInfo, 'exchange_args' : IDL.Variant({ - 'ICPSwap' : IDL.Record({ - 'zero_for_one' : IDL.Bool, - 'swap_canister_id' : CanisterId, - }), + 'Sonic' : ExchangeArgs, + 'ICPSwap' : ExchangeArgs, }), 'output_token' : TokenInfo, }); diff --git a/frontend/openchat-agent/src/services/user/candid/types.d.ts b/frontend/openchat-agent/src/services/user/candid/types.d.ts index a545d50806..87a1ba394a 100644 --- a/frontend/openchat-agent/src/services/user/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/user/candid/types.d.ts @@ -756,6 +756,10 @@ export interface EventsWindowArgs { 'thread_root_message_index' : [] | [MessageIndex], 'latest_known_update' : [] | [TimestampMillis], } +export interface ExchangeArgs { + 'zero_for_one' : boolean, + 'swap_canister_id' : CanisterId, +} export type FailedCryptoTransaction = { 'NNS' : NnsFailedCryptoTransaction } | { 'ICRC1' : Icrc1FailedCryptoTransaction } | { 'ICRC2' : Icrc2FailedCryptoTransaction }; @@ -2097,9 +2101,8 @@ export interface SwapTokensArgs { 'min_output_amount' : bigint, 'swap_id' : bigint, 'input_token' : TokenInfo, - 'exchange_args' : { - 'ICPSwap' : { 'zero_for_one' : boolean, 'swap_canister_id' : CanisterId } - }, + 'exchange_args' : { 'Sonic' : ExchangeArgs } | + { 'ICPSwap' : ExchangeArgs }, 'output_token' : TokenInfo, } export type SwapTokensResponse = { 'TooManyFailedPinAttempts' : Milliseconds } | diff --git a/frontend/openchat-agent/src/services/user/mappers.ts b/frontend/openchat-agent/src/services/user/mappers.ts index 6399f6cb9a..651eb426f2 100644 --- a/frontend/openchat-agent/src/services/user/mappers.ts +++ b/frontend/openchat-agent/src/services/user/mappers.ts @@ -62,6 +62,7 @@ import type { ApiTokenSwapStatusResponse, ApiApproveTransferResponse, ApiPinNumberSettings, + ApiExchangeArgs, } from "./candid/idl"; import type { EventsResponse, @@ -125,6 +126,7 @@ import type { TokenSwapStatusResponse, Result, ApproveTransferResponse, + ExchangeTokenSwapArgs, } from "openchat-shared"; import { nullMembership, CommonResponses, UnsupportedValueError } from "openchat-shared"; import { @@ -185,7 +187,11 @@ export function tipMessageResponse(candid: ApiTipMessageResponse): TipMessageRes return CommonResponses.success(); } - if ("PinRequired" in candid || "PinIncorrect" in candid || "TooManyFailedPinAttempts" in candid) { + if ( + "PinRequired" in candid || + "PinIncorrect" in candid || + "TooManyFailedPinAttempts" in candid + ) { return pinNumberFailureResponse(candid); } @@ -358,12 +364,16 @@ export function sendMessageWithTransferToChannelResponse( expiresAt: optional(candid.Success.expires_at, Number), transfer: completedCryptoTransfer(candid.Success.transfer, sender, recipient ?? ""), }; - } + } - if ("PinRequired" in candid || "PinIncorrect" in candid || "TooManyFailedPinAttempts" in candid) { + if ( + "PinRequired" in candid || + "PinIncorrect" in candid || + "TooManyFailedPinAttempts" in candid + ) { return pinNumberFailureResponse(candid); } - + console.warn("SendMessageWithTransferToChannel failed with", candid); return CommonResponses.failure(); } @@ -382,12 +392,16 @@ export function sendMessageWithTransferToGroupResponse( expiresAt: optional(candid.Success.expires_at, Number), transfer: completedCryptoTransfer(candid.Success.transfer, sender, recipient ?? ""), }; - } + } - if ("PinRequired" in candid || "PinIncorrect" in candid || "TooManyFailedPinAttempts" in candid) { + if ( + "PinRequired" in candid || + "PinIncorrect" in candid || + "TooManyFailedPinAttempts" in candid + ) { return pinNumberFailureResponse(candid); } - + console.warn("SendMessageWithTransferToGroup failed with", candid); return CommonResponses.failure(); } @@ -416,7 +430,11 @@ export function sendMessageResponse( expiresAt: optional(candid.TransferSuccessV2.expires_at, Number), }; } - if ("PinRequired" in candid || "PinIncorrect" in candid || "TooManyFailedPinAttempts" in candid) { + if ( + "PinRequired" in candid || + "PinIncorrect" in candid || + "TooManyFailedPinAttempts" in candid + ) { return pinNumberFailureResponse(candid); } if ("TransferCannotBeZero" in candid) { @@ -624,7 +642,7 @@ export function initialStateResponse(candid: ApiInitialStateResponse): InitialSt function pinNumberSettings(candid: ApiPinNumberSettings): PinNumberSettings { return { length: candid.length, - attemptsBlockedUntil: optional(candid.attempts_blocked_until, identity), + attemptsBlockedUntil: optional(candid.attempts_blocked_until, identity), }; } @@ -968,7 +986,11 @@ function completedIcrc1CryptoWithdrawal( export function withdrawCryptoResponse( candid: ApiWithdrawCryptoResponse, ): WithdrawCryptocurrencyResponse { - if ("PinRequired" in candid || "PinIncorrect" in candid || "TooManyFailedPinAttempts" in candid) { + if ( + "PinRequired" in candid || + "PinIncorrect" in candid || + "TooManyFailedPinAttempts" in candid + ) { return pinNumberFailureResponse(candid); } if ("CurrencyNotSupported" in candid) { @@ -988,7 +1010,7 @@ export function withdrawCryptoResponse( return completedIcrc1CryptoWithdrawal(candid.Success.ICRC1); } } - + throw new Error("Unexpected ApiWithdrawCryptocurrencyResponse type received"); } @@ -1144,7 +1166,11 @@ export function swapTokensResponse(candid: ApiSwapTokensResponse): SwapTokensRes error: candid.InternalError, }; } - if ("PinRequired" in candid || "PinIncorrect" in candid || "TooManyFailedPinAttempts" in candid) { + if ( + "PinRequired" in candid || + "PinIncorrect" in candid || + "TooManyFailedPinAttempts" in candid + ) { return pinNumberFailureResponse(candid); } @@ -1213,9 +1239,32 @@ export function approveTransferResponse( if ("ApproveError" in candid) { return { kind: "approve_error", error: JSON.stringify(candid.ApproveError) }; } - if ("PinRequired" in candid || "PinIncorrect" in candid || "TooManyFailedPinAttempts" in candid) { + if ( + "PinRequired" in candid || + "PinIncorrect" in candid || + "TooManyFailedPinAttempts" in candid + ) { return pinNumberFailureResponse(candid); } throw new UnsupportedValueError("Unexpected ApiApproveTransferResponse type received", candid); } + +export function apiExchangeArgs( + args: ExchangeTokenSwapArgs, +): { Sonic: ApiExchangeArgs } | { ICPSwap: ApiExchangeArgs } { + const value = { + swap_canister_id: Principal.fromText(args.swapCanisterId), + zero_for_one: args.zeroForOne, + }; + if (args.dex === "icpswap") { + return { + ICPSwap: value, + }; + } else if (args.dex === "sonic") { + return { + Sonic: value, + }; + } + throw new UnsupportedValueError("Unexpected dex", args.dex); +} diff --git a/frontend/openchat-agent/src/services/user/user.client.ts b/frontend/openchat-agent/src/services/user/user.client.ts index 488fbbac15..8f6078c548 100644 --- a/frontend/openchat-agent/src/services/user/user.client.ts +++ b/frontend/openchat-agent/src/services/user/user.client.ts @@ -110,6 +110,7 @@ import { swapTokensResponse, tokenSwapStatusResponse, approveTransferResponse, + apiExchangeArgs, } from "./mappers"; import { type Database, @@ -1279,12 +1280,7 @@ export class UserClient extends CandidService { fee: outputToken.transferFee, }, input_amount: amountIn, - exchange_args: { - ICPSwap: { - swap_canister_id: Principal.fromText(exchangeArgs.swapCanisterId), - zero_for_one: exchangeArgs.zeroForOne, - }, - }, + exchange_args: apiExchangeArgs(exchangeArgs), min_output_amount: minAmountOut, pin: apiOptional(identity, pin), }), @@ -1379,13 +1375,16 @@ export class UserClient extends CandidService { ); } - setPinNumber(currentPin: string | undefined, newPin: string | undefined): Promise { + setPinNumber( + currentPin: string | undefined, + newPin: string | undefined, + ): Promise { return this.handleResponse( this.userService.set_pin_number({ current: apiOptional(identity, currentPin), new: apiOptional(identity, newPin), }), setPinNumberResponse, - ); + ); } } diff --git a/frontend/openchat-shared/src/domain/dexes/index.ts b/frontend/openchat-shared/src/domain/dexes/index.ts index 5e940a5706..47e591d3df 100644 --- a/frontend/openchat-shared/src/domain/dexes/index.ts +++ b/frontend/openchat-shared/src/domain/dexes/index.ts @@ -1,4 +1,4 @@ -export type DexId = "icpswap"; +export type DexId = "icpswap" | "sonic"; export type TokenSwapPool = { dex: DexId; @@ -8,7 +8,7 @@ export type TokenSwapPool = { }; export type ExchangeTokenSwapArgs = { - dex: "icpswap"; + dex: DexId; swapCanisterId: string; zeroForOne: boolean; }; \ No newline at end of file