diff --git a/packages/swapper/src/constants.ts b/packages/swapper/src/constants.ts index a9ec29a4caa..56383c36703 100644 --- a/packages/swapper/src/constants.ts +++ b/packages/swapper/src/constants.ts @@ -124,7 +124,8 @@ const DEFAULT_CHAINFLIP_SLIPPAGE_DECIMAL_PERCENTAGE = '0.02' const DEFAULT_BUTTERSWAP_SLIPPAGE_DECIMAL_PERCENTAGE = '0.015' const DEFAULT_CETUS_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' const DEFAULT_SUNIO_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' -const DEFAULT_AVNU_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' +// Starknet swaps can have more latency, so use higher default slippage +const DEFAULT_AVNU_SLIPPAGE_DECIMAL_PERCENTAGE = '0.02' const DEFAULT_STONFI_SLIPPAGE_DECIMAL_PERCENTAGE = '0.01' export const getDefaultSlippageDecimalPercentageForSwapper = ( diff --git a/packages/swapper/src/swappers/AvnuSwapper/endpoints.ts b/packages/swapper/src/swappers/AvnuSwapper/endpoints.ts index 6b683a909b8..1b6d47a7bc6 100644 --- a/packages/swapper/src/swappers/AvnuSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/AvnuSwapper/endpoints.ts @@ -1,6 +1,6 @@ import { quoteToCalls } from '@avnu/avnu-sdk' import { toAddressNList } from '@shapeshiftoss/chain-adapters' -import { CallData, hash, num } from 'starknet' +import { CallData, hash, num, validateAndParseAddress } from 'starknet' import type { SwapperApi, TradeStatus } from '../../types' import { @@ -12,6 +12,33 @@ import { import { getTradeQuote } from './swapperApi/getTradeQuote' import { getTradeRate } from './swapperApi/getTradeRate' +/** + * Normalize a value to hex format for Starknet RPC + * Handles various input types: decimal strings, hex strings (with/without 0x), numbers, BigInts + */ +const toHexString = (value: unknown): string => { + const strValue = String(value) + + // Already a proper hex string with 0x prefix + if (strValue.startsWith('0x')) { + return strValue + } + + // Check if it looks like a hex string without 0x prefix (contains a-f characters) + // Starknet addresses and felts often come as hex without 0x prefix + if (/^[0-9a-fA-F]+$/.test(strValue) && /[a-fA-F]/.test(strValue)) { + return `0x${strValue}` + } + + // Otherwise treat as decimal and convert to hex + try { + return num.toHex(strValue) + } catch { + // If conversion fails, assume it's already hex and add prefix + return `0x${strValue}` + } +} + export const avnuApi: SwapperApi = { getTradeQuote, getTradeRate: (input, deps) => { @@ -35,18 +62,27 @@ export const avnuApi: SwapperApi = { const adapter = assertGetStarknetChainAdapter(sellAsset.chainId) + // Normalize the from address to ensure consistent format + // Starknet addresses can have different representations (with/without leading zeros) + const normalizedFrom = validateAndParseAddress(from) + // Convert slippage from decimal percentage string to number for AVNU format (e.g., "0.01" = 1%) + // Use a slightly higher default slippage (2%) to account for quote staleness const slippage: number = slippageTolerancePercentageDecimal ? parseFloat(slippageTolerancePercentageDecimal) - : 0.01 + : 0.02 // Get the swap calls from AVNU SDK const { calls: avnuCalls } = await quoteToCalls({ quoteId: avnuSpecific.quoteId, slippage, - takerAddress: from, + takerAddress: normalizedFrom, }) + if (!avnuCalls || avnuCalls.length === 0) { + throw new Error('No swap calls returned from AVNU - quote may have expired') + } + // Build the full invoke transaction calldata from AVNU calls // Format: [call_array_length, contract1, selector1, calldata_len1, ...calldata1, ...] const fullCalldata: string[] = [avnuCalls.length.toString()] @@ -55,33 +91,30 @@ export const avnuApi: SwapperApi = { const rawCalldata = call.calldata ?? [] const calldataArray = Array.isArray(rawCalldata) ? rawCalldata : CallData.compile(rawCalldata) const selector = hash.getSelectorFromName(call.entrypoint) + // Normalize contract address from AVNU to ensure consistent format + const normalizedContractAddress = validateAndParseAddress(call.contractAddress) fullCalldata.push( - call.contractAddress, + normalizedContractAddress, selector, calldataArray.length.toString(), - ...calldataArray.map(cd => cd.toString()), + ...calldataArray.map(cd => String(cd)), ) } - // Format calldata for RPC (convert numbers to hex) - const formattedCalldata = fullCalldata.map(data => { - if (!data.startsWith('0x')) { - return num.toHex(data) - } - return data - }) + // Format calldata for RPC (convert all values to proper hex format) + const formattedCalldata = fullCalldata.map(toHexString) // Get nonce using adapter method (checks deployment status and returns appropriate nonce) const chainIdHex = await adapter.getStarknetProvider().getChainId() - const nonce = await adapter.getNonce(from) + const nonce = await adapter.getNonce(normalizedFrom) // Estimate fees for the multi-call swap transaction const version = '0x3' as const const estimateTx = { type: 'INVOKE', version, - sender_address: from, + sender_address: normalizedFrom, calldata: formattedCalldata, signature: [], nonce, @@ -156,7 +189,7 @@ export const avnuApi: SwapperApi = { // Calculate transaction hash for signing const invokeHashInputs = { - senderAddress: from, + senderAddress: normalizedFrom, version, compiledCalldata: formattedCalldata, chainId: chainIdHex, @@ -189,7 +222,7 @@ export const avnuApi: SwapperApi = { addressNList: toAddressNList(adapter.getBip44Params({ accountNumber })), txHash, _txDetails: { - fromAddress: from, + fromAddress: normalizedFrom, calldata: formattedCalldata, nonce, version, diff --git a/packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeQuote.ts index 734a5aed783..3509c2aa563 100644 --- a/packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeQuote.ts @@ -2,6 +2,7 @@ import { getQuotes } from '@avnu/avnu-sdk' import { bn } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' +import { validateAndParseAddress } from 'starknet' import { v4 as uuid } from 'uuid' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' @@ -76,11 +77,16 @@ export const getTradeQuote = async ( const sellTokenAddress = getTokenAddress(sellAsset) const buyTokenAddress = getTokenAddress(buyAsset) + // Normalize addresses to ensure consistent format across quote and execution + // Starknet addresses can have different representations (with/without leading zeros) + const normalizedSendAddress = validateAndParseAddress(sendAddress) + const normalizedReceiveAddress = validateAndParseAddress(receiveAddress) + const quotes = await getQuotes({ sellTokenAddress, buyTokenAddress, sellAmount: BigInt(sellAmount), - takerAddress: sendAddress, + takerAddress: normalizedSendAddress, size: 1, integratorFees: affiliateBps ? BigInt(affiliateBps) : undefined, integratorFeeRecipient: getTreasuryAddressFromChainId(sellAsset.chainId), @@ -111,10 +117,10 @@ export const getTradeQuote = async ( const sellAdapter = deps.assertGetStarknetChainAdapter(sellAsset.chainId) const feeData = await sellAdapter.getFeeData({ - to: receiveAddress, + to: normalizedReceiveAddress, value: sellAmount, chainSpecific: { - from: sendAddress, + from: normalizedSendAddress, tokenContractAddress: sellTokenAddress, }, sendMax: false, @@ -142,7 +148,7 @@ export const getTradeQuote = async ( const tradeQuote: TradeQuote = { id: uuid(), - receiveAddress, + receiveAddress: normalizedReceiveAddress, affiliateBps, rate, slippageTolerancePercentageDecimal: diff --git a/packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts index 3f32139995a..474b6d8c1ab 100644 --- a/packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts @@ -2,6 +2,7 @@ import { getQuotes } from '@avnu/avnu-sdk' import { bn } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' +import { validateAndParseAddress } from 'starknet' import { v4 as uuid } from 'uuid' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' @@ -47,6 +48,10 @@ export const getTradeRate = async ( const sellTokenAddress = getTokenAddress(sellAsset) const buyTokenAddress = getTokenAddress(buyAsset) + // Normalize receive address if provided (for fee estimation) + // Rate quotes may not have a receive address, so this is optional + const normalizedReceiveAddress = receiveAddress ? validateAndParseAddress(receiveAddress) : '' + const quotes = await getQuotes({ sellTokenAddress, buyTokenAddress, @@ -82,10 +87,10 @@ export const getTradeRate = async ( // For rate quotes, use receiveAddress as a dummy from/to for fee estimation const feeData = await sellAdapter.getFeeData({ - to: receiveAddress ?? '', + to: normalizedReceiveAddress, value: sellAmount, chainSpecific: { - from: receiveAddress ?? '', + from: normalizedReceiveAddress, tokenContractAddress: sellTokenAddress, }, sendMax: false, @@ -113,7 +118,7 @@ export const getTradeRate = async ( const tradeRate: TradeRate = { id: uuid(), - receiveAddress, + receiveAddress: normalizedReceiveAddress || receiveAddress, affiliateBps, rate, slippageTolerancePercentageDecimal: diff --git a/packages/swapper/src/utils.ts b/packages/swapper/src/utils.ts index 91dd6b58ca2..391b66ae5a3 100644 --- a/packages/swapper/src/utils.ts +++ b/packages/swapper/src/utils.ts @@ -458,8 +458,17 @@ export const checkStarknetSwapStatus = async ({ const adapter = assertGetStarknetChainAdapter(starknetChainId) const provider = adapter.getStarknetProvider() - const receipt: any = await provider.getTransactionReceipt(txHash) + // Use raw RPC call for better compatibility with various RPC providers + const response = await provider.fetch('starknet_getTransactionReceipt', [txHash]) + const result: { result?: { execution_status?: string }; error?: unknown } = + await response.json() + + // If there's an error or no result, transaction might still be pending + if (result.error || !result.result) { + return createDefaultStatusResponse(txHash) + } + const receipt = result.result const status = receipt.execution_status === 'SUCCEEDED' ? TxStatus.Confirmed @@ -473,7 +482,7 @@ export const checkStarknetSwapStatus = async ({ message: undefined, } } catch (e) { - console.error(e) + // Don't log expected errors during status polling (tx might still be pending) return createDefaultStatusResponse(txHash) } } diff --git a/scripts/generateAssetData/blacklist.json b/scripts/generateAssetData/blacklist.json index e543d7b71fd..29292cefdd4 100644 --- a/scripts/generateAssetData/blacklist.json +++ b/scripts/generateAssetData/blacklist.json @@ -14,6 +14,5 @@ "eip155:1/erc20:0xde60adfddaabaaac3dafa57b26acc91cb63728c4", "eip155:1/erc20:0x1cdd2eab61112697626f7b4bb0e23da4febf7b7c", "eip155:137/erc20:0x0000000000000000000000000000000000001010", - "eip155:9745/erc20:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "starknet:SN_MAIN/erc20:0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" + "eip155:9745/erc20:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" ]