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
2 changes: 1 addition & 1 deletion .claude/skills/swapper-integration/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ swapper resembles:
- **SwapperMetadata union member + reads**: `ChainflipMetadata` (status polling by swap id), `NearIntentsMetadata` (deposit address for status + un-migrated exec), read via `getSwapMetadata`
- **checkTradeStatus with tracker links**: Chainflip (native id -> scan link), Relay (origin-tx link), CowSwapper (order uid link) - return `swapperTxId`/`swapperTxLink` constructed next to the provider response
- **HTTP service**: any `utils/[x]Service.ts` (`createCache` + `makeSwapperAxiosServiceMonadic`)
- **Tenderly simulation for rates** (unapproved sender still prices): PortalsSwapper, RelaySwapper, NearIntentsSwapper step data
- **State-override gas estimation** (unapproved/unfunded sender still estimates): pass `stateOverride: { sellAsset, sellAmountCryptoBaseUnit, spenderAddress }` to `getEvmNetworkFeeCryptoBaseUnit` in the quote arm (`spenderAddress` = the step's `allowanceContract`, `''` when no approval is involved) — see ButterSwap/Portals/Relay step data; `utils/evm/stateOverride.ts` handles slot discovery

## Registration example (one line per swapper)

Expand Down
3 changes: 0 additions & 3 deletions packages/public-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ ACROSS_INTEGRATOR_ID=
BEBOP_API_KEY=
CHAINFLIP_API_KEY=
NEAR_INTENTS_API_KEY=
TENDERLY_API_KEY=
TENDERLY_ACCOUNT_SLUG=
TENDERLY_PROJECT_SLUG=

# Affiliate
DEFAULT_AFFILIATE_BPS=60
Expand Down
4 changes: 2 additions & 2 deletions packages/public-api/docs/rest-api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ X-Partner-Code: your-partner-code

- `swapperName` comes from the rate you chose in step 2.
- `slippageTolerancePercentageDecimal` is optional; `accountNumber` is optional (defaults to `0`) and is needed for chains that derive addresses per account index (e.g. UTXO/Cosmos).
- The response includes a `quoteId` (needed for status tracking), an `approval` object (whether an ERC-20 approval is required and the approval tx to send first), and a `steps` array. Each step may include `transactionData` — a discriminated union on `type` (`evm`, `solana`, `utxo`, `cosmossdk_msg_send`, `cosmossdk_msg_deposit`) — describing exactly what to sign for that chain.
- The response includes a `quoteId` (needed for status tracking), an `approval` object (whether an ERC-20 approval is required and the spender to approve), and a `steps` array. Each step may include `transactionData` — a discriminated union on `type` (`evm`, `solana`, `utxo`, `cosmossdk_msg_send`, `cosmossdk_msg_deposit`) — describing exactly what to sign for that chain.
- Quotes expire: honor the `expiresAt` timestamp (≈ 60s after issue). Request a fresh quote rather than submitting an expired one.

## 4. Execute the swap

The API does **not** broadcast transactions — your application signs and broadcasts with the user's wallet:

1. If `approval.isRequired` is true and `approval.approvalTx` is present, send the approval transaction first and wait for it to confirm.
1. If `approval.isRequired` is true, send an ERC-20 `approve(approval.spender, amount)` transaction for the sell token first (amount ≥ the step's `sellAmountCryptoBaseUnit`) and wait for it to confirm. Quotes are issued before approval exists — network fees are estimated as if the approval were already in place.
2. For each step with `transactionData`, build, sign, and broadcast the transaction according to its `type` (EVM tx, Solana instructions, UTXO PSBT/deposit, or Cosmos message).
3. Capture the resulting transaction hash for status tracking.

Expand Down
3 changes: 0 additions & 3 deletions packages/public-api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ export const getServerConfig = (): SwapperConfig => ({
VITE_RELAY_API_URL: env.RELAY_API_URL,
VITE_BEBOP_API_KEY: env.BEBOP_API_KEY,
VITE_NEAR_INTENTS_API_KEY: env.NEAR_INTENTS_API_KEY,
VITE_TENDERLY_API_KEY: env.TENDERLY_API_KEY,
VITE_TENDERLY_ACCOUNT_SLUG: env.TENDERLY_ACCOUNT_SLUG,
VITE_TENDERLY_PROJECT_SLUG: env.TENDERLY_PROJECT_SLUG,
VITE_TRON_GRID_API_KEY: env.TRON_GRID_API_KEY,
VITE_SUI_NODE_URL: env.SUI_NODE_URL,
VITE_ACROSS_API_URL: env.ACROSS_API_URL,
Expand Down
3 changes: 0 additions & 3 deletions packages/public-api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,6 @@ const envSchema = z.object({
BOB_GATEWAY_API_KEY: z.string().default(''),
CHAINFLIP_API_KEY: z.string().min(1),
NEAR_INTENTS_API_KEY: z.string().min(1),
TENDERLY_API_KEY: z.string().min(1),
TENDERLY_ACCOUNT_SLUG: z.string().min(1),
TENDERLY_PROJECT_SLUG: z.string().min(1),
TRON_GRID_API_KEY: z.string().default(''),

// Feature flags
Expand Down
2 changes: 2 additions & 0 deletions packages/public-api/src/routes/rates/getRates.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isEvmChainId } from '@shapeshiftoss/chain-adapters'
import type { GetTradeRateInput } from '@shapeshiftoss/swapper'
import { getTradeRates, swappers, TradeQuoteError } from '@shapeshiftoss/swapper'
import type { Request, Response } from 'express'
Expand Down Expand Up @@ -84,6 +85,7 @@ export const getRates = async (req: Request, res: Response): Promise<void> => {
accountNumber: undefined,
quoteOrRate: 'rate' as const,
chainId: sellAsset.chainId,
...(isEvmChainId(sellAsset.chainId) && { supportsEIP1559: false as const }),
}

const ratePromises = ENABLED_SWAPPER_NAMES.map(async (swapperName): Promise<ApiRate | null> => {
Expand Down
2 changes: 1 addition & 1 deletion packages/swapper/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shapeshiftoss/swapper",
"version": "18.0.0",
"version": "18.1.0",
"repository": "https://github.com/shapeshift/web",
"license": "MIT",
"type": "module",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { AcrossSwapTx } from './types'

type BaseArgs = {
swapTx: AcrossSwapTx
spenderAddress: string
fallbackNetworkFeeCryptoBaseUnit: string
}

Expand All @@ -39,7 +40,16 @@ export function getAcrossStepData(
export async function getAcrossStepData(
args: GetAcrossStepDataArgs,
): Promise<Result<AcrossRateStepData | AcrossQuoteStepData, SwapErrorRight>> {
const { swapTx, sellAsset, from, type, input, fallbackNetworkFeeCryptoBaseUnit, deps } = args
const {
swapTx,
sellAsset,
spenderAddress,
from,
type,
input,
fallbackNetworkFeeCryptoBaseUnit,
deps,
} = args

const supportsEIP1559 = 'supportsEIP1559' in input ? input.supportsEIP1559 : false

Expand All @@ -56,6 +66,12 @@ export async function getAcrossStepData(
gasLimit: swapTx.gas,
}

const stateOverride = {
sellAsset,
sellAmountCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit,
spenderAddress,
}

if (type === 'rate') {
const networkFeeCryptoBaseUnit = await (async () => {
try {
Expand All @@ -64,6 +80,7 @@ export async function getAcrossStepData(
transactionData,
from,
supportsEIP1559,
stateOverride,
})
} catch {
return fallbackNetworkFeeCryptoBaseUnit
Expand All @@ -81,6 +98,7 @@ export async function getAcrossStepData(
transactionData,
from,
supportsEIP1559,
stateOverride,
})

const stepData: AcrossQuoteStepData = { transactionData, networkFeeCryptoBaseUnit }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ export const getAcrossTradeContext = async ({
return `${bridgeFeeAssetCaipChainId}/${tokenStandard}:${bridgeFeeAsset.address}`
})()

const allowanceContract = isEvmChainId(sellAsset.chainId) ? quote.checks.allowance.spender : ''

const protocolFees: QuoteFeeData['protocolFees'] = bridgeFeeAssetId
? {
[bridgeFeeAssetId]: {
Expand All @@ -214,7 +216,7 @@ export const getAcrossTradeContext = async ({
slippageTolerancePercentageDecimal: input.slippageTolerancePercentageDecimal,
},
stepCommon: {
allowanceContract: isEvmChainId(sellAsset.chainId) ? quote.checks.allowance.spender : '',
allowanceContract,
rate,
buyAmountBeforeFeesCryptoBaseUnit,
buyAmountAfterFeesCryptoBaseUnit,
Expand All @@ -240,6 +242,7 @@ export const getAcrossTradeContext = async ({
swapTx: quote.swapTx,
sellAsset,
from: depositor,
spenderAddress: allowanceContract,
fallbackNetworkFeeCryptoBaseUnit: quote.fees.originGas.amount,
deps,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type BaseArgs = {
bridgeType: BRIDGE_TYPE
sellAmountCryptoBaseUnit: string
buyAsset: Asset
spenderAddress: string
}

export type GetArbitrumBridgeStepDataArgs = StepDataArgs<
Expand All @@ -40,7 +41,8 @@ export function getArbitrumBridgeStepData(
export async function getArbitrumBridgeStepData(
args: GetArbitrumBridgeStepDataArgs,
): Promise<Result<ArbitrumBridgeRateStepData | ArbitrumBridgeQuoteStepData, SwapErrorRight>> {
const { input, deps, bridgeType, sellAmountCryptoBaseUnit, sellAsset, buyAsset } = args
const { input, deps, bridgeType, sellAmountCryptoBaseUnit, sellAsset, buyAsset, spenderAddress } =
args

const adapter = deps.assertGetEvmChainAdapter(sellAsset.chainId)
const supportsEIP1559 = 'supportsEIP1559' in input ? input.supportsEIP1559 : false
Expand All @@ -58,11 +60,13 @@ export async function getArbitrumBridgeStepData(
return Ok(stepData)
}

const { from, receiveAddress } = args

const request = await buildArbitrumBridgeRequest({
bridgeType,
sellAmountCryptoBaseUnit,
from: args.from,
receiveAddress: args.receiveAddress,
from,
receiveAddress,
sellAsset,
buyAsset,
})
Expand All @@ -83,8 +87,13 @@ export async function getArbitrumBridgeStepData(
const networkFeeCryptoBaseUnit = await getEvmNetworkFeeCryptoBaseUnit({
adapter,
transactionData,
from: args.from,
from,
supportsEIP1559,
stateOverride: {
sellAsset,
sellAmountCryptoBaseUnit,
spenderAddress,
},
})

const stepData: ArbitrumBridgeQuoteStepData = { transactionData, networkFeeCryptoBaseUnit }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const getArbitrumBridgeTradeContext = async ({
sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit,
buyAsset,
sellAsset,
spenderAddress: allowanceContract,
deps,
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { BebopQuoteResponse } from '../types'

type BaseArgs = {
tx: BebopQuoteResponse['tx']
spenderAddress: string
}

export type GetBebopStepDataArgs = StepDataArgs<BaseArgs, { from: string }>
Expand All @@ -27,7 +28,7 @@ export function getBebopStepData(
export async function getBebopStepData(
args: GetBebopStepDataArgs,
): Promise<Result<BebopRateStepData | BebopQuoteStepData, SwapErrorRight>> {
const { tx, sellAsset, type, input, from, deps } = args
const { tx, spenderAddress, sellAsset, type, input, from, deps } = args

const adapter = deps.assertGetEvmChainAdapter(sellAsset.chainId)
const supportsEIP1559 = 'supportsEIP1559' in input ? input.supportsEIP1559 : false
Expand Down Expand Up @@ -65,6 +66,11 @@ export async function getBebopStepData(
transactionData,
from,
supportsEIP1559,
stateOverride: {
sellAsset,
sellAmountCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit,
spenderAddress,
},
})

const stepData: BebopQuoteStepData = { transactionData, networkFeeCryptoBaseUnit }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export const getBebopTradeContext = async ({
const buyAmountBeforeFeesCryptoBaseUnit = buyTokenData.amountBeforeFee || buyAmount
const buyAmountAfterFeesCryptoBaseUnit = buyAmount

const allowanceContract = isNativeEvmAsset(sellAsset.assetId) ? '' : quote.approvalTarget

return Ok({
tradeCommon: {
id: uuid(),
Expand All @@ -91,7 +93,7 @@ export const getBebopTradeContext = async ({
slippageTolerancePercentageDecimal,
},
stepCommon: {
allowanceContract: isNativeEvmAsset(sellAsset.assetId) ? '' : quote.approvalTarget,
allowanceContract,
rate,
buyAmountBeforeFeesCryptoBaseUnit,
buyAmountAfterFeesCryptoBaseUnit,
Expand All @@ -112,6 +114,7 @@ export const getBebopTradeContext = async ({
},
stepDataArgs: {
tx: quote.tx,
spenderAddress: allowanceContract,
sellAsset,
from,
deps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { createBobGatewayOrder, toTronBase58 } from './helpers'
type BaseArgs = {
quote: GatewayQuoteV3
sellAmountCryptoBaseUnit: string
spenderAddress: string
}

export type GetBobGatewayStepDataArgs = StepDataArgs<BaseArgs>
Expand All @@ -45,7 +46,7 @@ export function getBobGatewayStepData(
export async function getBobGatewayStepData(
args: GetBobGatewayStepDataArgs,
): Promise<Result<BobGatewayRateStepData | BobGatewayQuoteStepData, SwapErrorRight>> {
const { input, quote, sellAsset, sellAmountCryptoBaseUnit, deps } = args
const { input, quote, sellAsset, sellAmountCryptoBaseUnit, spenderAddress, deps } = args
const { chainNamespace } = fromChainId(sellAsset.chainId)

// Rates estimate off the quote shape; quotes resolve the executable order once up front
Expand Down Expand Up @@ -176,7 +177,11 @@ export async function getBobGatewayStepData(
transactionData,
from,
supportsEIP1559: input.supportsEIP1559,
gasLimitBuffer: 1.2,
stateOverride: {
sellAsset,
sellAmountCryptoBaseUnit,
spenderAddress,
},
})

const stepData: BobGatewayQuoteStepData = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isEvmChainId } from '@shapeshiftoss/chain-adapters'
import type { Result } from '@sniptt/monads'
import { Err, Ok } from '@sniptt/monads'
import { v4 as uuid } from 'uuid'
Expand Down Expand Up @@ -107,6 +108,7 @@ export const getBobGatewayTradeContext = async ({
quote,
sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit,
sellAsset,
spenderAddress: isEvmChainId(sellAsset.chainId) ? allowanceContract : '',
deps,
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ vi.mock('../xhr', () => ({
isBuildTxSuccess: () => true,
}))

// The override path reads live chain state - resolve to no override so estimation exercises the
// mocked adapter
vi.mock('../../../utils/evm/stateOverride', async importOriginal => ({
...(await importOriginal<object>()),
getMinimalStateOverride: vi.fn().mockResolvedValue(undefined),
}))

const mockEvmChainAdapter = {
getGasFeeData: () =>
Promise.resolve({
Expand Down
Loading
Loading