UNPKG

@mytonswap/sdk

Version:

MyTonSwap Dex Aggregator SDK

538 lines (526 loc) 17.3 kB
// src/core/request.ts import axios from "axios"; import { defaultsDeep } from "lodash"; // src/constants/index.ts var defaultBaseUrl = process.env.BASE_URL ?? "https://app.mytonswap.com/api/"; var baseUrls = ["https://app.mytonswap.com/api/", "https://devtest.mytonswap.com/api/"]; // package.json var package_default = { name: "@mytonswap/sdk", description: "MyTonSwap Dex Aggregator SDK", author: { name: "MyTonSwap", url: "https://github.com/mytonswap" }, homepage: "https://docs.mytonswap.com", contributors: [ { email: "ho3einwave@gmail.com", name: "Ho3einWave" } ], repository: { type: "git", url: "git+https://github.com/MyTonSwap/sdk.git" }, main: "dist/index.cjs", module: "dist/index.js", types: "dist/index.d.ts", keywords: [ "MyTonSwap", "TON", "The Open Network", "TON Blockchain", "SDK", "Swap", "Telegram", "stonfi", "ston.fi", "dedust", "DEX", "DeFi" ], version: "1.1.2", type: "module", scripts: { build: "tsup" }, devDependencies: { "@types/bun": "^1.1.10", "@types/lodash": "^4.17.9", tsup: "^8.3.0" }, peerDependencies: { typescript: "^5.0.0" }, dependencies: { "@ton/ton": "^15.0.0", axios: "^1.7.7", "axios-retry": "^4.5.0", lodash: "^4.17.21" } }; // src/core/request.ts import axiosRetry from "axios-retry"; var Request = class { constructor(client) { this.client = client; axiosRetry(this.axiosInstance, this.attemptOptions); this.axiosInstance.interceptors.response.use((res) => { if (baseUrls.includes(res.config.baseURL ?? "")) { res.data = res.data.data; } return res; }); } axiosInstance = axios.create(); attemptOptions = { retries: 3, retryDelay: (retryCount) => { return retryCount * 1e3; } }; /** * send */ async send(userOptions) { const defaultOptions = { baseURL: this.client.options?.baseUrl ?? defaultBaseUrl, headers: userOptions.baseURL ? {} : { ...this.client.options?.apiKey && { "x-api-key": this.client.options.apiKey }, ...this.client.options?.headers ?? {}, "sdk-version": package_default.version }, method: "GET" }; const options = defaultsDeep(userOptions, defaultOptions, { headers: this.getDefaultHeaders }); const response = await this.faultTolerantRequest(options); this.handleErrors(response); return response.data; } handleErrors(responseBody) { if (!responseBody) return; if (responseBody.data.statusCode <= 200 && responseBody.data.statusCode >= 400) { throw new Error( `Request failed with ${responseBody.data.statusCode} Reason: ${responseBody.data.errorData}` ); } else { return; } } /** * faultTolerantRequest */ async faultTolerantRequest(options) { try { return await this.axiosInstance.request(options); } catch (err) { throw err; } } /** * getDefaultHeaders */ getDefaultHeaders() { return { stats_id: "SDK" }; } }; // src/core/services.ts var Services = class { client; constructor(client) { this.client = client; } }; // src/services/assets/assets.service.ts var Assets = class extends Services { /** * Retrieves an exact asset from the server. * * @param {string} asset - The token address you want. * @returns {Promise<Asset | null>} A promise that resolves to the token if found, or null if not found. */ async getExactAsset(token_address) { const assets = await this.client.request.send({ url: "/swap-process/data/assets/find/exactSearch", method: "POST", data: { assets: [token_address] } }); if (assets.length > 0) { return assets[0]; } else { return null; } } /** * Retrieves a list of assets from the server based on the provided asset addresses. * * @param {string[]} assetsAddress - An array of asset addresses to fetch. * @returns {Promise<Asset[]>} A promise that resolves to an array of assets. */ async getAssets(assetsAddress) { const listOfAssets = await this.client.request.send({ url: "/swap-process/data/assets", method: "POST", data: { assets: assetsAddress } }); return listOfAssets.list; } /** * Retrieves a paginated list of assets from the server. * * @param {number} [page=1] page - The page number to retrieve. Defaults to 1. * @param {boolean} [warning=false] warning - A boolean indicating whether to include warnings. Defaults to false. * @param {string} [phrase=''] phrase - An optional search phrase to filter the assets. * @returns {Promise<PaginatedAssets>} A promise that resolves to a paginated list of assets. */ async getPaginatedAssets(page = 1, warning = false, phrase) { const listOfAssets = await this.client.request.send({ url: `/swap-process/data/assets/find/${page}?warning=${warning}${phrase ? `&search=${phrase}` : ""}` }); return listOfAssets; } /** * Retrieves a paginated list of asset pairs from the server. * * @param {string} assetAddress - The address of the asset to fetch pairs for. * @param {number} [page=1] - The page number to retrieve. Defaults to 1. * @param {boolean} [warning=false] - A boolean indicating whether to include warnings. Defaults to false. * @param {string} [searchPhrase=''] - An optional search phrase to filter the asset pairs. * @returns {Promise<PaginatedAssets>} A promise that resolves to a paginated list of asset pairs. */ async getPairs(assetAddress, page = 1, warning = false, searchPhrase = "") { const listOfPairs = await this.client.request.send({ url: `https://app.mytonswap.com/api/swap-process/data/assets/pairs/${assetAddress}?page=${page}&warning=${warning}&search=${searchPhrase}` }); return listOfPairs; } }; // src/services/router/router.service.ts var Router = class extends Services { /** * Finds the best route for a given input and output asset address, pay amount, and optional slippage and DEX. * * @param {string} inputAssetAddress - The address of the input asset. * @param {string} outputAssetAddress - The address of the output asset. * @param {bigint} payAmount - The amount to be paid. * @param {number} [slippage] - Optional slippage percentage. * @param {Dex} [forceDex] - Optional DEX to force the route through. * @returns {Promise<BestRoute>} A promise that resolves to the best route. * * @todo Add validation for address and slippage. * @todo If the user doesn't input an address, get the address by asset service. */ async findBestRoute(inputAssetAddress, outputAssetAddress, payAmount, slippage, forceDex) { const body = { token0: inputAssetAddress, token1: outputAssetAddress, amount: payAmount.toString(), slippage: slippage ?? "auto", token0_symbol: "SDK", token1_symbol: "SDK", init: true, dex: forceDex }; const data = this.client.request.send({ url: "v2/routes/pair", method: "POST", data: body }); return data; } }; // src/core/client.ts import { TonClient } from "@ton/ton"; // src/services/tonapi/tonapi.service.ts import { address } from "@ton/ton"; var TonApi = class extends Services { /** * Fetches the jetton data for a given wallet address and jetton address. * * @param {string} walletAddr - The wallet address to fetch the jetton data for. * @param {string} jettonAddress - The jetton address to fetch the data from. * @returns {Promise<Balance>} A promise that resolves to the balance data. */ async getJettonData(walletAddr, jettonAddress) { const data = await this.client.request.send({ baseURL: "https://tonapi.io/v2", maxBodyLength: Infinity, url: `/accounts/${walletAddr}/jettons/${jettonAddress}?supported_extensions=custom_payload` }); return data; } /** * Retrieves a custom payload for a specific wallet and jetton address. * * @param {string} walletAddr - The address of the wallet. * @param {string} jettonAddress - The address of the jetton. * @returns {Promise<CustomPayload>} A promise that resolves to the custom payload. */ async getCustomPayload(walletAddr, jettonAddress) { const data = await this.client.request.send({ baseURL: "https://tonapi.io/v2", maxBodyLength: Infinity, url: `/jettons/${jettonAddress}/transfer/${walletAddr}/payload` }); return data; } /** * Retrieves wallet assets for a given wallet address, including balances and rates for jettons. * * @param {string} walletAddress - The address of the wallet to retrieve assets for. * @param {string[]} [currencies=['usd']] - An array of currency codes to retrieve rates for. * @param {boolean} [custom_payload=true] - Whether to include custom payload in the request. * @returns {Promise<Map<string, Balance>>} A promise that resolves to a map of balances keyed by jetton addresses. */ async getWalletAssets(walletAddress, currencies = ["usd"], custom_payload = true) { const { balances } = await this.client.request.send({ baseURL: "https://tonapi.io/v2", maxBodyLength: Infinity, url: `/accounts/${walletAddress}/jettons?currencies=${currencies.join(",")}${custom_payload ? "&supported_extensions=custom_payload" : ""}` }); const addresses = balances.map((item) => item.jetton.address).join(","); const { rates } = await this.client.request.send({ baseURL: "https://tonapi.io/v2", url: `/rates?tokens=${addresses.length > 0 ? addresses : ""},EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c&currencies=usd` }); const { balance: tonBalance } = await this.client.request.send({ baseURL: "https://tonapi.io/v2", url: `/accounts/${walletAddress}` }); balances.push({ balance: String(tonBalance), wallet_address: { address: walletAddress, is_scam: false, is_wallet: true }, jetton: { address: "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c", symbol: "TON", name: "TON", image: "https://asset.ston.fi/img/EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c/4ecd4687e0b5b8ff21a7fbe03f9d281c26a2dc13eac7b7d16048cc693fe0ec39", decimals: 9, verification: "" } }); const newBalances = balances.reduce((map, item) => { const userFriendlyAddr = address(item.jetton.address).toString(); item.price = rates[userFriendlyAddr]; map.set(userFriendlyAddr, item); return map; }, /* @__PURE__ */ new Map()); return newBalances; } /** * Fetches the rates of specified assets from the TON API. * * @param {string[]} assetsAddresses - An array of asset addresses to fetch rates for. * @returns {Promise<Map<string, Prices>>} A promise that resolves to a map where the keys are user-friendly asset addresses and the values are their corresponding prices. */ async getAssetsRates(assetsAddresses) { const addresses = assetsAddresses.join(","); const { rates } = await this.client.request.send({ baseURL: "https://tonapi.io/v2", url: `/rates?tokens=${addresses.length > 0 ? addresses : ""},EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c&currencies=usd` }); const ratesMap = assetsAddresses.reduce((map, item) => { const userFriendlyAddr = address(item).toString(); map.set(userFriendlyAddr, rates[userFriendlyAddr].prices); return map; }, /* @__PURE__ */ new Map()); return ratesMap; } /** * waitForTransactionResult */ /** * Waits for a transaction result by periodically checking the transaction status. * * @param {string} hash - The hash of the transaction to wait for. * @param {number} [period_ms=3000] - The period in milliseconds to wait between checks. * @param {number} [maxRetry=30] - The maximum number of retries before giving up. * @returns {Promise<TransactionEvent>} - A promise that resolves with the transaction result when complete. * @throws {Error} - Throws an error if the maximum number of retries is reached. */ async waitForTransactionResult(hash, period_ms = 3e3, maxRetry = 30) { let retries = 0; while (retries <= maxRetry) { try { let result = await this.client.tonapi.getTransactionEvent(hash); while (this.allTransactionComplete(result) === "inprogress") { await new Promise((resolve) => setTimeout(resolve, period_ms)); result = await this.client.tonapi.getTransactionEvent(hash); retries++; if (retries > maxRetry) { throw new Error("Max retries reached"); } } return result; } catch (error) { await new Promise((resolve) => setTimeout(resolve, period_ms)); retries++; if (retries > maxRetry) { throw new Error("Max retries reached"); } } } throw new Error("Max retries reached"); } /** * Fetches a transaction event from the TON API using the provided hash. * * @param {string} hash - The hash of the transaction event to retrieve. * @returns {Promise<TransactionEvent>} A promise that resolves to the transaction event. */ async getTransactionEvent(hash) { const event = await this.client.request.send({ baseURL: "https://tonapi.io/v2", url: `/events/${hash}` }); return event; } /** * Checks if all transactions in the given event are complete. * * @param {TransactionEvent} event - The transaction event to check. * @returns {string} - Returns `ok` if all transactions are complete and successful, otherwise `failed` or `inprogress`. * @throws {Error} - Throws an error if any transaction action has a status other than 'ok'. */ allTransactionComplete(event) { if (event.in_progress) return "inprogress"; if (event.actions.some((item) => item.status !== "ok")) return "failed"; return "ok"; } }; // src/services/swap/swap.service.ts var Swap = class extends Services { /** * Creates a swap request using the provided user wallet address and best route. * * @param {string} userWalletAddress - The address of the user's wallet. * @param {BestRoute} bestRoute - The best route from router. * @param {string} [app_id] - Optional application ID to include in the request headers. * @returns {Promise<SwapResponse>} A promise that resolves to the swap response. */ async createSwap(userWalletAddress, bestRoute, app_id) { return await this.client.request.send({ method: "POST", headers: { ...app_id ? { "app-id": app_id } : {} }, url: "v2/routes/boc", data: { userWallet: userWalletAddress, bestRoute } }); } }; // src/core/client.ts var MyTonSwapClient = class { options; request = new Request(this); assets = new Assets(this); router = new Router(this); tonapi = new TonApi(this); swap = new Swap(this); tonClient; constructor(options) { this.options = options; this.tonClient = new TonClient({ endpoint: "https://toncenter.com/api/v2/jsonRPC", apiKey: options?.tonCenterApiKey }); } }; // src/utils/convert.ts function toNano(src, decimals = 9) { if (typeof src === "bigint") { return src * 10n ** BigInt(decimals); } else { if (typeof src === "number") { if (!Number.isFinite(src)) { throw Error("Invalid number"); } if (parseInt(Math.log10(src).toString()) <= 6) { src = src.toLocaleString("en", { minimumFractionDigits: decimals, useGrouping: false }); } else if (src - Math.trunc(src) === 0) { src = src.toLocaleString("en", { maximumFractionDigits: 0, useGrouping: false }); } else { throw Error("Not enough precision for a number value. Use string value instead"); } } let neg = false; while (src.startsWith("-")) { neg = !neg; src = src.slice(1); } if (src === ".") { throw Error("Invalid number"); } let parts = src.split("."); if (parts.length > 2) { throw Error("Invalid number"); } let whole = parts[0]; let frac = parts[1]; if (!whole) { whole = "0"; } if (!frac) { frac = "0"; } if (frac.length > decimals) { throw Error("Invalid number"); } while (frac.length < decimals) { frac += "0"; } let r = BigInt(whole) * 10n ** BigInt(decimals) + BigInt(frac); if (neg) { r = -r; } return r; } } function fromNano(src, decimals = 9) { let v = BigInt(src); let neg = false; if (v < 0) { neg = true; v = -v; } let frac = v % 10n ** BigInt(decimals); let facStr = frac.toString(); while (facStr.length < decimals) { facStr = "0" + facStr; } facStr = facStr.match(/^([0-9]*[1-9]|0)(0*)/)[1]; let whole = v / 10n ** BigInt(decimals); let wholeStr = whole.toString(); let value = `${wholeStr}${facStr === "0" ? "" : `.${facStr}`}`; if (neg) { value = "-" + value; } return value; } export { MyTonSwapClient, fromNano, toNano };