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
3 changes: 2 additions & 1 deletion packages/swapper/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
65 changes: 49 additions & 16 deletions packages/swapper/src/swappers/AvnuSwapper/endpoints.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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) => {
Expand All @@ -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()]
Expand All @@ -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,
Expand Down Expand Up @@ -156,7 +189,7 @@ export const avnuApi: SwapperApi = {

// Calculate transaction hash for signing
const invokeHashInputs = {
senderAddress: from,
senderAddress: normalizedFrom,
version,
compiledCalldata: formattedCalldata,
chainId: chainIdHex,
Expand Down Expand Up @@ -189,7 +222,7 @@ export const avnuApi: SwapperApi = {
addressNList: toAddressNList(adapter.getBip44Params({ accountNumber })),
txHash,
_txDetails: {
fromAddress: from,
fromAddress: normalizedFrom,
calldata: formattedCalldata,
nonce,
version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -142,7 +148,7 @@ export const getTradeQuote = async (

const tradeQuote: TradeQuote = {
id: uuid(),
receiveAddress,
receiveAddress: normalizedReceiveAddress,
affiliateBps,
rate,
slippageTolerancePercentageDecimal:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -113,7 +118,7 @@ export const getTradeRate = async (

const tradeRate: TradeRate = {
id: uuid(),
receiveAddress,
receiveAddress: normalizedReceiveAddress || receiveAddress,
affiliateBps,
rate,
slippageTolerancePercentageDecimal:
Expand Down
13 changes: 11 additions & 2 deletions packages/swapper/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
3 changes: 1 addition & 2 deletions scripts/generateAssetData/blacklist.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]