From e88593cb2683bf040fb43756af5bcbee4933e8e4 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:25:29 +0100 Subject: [PATCH 01/10] feat: add SolanaChainId type and swapper Solana input types Co-Authored-By: Claude Sonnet 4.6 --- packages/swapper/src/types.ts | 16 ++++++++++++++++ packages/types/src/base.ts | 2 ++ 2 files changed, 18 insertions(+) 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 From 8dd9e7bfef63e11e36105cbede88e07ed2e08d1c Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:25:37 +0100 Subject: [PATCH 02/10] feat: add SOL to asset map generation script and regenerate Co-Authored-By: Claude Sonnet 4.6 --- .../generated/generatedTradableAssetMap.json | 1 + scripts/generateTradableAssetMap/utils.ts | 5 +++++ 2 files changed, 6 insertions(+) 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/scripts/generateTradableAssetMap/utils.ts b/scripts/generateTradableAssetMap/utils.ts index ac08e20f040..0f434434c85 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 } From ee7e51c5247cf57c8b393b75b18d141d6f4c581e Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:25:45 +0100 Subject: [PATCH 03/10] feat: create thorchain-utils/solana module getThorTxData, getUnsignedSolanaTransaction, getSolanaTransactionFees. Follows tron/ pattern: vault lookup + SPL Memo Program instruction for fee estimation, hdwallet SolanaTxInstruction for buildSendApiTransaction. SPL token support forward-compat via tokenId passthrough. Co-Authored-By: Claude Sonnet 4.6 --- .../solana/getSolanaTransactionFees.ts | 46 +++++++++++++ .../thorchain-utils/solana/getThorTxData.ts | 30 ++++++++ .../solana/getUnsignedSolanaTransaction.ts | 68 +++++++++++++++++++ .../src/thorchain-utils/solana/index.ts | 3 + 4 files changed, 147 insertions(+) create mode 100644 packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts create mode 100644 packages/swapper/src/thorchain-utils/solana/getThorTxData.ts create mode 100644 packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts create mode 100644 packages/swapper/src/thorchain-utils/solana/index.ts 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..62ae3fc5324 --- /dev/null +++ b/packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts @@ -0,0 +1,46 @@ +import { contractAddressOrUndefined } from '@shapeshiftoss/utils' +import { PublicKey, 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 { 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 { fast } = await adapter.getFeeData({ + to: vault, + value: '0', + chainSpecific: { + from, + tokenId: contractAddressOrUndefined(sellAsset.assetId), + instructions: [memoInstruction], + }, + }) + + 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..a813df8fc67 --- /dev/null +++ b/packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts @@ -0,0 +1,68 @@ +import type { SolanaSignTx } from '@shapeshiftoss/hdwallet-core' +import { contractAddressOrUndefined } from '@shapeshiftoss/utils' +import { PublicKey, 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 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 { fast } = await adapter.getFeeData({ + to: vault, + value: '0', + chainSpecific: { + from, + instructions: [memoInstruction], + }, + }) + + 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: fast.chainSpecific.computeUnits, + 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' From 228bc2ea9f395a00a59f8a7e56f9f809ac56816e Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:25:50 +0100 Subject: [PATCH 04/10] feat: wire Solana case in getL1RateOrQuote.ts Replaces the hard-reject stub with a full Memo Program fee estimation case. Uses TransactionInstruction (web3.js) for getFeeData, returns ThorUtxoOrCosmosTradeRateOrQuote routes. Also exports solana from thorchain-utils/index.ts. Co-Authored-By: Claude Sonnet 4.6 --- .../src/thorchain-utils/getL1RateOrQuote.ts | 47 +++++++++++++++++-- packages/swapper/src/thorchain-utils/index.ts | 1 + 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts index 2c8b2053840..0f7f9672df5 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, TransactionInstruction } from '@solana/web3.js' import { TronWeb } from 'tronweb' import { v4 as uuid } from 'uuid' @@ -57,6 +58,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 +104,7 @@ export const getL1RateOrQuote = async ( assertGetEvmChainAdapter, assertGetUtxoChainAdapter, assertGetCosmosSdkChainAdapter, + assertGetSolanaChainAdapter, } = deps // "NativePrecision" is intended to indicate the base unit precision of the asset @@ -442,12 +445,48 @@ 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 memoInstruction = new TransactionInstruction({ + keys: [], + programId: new PublicKey(SOLANA_MEMO_PROGRAM_ID), + data: Buffer.from(memo, 'utf8'), + }) + const { fast } = await adapter.getFeeData({ + to: '', + value: '0', + chainSpecific: { from: sendAddress, instructions: [memoInstruction] }, + }) + 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' From 8db140e8da2e8f1ffae698652e1199a65c1f9ddb Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:25:55 +0100 Subject: [PATCH 05/10] feat: wire Solana into ThorchainSwapper endpoints and execution Adds getUnsignedSolanaTransaction, getSolanaTransactionFees to endpoints.ts and executeSolanaTransaction to ThorchainSwapper.ts. Co-Authored-By: Claude Sonnet 4.6 --- .../swapper/src/swappers/ThorchainSwapper/ThorchainSwapper.ts | 3 +++ packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts | 3 +++ 2 files changed, 6 insertions(+) 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 ({ From 09fe1f69390f28dcccf675da9a0f487d89708d57 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:26:01 +0100 Subject: [PATCH 06/10] feat: create unchained-client Solana THORChain parseTx parser Detects SPL Memo Program instruction, base58-decodes data to UTF-8, delegates to shared thormaya.Parser. Adds bs58 dependency. Co-Authored-By: Claude Sonnet 4.6 --- packages/unchained-client/package.json | 1 + .../src/solana/parser/index.ts | 1 + .../src/solana/parser/thorchain.ts | 28 +++++++++++++++++++ .../src/solana/parser/types.ts | 9 ++++-- yarn.lock | 1 + 5 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 packages/unchained-client/src/solana/parser/thorchain.ts 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..db9cb6f5eec --- /dev/null +++ b/packages/unchained-client/src/solana/parser/thorchain.ts @@ -0,0 +1,28 @@ +import base58 from 'bs58' + +import * as thorchain from '../../parser/thorchain' +import type { SubParser, Tx, TxSpecific } from './types' + +const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' + +export interface 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 + + const memo = Buffer.from(base58.decode(memoInstruction.data)).toString('utf8') + if (!memo) return + + return await this.parser.parse(memo, tx.txid) + } +} 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/yarn.lock b/yarn.lock index 9fe7e6b3269..8c08dc577d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14035,6 +14035,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 From 53480e30336c4c64d02975fecbb83519c0ca56fa Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:48:06 +0100 Subject: [PATCH 07/10] feat: wire Solana into THORChain LP Adds solanaChainId to getThorchainTransactionType (returns 'Send') so the LP correctly routes SOL through the Send tx path. Adds SPL Memo Program instruction handling in Send/utils.ts: - estimateFees: builds TransactionInstruction (web3.js) when memo present, includes in getFeeData for accurate compute budget - handleSend: builds SolanaTxInstruction (hdwallet) when memo present, includes in buildSendTransaction chainSpecific Co-Authored-By: Claude Sonnet 4.6 --- src/components/Modals/Send/utils.ts | 47 +++++++++++++++++++++-------- src/lib/utils/thorchain/index.ts | 8 ++++- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/components/Modals/Send/utils.ts b/src/components/Modals/Send/utils.ts index 3a04495312c..22260728901 100644 --- a/src/components/Modals/Send/utils.ts +++ b/src/components/Modals/Send/utils.ts @@ -15,12 +15,13 @@ import type { GetFeeDataInput, } from '@shapeshiftoss/chain-adapters' import { utxoChainIds } from '@shapeshiftoss/chain-adapters' -import type { HDWallet } from '@shapeshiftoss/hdwallet-core' +import type { HDWallet, SolanaTxInstruction } from '@shapeshiftoss/hdwallet-core' import { isGridPlus, supportsETH, supportsSolana } from '@shapeshiftoss/hdwallet-core/wallet' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import { isTrezor } from '@shapeshiftoss/hdwallet-trezor' import type { CosmosSdkChainId, EvmChainId, KnownChainIds, UtxoChainId } from '@shapeshiftoss/types' import { contractAddressOrUndefined } from '@shapeshiftoss/utils' +import { PublicKey, TransactionInstruction } from '@solana/web3.js' import type { SendInput } from './Form' @@ -57,6 +58,8 @@ export type EstimateFeesInput = { contractAddress: string | undefined } +const SOLANA_MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' + export const estimateFees = async ({ amountCryptoPrecision, assetId, @@ -110,8 +113,17 @@ export const estimateFees = async ({ case CHAIN_NAMESPACE.Solana: { const adapter = assertGetSolanaChainAdapter(asset.chainId) + const memoInstruction: TransactionInstruction | undefined = memo + ? new TransactionInstruction({ + keys: [], + programId: new PublicKey(SOLANA_MEMO_PROGRAM_ID), + data: Buffer.from(memo, 'utf8'), + }) + : undefined + // For SPL transfers, build complete instruction set including compute budget - // For SOL transfers (pure sends i.e not e.g a Jup swap), pass no instructions to get 0 count (avoids blind signing) + // For SOL transfers with memo (e.g. THORChain), pass memo instruction for accurate fee estimation + // For pure SOL transfers, pass no instructions to get 0 count (avoids blind signing) const instructions = contractAddress ? await adapter.buildEstimationInstructions({ from: account, @@ -119,6 +131,8 @@ export const estimateFees = async ({ tokenId: contractAddress, value, }) + : memoInstruction + ? [memoInstruction] : undefined const getFeeDataInput: GetFeeDataInput = { @@ -336,29 +350,36 @@ export const handleSend = async ({ const solanaAdapter = assertGetSolanaChainAdapter(chainId) const { account } = fromAccountId(sendInput.accountId) - const instructions = await solanaAdapter.buildEstimationInstructions({ + + const memoInstruction: SolanaTxInstruction | undefined = memo + ? { keys: [], programId: SOLANA_MEMO_PROGRAM_ID, data: Buffer.from(memo, 'utf8') } + : undefined + + const estimationInstructions = await solanaAdapter.buildEstimationInstructions({ from: account, to, tokenId: contractAddress, value, }) + const shouldAddComputeBudget = estimationInstructions.length > 1 || Boolean(memoInstruction) + const input: BuildSendTxInput = { to, value, wallet, accountNumber: bip44Params.accountNumber, pubKey: skipDeviceDerivation ? fromAccountId(sendInput.accountId).account : undefined, - chainSpecific: - instructions.length <= 1 - ? { - tokenId: contractAddress, - } - : { - tokenId: contractAddress, - computeUnitLimit: fees.chainSpecific.computeUnits, - computeUnitPrice: fees.chainSpecific.priorityFee, - }, + chainSpecific: shouldAddComputeBudget + ? { + tokenId: contractAddress, + computeUnitLimit: fees.chainSpecific.computeUnits, + computeUnitPrice: fees.chainSpecific.priorityFee, + instructions: memoInstruction ? [memoInstruction] : undefined, + } + : { + tokenId: contractAddress, + }, } return solanaAdapter.buildSendTransaction(input) diff --git a/src/lib/utils/thorchain/index.ts b/src/lib/utils/thorchain/index.ts index 1f770627430..c3c497e8e0a 100644 --- a/src/lib/utils/thorchain/index.ts +++ b/src/lib/utils/thorchain/index.ts @@ -5,6 +5,7 @@ import { fromAccountId, fromAssetId, rujiAssetId, + solanaChainId, tcyAssetId, thorchainChainId, tronChainId, @@ -308,7 +309,12 @@ export const getThorchainTransactionType = (chainId: ChainId) => { if (supportedEvmChainIds.includes(chainId as KnownChainIds)) { return 'EvmCustomTx' } - if (isUtxoChainId(chainId) || chainId === cosmosChainId || chainId === tronChainId) { + if ( + isUtxoChainId(chainId) || + chainId === cosmosChainId || + chainId === tronChainId || + chainId === solanaChainId + ) { return 'Send' } From 2d6830940c9d7b366a857ca5f06fa9af162f335e Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Wed, 4 Mar 2026 19:18:03 +0100 Subject: [PATCH 08/10] fix: solana lp improvements (#12096) --- .../utils/poolAssetHelpers/poolAssetHelpers.ts | 6 +++++- packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts | 4 ++-- packages/swapper/src/thorchain-utils/solana/constants.ts | 3 +++ .../src/thorchain-utils/solana/getSolanaTransactionFees.ts | 3 +-- .../thorchain-utils/solana/getUnsignedSolanaTransaction.ts | 3 +-- packages/swapper/src/thorchain-utils/solana/index.ts | 1 + 6 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 packages/swapper/src/thorchain-utils/solana/constants.ts diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts index 46323455aec..633c1f720e1 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts @@ -5,7 +5,11 @@ import generatedTradableAssetMap from '../../generated/generatedTradableAssetMap const thorPoolIdAssetIdSymbolMap = generatedTradableAssetMap as Record -export const assetIdToThorPoolAssetIdMap = invert(thorPoolIdAssetIdSymbolMap) +// Lowercase keys so lookups are case-insensitive (required for Solana base58 addresses) +const rawMap = invert(thorPoolIdAssetIdSymbolMap) +export const assetIdToThorPoolAssetIdMap: Record = Object.fromEntries( + Object.entries(rawMap).map(([k, v]) => [k.toLowerCase(), v]), +) export const thorPoolAssetIdToAssetId = (id: string): AssetId | undefined => thorPoolIdAssetIdSymbolMap[id.toUpperCase()] diff --git a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts index 0f7f9672df5..b5c0ed25005 100644 --- a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts +++ b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts @@ -56,9 +56,9 @@ import type { TradeType, } from './types' import * as utxo from './utxo' +import { MEMO_PROGRAM_ID } from './solana/constants' const SAFE_GAS_LIMIT = '100000' // depositWithExpiry() -const SOLANA_MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' type ThorTradeRateOrQuote = ThorTradeRate | ThorTradeQuote type ThorEvmTradeRateOrQuote = ThorEvmTradeRate | ThorEvmTradeQuote @@ -457,7 +457,7 @@ export const getL1RateOrQuote = async ( if (!sendAddress) return { networkFeeCryptoBaseUnit: undefined, protocolFees } const memoInstruction = new TransactionInstruction({ keys: [], - programId: new PublicKey(SOLANA_MEMO_PROGRAM_ID), + programId: new PublicKey(MEMO_PROGRAM_ID), data: Buffer.from(memo, 'utf8'), }) const { fast } = await adapter.getFeeData({ diff --git a/packages/swapper/src/thorchain-utils/solana/constants.ts b/packages/swapper/src/thorchain-utils/solana/constants.ts new file mode 100644 index 00000000000..793406146df --- /dev/null +++ b/packages/swapper/src/thorchain-utils/solana/constants.ts @@ -0,0 +1,3 @@ +// Solana Memo Program v2 +// https://spl.solana.com/memo +export const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' diff --git a/packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts b/packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts index 62ae3fc5324..f76479a3b9e 100644 --- a/packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts +++ b/packages/swapper/src/thorchain-utils/solana/getSolanaTransactionFees.ts @@ -4,10 +4,9 @@ import { PublicKey, TransactionInstruction } from '@solana/web3.js' import type { GetUnsignedSolanaTransactionArgs, SwapperName } from '../../types' import { getExecutableTradeStep, isExecutableTradeQuote } from '../../utils' import type { ThorTradeQuote } from '../types' +import { MEMO_PROGRAM_ID } from './constants' import { getThorTxData } from './getThorTxData' -const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' - export const getSolanaTransactionFees = async ( args: GetUnsignedSolanaTransactionArgs, swapperName: SwapperName, diff --git a/packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts b/packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts index a813df8fc67..eff8db374be 100644 --- a/packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts +++ b/packages/swapper/src/thorchain-utils/solana/getUnsignedSolanaTransaction.ts @@ -5,10 +5,9 @@ import { PublicKey, TransactionInstruction } from '@solana/web3.js' import type { GetUnsignedSolanaTransactionArgs, SwapperName } from '../../types' import { getExecutableTradeStep, isExecutableTradeQuote } from '../../utils' import type { ThorTradeQuote } from '../types' +import { MEMO_PROGRAM_ID } from './constants' import { getThorTxData } from './getThorTxData' -const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' - export const getUnsignedSolanaTransaction = async ( args: GetUnsignedSolanaTransactionArgs, swapperName: SwapperName, diff --git a/packages/swapper/src/thorchain-utils/solana/index.ts b/packages/swapper/src/thorchain-utils/solana/index.ts index 321bb050176..dc3518fbf7f 100644 --- a/packages/swapper/src/thorchain-utils/solana/index.ts +++ b/packages/swapper/src/thorchain-utils/solana/index.ts @@ -1,3 +1,4 @@ +export * from './constants' export * from './getThorTxData' export * from './getUnsignedSolanaTransaction' export * from './getSolanaTransactionFees' From 857b36371e6980a486feec272d2d2619b9072051 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Sat, 7 Mar 2026 12:35:09 +0100 Subject: [PATCH 09/10] fix: preserve original-case assetId keys in thorchain pool asset map The assetIdToThorPoolAssetIdMap was lowercasing its keys (AssetIds) which broke Solana's base58 chain reference (case-sensitive). constants.ts iterated these lowercased keys through fromAssetId() which threw, crashing the entire app. Keep original-case keys in the exported map (used by constants.ts for chainId extraction) and use a separate lowercased map for case-insensitive lookups. Co-Authored-By: Claude Opus 4.6 --- .../utils/poolAssetHelpers/poolAssetHelpers.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts b/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts index a7a4a52ae40..f31433872f3 100644 --- a/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts +++ b/packages/swapper/src/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers.ts @@ -5,14 +5,16 @@ import generatedTradableAssetMap from '../../generated/generatedTradableAssetMap const thorPoolIdAssetIdSymbolMap = generatedTradableAssetMap as Record -// Lowercase keys so lookups are case-insensitive (required for Solana base58 addresses) -const rawMap = invert(thorPoolIdAssetIdSymbolMap) -export const assetIdToThorPoolAssetIdMap: Record = Object.fromEntries( - Object.entries(rawMap).map(([k, v]) => [k.toLowerCase(), v]), +export const assetIdToThorPoolAssetIdMap: Record = invert( + thorPoolIdAssetIdSymbolMap, +) + +const assetIdToThorPoolAssetIdMapLower: Record = Object.fromEntries( + Object.entries(assetIdToThorPoolAssetIdMap).map(([k, v]) => [k.toLowerCase(), v]), ) export const thorPoolAssetIdToAssetId = (id: string): AssetId | undefined => thorPoolIdAssetIdSymbolMap[id.toUpperCase()] export const assetIdToThorPoolAssetId = ({ assetId }: { assetId: AssetId }): string | undefined => - assetIdToThorPoolAssetIdMap[assetId] + assetIdToThorPoolAssetIdMapLower[assetId.toLowerCase()] From 2b791376015a712dd57cb52a52408de9ba5e8ae9 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Sat, 7 Mar 2026 13:04:59 +0100 Subject: [PATCH 10/10] feat: add data-testids to thorchain lp type selector and deposit button Co-Authored-By: Claude Opus 4.6 --- .../AddLiquidity/AddLiquidityInput.tsx | 1 + src/pages/ThorChainLP/components/LpType.tsx | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx index 4f841700b14..40f9a6635a4 100644 --- a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx @@ -1673,6 +1673,7 @@ export const AddLiquidityInput: React.FC = ({ {maybeOpportunityNotSupportedExplainer} {maybeAlert} = ({ assetIds }) => { ) } -const TypeRadio: React.FC = props => { - const { getInputProps, getRadioProps } = useRadio(props) +const TypeRadio: React.FC = props => { + const { 'data-testid': dataTestId, ...radioProps } = props + const { getInputProps, getRadioProps } = useRadio(radioProps) const input = getInputProps() const checkbox = getRadioProps() return ( - + + + {radioOptions} )