Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added e2e/screenshots/thor_solana/rune-to-sol.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added e2e/screenshots/thor_solana/sol-to-rune.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand Down
3 changes: 3 additions & 0 deletions packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
cosmossdk,
evm,
getInboundAddressDataForChain,
solana,
tron,
utxo,
} from '../../thorchain-utils'
Expand All @@ -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 ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
47 changes: 43 additions & 4 deletions packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -102,6 +104,7 @@ export const getL1RateOrQuote = async <T extends ThorTradeRateOrQuote>(
assertGetEvmChainAdapter,
assertGetUtxoChainAdapter,
assertGetCosmosSdkChainAdapter,
assertGetSolanaChainAdapter,
} = deps

// "NativePrecision" is intended to indicate the base unit precision of the asset
Expand Down Expand Up @@ -442,12 +445,48 @@ export const getL1RateOrQuote = async <T extends ThorTradeRateOrQuote>(
)
}
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<T> => {
const memo = getMemo(route)
const protocolFees = getProtocolFees(route.quote)

const feeData = await (async (): Promise<QuoteFeeData> => {
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] },
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return { networkFeeCryptoBaseUnit: fast.txFee, protocolFees }
})()

return makeThorTradeRateOrQuote<ThorUtxoOrCosmosTradeRateOrQuote>({
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(
Expand Down
1 change: 1 addition & 0 deletions packages/swapper/src/thorchain-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> => {
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: Number(sellAmountIncludingProtocolFeesCryptoBaseUnit),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const { fast } = await adapter.getFeeData({
to: vault,
value: '0',
chainSpecific: {
from,
tokenId: contractAddressOrUndefined(sellAsset.assetId),
instructions: [memoInstruction, transferInstruction],
},
})

return fast.txFee
}
30 changes: 30 additions & 0 deletions packages/swapper/src/thorchain-utils/solana/getThorTxData.ts
Original file line number Diff line number Diff line change
@@ -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<GetThorTxDataReturn> => {
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 }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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<SolanaSignTx> => {
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: Number(sellAmountIncludingProtocolFeesCryptoBaseUnit),
})

const { fast } = await adapter.getFeeData({
to: vault,
value: '0',
chainSpecific: {
from,
instructions: [memoInstruction, transferInstruction],
},
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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),
},
})
}
3 changes: 3 additions & 0 deletions packages/swapper/src/thorchain-utils/solana/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './getThorTxData'
export * from './getUnsignedSolanaTransaction'
export * from './getSolanaTransactionFees'
16 changes: 16 additions & 0 deletions packages/swapper/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
NearChainId,
OrderQuoteResponse,
PartialRecord,
SolanaChainId,
TonChainId,
TronChainId,
UtxoAccountType,
Expand Down Expand Up @@ -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
Expand All @@ -303,6 +316,7 @@ export type GetTradeQuoteInput =
| GetTronTradeQuoteInput
| GetNearTradeQuoteInput
| GetTonTradeQuoteInput
| GetSolanaTradeQuoteInput

export type GetTradeRateInput =
| GetEvmTradeRateInput
Expand All @@ -311,6 +325,7 @@ export type GetTradeRateInput =
| GetTronTradeRateInput
| GetNearTradeRateInput
| GetTonTradeRateInput
| GetSolanaTradeRateInput

export type GetTradeQuoteInputWithWallet =
| GetUtxoTradeQuoteWithWallet
Expand All @@ -319,6 +334,7 @@ export type GetTradeQuoteInputWithWallet =
| GetTronTradeQuoteInputBase
| GetNearTradeQuoteInputBase
| GetTonTradeQuoteInputBase
| GetSolanaTradeQuoteInputBase

export type EvmSwapperDeps = {
assertGetEvmChainAdapter: (chainId: ChainId) => EvmChainAdapter
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading