diff --git a/e2e/screenshots/thor_solana/all-swaps-pending.png b/e2e/screenshots/thor_solana/all-swaps-pending.png new file mode 100644 index 00000000000..6b2c294fffe Binary files /dev/null and b/e2e/screenshots/thor_solana/all-swaps-pending.png differ diff --git a/e2e/screenshots/thor_solana/eth-to-rune-confirm.png b/e2e/screenshots/thor_solana/eth-to-rune-confirm.png new file mode 100644 index 00000000000..073a31242c1 Binary files /dev/null and b/e2e/screenshots/thor_solana/eth-to-rune-confirm.png differ diff --git a/e2e/screenshots/thor_solana/eth-to-rune-pending.png b/e2e/screenshots/thor_solana/eth-to-rune-pending.png new file mode 100644 index 00000000000..0866e500f14 Binary files /dev/null and b/e2e/screenshots/thor_solana/eth-to-rune-pending.png differ diff --git a/e2e/screenshots/thor_solana/eth-to-rune-quote.png b/e2e/screenshots/thor_solana/eth-to-rune-quote.png new file mode 100644 index 00000000000..51463de93a9 Binary files /dev/null and b/e2e/screenshots/thor_solana/eth-to-rune-quote.png differ diff --git a/e2e/screenshots/thor_solana/eth-to-usdc-confirm.png b/e2e/screenshots/thor_solana/eth-to-usdc-confirm.png new file mode 100644 index 00000000000..9d8de36995b Binary files /dev/null and b/e2e/screenshots/thor_solana/eth-to-usdc-confirm.png differ diff --git a/e2e/screenshots/thor_solana/eth-to-usdc-pending.png b/e2e/screenshots/thor_solana/eth-to-usdc-pending.png new file mode 100644 index 00000000000..c41216526ee Binary files /dev/null and b/e2e/screenshots/thor_solana/eth-to-usdc-pending.png differ diff --git a/e2e/screenshots/thor_solana/eth-to-usdc-quote.png b/e2e/screenshots/thor_solana/eth-to-usdc-quote.png new file mode 100644 index 00000000000..f83856b665f Binary files /dev/null and b/e2e/screenshots/thor_solana/eth-to-usdc-quote.png differ diff --git a/e2e/screenshots/thor_solana/rune-to-sol.png b/e2e/screenshots/thor_solana/rune-to-sol.png new file mode 100644 index 00000000000..36396f09a12 Binary files /dev/null and b/e2e/screenshots/thor_solana/rune-to-sol.png differ diff --git a/e2e/screenshots/thor_solana/sol-rune-v2-confirm.png b/e2e/screenshots/thor_solana/sol-rune-v2-confirm.png new file mode 100644 index 00000000000..823912584f6 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-rune-v2-confirm.png differ diff --git a/e2e/screenshots/thor_solana/sol-rune-v2-quote.png b/e2e/screenshots/thor_solana/sol-rune-v2-quote.png new file mode 100644 index 00000000000..03b17402abe Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-rune-v2-quote.png differ diff --git a/e2e/screenshots/thor_solana/sol-rune-v2-success.png b/e2e/screenshots/thor_solana/sol-rune-v2-success.png new file mode 100644 index 00000000000..55284ba3328 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-rune-v2-success.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-confirm.png b/e2e/screenshots/thor_solana/sol-to-rune-confirm.png new file mode 100644 index 00000000000..f7f03c6dd4b Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-confirm.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-failed-compute-budget.png b/e2e/screenshots/thor_solana/sol-to-rune-failed-compute-budget.png new file mode 100644 index 00000000000..15ec7c96724 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-failed-compute-budget.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-fixed-confirm.png b/e2e/screenshots/thor_solana/sol-to-rune-fixed-confirm.png new file mode 100644 index 00000000000..bc6978c12c1 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-fixed-confirm.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-fixed-quote.png b/e2e/screenshots/thor_solana/sol-to-rune-fixed-quote.png new file mode 100644 index 00000000000..621e5f77461 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-fixed-quote.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-quote-e2e.png b/e2e/screenshots/thor_solana/sol-to-rune-quote-e2e.png new file mode 100644 index 00000000000..fded214f061 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-quote-e2e.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-success-final.png b/e2e/screenshots/thor_solana/sol-to-rune-success-final.png new file mode 100644 index 00000000000..e75d1a69b57 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-success-final.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-success.png b/e2e/screenshots/thor_solana/sol-to-rune-success.png new file mode 100644 index 00000000000..b5734f9c3f2 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-success.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-v3-confirm.png b/e2e/screenshots/thor_solana/sol-to-rune-v3-confirm.png new file mode 100644 index 00000000000..eb0813f4ba2 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-v3-confirm.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-v3-quote.png b/e2e/screenshots/thor_solana/sol-to-rune-v3-quote.png new file mode 100644 index 00000000000..8af60835922 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-v3-quote.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune-v3-result.png b/e2e/screenshots/thor_solana/sol-to-rune-v3-result.png new file mode 100644 index 00000000000..4a357266205 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune-v3-result.png differ diff --git a/e2e/screenshots/thor_solana/sol-to-rune.png b/e2e/screenshots/thor_solana/sol-to-rune.png new file mode 100644 index 00000000000..6e8213706e7 Binary files /dev/null and b/e2e/screenshots/thor_solana/sol-to-rune.png differ diff --git a/packages/swapper/src/swappers/MayachainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts b/packages/swapper/src/swappers/MayachainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts index 145df3b3514..3932760b802 100644 --- a/packages/swapper/src/swappers/MayachainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts +++ b/packages/swapper/src/swappers/MayachainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts @@ -11,4 +11,4 @@ export const mayaPoolAssetIdToAssetId = (id: string): AssetId | undefined => mayaPoolIdAssetIdSymbolMap[id.toUpperCase()] export const assetIdToMayaPoolAssetId = ({ assetId }: { assetId: AssetId }): string | undefined => - assetIdToMayaPoolAssetIdMap[assetId.toLowerCase()] + assetIdToMayaPoolAssetIdMap[assetId] diff --git a/packages/swapper/src/swappers/ThorchainSwapper/ThorchainSwapper.ts b/packages/swapper/src/swappers/ThorchainSwapper/ThorchainSwapper.ts index 5a76aa8674a..971a4ee6084 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/ThorchainSwapper.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/ThorchainSwapper.ts @@ -9,6 +9,9 @@ export const thorchainSwapper: Swapper = { executeUtxoTransaction: (txToSign, { signAndBroadcastTransaction }) => { return signAndBroadcastTransaction(txToSign) }, + executeSolanaTransaction: (txToSign, { signAndBroadcastTransaction }) => { + return signAndBroadcastTransaction(txToSign) + }, executeTronTransaction: (txToSign, { signAndBroadcastTransaction }) => { return signAndBroadcastTransaction(txToSign) }, diff --git a/packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts b/packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts index ad20a1b0d4f..d32fd143b54 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts @@ -7,6 +7,7 @@ import { cosmossdk, evm, getInboundAddressDataForChain, + solana, tron, utxo, } from '../../thorchain-utils' @@ -25,6 +26,8 @@ export const thorchainApi: SwapperApi = { getEvmTransactionFees: input => evm.getEvmTransactionFees(input, swapperName), getUnsignedUtxoTransaction: input => utxo.getUnsignedUtxoTransaction(input, swapperName), getUtxoTransactionFees: input => utxo.getUtxoTransactionFees(input, swapperName), + getUnsignedSolanaTransaction: input => solana.getUnsignedSolanaTransaction(input, swapperName), + getSolanaTransactionFees: input => solana.getSolanaTransactionFees(input, swapperName), getUnsignedTronTransaction: input => tron.getUnsignedTronTransaction(input, swapperName), getTronTransactionFees: input => tron.getTronTransactionFees(input, swapperName), getUnsignedCosmosSdkTransaction: async ({ diff --git a/packages/swapper/src/swappers/ThorchainSwapper/generated/generatedTradableAssetMap.json b/packages/swapper/src/swappers/ThorchainSwapper/generated/generatedTradableAssetMap.json index 907df310cc0..4fed2b764e5 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/generated/generatedTradableAssetMap.json +++ b/packages/swapper/src/swappers/ThorchainSwapper/generated/generatedTradableAssetMap.json @@ -37,6 +37,7 @@ "ETH.YFI-0X0BC529C00C6401AEF6D220BE8C6EA1667F6AD93E": "eip155:1/erc20:0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e", "GAIA.ATOM": "cosmos:cosmoshub-4/slip44:118", "LTC.LTC": "bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2", + "SOL.SOL": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501", "THOR.RUJI": "cosmos:thorchain-1/slip44:ruji", "THOR.TCY": "cosmos:thorchain-1/slip44:tcy", "TRON.TRX": "tron:0x2b6653dc/slip44:195", diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.test.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.test.ts index 9448dc40553..9340ed1e011 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.test.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.test.ts @@ -1,4 +1,4 @@ -import { btcAssetId, ethAssetId } from '@shapeshiftoss/caip' +import { btcAssetId, ethAssetId, solAssetId } from '@shapeshiftoss/caip' import { describe, expect, it } from 'vitest' import { assetIdToThorPoolAssetId, thorPoolAssetIdToAssetId } from './poolAssetHelpers' @@ -22,6 +22,11 @@ describe('poolAssetHelpers', () => { expect(result).toEqual(usdcAssetId) }) + it('returns Solana assetId when poolAssetId is SOL.SOL', () => { + const result = thorPoolAssetIdToAssetId('SOL.SOL') + expect(result).toEqual(solAssetId) + }) + it('returns undefined for an asset we dont support', () => { const result = thorPoolAssetIdToAssetId('BNB.AVA-645') expect(result).toEqual(undefined) @@ -55,6 +60,13 @@ describe('poolAssetHelpers', () => { expect(result).toEqual(poolAssetId) }) + it('returns SOL pool when assetId is solAssetId (base58 case-sensitive)', () => { + const assetId = solAssetId + const result = assetIdToThorPoolAssetId({ assetId }) + const poolAssetId = 'SOL.SOL' + expect(result).toEqual(poolAssetId) + }) + it('returns undefined for an unsupported asset', () => { const assetId = 'foobar' const result = assetIdToThorPoolAssetId({ assetId }) diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts index 46323455aec..0175c89e135 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts @@ -11,4 +11,4 @@ export const thorPoolAssetIdToAssetId = (id: string): AssetId | undefined => thorPoolIdAssetIdSymbolMap[id.toUpperCase()] export const assetIdToThorPoolAssetId = ({ assetId }: { assetId: AssetId }): string | undefined => - assetIdToThorPoolAssetIdMap[assetId.toLowerCase()] + assetIdToThorPoolAssetIdMap[assetId] diff --git a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts index 2c8b2053840..cda8ec7234f 100644 --- a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts +++ b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts @@ -14,6 +14,7 @@ import { } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' +import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js' import { TronWeb } from 'tronweb' import { v4 as uuid } from 'uuid' @@ -42,6 +43,7 @@ import { getNativePrecision, getSwapSource, } from './index' +import * as solana from './solana' import * as tron from './tron' import type { ThorEvmTradeQuote, @@ -57,6 +59,7 @@ import type { import * as utxo from './utxo' const SAFE_GAS_LIMIT = '100000' // depositWithExpiry() +const SOLANA_MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' type ThorTradeRateOrQuote = ThorTradeRate | ThorTradeQuote type ThorEvmTradeRateOrQuote = ThorEvmTradeRate | ThorEvmTradeQuote @@ -102,6 +105,7 @@ export const getL1RateOrQuote = async ( assertGetEvmChainAdapter, assertGetUtxoChainAdapter, assertGetCosmosSdkChainAdapter, + assertGetSolanaChainAdapter, } = deps // "NativePrecision" is intended to indicate the base unit precision of the asset @@ -442,12 +446,58 @@ export const getL1RateOrQuote = async ( ) } case CHAIN_NAMESPACE.Solana: { - return Err( - makeSwapErrorRight({ - message: 'Solana is not supported', - code: TradeQuoteError.UnsupportedTradePair, + const adapter = assertGetSolanaChainAdapter(sellAsset.chainId) + const sendAddress = (input as CommonTradeQuoteInput).sendAddress + + const maybeRoutes = await Promise.allSettled( + perRouteValues.map(async (route): Promise => { + const memo = getMemo(route) + const protocolFees = getProtocolFees(route.quote) + + const feeData = await (async (): Promise => { + if (!sendAddress) return { networkFeeCryptoBaseUnit: undefined, protocolFees } + const { vault } = await solana.getThorTxData({ sellAsset, config, swapperName }) + const memoInstruction = new TransactionInstruction({ + keys: [], + programId: new PublicKey(SOLANA_MEMO_PROGRAM_ID), + data: Buffer.from(memo, 'utf8'), + }) + const transferInstruction = SystemProgram.transfer({ + fromPubkey: new PublicKey(sendAddress), + toPubkey: new PublicKey(vault), + lamports: BigInt(sellAmountCryptoBaseUnit), + }) + const { fast } = await adapter.getFeeData({ + to: vault, + value: '0', + chainSpecific: { + from: sendAddress, + tokenId: contractAddressOrUndefined(sellAsset.assetId), + instructions: [memoInstruction, transferInstruction], + }, + }) + return { networkFeeCryptoBaseUnit: fast.txFee, protocolFees } + })() + + return makeThorTradeRateOrQuote({ + route, + allowanceContract: '0x0', + memo, + feeData, + }) }), ) + + const routes = maybeRoutes.filter(isFulfilled).map(r => r.value) + if (!routes.length) + return Err( + makeSwapErrorRight({ + message: 'Unable to create any routes', + code: TradeQuoteError.UnsupportedTradePair, + cause: maybeRoutes.filter(isRejected).map(r => r.reason), + }), + ) + return Ok(routes) } case CHAIN_NAMESPACE.Tron: { const maybeRoutes = await Promise.allSettled( diff --git a/packages/swapper/src/thorchain-utils/index.ts b/packages/swapper/src/thorchain-utils/index.ts index c5a68137658..2f6272e2828 100644 --- a/packages/swapper/src/thorchain-utils/index.ts +++ b/packages/swapper/src/thorchain-utils/index.ts @@ -35,6 +35,7 @@ export * from './getPoolDetails' export * as cosmossdk from './cosmossdk' export * as evm from './evm' +export * as solana from './solana' export * as tron from './tron' export * as utxo from './utxo' diff --git a/packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts b/packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts new file mode 100644 index 00000000000..ed8f222a592 --- /dev/null +++ b/packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts @@ -0,0 +1,52 @@ +import { contractAddressOrUndefined } from '@shapeshiftoss/utils' +import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js' + +import type { GetUnsignedSolanaTransactionArgs, SwapperName } from '../../types' +import { getExecutableTradeStep, isExecutableTradeQuote } from '../../utils' +import type { ThorTradeQuote } from '../types' +import { getThorTxData } from './getThorTxData' + +const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' + +export const getSolanaTransactionFees = async ( + args: GetUnsignedSolanaTransactionArgs, + swapperName: SwapperName, +): Promise => { + const { tradeQuote, stepIndex, from, assertGetSolanaChainAdapter, config } = args + + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const { memo } = tradeQuote as ThorTradeQuote + if (!memo) throw new Error('Memo is required') + + const step = getExecutableTradeStep(tradeQuote, stepIndex) + const { sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset } = step + + const { vault } = await getThorTxData({ sellAsset, config, swapperName }) + + const adapter = assertGetSolanaChainAdapter(sellAsset.chainId) + + const memoInstruction = new TransactionInstruction({ + keys: [], + programId: new PublicKey(MEMO_PROGRAM_ID), + data: Buffer.from(memo, 'utf8'), + }) + + const transferInstruction = SystemProgram.transfer({ + fromPubkey: new PublicKey(from), + toPubkey: new PublicKey(vault), + lamports: BigInt(sellAmountIncludingProtocolFeesCryptoBaseUnit), + }) + + const { fast } = await adapter.getFeeData({ + to: vault, + value: '0', + chainSpecific: { + from, + tokenId: contractAddressOrUndefined(sellAsset.assetId), + instructions: [memoInstruction, transferInstruction], + }, + }) + + return fast.txFee +} diff --git a/packages/swapper/src/thorchain-utils/solana/getThorTxData.ts b/packages/swapper/src/thorchain-utils/solana/getThorTxData.ts new file mode 100644 index 00000000000..1d15f15d098 --- /dev/null +++ b/packages/swapper/src/thorchain-utils/solana/getThorTxData.ts @@ -0,0 +1,30 @@ +import type { Asset } from '@shapeshiftoss/types' + +import type { SwapperConfig, SwapperName } from '../../types' +import { getInboundAddressDataForChain } from '../getInboundAddressDataForChain' +import { getDaemonUrl } from '../index' + +type GetThorTxDataArgs = { + sellAsset: Asset + config: SwapperConfig + swapperName: SwapperName +} + +type GetThorTxDataReturn = { + vault: string +} + +export const getThorTxData = async ({ + sellAsset, + config, + swapperName, +}: GetThorTxDataArgs): Promise => { + const daemonUrl = getDaemonUrl(config, swapperName) + + const res = await getInboundAddressDataForChain(daemonUrl, sellAsset.assetId, false, swapperName) + if (res.isErr()) throw res.unwrapErr() + + const { address: vault } = res.unwrap() + + return { vault } +} diff --git a/packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts b/packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts new file mode 100644 index 00000000000..d194e00020b --- /dev/null +++ b/packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts @@ -0,0 +1,81 @@ +import type { SolanaSignTx } from '@shapeshiftoss/hdwallet-core' +import { contractAddressOrUndefined } from '@shapeshiftoss/utils' +import { PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js' + +import type { GetUnsignedSolanaTransactionArgs, SwapperName } from '../../types' +import { getExecutableTradeStep, isExecutableTradeQuote } from '../../utils' +import type { ThorTradeQuote } from '../types' +import { getThorTxData } from './getThorTxData' + +const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' + +// Each ComputeBudgetProgram instruction (setComputeUnitLimit, setComputeUnitPrice) costs 150 CU. +// Fee estimation doesn't include these, so we add a fixed buffer to cover them. +const COMPUTE_BUDGET_INSTRUCTION_OVERHEAD_CU = 300 + +export const getUnsignedSolanaTransaction = async ( + args: GetUnsignedSolanaTransactionArgs, + swapperName: SwapperName, +): Promise => { + const { tradeQuote, stepIndex, from, assertGetSolanaChainAdapter, config } = args + + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const { memo } = tradeQuote as ThorTradeQuote + if (!memo) throw new Error('Memo is required') + + const step = getExecutableTradeStep(tradeQuote, stepIndex) + + const { accountNumber, sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAsset } = step + + const { vault } = await getThorTxData({ + sellAsset, + config, + swapperName, + }) + + const adapter = assertGetSolanaChainAdapter(sellAsset.chainId) + + const memoInstruction = new TransactionInstruction({ + keys: [], + programId: new PublicKey(MEMO_PROGRAM_ID), + data: Buffer.from(memo, 'utf8'), + }) + + const transferInstruction = SystemProgram.transfer({ + fromPubkey: new PublicKey(from), + toPubkey: new PublicKey(vault), + lamports: BigInt(sellAmountIncludingProtocolFeesCryptoBaseUnit), + }) + + const { fast } = await adapter.getFeeData({ + to: vault, + value: '0', + chainSpecific: { + from, + tokenId: contractAddressOrUndefined(sellAsset.assetId), + instructions: [memoInstruction, transferInstruction], + }, + }) + + const memoHdwalletInstruction = { + keys: [] as [], + programId: MEMO_PROGRAM_ID, + data: Buffer.from(memo, 'utf8'), + } + + return adapter.buildSendApiTransaction({ + from, + to: vault, + value: sellAmountIncludingProtocolFeesCryptoBaseUnit, + accountNumber, + chainSpecific: { + instructions: [memoHdwalletInstruction], + computeUnitLimit: String( + Number(fast.chainSpecific.computeUnits) + COMPUTE_BUDGET_INSTRUCTION_OVERHEAD_CU, + ), + computeUnitPrice: fast.chainSpecific.priorityFee, + tokenId: contractAddressOrUndefined(sellAsset.assetId), + }, + }) +} diff --git a/packages/swapper/src/thorchain-utils/solana/index.ts b/packages/swapper/src/thorchain-utils/solana/index.ts new file mode 100644 index 00000000000..321bb050176 --- /dev/null +++ b/packages/swapper/src/thorchain-utils/solana/index.ts @@ -0,0 +1,3 @@ +export * from './getThorTxData' +export * from './getUnsignedSolanaTransaction' +export * from './getSolanaTransactionFees' diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 582d4e1456a..4d51a2f7024 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -29,6 +29,7 @@ import type { NearChainId, OrderQuoteResponse, PartialRecord, + SolanaChainId, TonChainId, TronChainId, UtxoAccountType, @@ -278,6 +279,18 @@ export type GetTonTradeRateInput = CommonTradeRateInput & { chainId: TonChainId } +export type GetSolanaTradeQuoteInputBase = CommonTradeInput & { + chainId: SolanaChainId +} + +export type GetSolanaTradeQuoteInput = CommonTradeInput & { + chainId: SolanaChainId +} + +export type GetSolanaTradeRateInput = CommonTradeRateInput & { + chainId: SolanaChainId +} + type GetUtxoTradeQuoteWithWallet = CommonTradeQuoteInput & { chainId: UtxoChainId accountType: UtxoAccountType @@ -303,6 +316,7 @@ export type GetTradeQuoteInput = | GetTronTradeQuoteInput | GetNearTradeQuoteInput | GetTonTradeQuoteInput + | GetSolanaTradeQuoteInput export type GetTradeRateInput = | GetEvmTradeRateInput @@ -311,6 +325,7 @@ export type GetTradeRateInput = | GetTronTradeRateInput | GetNearTradeRateInput | GetTonTradeRateInput + | GetSolanaTradeRateInput export type GetTradeQuoteInputWithWallet = | GetUtxoTradeQuoteWithWallet @@ -319,6 +334,7 @@ export type GetTradeQuoteInputWithWallet = | GetTronTradeQuoteInputBase | GetNearTradeQuoteInputBase | GetTonTradeQuoteInputBase + | GetSolanaTradeQuoteInputBase export type EvmSwapperDeps = { assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter diff --git a/packages/types/src/base.ts b/packages/types/src/base.ts index 35d8c130e74..1aabf53fe56 100644 --- a/packages/types/src/base.ts +++ b/packages/types/src/base.ts @@ -120,6 +120,8 @@ export type SuiChainId = KnownChainIds.SuiMainnet export type NearChainId = KnownChainIds.NearMainnet +export type SolanaChainId = KnownChainIds.SolanaMainnet + export type StarknetChainId = KnownChainIds.StarknetMainnet export type TonChainId = KnownChainIds.TonMainnet diff --git a/packages/unchained-client/package.json b/packages/unchained-client/package.json index afb7dbfdee8..d859dcb15b1 100644 --- a/packages/unchained-client/package.json +++ b/packages/unchained-client/package.json @@ -38,6 +38,7 @@ "@shapeshiftoss/utils": "workspace:^", "axios": "^1.13.5", "bignumber.js": "^9.3.1", + "bs58": "^5.0.0", "ethers": "6.11.1", "isomorphic-ws": "^4.0.1", "viem": "2.43.5", diff --git a/packages/unchained-client/src/solana/parser/index.ts b/packages/unchained-client/src/solana/parser/index.ts index 39bb754f3c5..161cb83fe4f 100644 --- a/packages/unchained-client/src/solana/parser/index.ts +++ b/packages/unchained-client/src/solana/parser/index.ts @@ -58,6 +58,7 @@ export class TransactionParser { chainId: this.chainId, // all transactions from unchained are finalized with at least 1 confirmation (unused throughout web) confirmations: 1, + data: parserResult?.data, status: this.getStatus(tx), trade: parserResult?.trade, transfers: parserResult?.transfers ?? [], diff --git a/packages/unchained-client/src/solana/parser/thorchain.ts b/packages/unchained-client/src/solana/parser/thorchain.ts new file mode 100644 index 00000000000..6140c4709cc --- /dev/null +++ b/packages/unchained-client/src/solana/parser/thorchain.ts @@ -0,0 +1,33 @@ +import base58 from 'bs58' + +import * as thorchain from '../../parser/thorchain' +import type { SubParser, Tx, TxSpecific } from './types' + +const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' + +export type ParserArgs = { + midgardUrl: string +} + +export class Parser implements SubParser { + private readonly parser: thorchain.Parser + + constructor(args: ParserArgs) { + this.parser = new thorchain.Parser({ midgardUrl: args.midgardUrl }) + } + + async parse(tx: Tx): Promise { + const memoInstruction = tx.instructions.find(ix => ix.programId === MEMO_PROGRAM_ID) + if (!memoInstruction) return + + try { + const memo = Buffer.from(base58.decode(memoInstruction.data)).toString('utf8') + if (!memo) return + + return await this.parser.parse(memo, tx.txid) + } catch (err) { + console.error('Failed to decode or parse Solana THORChain memo', err) + return undefined + } + } +} diff --git a/packages/unchained-client/src/solana/parser/types.ts b/packages/unchained-client/src/solana/parser/types.ts index de2b3774dc0..a4a4d3c18d8 100644 --- a/packages/unchained-client/src/solana/parser/types.ts +++ b/packages/unchained-client/src/solana/parser/types.ts @@ -1,13 +1,18 @@ import type * as solana from '../../generated/solana' +import type * as thorchain from '../../parser/thorchain' import type { StandardTx } from '../../types' export * from '../../generated/solana' export type Tx = solana.Tx -export interface ParsedTx extends StandardTx {} +export type TxMetadata = thorchain.TxMetadata -export type TxSpecific = Partial> +export interface ParsedTx extends StandardTx { + data?: TxMetadata +} + +export type TxSpecific = Partial> export interface SubParser { parse(tx: T, address: string): Promise diff --git a/scripts/generateTradableAssetMap/utils.ts b/scripts/generateTradableAssetMap/utils.ts index ac08e20f040..fe30d0eaf3c 100644 --- a/scripts/generateTradableAssetMap/utils.ts +++ b/scripts/generateTradableAssetMap/utils.ts @@ -12,6 +12,7 @@ import { dogeChainId, ethChainId, ltcChainId, + solanaChainId, thorchainChainId, toAssetId, tronChainId, @@ -35,6 +36,7 @@ enum Chain { ETH = 'ETH', GAIA = 'GAIA', LTC = 'LTC', + SOL = 'SOL', THOR = 'THOR', TRON = 'TRON', ZEC = 'ZEC', @@ -52,6 +54,7 @@ const chainToChainId: Record = { [Chain.ETH]: ethChainId, [Chain.GAIA]: cosmosChainId, [Chain.LTC]: ltcChainId, + [Chain.SOL]: solanaChainId, [Chain.THOR]: thorchainChainId, [Chain.TRON]: tronChainId, [Chain.ZEC]: zecChainId, @@ -73,6 +76,8 @@ const getTokenStandardFromChainId = (chainId: ChainId): AssetNamespace | undefin return ASSET_NAMESPACE.erc20 case KnownChainIds.TronMainnet: return ASSET_NAMESPACE.trc20 + case KnownChainIds.SolanaMainnet: + return ASSET_NAMESPACE.splToken default: return undefined } @@ -106,7 +111,10 @@ export const getAssetIdPairFromPool = (pool: ThornodePoolResponse): AssetIdPair const assetId = toAssetId({ chainId, assetNamespace, - assetReference: assetReference.toLowerCase(), + assetReference: + assetNamespace === ASSET_NAMESPACE.splToken + ? assetReference + : assetReference.toLowerCase(), }) return [pool.asset, assetId] diff --git a/yarn.lock b/yarn.lock index 9168af7e828..d24e5e60f8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14036,6 +14036,7 @@ __metadata: "@shapeshiftoss/utils": "workspace:^" axios: ^1.13.5 bignumber.js: ^9.3.1 + bs58: ^5.0.0 ethers: 6.11.1 isomorphic-ws: ^4.0.1 viem: 2.43.5