Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.
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]
58 changes: 54 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, SystemProgram, TransactionInstruction } from '@solana/web3.js'
import { TronWeb } from 'tronweb'
import { v4 as uuid } from 'uuid'

Expand Down Expand Up @@ -42,6 +43,7 @@ import {
getNativePrecision,
getSwapSource,
} from './index'
import * as solana from './solana'
import * as tron from './tron'
import type {
ThorEvmTradeQuote,
Expand All @@ -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
Expand Down Expand Up @@ -102,6 +105,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 +446,58 @@ 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 { 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<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: BigInt(sellAmountIncludingProtocolFeesCryptoBaseUnit),
})

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,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<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: 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),
},
})
}
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'
Loading
Loading