Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
50 changes: 27 additions & 23 deletions packages/public-api/src/routes/quote/extractTransactionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
TransactionData,
UtxoTransactionData,
} from '../../types'
import type { DepositExtractionContext } from './types'
import { getEvmChainIdNumber } from './utils'

const extractEvmTransactionData = (step: TradeQuoteStep): EvmTransactionData | undefined => {
Expand Down Expand Up @@ -90,6 +89,16 @@ const extractEvmTransactionData = (step: TradeQuoteStep): EvmTransactionData | u
}
}

if (step.thorchainTransactionMetadata?.data) {
return {
type: 'evm' as const,
chainId,
to: step.thorchainTransactionMetadata.to,
data: step.thorchainTransactionMetadata.data,
value: step.thorchainTransactionMetadata.value ?? '0',
}
}

return undefined
})()

Expand Down Expand Up @@ -124,10 +133,7 @@ const extractSolanaTransactionData = (step: TradeQuoteStep): SolanaTransactionDa
}
}

const extractUtxoTransactionData = (
step: TradeQuoteStep,
context: DepositExtractionContext = {},
): UtxoTransactionData | undefined => {
const extractUtxoTransactionData = (step: TradeQuoteStep): UtxoTransactionData | undefined => {
if (step.relayTransactionMetadata?.to) {
return {
type: 'utxo_deposit',
Expand All @@ -146,39 +152,37 @@ const extractUtxoTransactionData = (
}
}

if (context.depositAddress && context.memo !== undefined) {
if (step.thorchainTransactionMetadata?.to) {
return {
type: 'utxo_deposit',
depositAddress: context.depositAddress,
memo: context.memo,
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
depositAddress: step.thorchainTransactionMetadata.to,
memo: step.thorchainTransactionMetadata.memo ?? '',
value:
step.thorchainTransactionMetadata.value ??
step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
}
}

return undefined
}

const extractCosmosTransactionData = (
step: TradeQuoteStep,
context: DepositExtractionContext = {},
): CosmosTransactionData | undefined => {
if (context.depositAddress && context.memo !== undefined) {
const extractCosmosTransactionData = (step: TradeQuoteStep): CosmosTransactionData | undefined => {
if (step.thorchainTransactionMetadata?.to) {
return {
type: 'cosmos',
chainId: step.sellAsset.chainId,
to: context.depositAddress,
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
memo: context.memo,
to: step.thorchainTransactionMetadata.to,
value:
step.thorchainTransactionMetadata.value ??
step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
memo: step.thorchainTransactionMetadata.memo ?? '',
}
}

return undefined
}

export const extractTransactionData = (
step: TradeQuoteStep,
context: DepositExtractionContext = {},
): TransactionData | undefined => {
export const extractTransactionData = (step: TradeQuoteStep): TransactionData | undefined => {
const { chainNamespace } = fromChainId(step.sellAsset.chainId)

if (chainNamespace === 'eip155') {
Expand All @@ -190,11 +194,11 @@ export const extractTransactionData = (
}

if (chainNamespace === 'bip122') {
return extractUtxoTransactionData(step, context)
return extractUtxoTransactionData(step)
}

if (chainNamespace === 'cosmos') {
return extractCosmosTransactionData(step, context)
return extractCosmosTransactionData(step)
}

return undefined
Expand Down
14 changes: 2 additions & 12 deletions packages/public-api/src/routes/quote/getQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { ErrorResponse } from '../../types'
import { PartnerCodeHeaderSchema, rateLimitResponse } from '../../types'
import type { QuoteResponse } from './types'
import { QuoteRequestSchema, QuoteResponseSchema } from './types'
import { buildApprovalInfo, resolveDepositContext, transformQuoteStep } from './utils'
import { buildApprovalInfo, transformQuoteStep } from './utils'

registry.registerPath({
method: 'post',
Expand Down Expand Up @@ -211,14 +211,6 @@ export const getQuote = async (req: Request, res: Response): Promise<void> => {
status: 'pending',
})

const depositContextResult = await resolveDepositContext(quote, firstStep, validSwapperName)
if (!depositContextResult.ok) {
res.status(depositContextResult.statusCode).json(depositContextResult.error)
return
}

const { context: depositContext } = depositContextResult

const response: QuoteResponse = {
quoteId,
swapperName: validSwapperName,
Expand All @@ -231,9 +223,7 @@ export const getQuote = async (req: Request, res: Response): Promise<void> => {
affiliateBps: req.affiliateInfo?.affiliateBps ?? env.DEFAULT_AFFILIATE_BPS,
slippageTolerancePercentageDecimal: quote.slippageTolerancePercentageDecimal,
networkFeeCryptoBaseUnit: firstStep.feeData.networkFeeCryptoBaseUnit,
steps: quote.steps.map((step, index) =>
transformQuoteStep(step, index === 0 ? depositContext : {}),
),
steps: quote.steps.map(transformQuoteStep),
approval: sendAddress
? await buildApprovalInfo(firstStep, sendAddress)
: { isRequired: false, spender: '' },
Expand Down
13 changes: 0 additions & 13 deletions packages/public-api/src/routes/quote/types.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import type { TradeQuote } from '@shapeshiftoss/swapper'
import { z } from 'zod'

import { booleanFromString } from '../../lib/zod'
import { registry } from '../../registry'
import type { ErrorResponse } from '../../types'
import { AssetSchema } from '../assets/types'

export type ThorLikeQuote = TradeQuote & { memo?: string }

export type DepositExtractionContext = {
memo?: string
depositAddress?: string
}

export type DepositContextResult =
| { ok: true; context: DepositExtractionContext }
| { ok: false; error: ErrorResponse; statusCode: number }

const EvmTransactionDataSchema = z.object({
type: z.literal('evm').openapi({ example: 'evm' }),
chainId: z.number().openapi({ example: 1 }),
Expand Down
65 changes: 4 additions & 61 deletions packages/public-api/src/routes/quote/utils.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,11 @@
import { CHAIN_NAMESPACE, fromAssetId, fromChainId } from '@shapeshiftoss/caip'
import { viemClientByChainId } from '@shapeshiftoss/contracts'
import type { SwapperName, TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper'
import { getDaemonUrl, getInboundAddressDataForChain } from '@shapeshiftoss/swapper'
import type { TradeQuoteStep } from '@shapeshiftoss/swapper'
import { isToken } from '@shapeshiftoss/utils'
import { erc20Abi, getAddress } from 'viem'

import { getServerConfig } from '../../config'
import { extractTransactionData } from './extractTransactionData'
import type {
ApiQuoteStep,
ApprovalInfo,
DepositContextResult,
DepositExtractionContext,
ThorLikeQuote,
} from './types'

export const fetchInboundAddress = async (
assetId: string,
swapperName: SwapperName,
): Promise<string | undefined> => {
const config = getServerConfig()
const daemonUrl = getDaemonUrl(config, swapperName)

const result = await getInboundAddressDataForChain(daemonUrl, assetId, false, swapperName)

if (result.isOk()) {
return result.unwrap().address
}

console.error(
`Failed to fetch inbound address for ${assetId} (${swapperName}):`,
result.unwrapErr(),
)
return undefined
}
import type { ApiQuoteStep, ApprovalInfo } from './types'

export const getEvmChainIdNumber = (chainId: string): number => {
const { chainReference } = fromChainId(chainId)
Expand Down Expand Up @@ -69,42 +41,13 @@ export const buildApprovalInfo = async (
}

// Transform quote step to API format
export const transformQuoteStep = (
step: TradeQuoteStep,
context: DepositExtractionContext = {},
): ApiQuoteStep => ({
export const transformQuoteStep = (step: TradeQuoteStep): ApiQuoteStep => ({
sellAsset: step.sellAsset,
buyAsset: step.buyAsset,
sellAmountCryptoBaseUnit: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit,
allowanceContract: step.allowanceContract,
estimatedExecutionTimeMs: step.estimatedExecutionTimeMs,
source: step.source,
transactionData: extractTransactionData(step, context),
transactionData: extractTransactionData(step),
})

export const resolveDepositContext = async (
quote: TradeQuote,
firstStep: TradeQuoteStep,
swapperName: SwapperName,
): Promise<DepositContextResult> => {
const thorLikeQuote = quote as ThorLikeQuote
if (!thorLikeQuote.memo) return { ok: true, context: {} }

const { chainNamespace } = fromChainId(firstStep.sellAsset.chainId)
if (chainNamespace !== 'bip122' && chainNamespace !== 'cosmos') return { ok: true, context: {} }

const depositAddress = await fetchInboundAddress(firstStep.sellAsset.assetId, swapperName)
if (!depositAddress) {
return {
ok: false,
statusCode: 503,
error: {
error: 'Failed to fetch deposit address for this swap',
code: 'DEPOSIT_ADDRESS_UNAVAILABLE',
},
}
}

return { ok: true, context: { memo: thorLikeQuote.memo, depositAddress } }
}
5 changes: 3 additions & 2 deletions packages/swap-widget/src/constants/swappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export const SWAPPER_ICONS: Partial<Record<SwapperName, string>> = {
'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/near-intents-icon.png',
[SwapperName.Relay]:
'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/relay-icon.svg',
//[SwapperName.Thorchain]: 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/thorchain-icon.png',
[SwapperName.Thorchain]:
'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/thorchain-icon.png',
//[SwapperName.Mayachain]: 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/maya_logo.png',
//[SwapperName.ArbitrumBridge]: 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/arbitrum-bridge-icon.png',
//[SwapperName.Bebop]: 'https://raw.githubusercontent.com/shapeshift/web/develop/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/bebop-icon.png',
Expand All @@ -19,7 +20,7 @@ export const SWAPPER_ICONS: Partial<Record<SwapperName, string>> = {
export const SWAPPER_COLORS: Partial<Record<SwapperName, string>> = {
[SwapperName.NearIntents]: '#000000',
[SwapperName.Relay]: '#6366F1',
//[SwapperName.Thorchain]: '#00CCFF',
[SwapperName.Thorchain]: '#00CCFF',
//[SwapperName.Mayachain]: '#4169E1',
//[SwapperName.ArbitrumBridge]: '#28A0F0',
//[SwapperName.Bebop]: '#E91E63',
Expand Down
26 changes: 14 additions & 12 deletions packages/swap-widget/src/hooks/useSwapApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,18 @@ import { switchOrAddChain, VIEM_CHAINS_BY_ID } from '../constants/viemChains'
import { useSwapWallet } from '../contexts/SwapWalletContext'
import { SwapMachineCtx } from '../machines/SwapMachineContext'
import { getEvmNetworkId } from '../types'
import { getErrorMessage } from '../utils/errors'

export const useSwapApproval = () => {
const stateValue = SwapMachineCtx.useSelector(s => s.value)
const context = SwapMachineCtx.useSelector(s => s.context)
const actorRef = SwapMachineCtx.useActorRef()

const { walletClient, walletAddress } = useSwapWallet()

const approvingRef = useRef(false)

useEffect(() => {
const snap = actorRef.getSnapshot()
if (!snap.matches('approving') || approvingRef.current) return
if (stateValue !== 'approving' || approvingRef.current) return
approvingRef.current = true

const executeApproval = async () => {
Expand All @@ -29,13 +28,14 @@ export const useSwapApproval = () => {
return
}

const quote = context.quote
const { quote, sellAsset, sellAmountBaseUnit } = actorRef.getSnapshot().context

if (!quote?.approval?.spender) {
actorRef.send({ type: 'APPROVAL_ERROR', error: 'No approval data in quote' })
return
}

const sellAssetAddress = context.sellAsset.assetId.split('/')[1]?.split(':')[1]
const sellAssetAddress = sellAsset.assetId.split('/')[1]?.split(':')[1]
Comment thread
kaladinlight marked this conversation as resolved.
if (!sellAssetAddress || !/^0x[a-fA-F0-9]{40}$/.test(sellAssetAddress)) {
actorRef.send({
type: 'APPROVAL_ERROR',
Expand All @@ -44,15 +44,15 @@ export const useSwapApproval = () => {
return
}

const requiredChainId = getEvmNetworkId(context.sellAsset.chainId)
const requiredChainId = getEvmNetworkId(sellAsset.chainId)
const client = walletClient as WalletClient

const currentChainId = await client.getChainId()
if (currentChainId !== requiredChainId) {
await switchOrAddChain(client, requiredChainId)
}

const baseAsset = getBaseAsset(context.sellAsset.chainId)
const baseAsset = getBaseAsset(sellAsset.chainId)
const nativeCurrency = baseAsset
? { name: baseAsset.name, symbol: baseAsset.symbol, decimals: baseAsset.precision }
: { name: 'ETH', symbol: 'ETH', decimals: 18 }
Expand All @@ -65,15 +65,15 @@ export const useSwapApproval = () => {
rpcUrls: { default: { http: [] } },
}

if (!context.sellAmountBaseUnit || context.sellAmountBaseUnit === '0') {
if (!sellAmountBaseUnit || sellAmountBaseUnit === '0') {
actorRef.send({ type: 'APPROVAL_ERROR', error: 'No sell amount specified' })
return
}

const approvalData = encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [quote.approval.spender as `0x${string}`, BigInt(context.sellAmountBaseUnit)],
args: [quote.approval.spender as `0x${string}`, BigInt(sellAmountBaseUnit)],
})

const approvalHash = await client.sendTransaction({
Expand All @@ -93,14 +93,16 @@ export const useSwapApproval = () => {

actorRef.send({ type: 'APPROVAL_SUCCESS', txHash: approvalHash })
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Approval failed'
actorRef.send({ type: 'APPROVAL_ERROR', error: errorMessage })
actorRef.send({
type: 'APPROVAL_ERROR',
error: getErrorMessage(error, 'Approval failed'),
})
} finally {
approvingRef.current = false
}
}

executeApproval()
// eslint-disable-next-line react-hooks/exhaustive-deps -- stateValue is the sole trigger; other deps are stable refs read from snapshot
// eslint-disable-next-line react-hooks/exhaustive-deps -- only re-fire on state machine transitions; wallet handles close over the latest render
}, [stateValue])
}
Loading
Loading