diff --git a/package.json b/package.json index 037e677..b1fbc7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hop.ag/sdk", - "version": "3.3.6", + "version": "4.0.0", "description": "Official Hop SDK to access Hop Aggregator", "type": "module", "main": "dist/cjs/index.js", @@ -18,6 +18,7 @@ "test:tokens": "npx tsx src/tests/tokens.test.ts", "test:base": "npx tsx src/tests/base.test.ts", "test:price": "npx tsx src/tests/price.test.ts", + "test:schema": "npx tsx src/tests/schema.test.ts", "build": "npm run build:esm && npm run build:cjs", "build:esm": "tsc && echo '{\"type\":\"module\"}' > dist/esm/package.json", "build:cjs": "tsc --project tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", diff --git a/readme.md b/readme.md index 2cf2785..f3878b9 100644 --- a/readme.md +++ b/readme.md @@ -14,13 +14,14 @@ const rpc_url = getFullNodeUrl("mainnet"); const hop_api_options: HopApiOptions = { api_key: "", fee_bps: 0, - fee_wallet: "0x2", + fee_wallet: "YOUR_SUI_ADDRESS_HERE", + charge_fees_in_sui: true, }; const sdk = new HopApi(rpc_url, hop_api_options); ``` -To use the Hop Aggregator API, please create an api key [here](https://hop.ag) first. +To use the Hop Aggregator API, please create an api key [here](https://t.me/HopAggregator) first. #### Get a Swap Quote @@ -41,9 +42,9 @@ Call this when a user clicks trade and wants to execute a transaction. ```typescript const tx = await sdk.fetchTx({ trade: quote.trade, - sui_address: "0x123", + sui_address: "VALID_SUI_ADDRESS_HERE", - gas_budget: 2e8, // optional default is 2e8 + gas_budget: 0.03e9, // optional default is 0.03 SUI max_slippage_bps: 100, // optional default is 1% return_output_coin_argument: false, // toggle to use the output coin in a ptb @@ -58,6 +59,10 @@ endpoint returns a curated list - with ordering - for your application. const tokens = await sdk.fetchTokens(); ``` +#### Automatic Updates +As soon as new liquidity sources become available, your +SDK will automatically aggregate them, without anything required on your end. + #### Attribution Please link to and/or mention `Powered by Hop` if you are using this SDK. diff --git a/src/sdk/api.ts b/src/sdk/api.ts index 4df4712..625fafc 100644 --- a/src/sdk/api.ts +++ b/src/sdk/api.ts @@ -11,6 +11,8 @@ import { fetchPrice, GetPriceParams, GetPriceResponse } from "./routes/price.js" export interface HopApiOptions { api_key: string; fee_bps: number; // fee to charge in bps (50% split with Hop / max fee of 5%) + charge_fees_in_sui?: boolean, + fee_wallet?: string; // sui address hop_server_url?: string; } diff --git a/src/sdk/routes/price.ts b/src/sdk/routes/price.ts index bdea755..15db2ee 100644 --- a/src/sdk/routes/price.ts +++ b/src/sdk/routes/price.ts @@ -13,7 +13,6 @@ export interface GetPriceResponse { price_usd: number; // returns usd per token sui_price: number; // returns usdc per token - } export async function fetchPrice( diff --git a/src/sdk/routes/quote.ts b/src/sdk/routes/quote.ts index a804a33..64635c2 100644 --- a/src/sdk/routes/quote.ts +++ b/src/sdk/routes/quote.ts @@ -1,7 +1,7 @@ import { HopApi } from "../api.js"; import { swapAPIResponseSchema } from "../types/api.js"; import { Trade } from "../types/trade.js"; -import { getAmountOutWithCommission, makeAPIRequest } from "../util.js"; +import { getAmountOutWithCommission, isSuiType, makeAPIRequest } from "../util.js"; export interface GetQuoteParams { token_in: string; @@ -28,6 +28,9 @@ export async function fetchQuote( token_out: params.token_out, amount_in: params.amount_in.toString(), use_alpha_router: client.use_v2, + + api_fee_bps: client.options.fee_bps, + charge_fees_in_sui: client.options.charge_fees_in_sui, }, method: "post", }, @@ -35,11 +38,20 @@ export async function fetchQuote( }); if (response?.trade) { - return { - amount_out_with_fee: getAmountOutWithCommission( + let amount_out_with_fee; + + if(client.options.charge_fees_in_sui && isSuiType(params.token_in)) { + // fee already charged + amount_out_with_fee = response.trade.amount_out.amount; + } else { + amount_out_with_fee = getAmountOutWithCommission( response.trade.amount_out.amount, - client.options.fee_bps, - ), + client.options.fee_bps + ); + } + + return { + amount_out_with_fee, trade: response.trade, }; } diff --git a/src/sdk/routes/tx.ts b/src/sdk/routes/tx.ts index 52979c5..8247829 100644 --- a/src/sdk/routes/tx.ts +++ b/src/sdk/routes/tx.ts @@ -89,7 +89,7 @@ export async function fetchTx( user_input_coins = await fetchCoins( client, params.sui_address, - params.trade.amount_in.token, + params.trade.amount_in.token ); if (user_input_coins.length == 0) { throw new Error( @@ -113,6 +113,7 @@ export async function fetchTx( client, params.sui_address, "0x2::sui::SUI", + 60 ); gas_coins = fetched_gas_coins.filter((struct) => Number(struct.amount) > 0).map((struct) => struct.object_id); } else { @@ -143,6 +144,7 @@ export async function fetchTx( let input_coin_argument = undefined; let input_coin_argument_nested = undefined; + let input_coin_argument_input = undefined; // @ts-expect-error if(params.input_coin_argument?.$kind === "Result" || params.input_coin_argument?.Result) { @@ -152,6 +154,10 @@ export async function fetchTx( } else if(params.input_coin_argument?.$kind === "NestedResult" || params.input_coin_argument?.NestedResult) { // @ts-expect-error input_coin_argument_nested = params?.input_coin_argument?.NestedResult; + // @ts-expect-error + } else if(params.input_coin_argument?.$kind === "Input" || params.input_coin_argument?.Input) { + // @ts-expect-error + input_coin_argument_input = params?.input_coin_argument?.Input; } let base_transaction = undefined; @@ -172,17 +178,20 @@ export async function fetchTx( user_input_coins, gas_coins, - gas_budget: params.gas_budget ?? 2e8, + gas_budget: params.gas_budget ?? 0.03e9, max_slippage_bps: params.max_slippage_bps, api_fee_wallet: client.options.fee_wallet, api_fee_bps: client.options.fee_bps, + charge_fees_in_sui: client.options.charge_fees_in_sui, sponsored: params.sponsored, base_transaction, input_coin_argument, input_coin_argument_nested, + input_coin_argument_input, + return_output_coin_argument: !!params.return_output_coin_argument, }, }); diff --git a/src/sdk/types/api.ts b/src/sdk/types/api.ts index d0676b4..6e16592 100644 --- a/src/sdk/types/api.ts +++ b/src/sdk/types/api.ts @@ -16,24 +16,31 @@ export const builderRequestSchema = z.object({ amount: z.string(), }), ), + sponsored: z.optional(z.boolean()), gas_coins: z.array(coinIdSchema), + gas_budget: z.number(), + max_slippage_bps: z.optional(z.number()), + api_fee_bps: z.optional(z.number()), api_fee_wallet: z.optional(z.string()), + charge_fees_in_sui: z.optional(z.boolean()), base_transaction: z.optional(z.string()), input_coin_argument: z.optional(z.number()), input_coin_argument_nested: z.optional(z.array(z.number()).length(2)), + input_coin_argument_input: z.optional(z.number()), + return_output_coin_argument: z.optional(z.boolean()) -}); +}).passthrough(); export type BuilderRequest = z.infer; export const compileRequestSchema = z.object({ - trade: tradeSchema, - builder_request: builderRequestSchema, + trade: tradeSchema.passthrough(), + builder_request: builderRequestSchema.passthrough(), }); export type CompileRequest = z.infer; @@ -41,8 +48,8 @@ export type CompileRequest = z.infer; export const swapAPIResponseSchema = z.object({ total_tests: z.number(), errors: z.number(), - trade: tradeSchema.nullable(), -}); + trade: tradeSchema.passthrough().nullable(), +}).passthrough(); export type SwapAPIResponse = z.infer; @@ -61,11 +68,11 @@ export const tokensResponseSchema = z.object({ icon_url: z.string(), decimals: z.number(), token_order: z.nullable(z.number()) - })) -}) + }).passthrough()) +}).passthrough(); export const priceResponseSchema = z.object({ coin_type: z.string(), price_sui: z.number(), sui_price: z.number() -}); +}).passthrough(); diff --git a/src/sdk/types/trade.ts b/src/sdk/types/trade.ts index 72706ff..5029cb3 100644 --- a/src/sdk/types/trade.ts +++ b/src/sdk/types/trade.ts @@ -11,18 +11,20 @@ export enum SuiExchange { SUISWAP = "SUISWAP", } -const poolExtraSchema = z.union([ +const suiExchangeSchema = z.nativeEnum(SuiExchange).or(z.string()); + +export const poolExtraSchema = z.union([ z.object({ AFTERMATH: z.object({ lp_coin_type: z.string(), - }), + }).passthrough(), }), z.object({ DEEPBOOK: z.object({ pool_type: z.string(), lot_size: z.coerce.bigint(), min_size: z.coerce.bigint() - }), + }).passthrough(), }), z.object({ TURBOS: z.object({ @@ -31,25 +33,26 @@ const poolExtraSchema = z.union([ fee_type: z.string(), tick_spacing: z.number(), tick_current_index: z.number(), - }), + }).passthrough(), }), z.object({ CETUS: z.object({ coin_type_a: z.string(), coin_type_b: z.string(), - }), + }).passthrough(), }), z.object({ FLOWX: z.object({ is_v3: z.boolean(), fee_rate: z.number().nullish(), - }) + }).passthrough() }), z.object({ KRIYA: z.object({ is_v3: z.boolean() - }) - }) + }).passthrough() + }), + z.object({}).passthrough() ]); export type PoolExtra = z.infer; @@ -57,11 +60,11 @@ export type PoolExtra = z.infer; const tradePoolSchema = z.object({ object_id: z.string(), initial_shared_version: z.number().nullable(), - sui_exchange: z.nativeEnum(SuiExchange), + sui_exchange: suiExchangeSchema, tokens: z.array(z.string()).nonempty(), is_active: z.boolean(), extra: poolExtraSchema.nullable(), -}); +}).passthrough(); export type TradePool = z.infer; @@ -84,6 +87,6 @@ export const tradeSchema = z.object({ edges: z.record(z.array(z.string())), amount_in: tokenAmountSchema, amount_out: tokenAmountSchema, -}); +}).passthrough(); export type Trade = z.infer; diff --git a/src/sdk/util.ts b/src/sdk/util.ts index 1a33036..0da68b3 100644 --- a/src/sdk/util.ts +++ b/src/sdk/util.ts @@ -1,6 +1,7 @@ import fetch from "cross-fetch"; import { z } from "zod"; import { API_SERVER_PREFIX, FEE_DENOMINATOR } from "./constants.js"; +import { normalizeStructTag } from "@mysten/sui/utils"; export interface RequestParams { hop_server_url?: string; @@ -26,7 +27,7 @@ export async function makeAPIRequest({ body: JSON.stringify( { ...options.data, - api_key: options.api_key, + api_key: options.api_key }, (_, v) => { const isBigIntString = typeof v === 'string' && /^\d+n$/.test(v); @@ -73,3 +74,9 @@ export function getAmountOutWithCommission( (amount_out * (FEE_DENOMINATOR - BigInt(fee_bps))) / BigInt(FEE_DENOMINATOR) ); } + +const NORMALIZED_SUI_COIN_TYPE = normalizeStructTag("0x2::sui::SUI"); + +export function isSuiType(coin_type: string) { + return NORMALIZED_SUI_COIN_TYPE == normalizeStructTag(coin_type); +} \ No newline at end of file diff --git a/src/tests/price.test.ts b/src/tests/price.test.ts index 341cde0..73fb4d9 100644 --- a/src/tests/price.test.ts +++ b/src/tests/price.test.ts @@ -6,7 +6,7 @@ async function priceTest() { const api = new HopApi(getFullnodeUrl("mainnet"), { api_key: "", fee_bps: 0, - // hop_server_url: "http://localhost:3002/api/v2" + hop_server_url: "http://localhost:3002/api/v2" }); const result = await api.fetchPrice({ diff --git a/src/tests/quote.test.ts b/src/tests/quote.test.ts index 815acc4..5c63154 100644 --- a/src/tests/quote.test.ts +++ b/src/tests/quote.test.ts @@ -6,15 +6,14 @@ async function quoteTest() { const api = new HopApi(getFullnodeUrl("mainnet"), { api_key: "", fee_bps: 0, - hop_server_url: "" + hop_server_url: "http://localhost:3002/api/v2", }); const result = await api.fetchQuote({ // @ts-ignore amount_in: 1_000_000_000n, token_in: "0x2::sui::SUI", - token_out: - "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN", + token_out: "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN", }); console.log("result", result); diff --git a/src/tests/schema.test.ts b/src/tests/schema.test.ts new file mode 100644 index 0000000..cdaf366 --- /dev/null +++ b/src/tests/schema.test.ts @@ -0,0 +1,22 @@ + +import { z } from "zod"; +import { poolExtraSchema } from "../sdk/types/trade"; + +// @ts-ignore +async function quoteTest() { + const result = poolExtraSchema.parse({ + hi: + { + b: 1, + t: true, + c: "yolo!", + x: { + "d": "hello" + } + } + }); + + console.log(result); +} + +quoteTest(); diff --git a/src/tests/tx.test.ts b/src/tests/tx.test.ts index e686018..62f1e62 100644 --- a/src/tests/tx.test.ts +++ b/src/tests/tx.test.ts @@ -6,26 +6,19 @@ async function txTest(): Promise { const sui_client = new SuiClient({ url: getFullnodeUrl('mainnet') }); const api = new HopApi(getFullnodeUrl("mainnet"), { api_key: "", - fee_bps: 0, - // fee_wallet: "0xa89611f02060bad390103e783a62c88725b47059e6460cf0d2f3ca32e2559641" + fee_bps: 30, + hop_server_url: "http://localhost:3002/api/v2", + charge_fees_in_sui: false, }); - const total_balance = await sui_client.getBalance({ - owner: "0x90afb76a8bfca719dadb4e77de50f65bba5327397cc7550d9c7b816907958943", - coinType: "0x71bd8693b1d17688e6671c9208e5e2499a95dce65ec690373002a72e6649f0e6::sure::SURE" - }); - - let amount_in = BigInt(total_balance.totalBalance); - const quote_result = await api.fetchQuote({ // @ts-ignore - amount_in, - token_out: "0x2::sui::SUI", - token_in: - "0x71bd8693b1d17688e6671c9208e5e2499a95dce65ec690373002a72e6649f0e6::sure::SURE", + amount_in: 1e9, + token_in: "0x2::sui::SUI", + token_out: + "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN", }); - console.log("amount_in", amount_in); console.log("quote_result.trade.amount_in", quote_result.trade.amount_in.amount); console.log("quote_result", quote_result); @@ -33,17 +26,13 @@ async function txTest(): Promise { const tx_result = await api.fetchTx({ // @ts-ignore trade: quote_result.trade, - sui_address: - "0x90afb76a8bfca719dadb4e77de50f65bba5327397cc7550d9c7b816907958943", - return_output_coin_argument: false, - // gas_budget: 1e7 - // max_slippage_bps: 100, + sui_address: "0x4466fe25550f648a4acd6823a90e1f96c77e1d37257ee3ed2d6e02a694984f73", // return_output_coin_argument: true, - // base_transaction: tx, - // input_coin_argument: coin, + gas_budget: 100000000, + max_slippage_bps: 1000 }); - // console.log("tx_result", tx_result); + console.log("tx_result", tx_result); const result = await sui_client.dryRunTransactionBlock({ transactionBlock: await tx_result.transaction.build({ client: sui_client }) }); // console.log("result", JSON.stringify(result, null, 2));