├── .github └── workflows │ ├── build.yml │ └── npm-publish.yml ├── .gitignore ├── .prettierrc ├── package.json ├── readme.md ├── src ├── index.ts ├── sdk │ ├── api.ts │ ├── constants.ts │ ├── routes │ │ ├── price.ts │ │ ├── quote.ts │ │ ├── tokens.ts │ │ └── tx.ts │ ├── types │ │ ├── api.ts │ │ └── trade.ts │ └── util.ts ├── tests │ ├── base.test.ts │ ├── price.test.ts │ ├── quote.test.ts │ ├── tokens.test.ts │ └── tx.test.ts └── types │ └── index.ts ├── tsconfig.cjs.json └── tsconfig.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build SDK 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Use Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: "18" 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Build package 26 | run: npm run build # Replace with your build script 27 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to npm 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # Set this to the branch you want to trigger the publish action 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: "18" # Specify the Node.js version 20 | registry-url: "https://registry.npmjs.org/" 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Build package 26 | run: npm run build # Replace with your build script 27 | 28 | - name: Publish to npm 29 | run: npm publish --access public 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | src/tests/.env 4 | .idea 5 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hop.ag/sdk", 3 | "version": "4.0.9", 4 | "description": "Official Hop SDK to access Hop Aggregator", 5 | "type": "module", 6 | "main": "dist/cjs/index.js", 7 | "module": "dist/esm/index.js", 8 | "types": "dist/esm/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/esm/index.js", 12 | "require": "./dist/cjs/index.js" 13 | } 14 | }, 15 | "scripts": { 16 | "test:quote": "npx tsx src/tests/quote.test.ts", 17 | "test:tx": "npx tsx src/tests/tx.test.ts", 18 | "test:tokens": "npx tsx src/tests/tokens.test.ts", 19 | "test:base": "npx tsx src/tests/base.test.ts", 20 | "test:price": "npx tsx src/tests/price.test.ts", 21 | "test:schema": "npx tsx src/tests/schema.test.ts", 22 | "build": "npm run build:esm && npm run build:cjs", 23 | "build:esm": "tsc && echo '{\"type\":\"module\"}' > dist/esm/package.json", 24 | "build:cjs": "tsc --project tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", 25 | "pretty": "prettier . --write" 26 | }, 27 | "author": "Hop Aggregator", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "@types/node": "^20.14.2", 31 | "prettier": "3.2.5", 32 | "typescript": "^5.4.5" 33 | }, 34 | "dependencies": { 35 | "@mysten/sui": "^1.2.0", 36 | "cross-fetch": "^4.0.0", 37 | "tslib": "^2.6.2", 38 | "zod": "^3.23.8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Hop SDK 2 | 3 | Use this library to interact with [Hop Aggregator](hop.ag)'s swap. To request an api key, please 4 | email [**api@hop.ag**](mailto:api@hop.ag). We do offer enterprise plans for specific traders and businesses. 5 | 6 | 7 | `npm install @hop.ag/sdk` 8 | 9 | #### Initialize 10 | 11 | ```typescript 12 | import { HopApi, HopApiOptions } from "@hop.ag/sdk"; 13 | import { getFullnodeUrl } from "@mysten/sui/client"; 14 | 15 | const rpc_url = getFullNodeUrl("mainnet"); 16 | const hop_api_options: HopApiOptions = { 17 | api_key: "", 18 | 19 | // 1bps = 0.01%. 10_000bps = 100%. 20 | // max fee is 470bps (4.7%). 21 | fee_bps: 0, 22 | fee_wallet: "Enter your sui address here" 23 | }; 24 | 25 | const sdk = new HopApi(rpc_url, hop_api_options); 26 | ``` 27 | 28 | #### 1. Get a Swap Quote 29 | 30 | Call this first to display the expected amount out. 31 | 32 | ```typescript 33 | const quote = await sdk.fetchQuote({ 34 | token_in: "", 35 | token_out: "", 36 | amount_in: 0, 37 | }); 38 | ``` 39 | 40 | #### 2. Get a Swap Transaction 41 | 42 | Call this when a user clicks trade and wants to execute a transaction. 43 | 44 | ```typescript 45 | const tx = await sdk.fetchTx({ 46 | trade: quote.trade, 47 | sui_address: "VALID_SUI_ADDRESS_HERE", 48 | 49 | gas_budget: 0.03e9, // optional default is 0.03 SUI 50 | max_slippage_bps: 100, // optional default is 1% 51 | 52 | return_output_coin_argument: false, // toggle to use the output coin in a ptb 53 | }); 54 | ``` 55 | 56 | #### 3. Get the price of a Token 57 | Return the real-time on-chain price of a token. This pricing API uses Defi pools. 58 | It will return two items: the price of the token in SUI as base units, and the price 59 | of SUI in USD. 60 | 61 | ```typescript 62 | const price = await sdk.fetchPrice({ 63 | coin_type: "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP" 64 | }); 65 | 66 | const deep_price_in_sui = price.price_sui; 67 | const deep_price_in_usd = price.price_usd; 68 | const price_of_sui_in_usd = price.sui_price; 69 | ``` 70 | 71 | #### 4. Get a list of Verified Tokens 72 | We maintain a list of verified SUI ecosystem tokens and their metadata. This 73 | endpoint returns a curated list - with ordering - for your application. 74 | 75 | ```typescript 76 | const tokens = await sdk.fetchTokens(); 77 | ``` 78 | 79 | #### Automatic Updates 80 | As soon as new liquidity sources become available, your 81 | SDK will automatically aggregate them, without anything required on your end. 82 | 83 | #### Attribution 84 | 85 | Please link to and/or mention `Powered by Hop` if you are using this SDK. -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { HopApi } from "./sdk/api.js"; 2 | export * from "./types/index.js"; 3 | -------------------------------------------------------------------------------- /src/sdk/api.ts: -------------------------------------------------------------------------------- 1 | import { SuiClient } from "@mysten/sui/client"; 2 | import { 3 | fetchQuote, 4 | GetQuoteParams, 5 | GetQuoteResponse, 6 | } from "./routes/quote.js"; 7 | import { fetchTx, GetTxParams, GetTxResponse } from "./routes/tx.js"; 8 | import { fetchTokens, GetTokensResponse } from "./routes/tokens.js"; 9 | import { fetchPrice, GetPriceParams, GetPriceResponse } from "./routes/price.js"; 10 | 11 | export interface HopApiOptions { 12 | api_key: string; 13 | fee_bps: number; // fee to charge in bps (50% split with Hop / max fee of 5%) 14 | charge_fees_in_sui?: boolean, 15 | 16 | fee_wallet?: string; // sui address 17 | hop_server_url?: string; 18 | } 19 | 20 | export class HopApi { 21 | readonly client: SuiClient; 22 | readonly options: HopApiOptions; 23 | readonly use_v2: boolean; 24 | 25 | constructor(rpc_endpoint: string, options: HopApiOptions, use_v2: boolean = true) { 26 | this.client = new SuiClient({ url: rpc_endpoint }); 27 | this.options = options; 28 | this.use_v2 = use_v2; 29 | 30 | this.validate_api_key(); 31 | this.validate_fee(); 32 | } 33 | 34 | private validate_api_key() { 35 | if (!this.options.api_key.startsWith("hopapi")) { 36 | console.error( 37 | "Error > Invalid api key:", 38 | this.options.api_key, 39 | ". Please contact us at hop.ag to request a new key.", 40 | ); 41 | } 42 | } 43 | 44 | private validate_fee() { 45 | let fee_bps = this.options.fee_bps; 46 | 47 | if (fee_bps < 0) { 48 | console.error("> fee_bps must be positive."); 49 | } else if (fee_bps > 500) { 50 | console.error("> fee_bps must be less than or equal to 5% (500 bps)."); 51 | } 52 | 53 | this.options.fee_bps = Math.max(this.options.fee_bps, 0); 54 | this.options.fee_bps = Math.min(this.options.fee_bps, 500); 55 | this.options.fee_bps = Number(this.options.fee_bps.toFixed(0)); 56 | } 57 | 58 | /* 59 | * Routes 60 | */ 61 | 62 | async fetchQuote(quote: GetQuoteParams): Promise { 63 | return fetchQuote(this, quote); 64 | } 65 | 66 | async fetchTx(tx: GetTxParams): Promise { 67 | return fetchTx(this, tx); 68 | } 69 | 70 | async fetchTokens(): Promise { 71 | return fetchTokens(this); 72 | } 73 | 74 | async fetchPrice(price: GetPriceParams): Promise { 75 | return fetchPrice(this, price); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/sdk/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_SERVER_PREFIX = "https://d2getbh8qk7p99.cloudfront.net/api/v2"; 2 | 3 | export const FEE_DENOMINATOR: bigint = 10_000n; 4 | -------------------------------------------------------------------------------- /src/sdk/routes/price.ts: -------------------------------------------------------------------------------- 1 | import { HopApi } from "../api.js"; 2 | import { makeAPIRequest } from "../util.js"; 3 | import { priceResponseSchema } from "../types/api.js"; 4 | 5 | export interface GetPriceParams { 6 | coin_type: string; 7 | } 8 | 9 | export interface GetPriceResponse { 10 | coin_type: string; 11 | 12 | price_sui: number; // returns sui per token 13 | price_usd: number; // returns usd per token 14 | 15 | sui_price: number; // returns usdc per token 16 | } 17 | 18 | export async function fetchPrice( 19 | client: HopApi, 20 | params: GetPriceParams 21 | ): Promise { 22 | const response = await makeAPIRequest<{ 23 | coin_type: string, 24 | price_sui: number, 25 | sui_price: number, 26 | }>({ 27 | route: `price`, 28 | options: { 29 | api_key: client.options.api_key, 30 | hop_server_url: client.options.hop_server_url, 31 | data: { 32 | coin_type: params.coin_type, 33 | }, 34 | method: "post", 35 | }, 36 | responseSchema: priceResponseSchema, 37 | }); 38 | 39 | if (response?.coin_type) { 40 | let price_usd = response.price_sui * response.sui_price; 41 | 42 | return { 43 | coin_type: response?.coin_type, 44 | price_sui: response?.price_sui, // price of token in sui 45 | price_usd, // price of token in usd 46 | sui_price: response?.sui_price // price of sui in usd 47 | }; 48 | } 49 | 50 | throw new Error("Unable to get price"); 51 | 52 | } -------------------------------------------------------------------------------- /src/sdk/routes/quote.ts: -------------------------------------------------------------------------------- 1 | import { HopApi } from "../api.js"; 2 | import { swapAPIResponseSchema } from "../types/api.js"; 3 | import { GammaTrade } from "../types/trade.js"; 4 | import { getAmountOutWithCommission, isSuiType, makeAPIRequest } from "../util.js"; 5 | 6 | export interface GetQuoteParams { 7 | token_in: string; 8 | token_out: string; 9 | amount_in: bigint; 10 | } 11 | 12 | export interface GetQuoteResponse { 13 | amount_out_with_fee: bigint; 14 | trade: GammaTrade; 15 | } 16 | 17 | export async function fetchQuote( 18 | client: HopApi, 19 | params: GetQuoteParams, 20 | ): Promise { 21 | const response = await makeAPIRequest({ 22 | route: "quote", 23 | options: { 24 | api_key: client.options.api_key, 25 | hop_server_url: client.options.hop_server_url, 26 | data: { 27 | token_in: params.token_in, 28 | token_out: params.token_out, 29 | amount_in: params.amount_in.toString(), 30 | use_alpha_router: client.use_v2, 31 | 32 | api_fee_bps: client.options.fee_bps, 33 | charge_fees_in_sui: client.options.charge_fees_in_sui, 34 | }, 35 | method: "post", 36 | }, 37 | responseSchema: swapAPIResponseSchema, 38 | }); 39 | 40 | if (response?.trade) { 41 | let amount_out_with_fee; 42 | 43 | if(client.options.charge_fees_in_sui && isSuiType(params.token_in)) { 44 | // fee already charged 45 | amount_out_with_fee = response.trade.quote; 46 | } else { 47 | amount_out_with_fee = getAmountOutWithCommission( 48 | response.trade.quote, 49 | client.options.fee_bps 50 | ); 51 | } 52 | 53 | return { 54 | amount_out_with_fee, 55 | trade: response.trade, 56 | }; 57 | } 58 | 59 | throw new Error("Unable to get quote"); 60 | } 61 | -------------------------------------------------------------------------------- /src/sdk/routes/tokens.ts: -------------------------------------------------------------------------------- 1 | import { HopApi } from "../api.js"; 2 | import { tokensResponseSchema } from "../types/api.js"; 3 | import { makeAPIRequest } from "../util.js"; 4 | 5 | export interface VerifiedToken { 6 | coin_type: string; 7 | name: string; 8 | ticker: string; 9 | icon_url: string; 10 | decimals: number; 11 | token_order?: number | null; // used for internal reasons 12 | } 13 | 14 | export interface GetTokensResponse { 15 | tokens: VerifiedToken[]; 16 | } 17 | 18 | export async function fetchTokens(client: HopApi): Promise { 19 | const response = await makeAPIRequest({ 20 | route: "tokens", 21 | options: { 22 | api_key: client.options.api_key, 23 | hop_server_url: client.options.hop_server_url, 24 | data: {}, 25 | method: "post", 26 | }, 27 | responseSchema: tokensResponseSchema, 28 | }); 29 | 30 | if (response?.tokens) { 31 | return { 32 | tokens: response.tokens, 33 | }; 34 | } 35 | 36 | throw new Error("Unable to get tokens"); 37 | } 38 | -------------------------------------------------------------------------------- /src/sdk/routes/tx.ts: -------------------------------------------------------------------------------- 1 | import { Transaction, Argument, TransactionResult } from "@mysten/sui/transactions"; 2 | import { CoinStruct } from "@mysten/sui/client"; 3 | import { HopApi } from "../api.js"; 4 | import { makeAPIRequest } from "../util.js"; 5 | import { compileRequestSchema, compileResponseSchema } from "../types/api.js"; 6 | import { GammaTrade } from "../types/trade.js"; 7 | import { normalizeStructTag, toB64 } from "@mysten/sui/utils"; 8 | 9 | export interface GetTxParams { 10 | trade: GammaTrade; 11 | sui_address: string; 12 | 13 | gas_budget?: number; 14 | max_slippage_bps?: number; 15 | 16 | /* FOR PTB USE */ 17 | sponsored?: boolean; 18 | 19 | base_transaction?: Transaction; 20 | input_coin_argument?: Argument; 21 | return_output_coin_argument?: boolean; 22 | } 23 | 24 | export interface GetTxResponse { 25 | transaction: Transaction; 26 | output_coin: TransactionResult | undefined; 27 | } 28 | 29 | interface CoinId { 30 | object_id: string; 31 | version: string; 32 | } 33 | 34 | interface InputToken { 35 | object_id: CoinId; 36 | coin_type: string; 37 | amount: string; 38 | } 39 | 40 | async function fetchCoins( 41 | client: HopApi, 42 | sui_address: string, 43 | coin_type: string, 44 | max = -1, 45 | ): Promise { 46 | let coins: CoinStruct[] = []; 47 | let cursor = null; 48 | 49 | do { 50 | let coin_response = await client.client.getCoins({ 51 | owner: sui_address, 52 | coinType: coin_type, 53 | cursor: cursor, 54 | }); 55 | coins.push(...coin_response.data); 56 | 57 | // if you only want x coins 58 | if (max != -1 && coins.length >= max) { 59 | break; 60 | } 61 | 62 | if (coin_response.hasNextPage) { 63 | cursor = coin_response.nextCursor; 64 | } else { 65 | cursor = null; 66 | } 67 | } while (cursor != null); 68 | 69 | return coins.map((coin_struct) => ({ 70 | object_id: { 71 | object_id: coin_struct.coinObjectId, 72 | version: coin_struct.version, 73 | digest: coin_struct.digest, 74 | }, 75 | coin_type: coin_struct.coinType, 76 | amount: coin_struct.balance, 77 | })); 78 | } 79 | 80 | export async function fetchTx( 81 | client: HopApi, 82 | params: GetTxParams, 83 | ): Promise { 84 | // get input coins 85 | let gas_coins: CoinId[] = []; 86 | let user_input_coins: InputToken[] = []; 87 | let coin_in = params.trade.routes[0]![0]!.coin_in; 88 | 89 | if(!params.input_coin_argument) { 90 | user_input_coins = await fetchCoins( 91 | client, 92 | params.sui_address, 93 | coin_in, 94 | ); 95 | if (user_input_coins.length == 0) { 96 | throw new Error( 97 | `HopApi > Error: sui address ${params.sui_address} does not have any input coins for tx.`, 98 | ); 99 | } 100 | let total_input = user_input_coins.reduce((c, t) => c + BigInt(t.amount), 0n); 101 | if (total_input < params.trade.quote) { 102 | throw new Error( 103 | `HopApi > Error: user does not have enough amount in for trade. 104 | User amount: ${total_input}. 105 | Trade amount: ${params.trade.quote}` 106 | ) 107 | } 108 | } 109 | 110 | // gas coins 111 | if(!params.sponsored) { 112 | if (normalizeStructTag(coin_in) != normalizeStructTag("0x2::sui::SUI") || user_input_coins.length == 0) { 113 | let fetched_gas_coins = await fetchCoins( 114 | client, 115 | params.sui_address, 116 | "0x2::sui::SUI", 117 | 60 118 | ); 119 | gas_coins = fetched_gas_coins.filter((struct) => Number(struct.amount) > 0).map((struct) => struct.object_id); 120 | } else { 121 | gas_coins = user_input_coins.filter((struct) => Number(struct.amount) > 0).map((struct) => struct.object_id); 122 | } 123 | } 124 | 125 | // add any input coins that match user type 126 | if(!params.input_coin_argument) { 127 | let single_output_coin: InputToken[] = await fetchCoins( 128 | client, 129 | params.sui_address, 130 | coin_in, 131 | 1, 132 | ); 133 | user_input_coins.push(...single_output_coin); 134 | } 135 | 136 | if (!params.sponsored && gas_coins.length === 0) { 137 | throw new Error( 138 | `HopApi > Error: sui address ${params.sui_address} does not have any gas coins for tx.`, 139 | ); 140 | } 141 | 142 | if(params.input_coin_argument && !params.base_transaction) { 143 | throw new Error("Input coin argument must be result from base transaction!"); 144 | } 145 | 146 | let input_coin_argument = undefined; 147 | let input_coin_argument_nested = undefined; 148 | let input_coin_argument_input = undefined; 149 | 150 | // @ts-expect-error 151 | if(params.input_coin_argument?.$kind === "Result" || params.input_coin_argument?.Result) { 152 | // @ts-expect-error 153 | input_coin_argument = params?.input_coin_argument?.Result; 154 | // @ts-expect-error 155 | } else if(params.input_coin_argument?.$kind === "NestedResult" || params.input_coin_argument?.NestedResult) { 156 | // @ts-expect-error 157 | input_coin_argument_nested = params?.input_coin_argument?.NestedResult; 158 | // @ts-expect-error 159 | } else if(params.input_coin_argument?.$kind === "Input" || params.input_coin_argument?.Input) { 160 | // @ts-expect-error 161 | input_coin_argument_input = params?.input_coin_argument?.Input; 162 | } 163 | 164 | let base_transaction = undefined; 165 | 166 | if(params.base_transaction) { 167 | const built_tx_array = await params.base_transaction.build({ 168 | client: client.client, 169 | onlyTransactionKind: true 170 | }); 171 | 172 | base_transaction = toB64(built_tx_array); 173 | } 174 | 175 | const compileRequest = compileRequestSchema.parse({ 176 | trade: params.trade, 177 | builder_request: { 178 | sender_address: params.sui_address, 179 | user_input_coins, 180 | gas_coins, 181 | 182 | gas_budget: params.gas_budget ?? 0.03e9, 183 | max_slippage_bps: params.max_slippage_bps, 184 | 185 | api_fee_wallet: client.options.fee_wallet, 186 | api_fee_bps: client.options.fee_bps, 187 | charge_fees_in_sui: true, 188 | 189 | sponsored: params.sponsored, 190 | base_transaction, 191 | 192 | input_coin_argument, 193 | input_coin_argument_nested, 194 | input_coin_argument_input, 195 | 196 | return_output_coin_argument: !!params.return_output_coin_argument, 197 | }, 198 | }); 199 | 200 | const response = await makeAPIRequest({ 201 | route: "tx/compile", 202 | options: { 203 | api_key: client.options.api_key, 204 | hop_server_url: client.options.hop_server_url, 205 | data: compileRequest, 206 | method: "post", 207 | }, 208 | responseSchema: compileResponseSchema, 209 | }); 210 | 211 | if (response.tx) { 212 | const tx_block = createFrontendTxBlock(response.tx); 213 | let output_coin: TransactionResult | undefined = undefined; 214 | 215 | if(params.return_output_coin_argument) { 216 | // order 217 | // last merge into final output coin 218 | // slippage check 219 | // fee 220 | // @ts-ignore 221 | output_coin = tx_block 222 | .getData() 223 | .commands.find( 224 | (tx) => 225 | tx.$kind == "MoveCall" && 226 | tx.MoveCall.function === "check_slippage_v2" && 227 | tx.MoveCall.module === "slippage", 228 | )?.MoveCall.arguments[0]; 229 | } 230 | 231 | return { 232 | transaction: tx_block, 233 | output_coin, 234 | }; 235 | } 236 | 237 | throw new Error("Could not construct transaction"); 238 | } 239 | 240 | // const ensure_array = (value: number | number[]): number[] => { 241 | // if (typeof value == "number") { 242 | // return [value]; 243 | // } else { 244 | // return value; 245 | // } 246 | // } 247 | 248 | const createFrontendTxBlock = (serialized: string): Transaction => { 249 | const txb = Transaction.from(serialized); 250 | const inputs = txb.getData().inputs; 251 | 252 | const newInputs = inputs.map((input) => { 253 | if (input.$kind === "Object") { 254 | const objectId = 255 | input.Object?.SharedObject?.objectId ?? 256 | input.Object?.ImmOrOwnedObject?.objectId; 257 | if (!objectId) { 258 | throw new Error(`Missing object ID for input ${input.$kind}`); 259 | } 260 | 261 | return { 262 | $kind: "UnresolvedObject", 263 | UnresolvedObject: { 264 | objectId, 265 | } 266 | } 267 | } 268 | return input; 269 | }); 270 | 271 | return Transaction.from( 272 | JSON.stringify({ 273 | ...txb.getData(), 274 | gasConfig: {}, 275 | inputs: newInputs, 276 | }) 277 | ); 278 | }; 279 | -------------------------------------------------------------------------------- /src/sdk/types/api.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { gammaTradeSchema } from "./trade.js"; 3 | 4 | const coinIdSchema = z.object({ 5 | object_id: z.string(), 6 | version: z.string(), 7 | digest: z.string(), 8 | }); 9 | 10 | export const builderRequestSchema = z.object({ 11 | sender_address: z.string(), 12 | user_input_coins: z.array( 13 | z.object({ 14 | object_id: coinIdSchema, 15 | coin_type: z.string(), 16 | amount: z.string(), 17 | }), 18 | ), 19 | 20 | sponsored: z.optional(z.boolean()), 21 | gas_coins: z.array(coinIdSchema), 22 | 23 | gas_budget: z.number(), 24 | 25 | max_slippage_bps: z.optional(z.number()), 26 | 27 | api_fee_bps: z.optional(z.number()), 28 | api_fee_wallet: z.optional(z.string()), 29 | charge_fees_in_sui: z.optional(z.boolean()), 30 | 31 | base_transaction: z.optional(z.string()), 32 | input_coin_argument: z.optional(z.number()), 33 | input_coin_argument_nested: z.optional(z.array(z.number()).length(2)), 34 | input_coin_argument_input: z.optional(z.number()), 35 | 36 | return_output_coin_argument: z.optional(z.boolean()) 37 | }).passthrough(); 38 | 39 | export type BuilderRequest = z.infer; 40 | 41 | export const compileRequestSchema = z.object({ 42 | trade: gammaTradeSchema.passthrough(), 43 | builder_request: builderRequestSchema.passthrough(), 44 | }); 45 | 46 | export type CompileRequest = z.infer; 47 | 48 | export const swapAPIResponseSchema = z.object({ 49 | total_tests: z.number(), 50 | errors: z.number(), 51 | trade: gammaTradeSchema.passthrough().nullable(), 52 | }).passthrough(); 53 | 54 | export type SwapAPIResponse = z.infer; 55 | 56 | export const compileResponseSchema = z.object({ 57 | tx: z.string(), 58 | output_coin: z.string().nullish(), 59 | }); 60 | 61 | export type CompileResponse = z.infer; 62 | 63 | export const tokensResponseSchema = z.object({ 64 | tokens: z.array(z.object({ 65 | coin_type: z.string(), 66 | name: z.string(), 67 | ticker: z.string(), 68 | icon_url: z.string(), 69 | decimals: z.number(), 70 | token_order: z.nullable(z.number()) 71 | }).passthrough()) 72 | }).passthrough(); 73 | 74 | export const priceResponseSchema = z.object({ 75 | coin_type: z.string(), 76 | price_sui: z.number(), 77 | sui_price: z.number() 78 | }).passthrough(); 79 | -------------------------------------------------------------------------------- /src/sdk/types/trade.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export enum SuiExchange { 4 | CETUS = "CETUS", 5 | FLOWX = "FLOWX", 6 | TURBOS = "TURBOS", 7 | AFTERMATH = "AFTERMATH", 8 | KRIYA = "KRIYA", 9 | BLUEMOVE = "BLUEMOVE", 10 | DEEPBOOK = "DEEPBOOK", 11 | SUISWAP = "SUISWAP", 12 | HOPFUN = "HOPFUN", 13 | BLUEFIN = "BLUEFIN", 14 | MOVEPUMP = "MOVEPUMP", 15 | TURBOSFUN = "TURBOSFUN", 16 | SPRINGSUI = "SPRINGSUI", 17 | STSUI = "STSUI", 18 | OBRIC = "OBRIC" 19 | } 20 | 21 | const suiExchangeSchema = z.nativeEnum(SuiExchange).or(z.string()); 22 | 23 | export const poolExtraSchema = z.union([ 24 | z.object({ 25 | AFTERMATH: z.object({ 26 | lp_coin_type: z.string(), 27 | }).passthrough(), 28 | }), 29 | z.object({ 30 | DEEPBOOK: z.object({ 31 | pool_type: z.string(), 32 | lot_size: z.coerce.bigint(), 33 | min_size: z.coerce.bigint() 34 | }).passthrough(), 35 | }), 36 | z.object({ 37 | TURBOS: z.object({ 38 | coin_type_a: z.string(), 39 | coin_type_b: z.string(), 40 | fee_type: z.string(), 41 | tick_spacing: z.number(), 42 | tick_current_index: z.number(), 43 | }).passthrough(), 44 | }), 45 | z.object({ 46 | CETUS: z.object({ 47 | coin_type_a: z.string(), 48 | coin_type_b: z.string(), 49 | }).passthrough(), 50 | }), 51 | z.object({ 52 | FLOWX: z.object({ 53 | is_v3: z.boolean(), 54 | fee_rate: z.number().nullish(), 55 | }).passthrough() 56 | }), 57 | z.object({ 58 | KRIYA: z.object({ 59 | is_v3: z.boolean() 60 | }).passthrough() 61 | }), 62 | z.object({ 63 | SPRINGSUI: z.object({ 64 | weighthook_id: z.string(), 65 | weighthook_version: z.number() 66 | }) 67 | }), 68 | z.object({}).passthrough() 69 | ]); 70 | 71 | export type PoolExtra = z.infer; 72 | 73 | const tradePoolSchema = z.object({ 74 | object_id: z.string(), 75 | initial_shared_version: z.number().nullable(), 76 | sui_exchange: suiExchangeSchema, 77 | tokens: z.array(z.string()).nonempty(), 78 | is_active: z.boolean(), 79 | extra: poolExtraSchema.nullable(), 80 | }).passthrough(); 81 | 82 | export type TradePool = z.infer; 83 | 84 | const routeNodeSchema = z.object({ 85 | coin_in: z.string(), 86 | coin_out: z.string(), 87 | pool_id: z.string(), 88 | amount_in: z.bigint(), 89 | amount_out: z.bigint() 90 | }); 91 | 92 | export type RouteNode = z.infer; 93 | 94 | export const gammaTradeSchema = z.object({ 95 | pools: z.map(z.string(), tradePoolSchema), 96 | routes: z.array(z.array(routeNodeSchema)), 97 | 98 | amount_in: z.bigint(), 99 | quote: z.bigint(), 100 | }).passthrough(); 101 | 102 | export type GammaTrade = z.infer; 103 | -------------------------------------------------------------------------------- /src/sdk/util.ts: -------------------------------------------------------------------------------- 1 | import fetch from "cross-fetch"; 2 | import { z } from "zod"; 3 | import { API_SERVER_PREFIX, FEE_DENOMINATOR } from "./constants.js"; 4 | import { normalizeStructTag } from "@mysten/sui/utils"; 5 | 6 | export interface RequestParams { 7 | hop_server_url?: string; 8 | api_key: string; 9 | data: object; 10 | method: "get" | "post"; 11 | } 12 | 13 | export async function makeAPIRequest({ 14 | route, 15 | options, 16 | responseSchema, 17 | }: { 18 | route: string; 19 | options: RequestParams; 20 | responseSchema: z.ZodSchema; 21 | }): Promise { 22 | try { 23 | const response = await fetch( 24 | `${options.hop_server_url ?? API_SERVER_PREFIX}/${route}`, 25 | { 26 | method: options.method, 27 | body: JSON.stringify( 28 | { 29 | ...options.data, 30 | api_key: options.api_key 31 | }, 32 | (_, v) => { 33 | const isBigIntString = typeof v === 'string' && /^\d+n$/.test(v); 34 | if (isBigIntString) v.slice(-1); 35 | return typeof v === 'bigint' || isBigIntString ? parseInt(v.toString()) : v; 36 | }, 37 | ), 38 | headers: { 39 | "Content-Type": "application/json", 40 | }, 41 | }, 42 | ); 43 | 44 | if (response.status !== 200) { 45 | throw new Error( 46 | `HopApi > Error on request '/${route}' : ${response.statusText}`, 47 | ); 48 | } 49 | 50 | const result = responseSchema.safeParse(await response.json()); 51 | if (result.success) { 52 | return result.data; 53 | } else { 54 | console.error(result.error); 55 | throw new Error(`Invalid response: ${result.error.message}`); 56 | } 57 | } catch (error) { 58 | console.error(error); 59 | throw new Error( 60 | `HopApi > Error on request '/${route}' : ${(error as Error).message}`, 61 | ); 62 | } 63 | } 64 | 65 | export function getAmountOutWithCommission( 66 | amount_out: bigint, 67 | fee_bps: number, 68 | ): bigint { 69 | if (fee_bps == 0) { 70 | return amount_out; 71 | } 72 | 73 | return ( 74 | (amount_out * (FEE_DENOMINATOR - BigInt(fee_bps))) / BigInt(FEE_DENOMINATOR) 75 | ); 76 | } 77 | 78 | const NORMALIZED_SUI_COIN_TYPE = normalizeStructTag("0x2::sui::SUI"); 79 | 80 | export function isSuiType(coin_type: string) { 81 | return NORMALIZED_SUI_COIN_TYPE == normalizeStructTag(coin_type); 82 | } -------------------------------------------------------------------------------- /src/tests/base.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { HopApi } from "@hop.ag/sdk"; 3 | import { 4 | normalizeStructTag, 5 | normalizeSuiAddress, 6 | SUI_TYPE_ARG, 7 | } from "@mysten/sui/utils"; 8 | 9 | import { getFullnodeUrl } from "@mysten/sui/client"; 10 | import { Transaction } from "@mysten/sui/transactions"; 11 | 12 | async function baseTest() { 13 | const api = new HopApi(getFullnodeUrl("mainnet"), { 14 | api_key: "", 15 | fee_bps: 1000, 16 | // hop_server_url: "http://localhost:3002/api/v2", 17 | }); 18 | 19 | const tx = new Transaction(); 20 | const coinInType = SUI_TYPE_ARG; 21 | const address = 22 | "0xc23ea8e493616b1510d9405ce05593f8bd1fb30f44f92303ab2c54f6c8680ecb"; 23 | const coinOutType = 24 | "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN"; 25 | // @ts-ignore 26 | const coinInAmount = 100_000_000n; 27 | const coinIn = tx.splitCoins(tx.gas, [tx.pure.u64(coinInAmount)]); 28 | 29 | await api.fetchQuote({ 30 | // @ts-ignore 31 | amount_in: 1_000_000_000n, 32 | token_in: "0x2::sui::SUI", 33 | token_out: 34 | "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN", 35 | }); 36 | 37 | // console.log(">> step 1 :: ", { 38 | // tx, 39 | // coinIn, 40 | // address, 41 | // coinInType, 42 | // coinOutType, 43 | // coinInAmount, 44 | // }); 45 | 46 | const { trade } = await api.fetchQuote({ 47 | amount_in: coinInAmount, 48 | token_in: normalizeStructTag(coinInType), 49 | token_out: normalizeStructTag(coinOutType), 50 | }); 51 | 52 | tx.setSender(address); 53 | 54 | // console.log(">> step 2 :: ", trade); 55 | 56 | const response = await api.fetchTx({ 57 | trade, 58 | sponsored: true, 59 | gas_budget: 1e8, 60 | base_transaction: tx, 61 | input_coin_argument: coinIn, 62 | return_output_coin_argument: true, 63 | sui_address: normalizeSuiAddress(address), 64 | }); 65 | 66 | console.log("result", JSON.stringify(response.transaction, null, 2)); 67 | } 68 | 69 | baseTest(); -------------------------------------------------------------------------------- /src/tests/price.test.ts: -------------------------------------------------------------------------------- 1 | import { HopApi } from "../index.js"; 2 | import { getFullnodeUrl } from "@mysten/sui/client"; 3 | 4 | // @ts-ignore 5 | async function priceTest() { 6 | const api = new HopApi(getFullnodeUrl("mainnet"), { 7 | api_key: "", 8 | fee_bps: 0, 9 | hop_server_url: "http://localhost:3002/api/v2" 10 | }); 11 | 12 | const result = await api.fetchPrice({ 13 | coin_type: "0x1c6cd615ed4c42a34977212a3407a28eec21acc572c8dbe7d0382bf0289a2590::plop::PLOP" 14 | }); 15 | 16 | console.log("result", result); 17 | } 18 | 19 | priceTest(); 20 | -------------------------------------------------------------------------------- /src/tests/quote.test.ts: -------------------------------------------------------------------------------- 1 | import { HopApi } from "../index.js"; 2 | import { getFullnodeUrl } from "@mysten/sui/client"; 3 | 4 | // @ts-ignore 5 | async function quoteTest() { 6 | const api = new HopApi(getFullnodeUrl("mainnet"), { 7 | api_key: "", 8 | fee_bps: 0, 9 | hop_server_url: "http://localhost:3002/api/v2", 10 | }); 11 | 12 | const result = await api.fetchQuote({ 13 | // @ts-ignore 14 | amount_in: 1_000_000_000n, 15 | token_in: "0x2::sui::SUI", 16 | token_out: "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN", 17 | }); 18 | 19 | console.log("result", result); 20 | } 21 | 22 | quoteTest(); 23 | -------------------------------------------------------------------------------- /src/tests/tokens.test.ts: -------------------------------------------------------------------------------- 1 | import { HopApi } from "../index.js"; 2 | import { getFullnodeUrl } from "@mysten/sui/client"; 3 | 4 | // @ts-ignore 5 | async function tokensTest() { 6 | const api = new HopApi(getFullnodeUrl("mainnet"), { 7 | api_key: "", 8 | fee_bps: 0 9 | }); 10 | 11 | const result = await api.fetchTokens(); 12 | 13 | console.log("result", result); 14 | } 15 | 16 | tokensTest(); 17 | -------------------------------------------------------------------------------- /src/tests/tx.test.ts: -------------------------------------------------------------------------------- 1 | import { HopApi } from "../index.js"; 2 | import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; 3 | 4 | // @ts-ignore 5 | async function txTest(): Promise { 6 | const sui_client = new SuiClient({ url: getFullnodeUrl('mainnet') }); 7 | const api = new HopApi(getFullnodeUrl("mainnet"), { 8 | api_key: "", 9 | fee_bps: 30, 10 | hop_server_url: "http://localhost:3002/api/v2", 11 | charge_fees_in_sui: false, 12 | }); 13 | 14 | const quote_result = await api.fetchQuote({ 15 | // @ts-ignore 16 | amount_in: 1e9, 17 | token_in: "0x2::sui::SUI", 18 | token_out: 19 | "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN", 20 | }); 21 | 22 | console.log("quote_result.trade.amount_in", quote_result.trade.quote); 23 | 24 | console.log("quote_result", quote_result); 25 | 26 | const tx_result = await api.fetchTx({ 27 | // @ts-ignore 28 | trade: quote_result.trade, 29 | sui_address: "0x4466fe25550f648a4acd6823a90e1f96c77e1d37257ee3ed2d6e02a694984f73", 30 | // return_output_coin_argument: true, 31 | gas_budget: 100000000, 32 | max_slippage_bps: 1000 33 | }); 34 | 35 | console.log("tx_result", tx_result); 36 | 37 | await sui_client.dryRunTransactionBlock({ transactionBlock: await tx_result.transaction.build({ client: sui_client }) }); 38 | // console.log("result", JSON.stringify(result, null, 2)); 39 | } 40 | 41 | txTest(); 42 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { GetQuoteParams, GetQuoteResponse } from "../sdk/routes/quote.js"; 2 | export type { VerifiedToken, GetTokensResponse } from "../sdk/routes/tokens.js"; 3 | export type { GetTxParams, GetTxResponse } from "../sdk/routes/tx.js"; 4 | export type { GetPriceParams, GetPriceResponse } from "../sdk/routes/price.js"; 5 | 6 | export * from "../sdk/types/trade.js"; 7 | 8 | export type { HopApiOptions } from "../sdk/api.js"; -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "target": "es2020", 7 | "skipLibCheck": false, 8 | "outDir": "./dist/cjs" 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["src/tests"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "target": "ES2022", 6 | "outDir": "./dist/esm", 7 | 8 | // build 9 | "types": ["node"], 10 | "esModuleInterop": true, 11 | "preserveConstEnums": true, 12 | "skipLibCheck": true, 13 | "importHelpers": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "allowJs": true, 17 | "checkJs": true, 18 | 19 | // linting 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noImplicitOverride": true, 24 | "noUncheckedIndexedAccess": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "noErrorTruncation": true, 28 | 29 | // sources 30 | "sourceMap": true, 31 | "declaration": true, 32 | "declarationMap": true, 33 | "inlineSources": true 34 | }, 35 | "include": ["src/**/*"], 36 | "exclude": ["src/tests"] 37 | } 38 | --------------------------------------------------------------------------------