From c2c35e550ca2e37045109539d1ca4e4d7f1d88cf Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:15:27 -0700 Subject: [PATCH 01/18] fix: kadena balance issue --- packages/extension/src/libs/keyring/public-keyring.ts | 11 +++++++---- packages/extension/src/providers/kadena/libs/api.ts | 11 +++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/extension/src/libs/keyring/public-keyring.ts b/packages/extension/src/libs/keyring/public-keyring.ts index 210b1d518..043d51530 100644 --- a/packages/extension/src/libs/keyring/public-keyring.ts +++ b/packages/extension/src/libs/keyring/public-keyring.ts @@ -87,13 +87,16 @@ class PublicKeyRing { walletType: WalletType.mnemonic, isHardware: false, }; - allKeys["tQvduDby4rvC6VU4rSirhVWuRYxbJz3rvUrVMkUWsZP"] = { - address: "tQvduDby4rvC6VU4rSirhVWuRYxbJz3rvUrVMkUWsZP", + allKeys[ + "0xf3d9631c023472a6875f35702d6fa344afaba96ea938e2b382f138680c033f82" + ] = { + address: + "0xf3d9631c023472a6875f35702d6fa344afaba96ea938e2b382f138680c033f82", basePath: "m/44'/501'/0'/1", - name: "fake sol acc 2", + name: "fake kda acc 2", pathIndex: 0, publicKey: "0x0", - signerType: SignerType.ed25519sol, + signerType: SignerType.ed25519kda, walletType: WalletType.mnemonic, isHardware: false, }; diff --git a/packages/extension/src/providers/kadena/libs/api.ts b/packages/extension/src/providers/kadena/libs/api.ts index 4b0a2cee8..4e8d6748e 100644 --- a/packages/extension/src/providers/kadena/libs/api.ts +++ b/packages/extension/src/providers/kadena/libs/api.ts @@ -68,7 +68,6 @@ class API implements ProviderAPIInterface { this.displayAddress(address), chainId ); - if (balance.result.status === "failure") { const error = balance.result.error as { message: string | undefined }; const message = error.message ?? "Unknown error retrieving balances"; @@ -78,11 +77,11 @@ class API implements ProviderAPIInterface { } throw new Error(message); } - - const balanceValue = parseFloat(balance.result.data.toString()).toFixed( - this.decimals - ); - + const balanceValue = parseFloat( + balance.result.data.decimal + ? balance.result.data.decimal + : balance.result.data.toString() + ).toString(); return toBase(balanceValue, this.decimals); } From 26412bc4355be6fe4182343c559eb788c1c716ee Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:15:43 -0700 Subject: [PATCH 02/18] fix: kadena balance issue --- packages/extension/src/libs/keyring/public-keyring.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/extension/src/libs/keyring/public-keyring.ts b/packages/extension/src/libs/keyring/public-keyring.ts index 043d51530..210b1d518 100644 --- a/packages/extension/src/libs/keyring/public-keyring.ts +++ b/packages/extension/src/libs/keyring/public-keyring.ts @@ -87,16 +87,13 @@ class PublicKeyRing { walletType: WalletType.mnemonic, isHardware: false, }; - allKeys[ - "0xf3d9631c023472a6875f35702d6fa344afaba96ea938e2b382f138680c033f82" - ] = { - address: - "0xf3d9631c023472a6875f35702d6fa344afaba96ea938e2b382f138680c033f82", + allKeys["tQvduDby4rvC6VU4rSirhVWuRYxbJz3rvUrVMkUWsZP"] = { + address: "tQvduDby4rvC6VU4rSirhVWuRYxbJz3rvUrVMkUWsZP", basePath: "m/44'/501'/0'/1", - name: "fake kda acc 2", + name: "fake sol acc 2", pathIndex: 0, publicKey: "0x0", - signerType: SignerType.ed25519kda, + signerType: SignerType.ed25519sol, walletType: WalletType.mnemonic, isHardware: false, }; From dc62c7e520dc9c2e47910bbcfbdbb904cd054533 Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:44:57 -0700 Subject: [PATCH 03/18] fix: no remote code on mv3 --- ...capture-browser-npm-1.0.3-edb25bef55.patch | 26 +++++++++++++++++++ package.json | 3 ++- yarn.lock | 14 +++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 .yarn/patches/@amplitude-plugin-autocapture-browser-npm-1.0.3-edb25bef55.patch diff --git a/.yarn/patches/@amplitude-plugin-autocapture-browser-npm-1.0.3-edb25bef55.patch b/.yarn/patches/@amplitude-plugin-autocapture-browser-npm-1.0.3-edb25bef55.patch new file mode 100644 index 000000000..145a91021 --- /dev/null +++ b/.yarn/patches/@amplitude-plugin-autocapture-browser-npm-1.0.3-edb25bef55.patch @@ -0,0 +1,26 @@ +diff --git a/lib/cjs/constants.js b/lib/cjs/constants.js +index 5772226ef895bff7a6f7fa291fc1138b10049dab..c65a57bf3535e6c6d4d894657da9b571d7f40d8a 100644 +--- a/lib/cjs/constants.js ++++ b/lib/cjs/constants.js +@@ -29,7 +29,7 @@ exports.AMPLITUDE_ORIGINS_MAP = { + EU: exports.AMPLITUDE_ORIGIN_EU, + STAGING: exports.AMPLITUDE_ORIGIN_STAGING, + }; +-exports.AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL = 'https://cdn.amplitude.com/libs/visual-tagging-selector-1.0.0-alpha.js.gz'; ++exports.AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL = ''; + // This is the class name used by the visual tagging selector to highlight the selected element. + // Should not use this class in the selector. + exports.AMPLITUDE_VISUAL_TAGGING_HIGHLIGHT_CLASS = 'amp-visual-tagging-selector-highlight'; +diff --git a/lib/esm/constants.js b/lib/esm/constants.js +index 17e665c05e62e488032673d312a69432297cb6fd..7d75951a29fad79ba49c83c076785294166dd0bf 100644 +--- a/lib/esm/constants.js ++++ b/lib/esm/constants.js +@@ -27,7 +27,7 @@ export var AMPLITUDE_ORIGINS_MAP = { + EU: AMPLITUDE_ORIGIN_EU, + STAGING: AMPLITUDE_ORIGIN_STAGING, + }; +-export var AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL = 'https://cdn.amplitude.com/libs/visual-tagging-selector-1.0.0-alpha.js.gz'; ++export var AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL = ''; + // This is the class name used by the visual tagging selector to highlight the selected element. + // Should not use this class in the selector. + export var AMPLITUDE_VISUAL_TAGGING_HIGHLIGHT_CLASS = 'amp-visual-tagging-selector-highlight'; diff --git a/package.json b/package.json index 72efb5069..4f44c10d0 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "resolutions": { "@ledgerhq/compressjs": "https://registry.yarnpkg.com/@favware/skip-dependency/-/skip-dependency-1.2.1.tgz", "@noble/hashes": "^1.4.0", - "fork-ts-checker-webpack-plugin": "^6.5.3" + "fork-ts-checker-webpack-plugin": "^6.5.3", + "@amplitude/plugin-autocapture-browser@^1.0.2": "patch:@amplitude/plugin-autocapture-browser@npm%3A1.0.3#./.yarn/patches/@amplitude-plugin-autocapture-browser-npm-1.0.3-edb25bef55.patch" } } diff --git a/yarn.lock b/yarn.lock index 4dd69d810..ec0fec3a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,7 +122,7 @@ __metadata: languageName: node linkType: hard -"@amplitude/plugin-autocapture-browser@npm:^1.0.2": +"@amplitude/plugin-autocapture-browser@npm:1.0.3": version: 1.0.3 resolution: "@amplitude/plugin-autocapture-browser@npm:1.0.3" dependencies: @@ -134,6 +134,18 @@ __metadata: languageName: node linkType: hard +"@amplitude/plugin-autocapture-browser@patch:@amplitude/plugin-autocapture-browser@npm%3A1.0.3#./.yarn/patches/@amplitude-plugin-autocapture-browser-npm-1.0.3-edb25bef55.patch::locator=enkrypt%40workspace%3A.": + version: 1.0.3 + resolution: "@amplitude/plugin-autocapture-browser@patch:@amplitude/plugin-autocapture-browser@npm%3A1.0.3#./.yarn/patches/@amplitude-plugin-autocapture-browser-npm-1.0.3-edb25bef55.patch::version=1.0.3&hash=c16bd2&locator=enkrypt%40workspace%3A." + dependencies: + "@amplitude/analytics-client-common": ">=1 <3" + "@amplitude/analytics-types": ^2.8.2 + rxjs: ^7.8.1 + tslib: ^2.4.1 + checksum: 2548ff0f76b2565bc5a34feb154788a945e625a07ab9c5c0561dc3b296e4dbbc32120b4cd9306ef2cce8b342c5c5cb0e8bc3e236ab95e4476cf0e05b7323a092 + languageName: node + linkType: hard + "@amplitude/plugin-page-view-tracking-browser@npm:^2.3.3": version: 2.3.3 resolution: "@amplitude/plugin-page-view-tracking-browser@npm:2.3.3" From 02fed3da0b359346d72665285b643c3fd5282b05 Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:45:59 -0700 Subject: [PATCH 04/18] devop: change description --- packages/extension/src/manifest/base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/src/manifest/base.json b/packages/extension/src/manifest/base.json index 0b20aafa8..fbf1509a4 100644 --- a/packages/extension/src/manifest/base.json +++ b/packages/extension/src/manifest/base.json @@ -41,7 +41,7 @@ "run_at": "document_start" } ], - "description": "Everything in the blockchain made easy", + "description": "The best multichain crypto wallet", "icons": { "16": "assets/img/icons/icon16.png", "32": "assets/img/icons/icon32.png", From 47a0961eb29b580e99b191bfd59ee4d7bf76d998 Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:22:55 -0700 Subject: [PATCH 05/18] devop: set charts to 24h --- packages/extension/package.json | 2 +- packages/extension/src/libs/market-data/ethvm.ts | 2 +- packages/extension/src/libs/market-data/types.ts | 4 ++-- .../src/providers/bitcoin/types/bitcoin-network.ts | 4 ++-- .../ethereum/libs/assets-handlers/assetinfo-mew.ts | 9 +++++---- .../src/providers/ethereum/networks/skale/skale-base.ts | 8 +++++--- .../src/providers/ethereum/types/evm-network.ts | 9 +++++---- .../src/providers/kadena/types/kadena-network.ts | 4 ++-- .../src/providers/polkadot/types/substrate-network.ts | 4 ++-- .../extension/src/providers/solana/types/sol-network.ts | 4 ++-- .../views/network-assets/components/custom-evm-token.vue | 8 ++++---- 11 files changed, 31 insertions(+), 27 deletions(-) diff --git a/packages/extension/package.json b/packages/extension/package.json index 362297d63..1449cffd4 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@enkryptcom/extension", - "version": "1.44.0", + "version": "1.45.0", "private": true, "scripts": { "zip": "cd dist; zip -r release.zip *;", diff --git a/packages/extension/src/libs/market-data/ethvm.ts b/packages/extension/src/libs/market-data/ethvm.ts index c81c335ae..eef9eddf8 100644 --- a/packages/extension/src/libs/market-data/ethvm.ts +++ b/packages/extension/src/libs/market-data/ethvm.ts @@ -77,7 +77,7 @@ export const getMarketInfoByIDs = ( return ethvmPost( '{"operationName":null,"variables":{},"query":"{\\n getCoinGeckoTokenMarketDataByIds(coinGeckoTokenIds: [' + params + - ']) {\\n id\\n symbol\\n name\\n image\\n market_cap\\n market_cap_rank\\n high_24h\\n low_24h\\n price_change_24h\\n price_change_percentage_24h\\n sparkline_in_7d {\\n price\\n }\\n price_change_percentage_7d_in_currency\\n current_price\\n }\\n}\\n"}' + ']) {\\n id\\n symbol\\n name\\n image\\n market_cap\\n market_cap_rank\\n high_24h\\n low_24h\\n price_change_24h\\n price_change_percentage_24h\\n sparkline_in_24h {\\n price\\n }\\n price_change_percentage_24h_in_currency\\n current_price\\n }\\n}\\n"}' ).then((json) => { return json.data.getCoinGeckoTokenMarketDataByIds as CoinGeckoTokenMarket[]; }); diff --git a/packages/extension/src/libs/market-data/types.ts b/packages/extension/src/libs/market-data/types.ts index 7e379dde3..0a74819d6 100644 --- a/packages/extension/src/libs/market-data/types.ts +++ b/packages/extension/src/libs/market-data/types.ts @@ -32,8 +32,8 @@ export interface CoinGeckoTokenMarket { low_24h: number; price_change_24h: number; price_change_percentage_24h: number; - sparkline_in_7d: { price: number[] }; - price_change_percentage_7d_in_currency: number; + sparkline_in_24h: { price: number[] }; + price_change_percentage_24h_in_currency: number; } export interface FiatMarket { fiat_currency: string; diff --git a/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts b/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts index 10268d9c6..cf9fc6db6 100644 --- a/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts +++ b/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts @@ -141,10 +141,10 @@ export class BitcoinNetwork extends BaseNetwork { contract: "", decimals: this.decimals, sparkline: marketData.length - ? new Sparkline(marketData[0]!.sparkline_in_7d.price, 25).dataValues + ? new Sparkline(marketData[0]!.sparkline_in_24h.price, 25).dataValues : "", priceChangePercentage: marketData.length - ? marketData[0]!.price_change_percentage_7d_in_currency + ? marketData[0]!.price_change_percentage_24h_in_currency : 0, }; return [nativeAsset]; diff --git a/packages/extension/src/providers/ethereum/libs/assets-handlers/assetinfo-mew.ts b/packages/extension/src/providers/ethereum/libs/assets-handlers/assetinfo-mew.ts index be356a4ed..def357a2e 100644 --- a/packages/extension/src/providers/ethereum/libs/assets-handlers/assetinfo-mew.ts +++ b/packages/extension/src/providers/ethereum/libs/assets-handlers/assetinfo-mew.ts @@ -263,8 +263,8 @@ export default ( low_24h: 0, price_change_24h: 0, price_change_percentage_24h: 0, - sparkline_in_7d: { price: [] }, - price_change_percentage_7d_in_currency: 0, + sparkline_in_24h: { price: [] }, + price_change_percentage_24h_in_currency: 0, }; } @@ -303,9 +303,10 @@ export default ( valuef: formatFiatValue(currentPrice.toString()).value, contract: address, decimals: tokenInfo[address].decimals, - sparkline: new Sparkline(market.sparkline_in_7d.price, 25).dataValues, + sparkline: new Sparkline(market.sparkline_in_24h.price, 25) + .dataValues, priceChangePercentage: - market.price_change_percentage_7d_in_currency || 0, + market.price_change_percentage_24h_in_currency || 0, }; if (address !== NATIVE_TOKEN_ADDRESS) assets.push(asset); else nativeAsset = asset; diff --git a/packages/extension/src/providers/ethereum/networks/skale/skale-base.ts b/packages/extension/src/providers/ethereum/networks/skale/skale-base.ts index 0ec71f4d4..6685a8675 100644 --- a/packages/extension/src/providers/ethereum/networks/skale/skale-base.ts +++ b/packages/extension/src/providers/ethereum/networks/skale/skale-base.ts @@ -160,11 +160,13 @@ async function getPreconfiguredTokens( ).value, decimals: assetDecimals, sparkline: nativeAssetMarketData[index] - ? new Sparkline(nativeAssetMarketData[index]?.sparkline_in_7d.price, 25) - .dataValues + ? new Sparkline( + nativeAssetMarketData[index]?.sparkline_in_24h.price, + 25 + ).dataValues : "", priceChangePercentage: - nativeAssetMarketData[index]?.price_change_percentage_7d_in_currency ?? + nativeAssetMarketData[index]?.price_change_percentage_24h_in_currency ?? 0, contract: asset.address, }; diff --git a/packages/extension/src/providers/ethereum/types/evm-network.ts b/packages/extension/src/providers/ethereum/types/evm-network.ts index 5b953a00a..697994ebc 100644 --- a/packages/extension/src/providers/ethereum/types/evm-network.ts +++ b/packages/extension/src/providers/ethereum/types/evm-network.ts @@ -156,10 +156,11 @@ export class EvmNetwork extends BaseNetwork { ).value, decimals: this.decimals, sparkline: nativeMarketData - ? new Sparkline(nativeMarketData.sparkline_in_7d.price, 25).dataValues + ? new Sparkline(nativeMarketData.sparkline_in_24h.price, 25) + .dataValues : "", priceChangePercentage: - nativeMarketData?.price_change_percentage_7d_in_currency ?? 0, + nativeMarketData?.price_change_percentage_24h_in_currency ?? 0, contract: NATIVE_TOKEN_ADDRESS, }; @@ -273,11 +274,11 @@ export class EvmNetwork extends BaseNetwork { marketInfo.current_price?.toString() ?? "0" ).value; asset.sparkline = new Sparkline( - marketInfo.sparkline_in_7d.price, + marketInfo.sparkline_in_24h.price, 25 ).dataValues; asset.priceChangePercentage = - marketInfo.price_change_percentage_7d_in_currency || 0; + marketInfo.price_change_percentage_24h_in_currency || 0; } return asset; diff --git a/packages/extension/src/providers/kadena/types/kadena-network.ts b/packages/extension/src/providers/kadena/types/kadena-network.ts index fc8107578..8deb67879 100644 --- a/packages/extension/src/providers/kadena/types/kadena-network.ts +++ b/packages/extension/src/providers/kadena/types/kadena-network.ts @@ -125,10 +125,10 @@ export class KadenaNetwork extends BaseNetwork { contract: "", decimals: this.decimals, sparkline: marketData.length - ? new Sparkline(marketData[0]!.sparkline_in_7d.price, 25).dataValues + ? new Sparkline(marketData[0]!.sparkline_in_24h.price, 25).dataValues : "", priceChangePercentage: marketData.length - ? marketData[0]!.price_change_percentage_7d_in_currency + ? marketData[0]!.price_change_percentage_24h_in_currency : 0, }; diff --git a/packages/extension/src/providers/polkadot/types/substrate-network.ts b/packages/extension/src/providers/polkadot/types/substrate-network.ts index 265c54ba5..710194c51 100644 --- a/packages/extension/src/providers/polkadot/types/substrate-network.ts +++ b/packages/extension/src/providers/polkadot/types/substrate-network.ts @@ -166,9 +166,9 @@ export class SubstrateNetwork extends BaseNetwork { name: st.name, symbol: st.symbol, priceChangePercentage: - market[idx]?.price_change_percentage_7d_in_currency || 0, + market[idx]?.price_change_percentage_24h_in_currency || 0, sparkline: market[idx] - ? new Sparkline(market[idx]?.sparkline_in_7d.price, 25).dataValues + ? new Sparkline(market[idx]?.sparkline_in_24h.price, 25).dataValues : "", value: market[idx]?.current_price?.toString() || "0", valuef: formatFloatingPointValue( diff --git a/packages/extension/src/providers/solana/types/sol-network.ts b/packages/extension/src/providers/solana/types/sol-network.ts index 6c4300f5f..748533fed 100644 --- a/packages/extension/src/providers/solana/types/sol-network.ts +++ b/packages/extension/src/providers/solana/types/sol-network.ts @@ -136,10 +136,10 @@ export class SolanaNetwork extends BaseNetwork { contract: "", decimals: this.decimals, sparkline: marketData.length - ? new Sparkline(marketData[0]!.sparkline_in_7d.price, 25).dataValues + ? new Sparkline(marketData[0]!.sparkline_in_24h.price, 25).dataValues : "", priceChangePercentage: marketData.length - ? marketData[0]!.price_change_percentage_7d_in_currency + ? marketData[0]!.price_change_percentage_24h_in_currency : 0, }; return [nativeAsset]; diff --git a/packages/extension/src/ui/action/views/network-assets/components/custom-evm-token.vue b/packages/extension/src/ui/action/views/network-assets/components/custom-evm-token.vue index 142332884..b840ed595 100644 --- a/packages/extension/src/ui/action/views/network-assets/components/custom-evm-token.vue +++ b/packages/extension/src/ui/action/views/network-assets/components/custom-evm-token.vue @@ -220,14 +220,14 @@ const addToken = async () => { fromBase(accountBalance.value ?? "0", tokenInfo.value.decimals) ).value; const balanceUSD = market.value - ? new BigNumber(balancef).times(market.value.current_price).toNumber() + ? new BigNumber(balancef).times(market.value.current_price!).toNumber() : 0; const balanceUSDf = market.value - ? new BigNumber(balancef).times(market.value.current_price).toString() + ? new BigNumber(balancef).times(market.value.current_price!).toString() : "0"; - const value = market.value?.current_price.toString() ?? "0"; + const value = market.value?.current_price!.toString() ?? "0"; const sparkline = market.value - ? new Sparkline(market.value?.sparkline_in_7d.price, 25).dataValues + ? new Sparkline(market.value?.sparkline_in_24h.price, 25).dataValues : ""; const priceChangePercentage = market.value?.price_change_percentage_24h ?? 0; From c5f76f494999eeaed06281b9c3621a6787c2d292 Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:10:03 -0700 Subject: [PATCH 06/18] fix: solana legacy tx sign --- .../src/providers/solana/ui/libs/decode-tx.ts | 204 +++++++++--------- .../solana/ui/sol-verify-transaction.vue | 41 +++- 2 files changed, 141 insertions(+), 104 deletions(-) diff --git a/packages/extension/src/providers/solana/ui/libs/decode-tx.ts b/packages/extension/src/providers/solana/ui/libs/decode-tx.ts index 8c8c8f513..8c5e50e73 100644 --- a/packages/extension/src/providers/solana/ui/libs/decode-tx.ts +++ b/packages/extension/src/providers/solana/ui/libs/decode-tx.ts @@ -4,7 +4,12 @@ import { AccountLayout, TOKEN_PROGRAM_ID, } from "@solana/spl-token"; -import { VersionedTransaction, PublicKey } from "@solana/web3.js"; +import { + VersionedTransaction, + PublicKey, + Transaction, + TransactionVersion, +} from "@solana/web3.js"; import SolanaAPI from "@/providers/solana/libs/api"; import { SolanaNetwork } from "../../types/sol-network"; import { toBN } from "web3-utils"; @@ -43,116 +48,121 @@ const assetFetch = ( }; const decodeTransaction = async ( - tx: VersionedTransaction, + tx: VersionedTransaction | Transaction, from: PublicKey, - network: SolanaNetwork + network: SolanaNetwork, + version: TransactionVersion ) => { const solAPI = (await network.api()).api as SolanaAPI; const allBalances = await network.getAllTokenInfo(from.toBase58()); const marketData = new MarketData(); - return solAPI.web3 - .simulateTransaction(tx, { - accounts: { - addresses: tx.message.staticAccountKeys.map((k) => k.toBase58()), - encoding: "base64", - }, - }) - .then(async (result) => { - if (result.value.err) return null; - const nativeChange = { - contract: NATIVE_TOKEN_ADDRESS, - amount: BigInt(result.value.accounts![0]!.lamports), - }; - const balanceChanges = result.value - .accounts!.filter((a) => { - const data = Buffer.from(a!.data[0], "base64"); - return ( - a!.owner === TOKEN_PROGRAM_ID.toBase58() && - data.length === ACCOUNT_SIZE - ); + return ( + version !== "legacy" + ? solAPI.web3.simulateTransaction(tx as VersionedTransaction, { + accounts: { + addresses: ( + tx as VersionedTransaction + ).message.staticAccountKeys.map((k) => k.toBase58()), + encoding: "base64", + }, }) - .map((a) => { - const data = Buffer.from(a!.data[0], "base64"); - return AccountLayout.decode(data); - }) - .filter((val) => val.owner.toBase58() === from.toBase58()) - .map((val) => { + : solAPI.web3.simulateTransaction(tx as Transaction, undefined, true) + ).then(async (result) => { + if (result.value.err) return null; + const nativeChange = { + contract: NATIVE_TOKEN_ADDRESS, + amount: BigInt(result.value.accounts![0]!.lamports), + }; + const balanceChanges = result.value + .accounts!.filter((a) => { + const data = Buffer.from(a!.data[0], "base64"); + return ( + a!.owner === TOKEN_PROGRAM_ID.toBase58() && + data.length === ACCOUNT_SIZE + ); + }) + .map((a) => { + const data = Buffer.from(a!.data[0], "base64"); + return AccountLayout.decode(data); + }) + .filter((val) => val.owner.toBase58() === from.toBase58()) + .map((val) => { + return { + contract: val.mint.toBase58(), + amount: val.amount, + }; + }); + const getTokenInfoPromises = await Promise.all( + balanceChanges.map((val) => { + return solAPI.getTokenInfo(val.contract).then((info) => { return { - contract: val.mint.toBase58(), - amount: val.amount, + ...info, + ...val, }; }); - const getTokenInfoPromises = await Promise.all( - balanceChanges.map((val) => { - return solAPI.getTokenInfo(val.contract).then((info) => { - return { - ...info, - ...val, - }; - }); - }) - ); - getTokenInfoPromises.unshift({ - amount: nativeChange.amount, - cgId: network.coingeckoID, - contract: NATIVE_TOKEN_ADDRESS, - decimals: network.decimals, - icon: network.icon, - name: network.currencyNameLong, - symbol: network.currencyName, - }); - const retVal: DecodedTxResponseType[] = []; - for (const token of getTokenInfoPromises) { - const res: DecodedTxResponseType = { - change: 0, - contract: token.contract, - decimals: token.decimals, - icon: token.icon || "", - isNegative: false, - name: token.name, - symbol: token.symbol, - price: "0", - USDval: "0", - }; - const balInfo = allBalances.find((b) => b.contract === token.contract); - if (balInfo) { - const curBalance = toBN(balInfo.balance); - const newBalance = toBN(token.amount.toString()); - const diff = newBalance.sub(curBalance).toNumber(); - res.change = Math.abs(diff); - res.isNegative = diff < 0; - res.USDval = new BigNumber(balInfo.value) + }) + ); + getTokenInfoPromises.unshift({ + amount: nativeChange.amount, + cgId: network.coingeckoID, + contract: NATIVE_TOKEN_ADDRESS, + decimals: network.decimals, + icon: network.icon, + name: network.currencyNameLong, + symbol: network.currencyName, + }); + const retVal: DecodedTxResponseType[] = []; + for (const token of getTokenInfoPromises) { + const res: DecodedTxResponseType = { + change: 0, + contract: token.contract, + decimals: token.decimals, + icon: token.icon || "", + isNegative: false, + name: token.name, + symbol: token.symbol, + price: "0", + USDval: "0", + }; + const balInfo = allBalances.find((b) => b.contract === token.contract); + if (balInfo) { + const curBalance = toBN(balInfo.balance); + const newBalance = toBN(token.amount.toString()); + const diff = newBalance.sub(curBalance).toNumber(); + res.change = Math.abs(diff); + res.isNegative = diff < 0; + res.USDval = new BigNumber(balInfo.value) + .times(fromBase(res.change.toString(), res.decimals)) + .toString(); + res.price = balInfo.value; + } else { + res.change = toBN(token.amount.toString()).toNumber(); + res.isNegative = false; + if (token.cgId) { + const val = await marketData.getMarketData([token.cgId]); + res.USDval = new BigNumber(val[0]!.current_price ?? 0) .times(fromBase(res.change.toString(), res.decimals)) .toString(); - res.price = balInfo.value; - } else { - res.change = toBN(token.amount.toString()).toNumber(); - res.isNegative = false; - if (token.cgId) { - const val = await marketData.getMarketData([token.cgId]); - res.USDval = new BigNumber(val[0]!.current_price ?? 0) - .times(fromBase(res.change.toString(), res.decimals)) - .toString(); - res.price = (val[0]!.current_price ?? 0).toString(); - } + res.price = (val[0]!.current_price ?? 0).toString(); } - if (token.decimals === 0) { - const assetDetails = await assetFetch(network.node, "getAsset", { - id: token.contract, - }).catch(() => { - return null; - }); - if (assetDetails) { - res.icon = `https://img.mewapi.io/?image=${assetDetails.result.content.links.image}`; - res.name = assetDetails.result.content.metadata.name; - res.symbol = assetDetails.result.content.metadata.symbol; - } + } + if (token.decimals === 0) { + const assetDetails = await assetFetch(network.node, "getAsset", { + id: token.contract, + }).catch(() => { + return null; + }); + if (assetDetails) { + res.icon = `https://img.mewapi.io/?image=${assetDetails.result.content.links.image}`; + res.name = assetDetails.result.content.metadata.name; + res.symbol = assetDetails.result.content.metadata.symbol; } - retVal.push(res); } - retVal.sort((v) => (v.isNegative ? -1 * v.change : v.change)); - return retVal; - }); + retVal.push(res); + } + retVal.sort((v) => (v.isNegative ? -1 * v.change : v.change)); + return retVal; + }); }; export default decodeTransaction; diff --git a/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue b/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue index 6b11a1d00..c94f961eb 100644 --- a/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue +++ b/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue @@ -144,7 +144,13 @@ import { trackSendEvents } from "@/libs/metrics"; import { SendEventType } from "@/libs/metrics/types"; import { SolanaNetwork } from "../types/sol-network"; import SolanaAPI from "@/providers/solana/libs/api"; -import { PublicKey, SendOptions, VersionedTransaction } from "@solana/web3.js"; +import { + PublicKey, + SendOptions, + VersionedTransaction, + Transaction as LegacyTransaction, + Connection, +} from "@solana/web3.js"; import { SolSignTransactionRequest } from "./types"; import bs58 from "bs58"; import DecodeTransaction, { DecodedTxResponseType } from "./libs/decode-tx"; @@ -175,7 +181,8 @@ const Options = ref({ }); const selectedFee = ref(GasPriceTypes.ECONOMY); const solConnection = ref(); -const Tx = ref(); +const Tx = ref(); +const TxType = ref<"legacy" | 0>(0); const accountPubkey = ref(); const sendOptions = ref({}); const payloadMethod = ref<"sol_signTransaction" | "sol_signAndSendTransaction">( @@ -209,7 +216,18 @@ onBeforeMount(async () => { ) as SolSignTransactionRequest; sendOptions.value = JSON.parse(Request.value.params![2]) as SendOptions; Tx.value = VersionedTransaction.deserialize(hexToBuffer(txMessage.hex)); - solConnection.value.web3.getFeeForMessage(Tx.value.message).then((fee) => { + TxType.value = Tx.value.version; + if (TxType.value === "legacy") { + Tx.value = LegacyTransaction.from(hexToBuffer(txMessage.hex)); + Tx.value.recentBlockhash = ( + await solConnection.value.web3.getLatestBlockhash() + ).blockhash; + } + const feeMessage = + TxType.value === "legacy" + ? (Tx.value as LegacyTransaction).compileMessage() + : (Tx.value as VersionedTransaction).message; + solConnection.value.web3.getFeeForMessage(feeMessage).then((fee) => { const getConvertedVal = () => fromBase(fee.value!.toString(), network.value.decimals); marketdata @@ -226,10 +244,12 @@ onBeforeMount(async () => { }; }); }); + DecodeTransaction( Tx.value, accountPubkey.value, - network.value as SolanaNetwork + network.value as SolanaNetwork, + TxType.value ) .then((vals) => { decodedTx.value = vals; @@ -243,7 +263,10 @@ const approve = async () => { trackSendEvents(SendEventType.SendAPIApprove, { network: network.value.name, }); - const msgToSign = Tx.value!.message.serialize(); + const msgToSign = + TxType.value !== "legacy" + ? (Tx.value! as VersionedTransaction).message.serialize() + : (Tx.value! as LegacyTransaction).serializeMessage(); const feePayer = new PublicKey( network.value.displayAddress(account.value.address) ); @@ -256,6 +279,10 @@ const approve = async () => { Tx.value!.addSignature(feePayer, res); const toData = decodedTx.value && decodedTx.value.length ? decodedTx.value[0] : null; + const TransactionHash = + TxType.value !== "legacy" + ? (Tx.value as VersionedTransaction).signatures[0] + : (Tx.value as LegacyTransaction).signatures[0].signature; const txActivity: Activity = { from: accountPubkey.value!.toBase58(), to: toData ? toData.contract : accountPubkey.value!.toBase58(), @@ -272,7 +299,7 @@ const approve = async () => { }, type: ActivityType.transaction, value: toData ? toData.change.toString() : "0", - transactionHash: bs58.encode(Tx.value!.signatures[0]), + transactionHash: bs58.encode(TransactionHash!), }; txActivity.to = txActivity.to === NATIVE_TOKEN_ADDRESS @@ -306,7 +333,7 @@ const approve = async () => { .sendRawTransaction(Tx.value!.serialize(), sendOptions.value) .then((sig) => { Resolve.value({ - result: JSON.stringify(sig), + result: JSON.stringify(bufferToHex(bs58.decode(sig))), }); }); } From cebd4944da3a2df4130876a340ca7390cf556411 Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:11:53 -0700 Subject: [PATCH 07/18] devop: add priority fees for legacy txs --- .../solana/ui/libs/get-priority-fees.ts | 7 +++-- .../solana/ui/sol-verify-transaction.vue | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/providers/solana/ui/libs/get-priority-fees.ts b/packages/extension/src/providers/solana/ui/libs/get-priority-fees.ts index 4705b1c9a..64537138f 100644 --- a/packages/extension/src/providers/solana/ui/libs/get-priority-fees.ts +++ b/packages/extension/src/providers/solana/ui/libs/get-priority-fees.ts @@ -22,7 +22,6 @@ const getPrioritizationFees = async ( (await connection.getRecentPrioritizationFees( config )) as PrioritizationFeeObject[]; - if (prioritizationFeeObjects.length === 0) { return { low: 1000, @@ -58,9 +57,9 @@ const getPrioritizationFees = async ( : Math.floor((sortedFees[midIndex - 1] + sortedFees[midIndex]) / 2); } return { - low: averageFeeIncludingZeros, - medium: medianFee, - high: averageFeeExcludingZeros, + low: averageFeeIncludingZeros || 1000, + medium: medianFee || 1500, + high: averageFeeExcludingZeros || 2000, }; } catch (error) { return { diff --git a/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue b/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue index c94f961eb..91e86346a 100644 --- a/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue +++ b/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue @@ -150,12 +150,14 @@ import { VersionedTransaction, Transaction as LegacyTransaction, Connection, + ComputeBudgetProgram, } from "@solana/web3.js"; import { SolSignTransactionRequest } from "./types"; import bs58 from "bs58"; import DecodeTransaction, { DecodedTxResponseType } from "./libs/decode-tx"; import { TransactionSigner } from "./libs/signer"; import { NATIVE_TOKEN_ADDRESS } from "@/providers/ethereum/libs/common"; +import getPrioritizationFees from "./libs/get-priority-fees"; const isProcessing = ref(false); const providerVerifyTransactionScrollRef = ref(); @@ -219,6 +221,22 @@ onBeforeMount(async () => { TxType.value = Tx.value.version; if (TxType.value === "legacy") { Tx.value = LegacyTransaction.from(hexToBuffer(txMessage.hex)); + let isPriorityFeesSet = false; + Tx.value.instructions.forEach((i) => { + if (i.programId.toBase58() === ComputeBudgetProgram.programId.toBase58()) + isPriorityFeesSet = true; + }); + const priorityFee = await getPrioritizationFees( + new PublicKey(network.value.displayAddress(account.value.address)), + solConnection.value!.web3 + ); + if (!isPriorityFeesSet && priorityFee) { + Tx.value.add( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: priorityFee.high * 100, + }) + ); + } Tx.value.recentBlockhash = ( await solConnection.value.web3.getLatestBlockhash() ).blockhash; @@ -263,6 +281,14 @@ const approve = async () => { trackSendEvents(SendEventType.SendAPIApprove, { network: network.value.name, }); + const latestBlockHash = (await solConnection.value!.web3.getLatestBlockhash()) + .blockhash; + if (TxType.value === "legacy") { + (Tx.value as LegacyTransaction).recentBlockhash = latestBlockHash; + } else { + (Tx.value as VersionedTransaction).message.recentBlockhash = + latestBlockHash; + } const msgToSign = TxType.value !== "legacy" ? (Tx.value! as VersionedTransaction).message.serialize() From 04303f49278b9a4cfca97f9e4c45a9d2a28554df Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:12:55 -0700 Subject: [PATCH 08/18] devop: add priority fees for legacy txs --- .../extension/src/providers/solana/ui/sol-verify-transaction.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue b/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue index 91e86346a..87672a501 100644 --- a/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue +++ b/packages/extension/src/providers/solana/ui/sol-verify-transaction.vue @@ -149,7 +149,6 @@ import { SendOptions, VersionedTransaction, Transaction as LegacyTransaction, - Connection, ComputeBudgetProgram, } from "@solana/web3.js"; import { SolSignTransactionRequest } from "./types"; From dcf822195da6b144e3b041c77c8ae12abb6d08d3 Mon Sep 17 00:00:00 2001 From: nickkelly1 Date: Wed, 16 Oct 2024 18:20:15 -0500 Subject: [PATCH 09/18] fix: jupiter referral addresses --- packages/swap/src/configs.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/swap/src/configs.ts b/packages/swap/src/configs.ts index 3dd83f545..39f761c32 100644 --- a/packages/swap/src/configs.ts +++ b/packages/swap/src/configs.ts @@ -57,19 +57,11 @@ const FEE_CONFIGS: Partial< // each kind of asset you want to receive fees for [ProviderName.jupiter]: { [WalletIdentifier.enkrypt]: { - // TODO: THIS IS NICK'S TESTING REFERRAL ADDRESS, NEEDS TO CHANGE BEFORE RELEASE - // referrer: "78ZKhPea9sVW3jLsjvQov9jUooGe3qGnwauA1QoJFCq1", - // referrer: "41gBjMVCQ4FD2GAsKdjVPEGLQc7K6AxoY8C7PJ7HDp6y", - referrer: "EcrQ8iRSczuUq8YPBQKx1mWRuH8xxi3t9uwfenb6o9aW", - // Rounded because Jupiter API only accepts bps integers + referrer: "D5qKNm99Fbh7FAVEQp5vTgRkw7NfdtSREW2rhNPFqq5x", fee: 0.01, }, [WalletIdentifier.mew]: { - // TODO: THIS IS NICK'S TESTING REFERRAL ADDRESS, NEEDS TO CHANGE BEFORE RELEASE - // referrer: "78ZKhPea9sVW3jLsjvQov9jUooGe3qGnwauA1QoJFCq1", - // referrer: "41gBjMVCQ4FD2GAsKdjVPEGLQc7K6AxoY8C7PJ7HDp6y", - referrer: "EcrQ8iRSczuUq8YPBQKx1mWRuH8xxi3t9uwfenb6o9aW", - // Rounded because Jupiter API only accepts bps integers + referrer: "AUX4AgB6rwsXudMJ3U3rPFCUajhxKbwdG149i5xeVyFq", fee: 0.03, }, }, From f7d1b1c2172f61e55a824f0740af76e15de0da72 Mon Sep 17 00:00:00 2001 From: nickkelly1 Date: Wed, 16 Oct 2024 21:07:05 -0500 Subject: [PATCH 10/18] fix: failing swap tests --- packages/swap/src/configs.ts | 4 ++-- packages/swap/src/providers/changelly/index.ts | 12 +++++++++++- packages/swap/src/providers/changelly/types.ts | 6 +++++- packages/swap/tests/changelly.test.ts | 10 +++++----- .../swap/tests/fixtures/mainnet/configs.ts | 17 ++++++++++++++++- packages/swap/tests/swap.test.ts | 18 +++++++++++------- 6 files changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/swap/src/configs.ts b/packages/swap/src/configs.ts index 3dd83f545..553c5f13f 100644 --- a/packages/swap/src/configs.ts +++ b/packages/swap/src/configs.ts @@ -101,7 +101,7 @@ const TOKEN_LISTS: { /** * ```sh - * curl -sL https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/swaplists/changelly.json | jq '.' -C | less -R + * curl -sL https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/swaplists/changelly.json | jq . -C | less -R * ``` */ const CHANGELLY_LIST = @@ -109,7 +109,7 @@ const CHANGELLY_LIST = /** * ```sh - * curl -sL https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/swaplists/top-tokens.json | jq '.' -C | less -R + * curl -sL https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/swaplists/top-tokens.json | jq . -C | less -R * ``` */ const TOP_TOKEN_INFO_LIST = diff --git a/packages/swap/src/providers/changelly/index.ts b/packages/swap/src/providers/changelly/index.ts index bb76c6aee..03890995d 100644 --- a/packages/swap/src/providers/changelly/index.ts +++ b/packages/swap/src/providers/changelly/index.ts @@ -473,7 +473,17 @@ class Changelly extends ProviderClass { else if (quoteRequestAmount.gt(minMax.maximumFrom)) quoteRequestAmount = minMax.maximumFrom; - if (quoteRequestAmount.toString() === "0") return null; + if (quoteRequestAmount.toString() === "0") { + debug( + "getQuote", + `No swap: Quote request amount is zero` + + ` fromToken=${options.fromToken.symbol}` + + ` toToken=${options.toToken.symbol}` + + ` minimumFrom=${minMax.minimumFrom.toString()}` + + ` maximumFrom=${minMax.maximumFrom.toString()}` + ); + return null; + } debug("getQuote", `Requesting changelly swap...`); diff --git a/packages/swap/src/providers/changelly/types.ts b/packages/swap/src/providers/changelly/types.ts index 77bfe2383..2c9577e2b 100644 --- a/packages/swap/src/providers/changelly/types.ts +++ b/packages/swap/src/providers/changelly/types.ts @@ -108,7 +108,8 @@ export type ChangellyApiValidateAddressResult = { * * @example * ```sh - * curl -sL https://partners.mewapi.io/changelly-v2 -X POST -H Accept:application/json -H Content-Type:application/json --data '{"id":"1","jsonrpc":"2.0","method":"getFixRate","params":{"from":"sol","to":"btc"}}' | jq '.' -C | less -R + * # SOL to BTC + * curl -sL https://partners.mewapi.io/changelly-v2 -X POST -H Accept:application/json -H Content-Type:application/json --data '{"id":"1","jsonrpc":"2.0","method":"getFixRate","params":{"from":"sol","to":"btc"}}' | jq . -C | less -R * # { * # "jsonrpc": "2.0", * # "result": [ @@ -129,6 +130,9 @@ export type ChangellyApiValidateAddressResult = { * # ], * # "id": "1" * # } + * + * # DAI to USDC + * curl https://partners.mewapi.io/changelly-v2 -sL -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":"1","method":"getFixRate","params":{"from":"dai","to":"usdc"}}' | jq . -C | less -R * ```` */ export type ChangellyApiGetFixRateParams = { diff --git a/packages/swap/tests/changelly.test.ts b/packages/swap/tests/changelly.test.ts index 9ccaba78c..529b583df 100644 --- a/packages/swap/tests/changelly.test.ts +++ b/packages/swap/tests/changelly.test.ts @@ -10,9 +10,9 @@ import { WalletIdentifier, } from "../src/types"; import { - fromToken, + fromTokenUSDT, toToken, - amount, + amountUSDT, fromAddress, toAddress, nodeURL as ethNodeURL, @@ -29,9 +29,9 @@ describe("Changelly Provider", () => { await init; const quote = await changelly.getQuote( { - amount, + amount: amountUSDT, fromAddress, - fromToken, + fromToken: fromTokenUSDT, toToken, toAddress, }, @@ -42,7 +42,7 @@ describe("Changelly Provider", () => { expect(quote?.quote.meta.walletIdentifier).to.be.eq( WalletIdentifier.enkrypt ); - expect(quote?.fromTokenAmount.gte(amount)).to.be.eq(true); + expect(quote?.fromTokenAmount.gte(amountUSDT)).to.be.eq(true); expect(quote?.toTokenAmount.gtn(0)).to.be.eq(true); const swap = await changelly.getSwap(quote!.quote); diff --git a/packages/swap/tests/fixtures/mainnet/configs.ts b/packages/swap/tests/fixtures/mainnet/configs.ts index 6c6d3d8b2..d27a34532 100644 --- a/packages/swap/tests/fixtures/mainnet/configs.ts +++ b/packages/swap/tests/fixtures/mainnet/configs.ts @@ -2,7 +2,8 @@ import { NetworkNames } from "@enkryptcom/types"; import { isAddress, toBN } from "web3-utils"; import { NetworkType, TokenType, TokenTypeTo } from "../../../src/types"; -const amount = toBN("100000000000000000000"); +const amount = toBN("100000000000000000000"); // DAI, $100, 18 decimals +const amountUSDT = toBN("1000000000"); // USDT, $1,000, 6 decimals const fromAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; const toAddress = "0x255d4D554325568A2e628A1E93120EbA1157C07e"; @@ -32,6 +33,18 @@ const fromToken: TokenType = { type: NetworkType.EVM, }; +const fromTokenUSDT: TokenType = { + address: "0xdac17f958d2ee523a2206206994597c13d831ec7", + decimals: 6, + logoURI: + "https://assets.coingecko.com/coins/images/325/standard/Tether.png?1696501661", + name: "Tether", + symbol: "USDT", + rank: 18, + cgId: "tether", + type: NetworkType.EVM, +}; + const fromTokenWBTC: TokenType = { address: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", decimals: 8, @@ -77,10 +90,12 @@ const toToken: TokenTypeTo = { export { fromToken, + fromTokenUSDT, toToken, toTokenWETH, fromTokenWBTC, amount, + amountUSDT, fromAddress, toAddress, nodeURL, diff --git a/packages/swap/tests/swap.test.ts b/packages/swap/tests/swap.test.ts index ac4c723c2..f790d7364 100644 --- a/packages/swap/tests/swap.test.ts +++ b/packages/swap/tests/swap.test.ts @@ -8,9 +8,11 @@ import { WalletIdentifier, } from "../src/types"; import { - fromToken, + // fromToken, + fromTokenUSDT, toToken, - amount, + // amount, + amountUSDT, fromAddress, toAddress, nodeURL, @@ -55,9 +57,9 @@ describe("Swap", () => { it("it should get quote and swap for different destination", async () => { await enkryptSwap.initPromise; const quotes = await enkryptSwap.getQuotes({ - amount, + amount: amountUSDT, fromAddress, - fromToken, + fromToken: fromTokenUSDT, toToken, toAddress, }); @@ -81,7 +83,9 @@ describe("Swap", () => { expect(oneInceQuote!.provider).to.be.eq(ProviderName.oneInch); expect(paraswapQuote!.provider).to.be.eq(ProviderName.paraswap); const swapOneInch = await enkryptSwap.getSwap(oneInceQuote!.quote); - expect(swapOneInch?.fromTokenAmount.toString()).to.be.eq(amount.toString()); + expect(swapOneInch?.fromTokenAmount.toString()).to.be.eq( + amountUSDT.toString() + ); expect(swapOneInch?.transactions.length).to.be.eq(2); const swapChangelly = await enkryptSwap.getSwap(changellyQuote!.quote); if (swapChangelly) expect(swapChangelly?.transactions.length).to.be.eq(1); @@ -90,9 +94,9 @@ describe("Swap", () => { it("it should get quote and swap for same destination", async () => { await enkryptSwap.initPromise; const quotes = await enkryptSwap.getQuotes({ - amount, + amount: amountUSDT, fromAddress, - fromToken, + fromToken: fromTokenUSDT, toToken, toAddress: fromAddress, }); From 82d6022eac1c45fc8fe5a0eab387321b26cd51fc Mon Sep 17 00:00:00 2001 From: nickkelly1 Date: Thu, 17 Oct 2024 22:01:16 -0500 Subject: [PATCH 11/18] feat: enable changelly on solana --- .../action/views/swap/libs/solana-gasvals.ts | 163 +++++++-------- packages/swap/src/index.ts | 3 +- .../swap/src/providers/changelly/index.ts | 189 ++++++++++++++---- .../swap/src/providers/changelly/supported.ts | 21 +- 4 files changed, 233 insertions(+), 143 deletions(-) diff --git a/packages/extension/src/ui/action/views/swap/libs/solana-gasvals.ts b/packages/extension/src/ui/action/views/swap/libs/solana-gasvals.ts index e02ac2ef4..7453d799b 100644 --- a/packages/extension/src/ui/action/views/swap/libs/solana-gasvals.ts +++ b/packages/extension/src/ui/action/views/swap/libs/solana-gasvals.ts @@ -11,23 +11,71 @@ import { import BigNumber from "bignumber.js"; import { toBN } from "web3-utils"; +type TaggedLegacyTransaction = { + kind: "legacy"; + instance: SolanaLegacyTransaction; + hasThirdPartySignatures: boolean; +}; + +type TaggedVersionedTransaction = { + kind: "versioned"; + instance: SolanaVersionedTransaction; + hasThirdPartySignatures: boolean; +}; + +type TaggedTransaction = TaggedLegacyTransaction | TaggedVersionedTransaction; + +function getTxBlockHash(tx: TaggedTransaction): undefined | string { + let recentBlockHash: undefined | string; + switch (tx.kind) { + case "legacy": + recentBlockHash = tx.instance.recentBlockhash; + break; + case "versioned": + recentBlockHash = tx.instance.message.recentBlockhash; + break; + default: + tx satisfies never; + throw new Error(`Unexpected Solana transaction kind ${(tx as any).kind}`); + } + return recentBlockHash; +} + +function setTxBlockHash(tx: TaggedTransaction, blockHash: string): void { + switch (tx.kind) { + case "legacy": + tx.instance.recentBlockhash = blockHash; + break; + case "versioned": + tx.instance.message.recentBlockhash = blockHash; + break; + default: + tx satisfies never; + throw new Error(`Unexpected Solana transaction kind ${(tx as any).kind}`); + } +} + +function getTxMessage(tx: TaggedTransaction): Message | VersionedMessage { + let msg: Message | VersionedMessage; + switch (tx.kind) { + case "legacy": + msg = tx.instance.compileMessage(); + break; + case "versioned": + msg = tx.instance.message; + break; + default: + throw new Error(`Unexpected Solana transaction kind ${(tx as any).kind}`); + } + return msg; +} + /** * Mutably updates transactions with the latest block hash * (not nice but convenient) */ export const getSolanaTransactionFees = async ( - txs: ( - | { - kind: "legacy"; - instance: SolanaLegacyTransaction; - hasThirdPartySignatures: boolean; - } - | { - kind: "versioned"; - instance: SolanaVersionedTransaction; - hasThirdPartySignatures: boolean; - } - )[], + txs: (TaggedLegacyTransaction | TaggedVersionedTransaction)[], network: SolanaNetwork, price: number, additionalFee: ReturnType @@ -58,19 +106,7 @@ export const getSolanaTransactionFees = async ( // Use the latest block hash in-case it's fallen too far behind // (can't change block hash if it's already signed) if (!tx.hasThirdPartySignatures) { - switch (tx.kind) { - case "legacy": - tx.instance.recentBlockhash = latestBlockHash.blockhash; - break; - case "versioned": - tx.instance.message.recentBlockhash = latestBlockHash.blockhash; - break; - default: - tx satisfies never; - throw new Error( - `Unexpected Solana transaction kind ${(tx as any).kind}` - ); - } + setTxBlockHash(tx, latestBlockHash.blockhash); } // Not sure why but getFeeForMessage sometimes returns null, so we will retry @@ -82,25 +118,12 @@ export const getSolanaTransactionFees = async ( // eslint-disable-next-line no-constant-condition while (true) { if (attempt >= backoff.length) { - let recentBlockHash: undefined | string; - switch (tx.kind) { - case "legacy": - recentBlockHash = tx.instance.recentBlockhash; - break; - case "versioned": - recentBlockHash = tx.instance.message.recentBlockhash; - break; - default: - tx satisfies never; - throw new Error( - `Unexpected Solana transaction kind ${(tx as any).kind}` - ); - } + const blockHash = getTxBlockHash(tx); throw new Error( `Failed to get fee for Solana transaction ${txi + 1}` + ` after ${backoff.length} attempts.` + ` Transaction block hash` + - ` ${recentBlockHash} possibly expired.` + + ` ${blockHash} possibly expired.` + ` txkind=${txkind}` ); } @@ -111,74 +134,24 @@ export const getSolanaTransactionFees = async ( // Update the block hash in-case it caused 0 fees to be returned if (attempt > 0) { if (!tx.hasThirdPartySignatures) { - let recentBlockHash: undefined | string; - switch (tx.kind) { - case "legacy": - recentBlockHash = tx.instance.recentBlockhash; - break; - case "versioned": - recentBlockHash = tx.instance.message.recentBlockhash; - break; - default: - tx satisfies never; - throw new Error( - `Unexpected Solana transaction kind ${(tx as any).kind}` - ); - } + const blockHash = getTxBlockHash(tx); console.warn( `Cannot update block hash for signed transaction` + ` ${txi + 1}, retrying getFeeForMessage using the same` + - ` block hash ${recentBlockHash}` + + ` block hash ${blockHash}` + ` txkind=${txkind}` ); } else { latestBlockHash = await conn.getLatestBlockhash(); - switch (tx.kind) { - case "legacy": - tx.instance.recentBlockhash = latestBlockHash.blockhash; - break; - case "versioned": - tx.instance.message.recentBlockhash = latestBlockHash.blockhash; - break; - default: - tx satisfies never; - throw new Error( - `Unexpected Solana transaction kind ${(tx as any).kind}` - ); - } + setTxBlockHash(tx, latestBlockHash.blockhash); } } /** Base fee + priority fee (Don't know why this returns null sometimes) */ - let msg: Message | VersionedMessage; - switch (tx.kind) { - case "legacy": - msg = tx.instance.compileMessage(); - break; - case "versioned": - msg = tx.instance.message; - break; - default: - throw new Error( - `Unexpected Solana transaction kind ${(tx as any).kind}` - ); - } + const msg = getTxMessage(tx); const feeResult = await conn.getFeeForMessage(msg); if (feeResult.value == null) { - let recentBlockHash: undefined | string; - switch (tx.kind) { - case "legacy": - recentBlockHash = tx.instance.recentBlockhash; - break; - case "versioned": - recentBlockHash = tx.instance.message.recentBlockhash; - break; - default: - tx satisfies never; - throw new Error( - `Unexpected Solana transaction kind ${(tx as any).kind}` - ); - } + const recentBlockHash = getTxBlockHash(tx); console.warn( `Failed to get fee for Solana transaction ${txi + 1}/${len}.` + ` Transaction block hash ${recentBlockHash}` + diff --git a/packages/swap/src/index.ts b/packages/swap/src/index.ts index 9bee1de02..0365a909d 100644 --- a/packages/swap/src/index.ts +++ b/packages/swap/src/index.ts @@ -118,8 +118,7 @@ class Swap extends EventEmitter { new Jupiter(this.api as Web3Solana, this.network), // TODO: re-enable Rango on Solana when issues with it are fixed // new Rango(this.api as Web3Solana, this.network), - // TODO: re-enable Changelly on Solana when issues with it are fixed - // new Changelly(this.api, this.network), + new Changelly(this.api, this.network), ]; break; default: diff --git a/packages/swap/src/providers/changelly/index.ts b/packages/swap/src/providers/changelly/index.ts index bb76c6aee..0cf91cbdc 100644 --- a/packages/swap/src/providers/changelly/index.ts +++ b/packages/swap/src/providers/changelly/index.ts @@ -10,6 +10,8 @@ import { TransactionMessage, Connection, TransactionInstruction, + ComputeBudgetInstruction, + ComputeBudgetProgram, } from "@solana/web3.js"; import { ASSOCIATED_TOKEN_PROGRAM_ID, @@ -44,7 +46,7 @@ import { } from "../../configs"; import { getTransfer } from "../../utils/approvals"; -import supportedNetworks from "./supported"; +import supportedNetworks, { supportedNetworksSet } from "./supported"; import { ChangellyApiCreateFixedRateTransactionParams, ChangellyApiCreateFixedRateTransactionResult, @@ -133,6 +135,7 @@ class Changelly extends ProviderClass { async init(): Promise { debug("init", "Initialising..."); + if (!Changelly.isSupported(this.network)) { debug( "init", @@ -151,15 +154,16 @@ class Changelly extends ProviderClass { }); /** List of changelly blockchain names */ - const supportedChangellyNames = Object.values(supportedNetworks).map( - (s) => s.changellyName + const supportedChangellyNames = new Set( + Object.values(supportedNetworks).map((s) => s.changellyName) ); this.changellyList.forEach((cur) => { // Must be a supported token - if (!supportedChangellyNames.includes(cur.blockchain)) { + if (!supportedChangellyNames.has(cur.blockchain)) { return; } + if ( cur.enabledFrom && cur.fixRateEnabled && @@ -184,6 +188,7 @@ class Changelly extends ProviderClass { }, }; } + if (cur.token) this.setTicker( cur.token, @@ -211,9 +216,7 @@ class Changelly extends ProviderClass { } static isSupported(network: SupportedNetworkName) { - return Object.keys(supportedNetworks).includes( - network as unknown as string - ); + return supportedNetworksSet.has(network); } /** @@ -561,15 +564,24 @@ class Changelly extends ProviderClass { ); const original = firstChangellyFixRateQuote.amountTo; // eslint-disable-next-line no-use-before-define - const [success, fixed] = trimDecimals( + const [success, fixed] = fixBaseAndTrimDecimals( original, options.toToken.decimals ); if (!success) throw err; - const rounded = ( - BigInt(toBase(fixed, options.toToken.decimals)) - BigInt(1) - ).toString(); + const rounded = (BigInt(fixed) - BigInt(1)).toString(); toTokenAmountBase = rounded; + + debug( + "getQuote", + `Fixed amountTo` + + ` firstChangellyFixRateQuote.amountTo=${firstChangellyFixRateQuote.amountTo}` + + ` toTokenAmountBase=${toTokenAmountBase}` + + ` options.toToken.decimals=${options.toToken.decimals}` + + ` options.toToken.symbol=${options.toToken.symbol}` + + ` options.toToken.name=${options.toToken.name}` + + ` options.toToken.address=${options.toToken.address}` + ); } // `toBase` fails sometimes because Changelly returns more decimals than the token has @@ -589,15 +601,24 @@ class Changelly extends ProviderClass { ); const original = firstChangellyFixRateQuote.networkFee; // eslint-disable-next-line no-use-before-define - const [success, fixed] = trimDecimals( + const [success, fixed] = fixBaseAndTrimDecimals( original, options.toToken.decimals ); if (!success) throw err; - const rounded = ( - BigInt(toBase(fixed, options.toToken.decimals)) + BigInt(1) - ).toString(); + const rounded = (BigInt(fixed) + BigInt(1)).toString(); networkFeeBase = rounded; + + debug( + "getQuote", + `Fixed networkFee` + + ` firstChangellyFixRateQuote.networkFee=${firstChangellyFixRateQuote.networkFee}` + + ` networkFeeBase=${networkFeeBase}` + + ` options.toToken.decimals=${options.toToken.decimals}` + + ` options.toToken.symbol=${options.toToken.symbol}` + + ` options.toToken.name=${options.toToken.name}` + + ` options.toToken.address=${options.toToken.address}` + ); } const providerQuoteResponse: ProviderQuoteResponse = { @@ -751,9 +772,7 @@ class Changelly extends ProviderClass { break; } case NetworkType.Solana: { - // TODO: finish implementing support for Solana debug("getSwap", `Changelly is not supported on Solana at this time`); - if (true as any) return null; const conn = this.web3 as Connection; @@ -762,6 +781,8 @@ class Changelly extends ProviderClass { // Create a transaction to transfer this much of that token to that thing let versionedTx: VersionedTransaction; if (quote.options.fromToken.address === NATIVE_TOKEN_ADDRESS) { + // Swapping from native SOL + debug( "getSwap", `Preparing Solana Changelly SOL swap transaction` + @@ -785,52 +806,62 @@ class Changelly extends ProviderClass { }).compileToV0Message() ); } else { - const wallet = new PublicKey(quote.options.fromAddress); + // Swapping from a token on SOL + + // We need to send our src tokens to the payin address + // for Changelly to begin the cross-chain swap + // But first we'll need to create the src mint ATA for the payin address + // so it can receive the tokens const mint = new PublicKey(quote.options.fromToken.address); const tokenProgramId = await getTokenProgramOfMint(conn, mint); - const walletMintAta = getSPLAssociatedTokenAccountPubkey( + const wallet = new PublicKey(quote.options.fromAddress); + const walletAta = getSPLAssociatedTokenAccountPubkey( wallet, mint, tokenProgramId ); - // TODO: is payin address an ATA or Wallet address? (must be wallet, right?) - const payinAta = new PublicKey(changellyFixedRateTx.payinAddress); + const payin = new PublicKey(changellyFixedRateTx.payinAddress); + const payinAta = getSPLAssociatedTokenAccountPubkey( + payin, + mint, + tokenProgramId + ); const amount = BigInt(quote.options.amount.toString()); debug( "getSwap", - // eslint-disable-next-line prefer-template `Preparing Solana Changelly SPL token swap transaction` + ` srcMint=${mint.toBase58()}` + + ` srcTokenProgramId=${tokenProgramId.toBase58()}` + ` wallet=${wallet.toBase58()}` + - ` walletSrcMintAta=${tokenProgramId.toBase58()}` + - ` dstMintAta=${payinAta.toBase58()}` + - ` tokenProgramId=${tokenProgramId.toBase58()}` + + ` walletAta=${tokenProgramId.toBase58()}` + + ` payin=${payin.toBase58()}` + + ` payinAta=${payinAta.toBase58()}` + ` latestBlockHash=${latestBlockHash.blockhash}` + ` lastValidBlockHeight=${latestBlockHash.lastValidBlockHeight}` + - ` payinAddress=${changellyFixedRateTx.payinAddress}` + ` amount=${amount}` ); // If the ATA account doesn't exist we need create it - const ataExists = await solAccountExists(conn, payinAta); + const payinAtaExists = await solAccountExists(conn, payinAta); + + // We probably need to set some priority fees? IDK... const instructions: TransactionInstruction[] = []; - if (ataExists) { + if (payinAtaExists) { debug( "getSwap", `Payin ATA already exists. No need to create it.` ); } else { debug("getSwap", `Payin ATA does not exist. Need to create it.`); - // TODO: finish implementing const extraRentFee = await conn.getMinimumBalanceForRentExemption( SPL_TOKEN_ATA_ACCOUNT_SIZE_BYTES ); const instruction = getCreateAssociatedTokenAccountIdempotentInstruction({ payerPubkey: wallet, - ataPubkey: payinAta, // TODO: we'd need to get the owner - ownerPubkey: new PublicKey("!! TODO !!"), + ataPubkey: payinAta, + ownerPubkey: payin, mintPubkey: mint, systemProgramId: SystemProgram.programId, tokenProgramId, @@ -843,9 +874,82 @@ class Changelly extends ProviderClass { ); } + /** Priority fee (units: micro lamports per compute unit) in recent transactions */ + const recentFees = await conn.getRecentPrioritizationFees(); + // Sort by fee amount ascending so we can get the median + recentFees.sort( + (a, b) => a.prioritizationFee - b.prioritizationFee + ); + const recentFeeCount = recentFees.length; + let recentFeeCountWithoutZeroes = 0; + let recentFeeMin = 0; + let recentFeeMax = 0; + let recentFeeSum = 0; + let recentFeeMedian = 0; + const recentFeeMediani = Math.floor(recentFeeCount / 2); + // Use the minimum of the mean average and median average as our priority fee + for ( + let recentFeei = 0; + recentFeei < recentFeeCount; + recentFeei++ + ) { + const { prioritizationFee } = recentFees[recentFeei]; + recentFeeSum += prioritizationFee; + if (recentFeei === recentFeeMediani) + recentFeeMedian = prioritizationFee; + if (prioritizationFee > 0) recentFeeCountWithoutZeroes++; + if (prioritizationFee < recentFeeMin) + recentFeeMin = prioritizationFee; + if (prioritizationFee > recentFeeMax) + recentFeeMax = prioritizationFee; + } + const recentFeeMean = recentFeeSum / recentFeeCountWithoutZeroes; + const recentFeeMinAvg = Math.min(recentFeeMean, recentFeeMedian); + + // TODO: Do we want to set the compute budget? What should we set it to? + // Set compute budget + // instructions.unshift( + // ComputeBudgetProgram.setComputeUnitLimit() + // ) + + // Set priority fee + if (recentFeeMinAvg <= 0) { + debug( + "getSwap", + `No recent fees, not setting priority fee` + + ` recentFeeCount=${recentFeeCount}` + + ` recentFeeCountWithoutZeroes=${recentFeeCountWithoutZeroes}` + + ` recentFeeSum=${recentFeeSum}` + + ` recentFeeMin=${recentFeeMin}` + + ` recentFeeMax=${recentFeeMax}` + + ` recentFeeMean=${recentFeeMean}` + + ` recentFeeMedian=${recentFeeMedian}` + + ` recentFeeMinAvg=${recentFeeMinAvg}` + ); + } else { + debug( + "getSwap", + `Setting priority fee` + + ` priority_fee=${recentFeeMinAvg} micro_lamports/compute_unit` + + ` recentFeeCount=${recentFeeCount}` + + ` recentFeeCountWithoutZeroes=${recentFeeCountWithoutZeroes}` + + ` recentFeeSum=${recentFeeSum}` + + ` recentFeeMin=${recentFeeMin}` + + ` recentFeeMax=${recentFeeMax}` + + ` recentFeeMean=${recentFeeMean}` + + ` recentFeeMedian=${recentFeeMedian}` + + ` recentFeeMinAvg=${recentFeeMinAvg}` + ); + instructions.unshift( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: recentFeeMinAvg, + }) + ); + } + instructions.push( createSPLTransferInstruction( - /** source */ walletMintAta, + /** source */ walletAta, /** destination */ payinAta, /** owner */ wallet, /** amount */ amount, @@ -873,7 +977,9 @@ class Changelly extends ProviderClass { }; break; } + default: { + // Not EVM, not SOlana transaction = { from: quote.options.fromAddress, to: changellyFixedRateTx.payinAddress, @@ -902,15 +1008,24 @@ class Changelly extends ProviderClass { ); const original = changellyFixedRateTx.amountExpectedTo; // eslint-disable-next-line no-use-before-define - const [success, fixed] = trimDecimals( + const [success, fixed] = fixBaseAndTrimDecimals( original, quote.options.toToken.decimals ); if (!success) throw err; - const rounded = ( - BigInt(toBase(fixed, quote.options.toToken.decimals)) - BigInt(1) - ).toString(); + const rounded = (BigInt(fixed) - BigInt(1)).toString(); baseToAmount = rounded; + + debug( + "getQuote", + `Fixed amountExpectedTo` + + ` changellyFixedRateTx.amountExpectedTo=${changellyFixedRateTx.amountExpectedTo}` + + ` baseToAmount=${baseToAmount}` + + ` quote.options.toToken.decimals=${quote.options.toToken.decimals}` + + ` quote.options.toToken.symbol=${quote.options.toToken.symbol}` + + ` quote.options.toToken.name=${quote.options.toToken.name}` + + ` quote.options.toToken.address=${quote.options.toToken.address}` + ); } const retResponse: ProviderSwapResponse = { @@ -974,7 +1089,7 @@ class Changelly extends ProviderClass { } } -function trimDecimals( +function fixBaseAndTrimDecimals( value: string, decimals: number ): [success: boolean, fixed: string] { diff --git a/packages/swap/src/providers/changelly/supported.ts b/packages/swap/src/providers/changelly/supported.ts index e4260486c..73b2a4f4c 100644 --- a/packages/swap/src/providers/changelly/supported.ts +++ b/packages/swap/src/providers/changelly/supported.ts @@ -1,6 +1,7 @@ // import { isValidSolanaAddress } from "../../utils/solana"; import { isPolkadotAddress, isEVMAddress } from "../../utils/common"; import { SupportedNetworkName } from "../../types"; +import { isValidSolanaAddress } from "../../utils/solana"; /** * Blockchain names: @@ -13,7 +14,7 @@ import { SupportedNetworkName } from "../../types"; * ```` */ const supportedNetworks: { - [key in SupportedNetworkName]?: { + readonly [key in SupportedNetworkName]?: { changellyName: string; isAddress?: (addr: string) => Promise; }; @@ -61,17 +62,19 @@ const supportedNetworks: { [SupportedNetworkName.Dogecoin]: { changellyName: "doge", }, - // TODO: Re-enable Solana when all issues with Changelly and Solana on Enkrypt are fixed - // @2024-10-01 - // [SupportedNetworkName.Solana]: { - // changellyName: "solana", - // async isAddress(address: string) { - // return isValidSolanaAddress(address); - // }, - // }, + [SupportedNetworkName.Solana]: { + changellyName: "solana", + async isAddress(address: string) { + return isValidSolanaAddress(address); + }, + }, [SupportedNetworkName.Rootstock]: { changellyName: "rootstock", }, }; +export const supportedNetworksSet = new Set( + Object.keys(supportedNetworks) +) as unknown as Set; + export default supportedNetworks; From e450a83c031e6e107f7950657e2519622594a3d9 Mon Sep 17 00:00:00 2001 From: nickkelly1 Date: Fri, 18 Oct 2024 14:02:05 -0500 Subject: [PATCH 12/18] fix: changelly swap tests --- packages/swap/tests/changelly.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/swap/tests/changelly.test.ts b/packages/swap/tests/changelly.test.ts index 529b583df..ae3bfe796 100644 --- a/packages/swap/tests/changelly.test.ts +++ b/packages/swap/tests/changelly.test.ts @@ -87,12 +87,10 @@ describe("Changelly Provider", () => { expect(Object.values(fromTokens).length).to.be.eq(1); expect(fromTokens[NATIVE_TOKEN_ADDRESS].name).to.be.eq("Polkadot"); }); - // TODO: switch this test to assert that Solana DOES initialise - // once we enable Changelly on Solana - it("it NOT should initialize other networks: Solana", async () => { + it("it should initialize other networks: Solana", async () => { const changelly2 = new Changelly(solConn, SupportedNetworkName.Solana); await changelly2.init(); const fromTokens = changelly2.getFromTokens(); - expect(Object.values(fromTokens).length).to.be.eq(0); + expect(Object.values(fromTokens).length).to.be.gte(1); }); }); From 02bd515d295a4f38e8f3574efb1957f173d7d151 Mon Sep 17 00:00:00 2001 From: nickkelly1 Date: Fri, 18 Oct 2024 14:31:07 -0500 Subject: [PATCH 13/18] fix: swap test fixture --- packages/swap/tests/fixtures/mainnet/configs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swap/tests/fixtures/mainnet/configs.ts b/packages/swap/tests/fixtures/mainnet/configs.ts index d27a34532..469ce18c2 100644 --- a/packages/swap/tests/fixtures/mainnet/configs.ts +++ b/packages/swap/tests/fixtures/mainnet/configs.ts @@ -3,7 +3,7 @@ import { isAddress, toBN } from "web3-utils"; import { NetworkType, TokenType, TokenTypeTo } from "../../../src/types"; const amount = toBN("100000000000000000000"); // DAI, $100, 18 decimals -const amountUSDT = toBN("1000000000"); // USDT, $1,000, 6 decimals +const amountUSDT = toBN("100000000"); // USDT, $100, 6 decimals const fromAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; const toAddress = "0x255d4D554325568A2e628A1E93120EbA1157C07e"; From 4e4570a9b602b8d1472fd7cfbd10a44a160a26a5 Mon Sep 17 00:00:00 2001 From: nickkelly1 Date: Fri, 18 Oct 2024 22:34:32 -0500 Subject: [PATCH 14/18] feat: enable rango swap on solana --- .../views/swap/libs/send-transactions.ts | 157 ++++- packages/swap/src/index.ts | 3 +- packages/swap/src/providers/rango/index.ts | 585 ++++++++++++------ 3 files changed, 557 insertions(+), 188 deletions(-) diff --git a/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts b/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts index ba924858d..d2fd2f22e 100644 --- a/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts +++ b/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts @@ -184,8 +184,9 @@ export const executeSwap = async ( const enkSolTxs = options.swap.transactions as EnkryptSolanaTransaction[]; // Execute each transaction in-order one-by-one - for (const enkSolTx of enkSolTxs) { + for (let i = 0, len = enkSolTxs.length; i < len; i++) { // Transform the Enkrypt representation of the transaction into the Solana lib's representation + const enkSolTx = enkSolTxs[i]; let serialized: Uint8Array; switch (enkSolTx.kind) { @@ -194,6 +195,73 @@ export const executeSwap = async ( // (note: the transaction may already be signed by a third party like Rango exchange) const bytes = Buffer.from(enkSolTx.serialized, "base64"); const legacyTx = SolanaLegacyTransaction.from(bytes); + const { thirdPartySignatures } = enkSolTx; + + const hasThirdPartySignatures = + // Serialized versioned transaction was already signed + legacyTx.signatures.length > 1 || + // Need to apply third aprty signatures to the transaction + thirdPartySignatures.length > 0; + + if (hasThirdPartySignatures) { + // Can't sign the transaction since it's signed / to be signed by third parties + } else { + // We can update the transaction since it's unsigned + // Check if we need to update the block hash + let shouldUpdateBlockHash: boolean; + try { + // We won't be able to get the fee if the block hash is too old + const fee = await legacyTx.getEstimatedFee(conn); + shouldUpdateBlockHash = fee == null; + } catch (err) { + // Might need to update the block hash + console.warn( + `Failed to get fee for legacy transaction while checking` + + ` whether to update block hash: ${String(err)}` + ); + shouldUpdateBlockHash = true; + } + + // Try to update the block hash + if (shouldUpdateBlockHash) { + console.warn( + `Unsigned legacy transaction might have an` + + ` out-of-date block hash, trying to update it...` + ); + const backoff = [0, 500, 1_000, 2_000]; + let backoffi = 0; + // eslint-disable-next-line no-constant-condition + update_block_hash: while (true) { + if (backoffi >= backoff.length) { + // Just continue and hope for the best with old block hash... + console.warn( + `Failed to get latest blockhash after ${backoffi} attempts,` + + ` continuing with old block hash for legacy transaction...` + ); + break update_block_hash; + } + const backoffMs = backoff[backoffi]; + if (backoffMs > 0) { + console.warn( + `Waiting ${backoffMs}ms before retrying latest block` + + ` hash for legacy transaction...` + ); + await new Promise((res) => setTimeout(res, backoffMs)); + } + try { + const latest = await conn.getLatestBlockhash(); + legacyTx.recentBlockhash = latest.blockhash; + break update_block_hash; + } catch (err) { + console.warn( + `Failed to get latest blockhash on attempt` + + ` ${backoffi + 1}: ${String(err)}` + ); + } + backoffi++; + } + } + } // Sign the transaction message // Use the keyring running in the background script @@ -211,7 +279,6 @@ export const executeSwap = async ( ); }); // Add third party signatures (eg Rango) - const { thirdPartySignatures } = enkSolTx; for (let i = 0, len = thirdPartySignatures.length; i < len; i++) { const { pubkey, signature } = enkSolTx.thirdPartySignatures[i]; legacyTx.addSignature( @@ -236,10 +303,77 @@ export const executeSwap = async ( const bytes = Buffer.from(enkSolTx.serialized, "base64"); const versionedTx = SolanaVersionedTransaction.deserialize(bytes); + const { thirdPartySignatures } = enkSolTx; + + const hasThirdPartySignatures = + // Serialized versioned transaction was already signed + versionedTx.signatures.length > 1 || + // Need to apply third aprty signatures to the transaction + thirdPartySignatures.length > 0; + + if (hasThirdPartySignatures) { + // Can't sign the transaction since it's signed / to be signed by third parties + } else { + // We can update the transaction since it's unsigned + // Check if we need to update the block hash + let shouldUpdateBlockHash: boolean; + try { + // We won't be able to get the fee if the block hash is too old + const msg = versionedTx.message; + const fee = await conn.getFeeForMessage(msg); + shouldUpdateBlockHash = fee.value == null; + } catch (err) { + // Might need to update the block hash + console.warn( + `Failed to get fee for versioned transaction while checking` + + ` whether to update block hash: ${String(err)}` + ); + shouldUpdateBlockHash = true; + } + + // Try to update the block hash + if (shouldUpdateBlockHash) { + console.warn( + `Unsigned versioned transaction might have an` + + ` out-of-date block hash, trying to update it...` + ); + const backoff = [0, 500, 1_000, 2_000]; + let backoffi = 0; + // eslint-disable-next-line no-constant-condition + update_block_hash: while (true) { + if (backoffi >= backoff.length) { + // Just continue and hope for the best with old block hash... + console.warn( + `Failed to get latest blockhash after ${backoffi} attempts,` + + ` continuing with old block hash for versioned transaction...` + ); + break update_block_hash; + } + const backoffMs = backoff[backoffi]; + if (backoffMs > 0) { + console.warn( + `Waiting ${backoffMs}ms before retrying latest block` + + ` hash for versioned transaction...` + ); + await new Promise((res) => setTimeout(res, backoffMs)); + } + try { + const latest = await conn.getLatestBlockhash(); + versionedTx.message.recentBlockhash = latest.blockhash; + break update_block_hash; + } catch (err) { + console.warn( + `Failed to get latest blockhash on attempt` + + ` ${backoffi + 1}: ${String(err)}` + ); + } + backoffi++; + } + } + } // Sign the transaction message // Use the keyring running in the background script - const sigRes = await SolanaTransactionSigner({ account: options.from, network: options.network, @@ -252,7 +386,6 @@ export const executeSwap = async ( }); // Add third party signatures (eg Rango) - const { thirdPartySignatures } = enkSolTx; for (let i = 0, len = thirdPartySignatures.length; i < len; i++) { const { pubkey, signature } = enkSolTx.thirdPartySignatures[i]; versionedTx.addSignature( @@ -286,6 +419,22 @@ export const executeSwap = async ( // The Solana web3 library prompts you to call getLogs if your error is of type // SendTransactionError to get more info about the error if (err instanceof SendTransactionError) { + const errstr = String(err); + + // The error thrown here is shown to the user in the UI + // so make it descriptive if possible + if (errstr.includes("Blockhash not found")) { + console.error( + `Failed to send Solana swap transaction: blockhash not found`, + err + ); + throw new Error( + "Failed to send Solana swap transaction: blockhash not found." + + " Too much time may have passed between the creation and sending" + + " of the transaction" + ); + } + try { const logs = await err.getLogs(conn); console.error( diff --git a/packages/swap/src/index.ts b/packages/swap/src/index.ts index 9bee1de02..2acd9ba28 100644 --- a/packages/swap/src/index.ts +++ b/packages/swap/src/index.ts @@ -116,8 +116,7 @@ class Swap extends EventEmitter { // Solana this.providers = [ new Jupiter(this.api as Web3Solana, this.network), - // TODO: re-enable Rango on Solana when issues with it are fixed - // new Rango(this.api as Web3Solana, this.network), + new Rango(this.api as Web3Solana, this.network), // TODO: re-enable Changelly on Solana when issues with it are fixed // new Changelly(this.api, this.network), ]; diff --git a/packages/swap/src/providers/rango/index.ts b/packages/swap/src/providers/rango/index.ts index 4c89048e9..e604814d3 100644 --- a/packages/swap/src/providers/rango/index.ts +++ b/packages/swap/src/providers/rango/index.ts @@ -13,7 +13,6 @@ import { EvmTransaction as RangoEvmTransaction, RangoClient, TransactionStatus as RangoTransactionStatus, - MetaResponse, SwapRequest, BlockchainMeta, RoutingResultType, @@ -105,6 +104,15 @@ if (DEBUG) { debug = () => {}; } +type SupportedNetworkInfo = { + /** Standard base10 chain ID, can be obtained from `https://chainlist.org` */ + realChainId: string; + /** Rango's chainId for Solana is "mainnet-beta" */ + rangoChainId: string; + /** Rango blockchain name (Rango's identifier for the chain) of a network */ + rangoBlockchain: string; +}; + /** * `name` is the blockchain id on Rango * @@ -113,88 +121,164 @@ if (DEBUG) { * @see https://rango-api.readme.io/reference/meta * * ```sh - * curl 'https://api.rango.exchange/basic/meta?apiKey=c6381a79-2817-4602-83bf-6a641a409e32' -H 'Accept:application/json' + * # Rango token meta (list of all tokens with token metadata, blockchain info, etc) + * curl 'https://api.rango.exchange/basic/meta?apiKey=c6381a79-2817-4602-83bf-6a641a409e32' -sL -H 'Accept:application/json' | jq . + * # { + * # "tokens": [ + * # { + * # "blockchain": "ETH", + * # "symbol": "USDT", + * # "name": "USD Tether", + * # "isPopular": true, + * # "chainId": "1", + * # "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + * # "decimals": 6, + * # "image": "https://rango.vip/i/r3Oex6", + * # "blockchainImage": "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/ETH/icon.svg", + * # "usdPrice": 1.001, + * # "supportedSwappers": [ + * # "Arbitrum Bridge", + * # "ThorChain", + * # ... + * + * # Rango token count per blockchain + * curl 'https://api.rango.exchange/basic/meta?apiKey=c6381a79-2817-4602-83bf-6a641a409e32' -sL -H 'Accept:application/json' | jq --raw-output .tokens[].blockchain | sort | uniq -c | sort -n + * # count blockchain + * # ... + * # 36 MOONBEAM + * # 42 CELO + * # 48 OKC + * # 50 MOONRIVER + * # 55 AURORA + * # 56 LINEA + * # 58 ZKSYNC + * # 61 BLAST + * # 146 OSMOSIS + * # 147 HECO + * # 158 CRONOS + * # 301 OPTIMISM + * # 368 AVAX_CCHAIN + * # 437 BASE + * # 594 POLYGON + * # 596 ARBITRUM + * # 833 BSC + * # 1509 SOLANA + * # 5610 ETH + * + * # Rango token count per blockchain & chain id + * curl 'https://api.rango.exchange/basic/meta?apiKey=c6381a79-2817-4602-83bf-6a641a409e32' -sL -H 'Accept:application/json' | jq -r '.tokens[] | "\(.blockchain)\t\(.chainId)"' | sort | uniq -c | sort -n | sed 's/^ *\([0-9]*\) *\(.*\)/\1\t\2/' | column -s $'\t' -t + * # count blockchain chain id + * # ... + * # 50 MOONRIVER 1285 + * # 55 AURORA 1313161554 + * # 56 LINEA 59144 + * # 58 ZKSYNC 324 + * # 61 BLAST 81457 + * # 146 OSMOSIS osmosis-1 + * # 147 HECO 128 + * # 158 CRONOS 25 + * # 301 OPTIMISM 10 + * # 368 AVAX_CCHAIN 43114 + * # 437 BASE 8453 + * # 594 POLYGON 137 + * # 596 ARBITRUM 42161 + * # 833 BSC 56 + * # 1509 SOLANA mainnet-beta + * # 5610 ETH 1 * ``` */ -const supportedNetworks: { - [key in SupportedNetworkName]?: { - /** Standard base10 chain ID, can be obtained from `https://chainlist.org` */ - realChainId: string; - /** Rango's chainId for Solana is "mainnet-beta" */ - rangoChainId: string; - /** Rango name (Rango's identifier for the chain) of a network */ - name: string; - }; -} = { +const supportedNetworks: Readonly<{ + [key in SupportedNetworkName]?: SupportedNetworkInfo; +}> = { [SupportedNetworkName.Ethereum]: { realChainId: "1", rangoChainId: "1", - name: "ETH", + rangoBlockchain: "ETH", }, [SupportedNetworkName.Binance]: { realChainId: "56", rangoChainId: "56", - name: "BSC", + rangoBlockchain: "BSC", }, [SupportedNetworkName.Matic]: { realChainId: "137", rangoChainId: "137", - name: "POLYGON", + rangoBlockchain: "POLYGON", }, [SupportedNetworkName.Optimism]: { realChainId: "10", rangoChainId: "10", - name: "OPTIMISM", + rangoBlockchain: "OPTIMISM", }, [SupportedNetworkName.Avalanche]: { realChainId: "43114", rangoChainId: "43114", - name: "AVAX_CCHAIN", + rangoBlockchain: "AVAX_CCHAIN", }, [SupportedNetworkName.Fantom]: { realChainId: "250", rangoChainId: "250", - name: "FANTOM", + rangoBlockchain: "FANTOM", }, [SupportedNetworkName.Aurora]: { realChainId: "1313161554", rangoChainId: "1313161554", - name: "AURORA", + rangoBlockchain: "AURORA", }, [SupportedNetworkName.Gnosis]: { realChainId: "100", rangoChainId: "100", - name: "GNOSIS", + rangoBlockchain: "GNOSIS", }, [SupportedNetworkName.Arbitrum]: { realChainId: "42161", rangoChainId: "42161", - name: "ARBITRUM", + rangoBlockchain: "ARBITRUM", }, [SupportedNetworkName.Moonbeam]: { realChainId: "1284", rangoChainId: "1284", - name: "MOONBEAM", + rangoBlockchain: "MOONBEAM", + }, + [SupportedNetworkName.Solana]: { + realChainId: "900", + rangoChainId: "mainnet-beta", + rangoBlockchain: "SOLANA", }, - // TODO: Re-enable Solana when all issues with Rango and Solana on Enkrypt are fixed - // @2024-10-01 - // [SupportedNetworkName.Solana]: { - // realChainId: "900", - // rangoChainId: "mainnet-beta", - // name: "SOLANA", - // }, [SupportedNetworkName.Blast]: { realChainId: "81457", rangoChainId: "81457", - name: "BLAST", + rangoBlockchain: "BLAST", }, [SupportedNetworkName.Telos]: { realChainId: "40", rangoChainId: "40", - name: "TELOS", + rangoBlockchain: "TELOS", }, }; +// Freeze because we index below so modifications would make the indexes stale +Object.freeze(supportedNetworks); + +/** Enkrypt supported network name -> network info */ +const supportedNetworkInfoByName = new Map( + Object.entries(supportedNetworks) +) as unknown as Map; + +/** Rango blockchain name -> network info & enkrypt network name */ +const supportedNetworkByRangoBlockchain = new Map< + string, + { info: SupportedNetworkInfo; name: SupportedNetworkName } +>( + Object.entries(supportedNetworks).map(([supportedNetwork, networkInfo]) => [ + networkInfo.rangoBlockchain, + { + info: networkInfo, + name: supportedNetwork as unknown as SupportedNetworkName, + }, + ]) +); + type RangoEnkryptToken = { rangoMeta: RangoToken; token?: TokenType; @@ -213,7 +297,16 @@ class Rango extends ProviderClass { toTokens: ProviderToTokenResponse; - rangoMeta: MetaResponse; + rangoMeta: Readonly<{ + blockchains: ReadonlyArray; + blockchainsByName: ReadonlyMap; + tokens: ReadonlyArray; + tokensByAddress: ReadonlyMap, RangoToken>; + tokensByBlockchainAddress: ReadonlyMap< + `${string}-${Lowercase}`, + RangoToken + >; + }>; /** From GitHub */ swaplist: RangoEnkryptToken[]; @@ -231,7 +324,13 @@ class Rango extends ProviderClass { this.name = ProviderName.rango; this.fromTokens = {}; this.toTokens = {}; - this.rangoMeta = { blockchains: [], tokens: [], swappers: [] }; + this.rangoMeta = { + blockchains: [], + blockchainsByName: new Map(), + tokens: [], + tokensByAddress: new Map(), + tokensByBlockchainAddress: new Map(), + }; this.transactionsStatus = []; this.swaplist = []; this.swaplistMap = new Map(); @@ -240,18 +339,18 @@ class Rango extends ProviderClass { async init(tokenList?: TokenType[]): Promise { debug("init", `Initialising against ${tokenList?.length} tokens...`); - const [resMeta, swaplist] = await Promise.all([ + const [rangoMeta, swaplist] = await Promise.all([ rangoClient.meta({ excludeNonPopulars: true, transactionTypes: [ RangoTransactionType.EVM, - // TODO: re-enable Solana transactions when Rango issues on Solana are fixed - // RangoTransactionType.SOLANA, + RangoTransactionType.SOLANA, ], }), fetchRangoSwaplist(), ]); + /** Rango blockchain id + lowercase address -> rango token meta */ const swaplistMap = new Map< `${string}-${Lowercase}`, RangoEnkryptToken @@ -264,63 +363,145 @@ class Rango extends ProviderClass { swaplistMap.set(key, swaplistToken); } + const rangoBlockchains = rangoMeta.blockchains; + const rangoBlockchainsByName = new Map(); + for (let i = 0, len = rangoBlockchains.length; i < len; i++) { + const rangoBlockchain = rangoBlockchains[i]; + rangoBlockchainsByName.set(rangoBlockchain.name, rangoBlockchain); + } + + const rangoTokens = rangoMeta.tokens; + const rangoTokensByAddress = new Map, RangoToken>(); + for (let i = 0, len = rangoTokens.length; i < len; i++) { + const rangoToken = rangoTokens[i]; + const lcAddress = (rangoToken.address?.toLowerCase() ?? + NATIVE_TOKEN_ADDRESS) as Lowercase; + rangoTokensByAddress.set(lcAddress, rangoToken); + } + const rangoTokensByBlockchainAddress = new Map< + `${string}-${Lowercase}`, + RangoToken + >(); + for (let i = 0, len = rangoTokens.length; i < len; i++) { + const rangoToken = rangoTokens[i]; + const lcAddress = (rangoToken.address?.toLowerCase() ?? + NATIVE_TOKEN_ADDRESS) as Lowercase; + const key: `${string}-${Lowercase}` = `${rangoToken.blockchain}-${lcAddress}`; + rangoTokensByBlockchainAddress.set(key, rangoToken); + } + + this.tokenList = tokenList ?? []; this.swaplist = swaplist; this.swaplistMap = swaplistMap; + this.rangoMeta = { + blockchains: rangoBlockchains, + blockchainsByName: rangoBlockchainsByName, + tokens: rangoTokens, + tokensByAddress: rangoTokensByAddress, + tokensByBlockchainAddress: rangoTokensByBlockchainAddress, + }; - this.rangoMeta = resMeta; debug( "init", "Rango meta" + - ` tokens.length=${resMeta.tokens.length}` + - ` blockchains.length=${resMeta.blockchains.length}` + ` tokens.length=${rangoMeta.tokens.length}` + + ` blockchains.length=${rangoMeta.blockchains.length}` ); - const { blockchains, tokens } = resMeta; - if (!Rango.isSupported(this.network, blockchains)) { - debug("init", `Not supported on network ${this.network}`); + const supportedNetworkInfo = supportedNetworkInfoByName.get(this.network); + + if (!supportedNetworkInfo) { + debug("init", `Network not supported on Enkrypt+Rango: ${this.network}`); return; } - tokenList?.forEach((t) => { - const tokenSupport = tokens.find((token) => token.address === t.address); - if (this.isNativeToken(t.address) || tokenSupport) { - this.fromTokens[t.address] = t; + if ( + !Rango.isNetworkSupportedByRango(supportedNetworkInfo, rangoBlockchains) + ) { + debug("init", `Network not supported on Rango: ${this.network}`); + return; + } + + if (tokenList) { + // List available "from" tokens by matching between our swap list info and Rango token info, on this chain + for (let i = 0, len = tokenList.length; i < len; i++) { + /** Token from */ + const listToken = tokenList[i]; + const casedAddress = listToken.address; + const lcAddress = casedAddress.toLowerCase() as Lowercase; + const key: `${string}-${Lowercase}` = `${supportedNetworkInfo.rangoBlockchain}-${lcAddress}`; + /** Token from Rango API */ + const rangoToken = rangoTokensByBlockchainAddress.get(key); + if (this.isNativeToken(casedAddress) || rangoToken) { + this.fromTokens[casedAddress] = listToken; + } } - }); + } debug("init", `Finished initialising`); } + static findRangoBlockchainForSupportedNetwork( + supportedNetworkInfo: SupportedNetworkInfo, + rangoBlockchains: ReadonlyArray + ): undefined | BlockchainMeta { + const matchingRangoBlockchain = rangoBlockchains.find( + (rangoBlockchain: BlockchainMeta) => + rangoChainIdsEq( + rangoBlockchain.chainId, + supportedNetworkInfo.rangoChainId + ) + ); + + return matchingRangoBlockchain; + } + /** - * Do we support any of the blockchains that Rango supports? + * Is this network supported by both enkrypt and rango? */ - static isSupported( - network: SupportedNetworkName, - blockchains: BlockchainMeta[] - ) { - // We must support this network - if ( - !Object.keys(supportedNetworks).includes(network as unknown as string) - ) { + static isNetworkSupported( + supportedNetworkName: SupportedNetworkName, + rangoBlockchains: ReadonlyArray + ): boolean { + const supportedNetworkInfo = + supportedNetworkInfoByName.get(supportedNetworkName); + + // Enkrypt must support this network on Rango + if (!supportedNetworkInfo) { return false; } - if (blockchains.length) { - // Join Rango networks and our supported networks by their chain id + return this.isNetworkSupportedByRango( + supportedNetworkInfo, + rangoBlockchains + ); + } - // Extract our info about this supported network - const [, { rangoChainId }] = Object.entries(supportedNetworks).find( - ([supportedNetworkName]) => - supportedNetworkName === (network as unknown as string) - ); - const enabled = !!blockchains.find((rangoBlockchain: BlockchainMeta) => - rangoChainIdsEq(rangoBlockchain.chainId, rangoChainId) - )?.enabled; - return enabled; + /** + * Is this network that *we* support also supported by rango? + */ + static isNetworkSupportedByRango( + supportedNetworkInfo: SupportedNetworkInfo, + rangoBlockchains: ReadonlyArray + ): boolean { + if (!rangoBlockchains.length) { + // Rango didn't give us anything so just assume Rango supports this network + // (maybe due to rango api error or something?) + return true; } - // Rango didn't give us anything so just assume Rango supports this network - return true; + // Rango must support this network + + // Find the rango blockchain that corresponds to this enkrypt network, if exists + const matchingRangoBlockchain = this.findRangoBlockchainForSupportedNetwork( + supportedNetworkInfo, + rangoBlockchains + ); + + // Supported if + // 1. Rango supports this chain and + // 2. Rango is enabled on this chain + return matchingRangoBlockchain?.enabled ?? false; } getFromTokens(): ProviderFromTokenResponse { @@ -329,30 +510,23 @@ class Rango extends ProviderClass { getToTokens(): ProviderToTokenResponse { const { tokens } = this.rangoMeta; - const supportedCRangoNames = Object.values(supportedNetworks).map( - (s) => s.name - ); - const rangoToNetwork: Record = {}; - Object.keys(supportedNetworks).forEach((net) => { - rangoToNetwork[supportedNetworks[net].name] = - net as unknown as SupportedNetworkName; - }); - tokens?.forEach((rangoToken) => { - // Unrecognised network - if (!supportedCRangoNames.includes(rangoToken.blockchain)) return; - - const network = rangoToNetwork[rangoToken.blockchain]; + for (let i = 0, len = tokens.length; i < len; i++) { + const token = tokens[i]; + const supportedNetwork = supportedNetworkByRangoBlockchain.get( + token.blockchain + ); - this.toTokens[network] ??= {}; + // Unrecognised network (Rango supports it but we don't) + if (!supportedNetwork) continue; - const address = rangoToken.address || NATIVE_TOKEN_ADDRESS; + const address = token.address || NATIVE_TOKEN_ADDRESS; const lcaddress = address.toLowerCase() as Lowercase; - const key: `${string}-${Lowercase}` = `${rangoToken.blockchain}-${lcaddress}`; + const key: `${string}-${Lowercase}` = `${token.blockchain}-${lcaddress}`; const swaplistToken = this.swaplistMap.get(key); let type: NetworkType; - switch (network) { + switch (supportedNetwork.name) { case SupportedNetworkName.Solana: type = NetworkType.Solana; break; @@ -362,24 +536,26 @@ class Rango extends ProviderClass { } const toToken: TokenTypeTo = { - ...rangoToken, + ...token, address, - name: rangoToken.name || rangoToken.symbol, - logoURI: rangoToken.image, + name: token.name || token.symbol, + logoURI: token.image, type, - price: rangoToken.usdPrice, + price: token.usdPrice, cgId: swaplistToken?.token?.cgId, - symbol: rangoToken.symbol, - decimals: rangoToken.decimals, + symbol: token.symbol, + decimals: token.decimals, balance: undefined, rank: swaplistToken?.token?.rank, networkInfo: { - name: rangoToNetwork[rangoToken.blockchain], - isAddress: getIsAddressAsync(network), + name: supportedNetwork.name, + isAddress: getIsAddressAsync(supportedNetwork.name), }, }; - this.toTokens[network][address] = toToken; - }); + + this.toTokens[supportedNetwork.name] ??= {}; + this.toTokens[supportedNetwork.name][address] = toToken; + } return this.toTokens; } @@ -389,8 +565,11 @@ class Rango extends ProviderClass { * For cross-chain tokens like Ethereum (ETH) on the Binance Smart Chain (BSC) network, * it returns the corresponding symbol like WETH. */ - private getSymbol(token: TokenType): string | undefined { - const { tokens } = this.rangoMeta; + private getRangoTokenSymbol( + token: TokenType, + rangoBlockchain: BlockchainMeta + ): string | undefined { + const { tokensByBlockchainAddress } = this.rangoMeta; if (this.isNativeToken(token.address)) return token.symbol; if (token.address == null) { console.warn( @@ -399,10 +578,9 @@ class Rango extends ProviderClass { ); return undefined; } - const lc = token.address.toLowerCase(); - return Object.values(tokens || []).find( - (t) => t.address?.toLowerCase() === lc - )?.symbol; + const lcAddress = token.address.toLowerCase() as Lowercase; + const key: `${string}-${Lowercase}` = `${rangoBlockchain.name}-${lcAddress}`; + return tokensByBlockchainAddress.get(key)?.symbol; } private isNativeToken(address: string) { @@ -430,79 +608,113 @@ class Rango extends ProviderClass { 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}` + ` toNetwork=${options.toToken.networkInfo.name}` + + ` fromToken=${options.fromToken.symbol}` + + ` toToken=${options.toToken.symbol}` + + ` fromAddress=${options.fromAddress}` + + ` toAddress=${options.toAddress}` ); try { // Determine whether Enkrypt + Rango supports this swap abortable?.signal?.throwIfAborted(); - // Do we support Rango on this network? - if ( - !Rango.isSupported( - options.toToken.networkInfo.name as SupportedNetworkName, - blockchains - ) || - !Rango.isSupported(this.network, blockchains) - ) { + // We must support Rango on the source network + // (note: probably redundant since we should always support rango on the source network if we got this far) + const fromNetworkInfo = supportedNetworkInfoByName.get(this.network); + if (!fromNetworkInfo) { + debug( + "getRangoSwap", + "No swap:" + + ` Enkrypt does not support Rango swap on the source network` + + ` fromNetwork=${this.network}` + ); + } + + // We must support Rango on the destination network + const toNetworkInfo = supportedNetworkInfoByName.get( + options.toToken.networkInfo.name as SupportedNetworkName + ); + if (!toNetworkInfo) { + debug( + "getRangoSwap", + "No swap:" + + ` Enkrypt does not support Rango swap on the destination network` + + ` fromNetwork=${this.network}` + ); + } + + // Rango must support the source network + const fromRangoBlockchain = Rango.findRangoBlockchainForSupportedNetwork( + fromNetworkInfo, + blockchains + ); + if (!fromRangoBlockchain?.enabled) { debug( "getRangoSwap", `No swap:` + - ` Enkrypt does not support Rango swap on the destination` + - ` network ${options.toToken.networkInfo.name}` + ` Rango does not support swap on the source network` + + ` fromNetwork=${this.network}` + + ` fromBlockchain=${fromRangoBlockchain.name}` + + ` enabled=${fromRangoBlockchain.enabled}` ); - return Promise.resolve(null); + return null; } + const fromRangoBlockchainName = fromRangoBlockchain.name; + + // Rango must support the destination network + const toRangoBlockchain = Rango.findRangoBlockchainForSupportedNetwork( + toNetworkInfo, + blockchains + ); + if (!toRangoBlockchain?.enabled) { + debug( + "getRangoSwap", + `No swap:` + + ` Rango does not support swap on the destination network` + + ` toNetwork=${options.toToken.networkInfo.name}` + + ` toBlockchain=${toRangoBlockchain.name}` + + ` enabled=${toRangoBlockchain.enabled}` + ); + return null; + } + const toRangoBlockchainName = toRangoBlockchain.name; // Does Rango support these tokens? const feeConfig = FEE_CONFIGS[this.name][meta.walletIdentifier]; - /** Source token Rango blockchain name */ - const fromBlockchain = blockchains.find((rangoBlockchain) => - rangoChainIdsEq( - rangoBlockchain.chainId, - supportedNetworks[this.network].rangoChainId - ) - )?.name; - - /** Destination token Rango blockchain name */ - const toBlockchain = blockchains.find((rangoBlockchain) => - rangoChainIdsEq( - rangoBlockchain.chainId, - supportedNetworks[options.toToken.networkInfo.name].rangoChainId - ) - )?.name; - debug( "getRangoSwap", `Rango block chains ids` + - ` fromBlokchain=${fromBlockchain}` + - ` toBlockchain=${toBlockchain}` + ` fromRangoBlockchain=${fromRangoBlockchainName}` + + ` toRangoBlockchain=${toRangoBlockchainName}` ); const fromTokenAddress = options.fromToken.address; const toTokenAddress = options.toToken.address; /** Source token symbol */ - const fromSymbol = this.getSymbol(options.fromToken); + const fromRangoTokenSymbol = this.getRangoTokenSymbol( + options.fromToken, + fromRangoBlockchain + ); /** Destination token symbol */ - const toSymbol = this.getSymbol(options.toToken); + const toRangoTokenSymbol = this.getRangoTokenSymbol( + options.toToken, + toRangoBlockchain + ); // If we can't get symbols for the tokens then we don't support them - if (!fromSymbol || !toSymbol) { + if (!fromRangoTokenSymbol || !toRangoTokenSymbol) { debug( "getRangoSwap", `No swap: No symbol for src token or dst token` + - ` srcTokenSymbol=${fromSymbol}` + - ` dstTokenSymbol=${toSymbol}` + ` fromTokenSymbol=${fromRangoTokenSymbol}` + + ` toTokenSymbol=${toRangoTokenSymbol}` ); - return Promise.resolve(null); + return null; } // Enkrypt & Rango both likely support this swap (pair & networks) @@ -518,13 +730,13 @@ class Rango extends ProviderClass { address: !this.isNativeToken(fromTokenAddress) ? fromTokenAddress : null, - blockchain: fromBlockchain, - symbol: fromSymbol, + blockchain: fromRangoBlockchainName, + symbol: fromRangoTokenSymbol, }, to: { address: !this.isNativeToken(toTokenAddress) ? toTokenAddress : null, - blockchain: toBlockchain, - symbol: toSymbol, + blockchain: toRangoBlockchainName, + symbol: toRangoTokenSymbol, }, amount: options.amount.toString(), fromAddress: options.fromAddress, @@ -538,17 +750,17 @@ class Rango extends ProviderClass { debug( "getRangoSwap", `Requesting quote from rango sdk...` + - ` fromBlockchain=${fromBlockchain}` + - ` toBlockchain=${toBlockchain}` + - ` fromToken=${fromSymbol}` + - ` toToken=${toSymbol}` + + ` fromRangoBlockchain=${fromRangoBlockchainName}` + + ` toRangoBlockchain=${toRangoBlockchainName}` + + ` fromToken=${fromRangoTokenSymbol}` + + ` toToken=${toRangoTokenSymbol}` + ` fromAddress=${options.fromAddress}` + ` toAddress=${options.toAddress}` + ` amount=${options.amount.toString()}` + ` slippage=${slippage}` + ` referrerFee=${params.referrerFee}` ); - const rangoSwapResponse = await rangoClient.swap(params); + const rangoSwapResponse = await rangoClient.swap(params, abortable); debug("getRangoSwap", `Received quote from rango sdk`); abortable?.signal?.throwIfAborted(); @@ -560,7 +772,7 @@ class Rango extends ProviderClass { // Rango experienced some kind of error or is unable to route the swap debug("getRangoSwap", `Rango swap SDK returned an error`); console.error("Rango swap SDK error:", rangoSwapResponse.error); - return Promise.resolve(null); + return null; } debug("getRangoSwap", `Rango swap SDK returned OK`); @@ -702,10 +914,10 @@ class Rango extends ProviderClass { sig.signature ).toString("hex")}`; } - warnMsg += ` fromBlockchain=${fromBlockchain}`; - warnMsg += ` toBlockchain=${toBlockchain}`; - warnMsg += ` fromToken=${fromSymbol}`; - warnMsg += ` toToken=${toSymbol}`; + warnMsg += ` fromRangoBlockchain=${fromRangoBlockchainName}`; + warnMsg += ` toRangoBlockchain=${toRangoBlockchainName}`; + warnMsg += ` fromToken=${fromRangoTokenSymbol}`; + warnMsg += ` toToken=${toRangoTokenSymbol}`; warnMsg += ` fromAddress=${options.fromAddress}`; warnMsg += ` toAddress=${options.toAddress}`; warnMsg += ` amount=${options.amount.toString()}`; @@ -731,10 +943,10 @@ class Rango extends ProviderClass { else warnMsg += ` unsigned`; warnMsg += ` legacy transaction,`; warnMsg += ` dropping Rango swap transaction`; - warnMsg += ` fromBlockchain=${fromBlockchain}`; - warnMsg += ` toBlockchain=${toBlockchain}`; - warnMsg += ` fromToken=${fromSymbol}`; - warnMsg += ` toToken=${toSymbol}`; + warnMsg += ` fromRangoBlockchain=${fromRangoBlockchainName}`; + warnMsg += ` toRangoBlockchain=${toRangoBlockchainName}`; + warnMsg += ` fromToken=${fromRangoTokenSymbol}`; + warnMsg += ` toToken=${toRangoTokenSymbol}`; warnMsg += ` fromAddress=${options.fromAddress}`; warnMsg += ` toAddress=${options.toAddress}`; warnMsg += ` amount=${options.amount.toString()}`; @@ -795,7 +1007,14 @@ class Rango extends ProviderClass { extractSignaturesFromRangoTransaction(rangoSwapResponse.tx); // Verify Rango signatures incase rango gives us invalid transaction signatures - debug("getRangoSwap", "Checking Rango signatures..."); + debug( + "getRangoSwap", + `Checking Rango signatures...` + + ` signatures=${thirdPartySignatures.length}`, + ` pubkeys=${thirdPartySignatures + .map(({ pubkey }) => pubkey) + .join(",")}` + ); const signaturesAreValid = checkSolanaVersionedTransactionSignatures( versionedTx, @@ -805,8 +1024,8 @@ class Rango extends ProviderClass { let warnMsg = `Rango Solana signed versioned transaction has invalid Rango signatures,`; warnMsg += ` dropping Rango swap transaction`; for ( - let tpsigi = 0; - tpsigi < thirdPartySignatures.length; + let tpsigi = 0, tpsiglen = thirdPartySignatures.length; + tpsigi < tpsiglen; tpsigi++ ) { const sig = thirdPartySignatures[tpsigi]; @@ -815,10 +1034,10 @@ class Rango extends ProviderClass { sig.signature ).toString("hex")}`; } - warnMsg += ` fromBlockchain=${fromBlockchain}`; - warnMsg += ` toBlockchain=${toBlockchain}`; - warnMsg += ` fromToken=${fromSymbol}`; - warnMsg += ` toToken=${toSymbol}`; + warnMsg += ` fromRangoBlockchain=${fromRangoBlockchainName}`; + warnMsg += ` toRangoBlockchain=${toRangoBlockchainName}`; + warnMsg += ` fromToken=${fromRangoTokenSymbol}`; + warnMsg += ` toToken=${toRangoTokenSymbol}`; warnMsg += ` fromAddress=${options.fromAddress}`; warnMsg += ` toAddress=${options.toAddress}`; warnMsg += ` amount=${options.amount.toString()}`; @@ -829,7 +1048,7 @@ class Rango extends ProviderClass { } // Sometimes Rango gives us bad transactions @ 2024-09-25 - // so we don't let them quote the user + // Simulate the transaction to check if it actually works so we don't use it to quote the user debug("getRangoSwap", "Simulating transaction..."); const statusResult = await checkExpectedSolanaVersionedTransactionStatus( @@ -844,10 +1063,10 @@ class Rango extends ProviderClass { else warnMsg += ` unsigned`; warnMsg += ` versioned transaction,`; warnMsg += ` dropping Rango swap transaction`; - warnMsg += ` fromBlockchain=${fromBlockchain}`; - warnMsg += ` toBlockchain=${toBlockchain}`; - warnMsg += ` fromToken=${fromSymbol}`; - warnMsg += ` toToken=${toSymbol}`; + warnMsg += ` fromRangoBlockchain=${fromRangoBlockchainName}`; + warnMsg += ` toRangoBlockchain=${toRangoBlockchainName}`; + warnMsg += ` fromToken=${fromRangoTokenSymbol}`; + warnMsg += ` toToken=${toRangoTokenSymbol}`; warnMsg += ` fromAddress=${options.fromAddress}`; warnMsg += ` toAddress=${options.toAddress}`; warnMsg += ` amount=${options.amount.toString()}`; @@ -863,7 +1082,7 @@ class Rango extends ProviderClass { type: TransactionType.solana, from: rangoSwapResponse.tx.from, to: options.toToken.address, - kind: "legacy", + kind: "versioned", serialized: Buffer.from(versionedTx.serialize()).toString( "base64" ), @@ -912,14 +1131,12 @@ class Rango extends ProviderClass { return result; } catch (err) { - if (abortable?.signal?.aborted && err === abortable.signal.reason) { - // Aborted & error is the abort error - return null; + if (!abortable?.signal?.aborted) { + console.error( + `Error getting Rango swap, returning empty response (no swap)`, + err + ); } - console.error( - `Error getting Rango swap, returning empty response (no swap)`, - err - ); return null; } } @@ -1206,7 +1423,9 @@ async function fetchRangoSwaplist(abortable?: { 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 len = msg.length; if (len > 512 + 10 + len.toString().length) msg = `${msg.slice(0, 512)}... (512/${len})`; @@ -1375,11 +1594,12 @@ async function checkExpectedSolanaLegacyTransactionStatus( | { succeeds: true; error?: undefined } | { succeeds: false; error: TransactionError } > { - const retries = [0, 1_000, 2_000]; + const retries = [0, 500, 1_000, 2_000]; let retryidx = 0; let errref: undefined | { txerr: TransactionError }; let success = false; while (!success) { + abortable?.signal?.throwIfAborted(); if (retryidx >= retries.length) { return { succeeds: false, error: errref!.txerr }; } @@ -1396,6 +1616,7 @@ async function checkExpectedSolanaLegacyTransactionStatus( ` with updated block hash ${latestBlockHash.blockhash}...` ); legacyTx.recentBlockhash = latestBlockHash.blockhash; + abortable?.signal?.throwIfAborted(); } const sim = await conn.simulateTransaction(legacyTx); if (sim.value.err) { From 0b19bc9b3fe961cee6430327f19cc61701c0dda7 Mon Sep 17 00:00:00 2001 From: nickkelly1 Date: Fri, 18 Oct 2024 22:52:14 -0500 Subject: [PATCH 15/18] chore: fix comments --- .../src/ui/action/views/swap/libs/send-transactions.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts b/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts index d2fd2f22e..983a0111c 100644 --- a/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts +++ b/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts @@ -204,7 +204,9 @@ export const executeSwap = async ( thirdPartySignatures.length > 0; if (hasThirdPartySignatures) { - // Can't sign the transaction since it's signed / to be signed by third parties + // Can't update the block hash since it's signed / to be signed by third parties + // If the user waits too long before sending, the transaction may fail due to + // becoming out-of-date } else { // We can update the transaction since it's unsigned // Check if we need to update the block hash @@ -312,7 +314,9 @@ export const executeSwap = async ( thirdPartySignatures.length > 0; if (hasThirdPartySignatures) { - // Can't sign the transaction since it's signed / to be signed by third parties + // Can't update the block hash since it's signed / to be signed by third parties + // If the user waits too long before sending, the transaction may fail due to + // becoming out-of-date } else { // We can update the transaction since it's unsigned // Check if we need to update the block hash From 76430adb83b12b6c70c175825490f09c32b08e2f Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:26:00 -0700 Subject: [PATCH 16/18] fix: solana max send --- .../solana/ui/send-transaction/verify-transaction/index.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/providers/solana/ui/send-transaction/verify-transaction/index.vue b/packages/extension/src/providers/solana/ui/send-transaction/verify-transaction/index.vue index f35413cd1..7222d543e 100644 --- a/packages/extension/src/providers/solana/ui/send-transaction/verify-transaction/index.vue +++ b/packages/extension/src/providers/solana/ui/send-transaction/verify-transaction/index.vue @@ -158,12 +158,16 @@ const sendAction = async () => { transactiontemp.instructions, transactiontemp.feePayer!, [] - ); + ).catch((e) => { + console.error("ERROR", e); + return 0; + }); if (computeUnits) { transactiontemp.instructions.unshift( ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits + 5000 }) // adding few extra CUs as a buffer ); } + const latestBlock = await solAPI.web3.getLatestBlockhash(); transactiontemp.recentBlockhash = latestBlock.blockhash; const transaction = new VersionedTransaction( From af95d8c85b1279d19aab834dc859b7dcabcef119 Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:12:04 -0700 Subject: [PATCH 17/18] fix: tests --- packages/swap/tests/swap.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swap/tests/swap.test.ts b/packages/swap/tests/swap.test.ts index f790d7364..0eb5de6c1 100644 --- a/packages/swap/tests/swap.test.ts +++ b/packages/swap/tests/swap.test.ts @@ -89,7 +89,7 @@ describe("Swap", () => { expect(swapOneInch?.transactions.length).to.be.eq(2); const swapChangelly = await enkryptSwap.getSwap(changellyQuote!.quote); if (swapChangelly) expect(swapChangelly?.transactions.length).to.be.eq(1); - }).timeout(10000); + }).timeout(20000); it("it should get quote and swap for same destination", async () => { await enkryptSwap.initPromise; From aa8058a4ecd70c950b9c4e7fb90f54e45743f150 Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:21:46 -0700 Subject: [PATCH 18/18] fix: tests --- packages/swap/tests/swap.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swap/tests/swap.test.ts b/packages/swap/tests/swap.test.ts index 0eb5de6c1..862140943 100644 --- a/packages/swap/tests/swap.test.ts +++ b/packages/swap/tests/swap.test.ts @@ -117,5 +117,5 @@ describe("Swap", () => { expect(oneInceQuote!.provider).to.be.eq(ProviderName.oneInch); expect(paraswapQuote!.provider).to.be.eq(ProviderName.paraswap); // expect(rangoQuote!.provider).to.be.eq(ProviderName.rango); - }).timeout(10000); + }).timeout(20000); });