Skip to content

Commit

Permalink
Integrate with Sonic for token swaps (#5908)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Jun 6, 2024
1 parent ee63ee8 commit 01426a3
Show file tree
Hide file tree
Showing 22 changed files with 364 additions and 62 deletions.
11 changes: 7 additions & 4 deletions backend/canisters/user/api/can.did
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions backend/canisters/user/api/src/updates/swap_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
Expand All @@ -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),
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/impl/src/token_swaps/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod icpswap;
pub mod sonic;
pub mod swap_client;
24 changes: 24 additions & 0 deletions backend/canisters/user/impl/src/token_swaps/sonic.rs
Original file line number Diff line number Diff line change
@@ -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<Account> {
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<Result<u128, String>> {
self.swap(amount, min_amount_out).await
}

async fn withdraw(&self, successful_swap: bool, amount: u128) -> CallResult<u128> {
self.withdraw(successful_swap, amount).await
}
}
11 changes: 11 additions & 0 deletions backend/canisters/user/impl/src/updates/swap_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*, *};
Expand Down Expand Up @@ -213,6 +214,16 @@ fn build_swap_client(args: &Args, state: &RuntimeState) -> Box<dyn SwapClient> {
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,
))
}
}
}

Expand Down
22 changes: 10 additions & 12 deletions backend/libraries/sonic_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ pub struct SonicClient {
token0: TokenInfo,
token1: TokenInfo,
zero_for_one: bool,
deposit_subaccount: [u8; 32],
}

impl SonicClient {
Expand All @@ -27,15 +26,13 @@ impl SonicClient {
token0: TokenInfo,
token1: TokenInfo,
zero_for_one: bool,
deposit_subaccount: [u8; 32],
) -> Self {
SonicClient {
this_canister_id,
sonic_canister_id,
token0,
token1,
zero_for_one,
deposit_subaccount,
}
}

Expand All @@ -47,17 +44,18 @@ impl SonicClient {
}

pub async fn deposit(&self, amount: u128) -> CallResult<u128> {
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<u128> {
pub async fn swap(&self, amount: u128, min_amount_out: u128) -> CallResult<Result<u128, String>> {
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),
Expand All @@ -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<u128> {
let args = (self.output_token().ledger, amount.into());
pub async fn withdraw(&self, successful_swap: bool, amount: u128) -> CallResult<u128> {
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)),
Expand Down
3 changes: 3 additions & 0 deletions frontend/app/src/components/home/profile/SwapCrypto.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
switch (dex) {
case "icpswap":
return "ICPSwap";
case "sonic":
return "Sonic";
}
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/openchat-agent/codegen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
51 changes: 40 additions & 11 deletions frontend/openchat-agent/src/services/dexes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>): Promise<TokenSwapPool[]> {
const allPools = await this._icpSwapIndexClient.getPools();
const allPools = await this.getSwapPoolsUnfiltered();

return allPools.filter(
(p) =>
Expand All @@ -24,7 +27,7 @@ export class DexesAgent {
}

async canSwap(tokens: Set<string>): Promise<Set<string>> {
const allPools = await this._icpSwapIndexClient.getPools();
const allPools = await this.getSwapPoolsUnfiltered();

const available = new Set<string>();

Expand All @@ -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<TokenSwapPool[]> {
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<bigint> {
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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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 []; };
Original file line number Diff line number Diff line change
@@ -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<PairInfoExt>>,
'getPair' : ActorMethod<[Principal, Principal], [] | [PairInfoExt]>,
}
export declare const idlFactory: IDL.InterfaceFactory;
export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];
32 changes: 32 additions & 0 deletions frontend/openchat-agent/src/services/dexes/sonic/swaps/mappers.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Loading

0 comments on commit 01426a3

Please sign in to comment.