diff --git a/packages/swap/src/providers/changelly/index.ts b/packages/swap/src/providers/changelly/index.ts index c928f50e0..79ec0b849 100644 --- a/packages/swap/src/providers/changelly/index.ts +++ b/packages/swap/src/providers/changelly/index.ts @@ -35,8 +35,39 @@ import supportedNetworks from "./supported"; import { ChangellyCurrency } from "./types"; import estimateEVMGasList from "../../common/estimateGasList"; +const DEBUG = false; + const BASE_URL = "https://partners.mewapi.io/changelly-v2"; +let debug: (context: string, message: string, ...args: any[]) => void; +if (DEBUG) { + debug = (context: string, message: string, ...args: any[]): void => { + const now = new Date(); + const ymdhms = + // eslint-disable-next-line prefer-template + now.getFullYear().toString().padStart(4, "0") + + "-" + + (now.getMonth() + 1).toString().padStart(2, "0") + + "-" + + now.getDate().toString().padStart(2, "0") + + " " + + now.getHours().toString().padStart(2, "0") + + ":" + + now.getMinutes().toString().padStart(2, "0") + + ":" + + now.getSeconds().toString().padStart(2, "0") + + "." + + now.getMilliseconds().toString().padStart(3, "0"); + console.info( + `\x1b[90m${ymdhms}\x1b[0m \x1b[32mChangellySwapProvider.${context}\x1b[0m: ${message}`, + ...args + ); + }; +} else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + debug = () => {}; +} + class Changelly extends ProviderClass { tokenList: TokenType[]; @@ -64,7 +95,14 @@ class Changelly extends ProviderClass { } async init(): Promise { - if (!Changelly.isSupported(this.network)) return; + debug("init", "Initialising..."); + if (!Changelly.isSupported(this.network)) { + debug( + "init", + `Enkrypt does not support Changelly on this network network=${this.network}` + ); + return; + } this.changellyList = await fetch(CHANGELLY_LIST).then((res) => res.json()); /** Mapping of changelly network name -> enkrypt network name */ @@ -117,6 +155,11 @@ class Changelly extends ProviderClass { cur.ticker ); }); + + debug( + "init", + `Finished initialising this.changellyList.length=${this.changellyList.length}` + ); } private setTicker( @@ -156,8 +199,16 @@ class Changelly extends ProviderClass { currency: ticker, address, }).then((response) => { - if (response.error || !response.result) return false; - return response.result[0].result; + if (response.error) { + debug( + "isValidAddress", + `Error in response when validating address` + + ` address=${address}` + + ` err=${String(response.error.message)}` + ); + return false; + } + return response.result?.[0]?.result ?? false; }); } @@ -177,13 +228,21 @@ class Changelly extends ProviderClass { fromToken: TokenType; toToken: TokenTypeTo; }): Promise { + const startedAt = Date.now(); + debug( + "getMinMaxAmount", + `Getting min and max of swap pair` + + ` fromToken=${fromToken.symbol}` + + ` toToken=${toToken.symbol}` + ); const emptyResponse = { minimumFrom: toBN("0"), maximumFrom: toBN("0"), minimumTo: toBN("0"), maximumTo: toBN("0"), }; - return this.changellyRequest("getFixRate", { + const method = "getFixRate"; + return this.changellyRequest(method, { from: this.getTicker(fromToken, this.network), to: this.getTicker( toToken as TokenType, @@ -193,43 +252,118 @@ class Changelly extends ProviderClass { .then((response) => { if (response.error) return emptyResponse; const result = response.result[0]; - return { + const minMax = { minimumFrom: toBN(toBase(result.minFrom, fromToken.decimals)), maximumFrom: toBN(toBase(result.maxFrom, fromToken.decimals)), minimumTo: toBN(toBase(result.minTo, toToken.decimals)), maximumTo: toBN(toBase(result.maxTo, toToken.decimals)), }; + debug( + "getMinMaxAmount", + `Successfully got min and max of swap pair` + + ` method=${method}` + + ` fromToken=${fromToken.symbol}` + + ` toToken=${toToken.symbol}` + + ` took=${(Date.now() - startedAt).toLocaleString()}ms` + ); + return minMax; }) - .catch(() => emptyResponse); + .catch((err: Error) => { + debug( + "getMinMaxAmount", + `Errored calling getFixRate to get the min and max of swap pair` + + ` method=${method}` + + ` fromToken=${fromToken.symbol}` + + ` toToken=${toToken.symbol}` + + ` took=${(Date.now() - startedAt).toLocaleString()}ms` + + ` err=${String(err)}` + ); + return emptyResponse; + }); } async getQuote( options: getQuoteOptions, meta: QuoteMetaOptions ): Promise { + const startedAt = Date.now(); + + debug( + "getQuote", + `Getting Changelly quote` + + ` srcToken=${options.fromToken.symbol}` + + ` dstToken=${options.toToken.symbol}` + + ` fromAddress=${options.fromAddress}` + + ` toAddress=${options.toAddress}` + + ` fromNetwork=${this.network}` + + ` toNetwork=${options.toToken.networkInfo.name}` + ); + if ( !Changelly.isSupported( options.toToken.networkInfo.name as SupportedNetworkName - ) || - !Changelly.isSupported(this.network) || - !this.getTicker(options.fromToken, this.network) || + ) + ) { + debug( + "getQuote", + `No swap: Enkrypt does not support Changelly on the destination network` + + ` dstNetwork=${options.toToken.networkInfo.name}` + ); + return null; + } + + if (!Changelly.isSupported(this.network)) { + debug( + "getQuote", + `No swap: Enkrypt does not support Changelly on the source network` + + ` srcNetwork=${this.network}` + ); + return null; + } + + if (!this.getTicker(options.fromToken, this.network)) { + debug( + "getQuote", + `No swap: Failed to find ticker for src token` + + ` srcToken=${options.fromToken.symbol}` + + ` srcNetwork=${this.network}` + ); + return null; + } + + if ( !this.getTicker( options.toToken as TokenType, options.toToken.networkInfo.name as SupportedNetworkName ) - ) - return Promise.resolve(null); + ) { + debug( + "getQuote", + `No swap: Failed to find ticker for dst token` + + ` dstToken=${options.toToken.symbol}` + + ` dstNetwork=${options.toToken.networkInfo.name}` + ); + return null; + } + const minMax = await this.getMinMaxAmount({ fromToken: options.fromToken, toToken: options.toToken, }); + let quoteRequestAmount = options.amount; + + // Clamp `quoteRequestAmount` if (quoteRequestAmount.lt(minMax.minimumFrom)) quoteRequestAmount = minMax.minimumFrom; else if (quoteRequestAmount.gt(minMax.maximumFrom)) quoteRequestAmount = minMax.maximumFrom; + if (quoteRequestAmount.toString() === "0") return null; - return this.changellyRequest("getFixRateForAmount", { + + const method = "getFixRateForAmount"; + debug("getQuote", `Requesting changelly swap... method=${method}`); + return this.changellyRequest(method, { from: this.getTicker(options.fromToken, this.network), to: this.getTicker( options.toToken as TokenType, @@ -241,8 +375,16 @@ class Changelly extends ProviderClass { ), }) .then(async (response) => { - if (response.error || !response.result || !response.result[0].id) + debug("getQuote", `Received Changelly swap response method=${method}`); + if (response.error || !response.result || !response.result[0].id) { + debug( + "getQuote", + `No swap: response either contains error, no result or no id` + + ` method=${method}` + + ` took=${(Date.now() - startedAt).toLocaleString()}ms` + ); return null; + } const result = response.result[0]; const evmGasLimit = options.fromToken.address === NATIVE_TOKEN_ADDRESS && @@ -274,19 +416,53 @@ class Changelly extends ProviderClass { options.fromToken.type === NetworkType.EVM ? evmGasLimit : 0, minMax, }; + debug( + "getQuote", + `Successfully processed Changelly swap response` + + ` took=${(Date.now() - startedAt).toLocaleString()}ms` + ); return retResponse; }) - .catch(() => null); + .catch((err) => { + debug( + "getQuote", + `Changelly request failed` + + ` method=${method}` + + ` took=${(Date.now() - startedAt).toLocaleString()}ms` + + ` err=${String(err)}` + ); + return null; + }); } getSwap(quote: SwapQuote): Promise { + const startedAt = Date.now(); + debug("getSwap", `Getting Changelly swap`); + + if (!Changelly.isSupported(this.network)) { + debug( + "getSwap", + `No swap: Enkrypt does not support Changelly on the source network` + + ` srcNetwork=${this.network}` + ); + return Promise.resolve(null); + } + if ( !Changelly.isSupported( quote.options.toToken.networkInfo.name as SupportedNetworkName - ) || - !Changelly.isSupported(this.network) - ) + ) + ) { + debug( + "getSwap", + `No swap: Enkrypt does not support Changelly on the destination network` + + ` dstNetwork=${quote.options.toToken.networkInfo.name}` + ); return Promise.resolve(null); + } + + const method = "createFixTransaction"; + debug("getSwap", `Requesting Changelly swap... method=${method}`); return this.changellyRequest("createFixTransaction", { from: this.getTicker(quote.options.fromToken, this.network), to: this.getTicker( @@ -302,7 +478,16 @@ class Changelly extends ProviderClass { rateId: quote.meta.changellyQuoteId, }) .then(async (response) => { - if (response.error || !response.result.id) return null; + debug("getSwap", `Received Changelly swap response method=${method}`); + if (response.error || !response.result.id) { + debug( + "getSwap", + `No swap: response either contains error or no id` + + ` method=${method}` + + ` took=${(Date.now() - startedAt).toLocaleString()}ms` + ); + return null; + } const { result } = response; let transaction: SwapTransaction; if (quote.options.fromToken.type === NetworkType.EVM) { @@ -360,9 +545,23 @@ class Changelly extends ProviderClass { provider: this.name, }), }; + debug( + "getSwap", + `Successfully processed Changelly swap response` + + ` took=${(Date.now() - startedAt).toLocaleString()}ms` + ); return retResponse; }) - .catch(() => null); + .catch((err) => { + debug( + "getSwap", + `Changelly request failed` + + ` method=${method}` + + ` took=${(Date.now() - startedAt).toLocaleString()}ms` + + ` err=${String(err)}` + ); + return null; + }); } getStatus(options: StatusOptions): Promise { diff --git a/packages/swap/src/providers/jupiter/index.ts b/packages/swap/src/providers/jupiter/index.ts index 3a0d124d2..1edfefb9b 100644 --- a/packages/swap/src/providers/jupiter/index.ts +++ b/packages/swap/src/providers/jupiter/index.ts @@ -158,12 +158,33 @@ const JUPITER_REFERRAL_ATA_ACCOUNT_SIZE_BYTES = 165; const SPL_TOKEN_ATA_ACCOUNT_SIZE_BYTES = 165; -let debug: (...args: any[]) => void; +let debug: (context: string, message: string, ...args: any[]) => void; if (DEBUG) { - console.debug.bind(console); + debug = (context: string, message: string, ...args: any[]): void => { + const now = new Date(); + const ymdhms = + // eslint-disable-next-line prefer-template + now.getFullYear().toString().padStart(4, "0") + + "-" + + (now.getMonth() + 1).toString().padStart(2, "0") + + "-" + + now.getDate().toString().padStart(2, "0") + + " " + + now.getHours().toString().padStart(2, "0") + + ":" + + now.getMinutes().toString().padStart(2, "0") + + ":" + + now.getSeconds().toString().padStart(2, "0") + + "." + + now.getMilliseconds().toString().padStart(3, "0"); + console.info( + `\x1b[90m${ymdhms}\x1b[0m \x1b[32mRangoSwapProvider.${context}\x1b[0m: ${message}`, + ...args + ); + }; } else { // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function - debug = (..._args: any[]) => {}; + debug = () => {}; } // Jupiter API Tokens @@ -505,7 +526,8 @@ export class Jupiter extends ProviderClass { if (referrerATAExists) { debug( - `[JupiterSwapProvider.querySwapInfo] Referrer ATA already exists. No need to record additional rent fees.` + + "querySwapInfo", + `Referrer ATA already exists. No need to record additional rent fees.` + ` ATA pubkey: ${referrerATAPubkey.toBase58()},` + ` Source mint: ${srcMint.toBase58()}` ); @@ -531,7 +553,8 @@ export class Jupiter extends ProviderClass { rentFees += extraRentFees; debug( - `[JupiterSwapProvider.querySwapInfo] Referrer ATA does not exist. Updating transaction with instruction to create it.` + + "querySwapInfo", + `Referrer ATA does not exist. Updating transaction with instruction to create it.` + ` Referral ATA pubkey: ${referrerATAPubkey.toBase58()},` + ` Rent: ${extraRentFees} lamports,` + ` Total Rent: ${extraRentFees} lamports` @@ -543,7 +566,8 @@ export class Jupiter extends ProviderClass { if (dstATAExists) { debug( - `[JupiterSwapProvider.querySwapInfo] Wallet destination mint ATA already exists. No need to record additional rent fees.` + + "querySwapInfo", + `Wallet destination mint ATA already exists. No need to record additional rent fees.` + ` ATA pubkey: ${dstATAPubkey.toBase58()},` + ` Destination mint: ${dstMint.toBase58()}` ); @@ -566,7 +590,8 @@ export class Jupiter extends ProviderClass { extraInstructions.push(instruction); debug( - `[JupiterSwapProvider.querySwapInfo] Wallet destination mint ATA does not exist, registering custom instruction to create it.` + + "querySwapInfo", + `Wallet destination mint ATA does not exist, registering custom instruction to create it.` + ` Adding ATA rent to extra transaction fees.` + ` ATA pubkey: ${dstATAPubkey.toBase58()},` + ` Destination mint: ${dstMint.toBase58()},` + @@ -603,7 +628,9 @@ export class Jupiter extends ProviderClass { ): Promise { if (options.toToken.networkInfo.name !== SupportedNetworkName.Solana) { debug( - `[JupiterSwapProvider.getQuote] ignoring quote request to network ${options.toToken.networkInfo.name}, cross network swaps not supported` + "getQuote", + `ignoring quote request to network ${options.toToken.networkInfo.name},` + + ` cross network swaps not supported` ); return null; } @@ -618,10 +645,12 @@ export class Jupiter extends ProviderClass { // 4. Rent for ATA accounts that may need to be created; the referral fee account and mint account debug( - `[JupiterSwapProvider.getQuote] Quote inAmount: ${jupiterQuote.inAmount} ${options.fromToken.symbol}` + "getQuote", + `Quote inAmount: ${jupiterQuote.inAmount} ${options.fromToken.symbol}` ); debug( - `[JupiterSwapProvider.getQuote] Quote outAmount: ${jupiterQuote.outAmount} ${options.toToken.symbol}` + "getQuote", + `Quote outAmount: ${jupiterQuote.outAmount} ${options.toToken.symbol}` ); const result: ProviderQuoteResponse = { @@ -666,10 +695,12 @@ export class Jupiter extends ProviderClass { }; debug( - `[JupiterSwapProvider.getSwap] Quote inAmount: ${jupiterQuote.inAmount} ${quote.options.fromToken.symbol}` + "getSwap", + `Quote inAmount: ${jupiterQuote.inAmount} ${quote.options.fromToken.symbol}` ); debug( - `[JupiterSwapProvider.getSwap] Quote outAmount: ${jupiterQuote.outAmount} ${quote.options.toToken.symbol}` + "getSwap", + `Quote outAmount: ${jupiterQuote.outAmount} ${quote.options.toToken.symbol}` ); const result: ProviderSwapResponse = { @@ -794,9 +825,7 @@ async function getJupiterTokens(context?: { if (backoff[backoffi] > 0) { // Previous request failed, wait before retrying - debug( - `[JupiterSwapProvider.getJupiterTokens] Retrying after ${backoff[backoffi]}ms...` - ); + debug("getJupiterTokens", `Retrying after ${backoff[backoffi]}ms...`); await new Promise((res, rej) => { function onTimeout() { cleanupSleep(); @@ -837,7 +866,8 @@ async function getJupiterTokens(context?: { try { debug( - `[JupiterSwapProvider.getJupiterTokens] Initiating HTTP request for Jupiter tokens ${url}` + "getJupiterTokens", + `Initiating HTTP request for Jupiter tokens ${url}` ); const res = await fetch(url, { signal: aborter.signal, @@ -848,7 +878,9 @@ async function getJupiterTokens(context?: { if (!res.ok) { let msg = await res .text() - .catch((err) => `Failed to decode response text: ${String(err)}`); + .catch( + (err: Error) => `Failed to decode response text: ${String(err)}` + ); const msglen = msg.length; if (msglen > 512 + 7 + 3 + msglen.toString().length) { msg = `${msg.slice(0, 512)}... (512/${msglen})`; @@ -884,9 +916,10 @@ async function getJupiterTokens(context?: { if (signal?.aborted) throw signal.reason; if (failed) throw err; debug( - `[JupiterSwapProvider.getJupiterTokens] Failed to get Jupiter tokens on attempt ${ - backoffi + 1 - }/${backoff.length}: ${String(err)}` + "getJupiterTokens", + `Failed to get Jupiter tokens on attempt ${backoffi + 1}/${ + backoff.length + }: ${String(err)}` ); errRef ??= { err: err as Error }; } finally { @@ -986,7 +1019,8 @@ async function getJupiterQuote( if (backoff[backoffi] > 0) { // Previous request failed, wait before retrying debug( - `[JupiterSwapProvider.getJupiterQuote] Retrying ${url} after ${backoff[backoffi]}ms...` + "getJupiterQuote", + `Retrying ${url} after ${backoff[backoffi]}ms...` ); await new Promise((res, rej) => { function onTimeout() { @@ -1028,7 +1062,8 @@ async function getJupiterQuote( try { debug( - `[JupiterSwapProvider.getJupiterQuote] Initiating HTTP request for Jupiter quote ${url}` + "getJupiterQuote", + `Initiating HTTP request for Jupiter quote ${url}` ); const res = await fetch(url, { signal: aborter.signal, @@ -1039,7 +1074,9 @@ async function getJupiterQuote( if (!res.ok) { let msg = await res .text() - .catch((err) => `Failed to decode response text: ${String(err)}`); + .catch( + (err: Error) => `Failed to decode response text: ${String(err)}` + ); const msglen = msg.length; if (msglen > 512 + 7 + 3 + msglen.toString().length) { msg = `${msg.slice(0, 512)}... (512/${msglen})`; @@ -1144,7 +1181,8 @@ async function getJupiterSwap( if (backoff[backoffi] > 0) { // Previous request failed, wait before retrying debug( - `[JupiterSwapProvider.getJupiterSwap] Retrying ${url} after ${backoff[backoffi]}ms...` + "getJupiterSwap", + `Retrying ${url} after ${backoff[backoffi]}ms...` ); await new Promise((res, rej) => { function onTimeout() { @@ -1186,7 +1224,8 @@ async function getJupiterSwap( try { debug( - `[JupiterSwapProvider.getJupiterSwap] Initiating HTTP request for Jupiter swap ${url}` + "getJupiterSwap", + `Initiating HTTP request for Jupiter swap ${url}` ); const res = await fetch(url, { signal: aborter.signal, @@ -1202,7 +1241,9 @@ async function getJupiterSwap( if (!res.ok) { let msg = await res .text() - .catch((err) => `Failed to decode response text: ${String(err)}`); + .catch( + (err: Error) => `Failed to decode response text: ${String(err)}` + ); const msglen = msg.length; if (msglen > 512 + 7 + 3 + msglen.toString().length) { msg = `${msg.slice(0, 512)}... (512/${msglen})`; @@ -1238,9 +1279,10 @@ async function getJupiterSwap( } catch (err) { if (failed) throw err; debug( - `[JupiterSwapProvider.getJupiterSwap] Failed to get Jupiter swap on attempt ${ - backoffi + 1 - }/${backoff.length}: ${String(err)}` + "getJupiterSwap", + `Failed to get Jupiter swap on attempt ${backoffi + 1}/${ + backoff.length + }: ${String(err)}` ); errRef ??= { err: err as Error }; } finally { @@ -1535,9 +1577,8 @@ async function insertInstructionsAtStartOfTransaction( `Failed to get address lookup table for ${lookup.accountKey}` ); debug( - `[JupiterSwapProvider.insertInstructionsAtStartOfTransaction] Fetching lookup account ${ - i + 1 - }. ${lookup.accountKey.toBase58()}` + "insertInstructionsAtStartOfTransaction", + `Fetching lookup account ${i + 1}. ${lookup.accountKey.toBase58()}` ); addressLookupTableAccounts[i] = addressLookupTableAccount; } @@ -1566,7 +1607,8 @@ async function insertInstructionsAtStartOfTransaction( default: { // insert our instruction here & continue debug( - `[JupiterSwapProvider.insertInstructionsAtStartOfTransaction] Inserting instruction to create an ATA account for Jupiter referrer with mint at instruction index ${i}` + "insertInstructionsAtStartOfTransaction", + `Inserting instruction to create an ATA account for Jupiter referrer with mint at instruction index ${i}` ); inserted = true; decompiledTransactionMessage.instructions.splice(i, 0, ...instructions); @@ -1578,7 +1620,8 @@ async function insertInstructionsAtStartOfTransaction( if (!inserted) { // If there were no compute budget instructions then just add it at the start debug( - `[JupiterSwapProvider.insertInstructionsAtStartOfTransaction] Inserting instruction to create an ATA account for Jupiter referrer with mint at start of instructions` + "insertInstructionsAtStartOfTransaction", + `Inserting instruction to create an ATA account for Jupiter referrer with mint at start of instructions` ); for (let len = instructions.length - 1, i = len - 1; i >= 0; i--) { decompiledTransactionMessage.instructions.unshift(instructions[i]); @@ -1586,9 +1629,7 @@ async function insertInstructionsAtStartOfTransaction( } // Switch to using this modified transaction - debug( - `[JupiterSwapProvider.insertInstructionsAtStartOfTransaction] Re-compiling transaction` - ); + debug("insertInstructionsAtStartOfTransaction", `Re-compiling transaction`); const modifiedTx = new VersionedTransaction( decompiledTransactionMessage.compileToV0Message(addressLookupTableAccounts) ); @@ -1607,9 +1648,7 @@ async function getTokenProgramOfMint( conn: Connection, mint: PublicKey ): Promise { - debug( - `[JupiterSwapProvider.getTokenProgramOfMint] Checking mint account of ${mint.toBase58()}` - ); + debug("getTokenProgramOfMint", `Checking mint account of ${mint.toBase58()}`); const srcMintAcc = await conn.getAccountInfo(mint); if (srcMintAcc == null) { diff --git a/packages/swap/src/providers/rango/index.ts b/packages/swap/src/providers/rango/index.ts index ae26e91ca..69cb67244 100644 --- a/packages/swap/src/providers/rango/index.ts +++ b/packages/swap/src/providers/rango/index.ts @@ -46,6 +46,37 @@ import { isEVMAddress } from "../../utils/common"; const RANGO_PUBLIC_API_KEY = "ee7da377-0ed8-4d42-aaf9-fa978a32b18d"; const rangoClient = new RangoClient(RANGO_PUBLIC_API_KEY); +const DEBUG = false; + +let debug: (context: string, message: string, ...args: any[]) => void; +if (DEBUG) { + debug = (context: string, message: string, ...args: any[]): void => { + const now = new Date(); + const ymdhms = + // eslint-disable-next-line prefer-template + now.getFullYear().toString().padStart(4, "0") + + "-" + + (now.getMonth() + 1).toString().padStart(2, "0") + + "-" + + now.getDate().toString().padStart(2, "0") + + " " + + now.getHours().toString().padStart(2, "0") + + ":" + + now.getMinutes().toString().padStart(2, "0") + + ":" + + now.getSeconds().toString().padStart(2, "0") + + "." + + now.getMilliseconds().toString().padStart(3, "0"); + console.info( + `\x1b[90m${ymdhms}\x1b[0m \x1b[32mRangoSwapProvider.${context}\x1b[0m: ${message}`, + ...args + ); + }; +} else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + debug = () => {}; +} + /** * `name` is the blockchain id on Rango * @@ -149,13 +180,21 @@ class Rango extends ProviderClass { } async init(tokenList?: TokenType[]): Promise { + debug("init", `Initialising against ${tokenList?.length} tokens...`); const resMeta = await rangoClient.meta({ excludeNonPopulars: true, transactionTypes: [RangoTransactionType.EVM, RangoTransactionType.SOLANA], }); this.rangoMeta = resMeta; + debug( + "init", + "Rango meta" + + ` tokens.length=${resMeta.tokens.length}` + + ` blockchains.length=${resMeta.blockchains.length}` + ); const { blockchains, tokens } = resMeta; if (!Rango.isSupported(this.network, blockchains)) { + debug("init", `Not supported on network ${this.network}`); return; } tokenList?.forEach((t) => { @@ -164,6 +203,7 @@ class Rango extends ProviderClass { this.fromTokens[t.address] = t; } }); + debug("init", `Finished initialising`); } static isSupported( @@ -206,11 +246,11 @@ class Rango extends ProviderClass { return true; } - getFromTokens() { + getFromTokens(): ProviderFromTokenResponse { return this.fromTokens; } - getToTokens() { + getToTokens(): ProviderToTokenResponse { const { tokens } = this.rangoMeta; const supportedCRangoNames = Object.values(supportedNetworks).map( (s) => s.name @@ -275,6 +315,18 @@ class Rango extends ProviderClass { accurateEstimate: boolean ): Promise { const { blockchains } = this.rangoMeta; + const startedAt = Date.now(); + + debug( + "getRangoSwap", + `Getting swap` + + ` srcToken=${options.fromToken.symbol}` + + ` dstToken=${options.toToken.symbol}` + + ` fromAddress=${options.fromAddress}` + + ` toAddress=${options.toAddress}` + + ` fromNetwork=${this.network}` + + ` toNetwork=${options.toToken.networkInfo.name}` + ); // Determine whether Enkrypt + Rango supports this swap @@ -285,8 +337,15 @@ class Rango extends ProviderClass { blockchains ) || !Rango.isSupported(this.network, blockchains) - ) + ) { + debug( + "getRangoSwap", + `No swap:` + + ` Enkrypt does not support Rango swap on the destination` + + ` network ${options.toToken.networkInfo.name}` + ); return Promise.resolve(null); + } // Does Rango support these tokens? const feeConfig = FEE_CONFIGS[this.name][meta.walletIdentifier]; @@ -329,6 +388,13 @@ class Rango extends ProviderClass { return false; })?.name; + debug( + "getRangoSwap", + `Rango block chains ids` + + ` fromBlokchain=${fromBlockchain}` + + ` toBlockchain=${toBlockchain}` + ); + const fromTokenAddress = options.fromToken.address; const toTokenAddress = options.toToken.address; @@ -339,10 +405,23 @@ class Rango extends ProviderClass { const toSymbol = this.getSymbol(options.toToken); // If we can't get symbols for the tokens then we don't support them - if (!fromSymbol || !toSymbol) return Promise.resolve(null); + if (!fromSymbol || !toSymbol) { + debug( + "getRangoSwap", + `No swap: No symbol for src token or dst token` + + ` srcTokenSymbol=${fromSymbol}` + + ` dstTokenSymbol=${toSymbol}` + ); + return Promise.resolve(null); + } // Enkrypt & Rango both likely support this swap (pair & networks) + const slippage = Number(meta.slippage || DEFAULT_SLIPPAGE); + if (!Number.isFinite(slippage)) { + throw new Error(`Slippage is not a number: ${slippage}`); + } + // Request a swap transaction from Rango for the pair & networks const params: SwapRequest = { from: { @@ -360,12 +439,13 @@ class Rango extends ProviderClass { amount: options.amount.toString(), fromAddress: options.fromAddress, toAddress: options.toAddress, - slippage: meta.slippage || DEFAULT_SLIPPAGE, + slippage, referrerFee: feeConfig ? (feeConfig.fee * 100).toFixed(3) : undefined, referrerAddress: feeConfig?.referrer || undefined, disableEstimate: true, }; + debug("getRangoSwap", `Requesting quote from rango sdk...`); const rangoSwapResponse = await rangoClient.swap(params); if ( @@ -373,10 +453,13 @@ class Rango extends ProviderClass { rangoSwapResponse.resultType !== RoutingResultType.OK ) { // Rango experienced some kind of error or is unable to route the swap - console.error(rangoSwapResponse.error); + debug("getRangoSwap", `Rango swap SDK returned an error`); + console.error("Rango swap SDK error:", rangoSwapResponse.error); return Promise.resolve(null); } + debug("getRangoSwap", `Rango swap SDK returned OK`); + // We have a swap transaction provided by Rango that can be executed // Note additional routing fees @@ -397,21 +480,27 @@ class Rango extends ProviderClass { switch (rangoSwapResponse.tx?.type) { // Process Rango swap Solana transaction case RangoTransactionType.SOLANA: { + debug("getRangoSwap", "Received Solana transaction"); + let versionedTransaction: VersionedTransaction; if (rangoSwapResponse.tx.serializedMessage == null) { - // TODO: when and why does this happen? - throw new Error( - `Rango did not return a serialized message for the Solana transaction` + // TODO: When how and why does this happen? + debug( + "getRangoSwap", + "Dropping rango swap transaction: Rango SDK returned a Solana transaction without any serializedMessage" ); + return null; } switch (rangoSwapResponse.tx.txType) { case "VERSIONED": { + debug("getRangoSwap", `Deserializing Solana versioned transaction`); versionedTransaction = VersionedTransaction.deserialize( new Uint8Array(rangoSwapResponse.tx.serializedMessage) ); break; } case "LEGACY": { + debug("getRangoSwap", `Deserializing Solana legacy transaction`); // TODO: does this work? versionedTransaction.version has type `'legacy' | 0` so maybe? versionedTransaction = VersionedTransaction.deserialize( new Uint8Array(rangoSwapResponse.tx.serializedMessage) @@ -444,9 +533,9 @@ class Rango extends ProviderClass { // Process Rango swap EVM transaction case RangoTransactionType.EVM: { - const transactions: EVMTransaction[] = []; + debug("getRangoSwap", `Received EVM transaction`); - // TODO: handle Solana transactions + const transactions: EVMTransaction[] = []; const tx = rangoSwapResponse.tx as RangoEvmTransaction; if (!this.isNativeToken(options.fromToken.address) && tx.approveTo) { // The user needss to approve Rango to swap tokens on their behalf @@ -511,6 +600,11 @@ class Rango extends ProviderClass { requestId: rangoSwapResponse.requestId, }; + debug( + "getRangoSwap", + `Done took=${(Date.now() - startedAt).toLocaleString()}ms` + ); + return result; }