Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8c12860
feat: integrate Garden swapper for BTC ↔ strkBTC
swdiscordia May 15, 2026
cd25497
chore(swapper): tighten Garden config for review
swdiscordia May 15, 2026
093e26f
chore(assets): inject strkBTC into generated asset data
swdiscordia May 15, 2026
729dd56
fix(swapper): pre-empt Garden review feedback
swdiscordia May 15, 2026
8f980a5
refactor(swapper): extract shared starknet-utils
swdiscordia May 15, 2026
ba36446
fix(assets): strkBTC relatedAssetKey self-reference
swdiscordia May 16, 2026
faff23b
chore(assets): defer generated asset data to daily bot
swdiscordia May 16, 2026
ed8e1d8
docs(swapper): clarify Garden fee model in INTEGRATION.md
swdiscordia May 16, 2026
60c116f
fix(swapper): surface actualBuyAmountCryptoBaseUnit on Garden status
swdiscordia May 16, 2026
eb2a8a4
fix(assets): strkBTC relatedAssetKey points to wBTC.eth for price dis…
swdiscordia May 16, 2026
ab867e4
refactor(swapper): align Garden with ShapeShift conventions (audit fi…
swdiscordia May 16, 2026
cf7aebb
revert: map Garden insufficient liquidity back to NoRouteFound
swdiscordia May 16, 2026
5cbf758
feat(swapper): pre-validate Garden min/max via /v2/assets
swdiscordia May 16, 2026
3567316
fix(swapper): post-multi-agent-audit cleanups
swdiscordia May 16, 2026
0d72d8e
fix(swapper): use DAO_TREASURY_BITCOIN for Bitcoin fee simulation
swdiscordia May 16, 2026
6d58e30
fix(assets): surface strkBTC under Wrapped Bitcoin family
swdiscordia May 19, 2026
bbb880c
fix(selectors): surface held single-variant first in family expansion
swdiscordia May 19, 2026
2c2551b
fix(swapper): omit Garden actualBuyAmount when filled_amount is zero
swdiscordia May 19, 2026
d8b3b61
fix(swapper): address CodeRabbit review findings
swdiscordia May 19, 2026
72e38a3
fix(swapper): address parallel-agent adversarial review findings
swdiscordia May 19, 2026
13a1b13
fix(swapper): surface Garden HTTP 4xx error bodies as typed TradeQuot…
swdiscordia May 19, 2026
6835458
fix(swapper): handle null Garden approval_transaction on Starknet source
swdiscordia May 19, 2026
673f284
fix(swapper): align Garden trade-rate fee fallback with NearIntents
swdiscordia May 19, 2026
b2d8b4f
Merge remote-tracking branch 'upstream/develop' into feat_garden_swap…
swdiscordia May 19, 2026
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
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ VITE_FEATURE_TON=true
VITE_FEATURE_EARN_TAB=true
VITE_FEATURE_ACROSS_SWAP=true
VITE_FEATURE_DEBRIDGE_SWAP=true
VITE_FEATURE_GARDEN_SWAP=false
VITE_FEATURE_USERBACK=true
VITE_FEATURE_AGENTIC_CHAT=false
VITE_FEATURE_MM_NATIVE_MULTICHAIN=false
Expand Down Expand Up @@ -330,6 +331,9 @@ VITE_FASTNEAR_API_URL=https://api.fastnear.com
# jito
VITE_JITO_BLOCK_ENGINE_URL=https://mainnet.block-engine.jito.wtf

# Garden
VITE_GARDEN_API_KEY=

# relay
VITE_RELAY_API_URL=https://api.relay.link

Expand Down
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ VITE_FEATURE_TX_HISTORY_BYE_BYE=true
VITE_FEATURE_SWAPPER_FIAT_RAMPS=true
VITE_FEATURE_WC_DIRECT_CONNECTION=true
VITE_FEATURE_CETUS_SWAP=true
VITE_FEATURE_GARDEN_SWAP=true
VITE_GARDEN_API_KEY=
VITE_FEATURE_MANTLE=true
VITE_FEATURE_INK=true
VITE_FEATURE_CRONOS=true
Expand Down
5 changes: 5 additions & 0 deletions headers/csps/defi/swappers/Garden.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Csp } from '../../../types'

export const csp: Csp = {
'connect-src': ['https://api.garden.finance'],
}
2 changes: 2 additions & 0 deletions headers/csps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { csp as avnu } from './defi/swappers/Avnu'
import { csp as bebop } from './defi/swappers/Bebop'
import { csp as butterSwap } from './defi/swappers/ButterSwap'
import { csp as cowSwap } from './defi/swappers/CowSwap'
import { csp as garden } from './defi/swappers/Garden'
import { csp as nearIntents } from './defi/swappers/NearIntents'
import { csp as oneInch } from './defi/swappers/OneInch'
import { csp as portals } from './defi/swappers/Portals'
Expand Down Expand Up @@ -186,6 +187,7 @@ export const csps = [
avnu,
bebop,
cowSwap,
garden,
nearIntents,
oneInch,
portals,
Expand Down
1 change: 1 addition & 0 deletions packages/public-api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ 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_GARDEN_API_KEY: env.GARDEN_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,
Expand Down
1 change: 1 addition & 0 deletions packages/public-api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const envSchema = z.object({
ACROSS_INTEGRATOR_ID: z.string().default(''),
BEBOP_API_KEY: z.string().min(1),
CHAINFLIP_API_KEY: z.string().min(1),
GARDEN_API_KEY: z.string().default(''),
NEAR_INTENTS_API_KEY: z.string().min(1),
TENDERLY_API_KEY: z.string().min(1),
TENDERLY_ACCOUNT_SLUG: z.string().min(1),
Expand Down
9 changes: 9 additions & 0 deletions packages/swapper/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { cowSwapper } from './swappers/CowSwapper/CowSwapper'
import { cowApi } from './swappers/CowSwapper/endpoints'
import { debridgeSwapper } from './swappers/DebridgeSwapper'
import { debridgeApi } from './swappers/DebridgeSwapper/endpoints'
import { gardenApi } from './swappers/GardenSwapper/endpoints'
import { gardenSwapper } from './swappers/GardenSwapper/GardenSwapper'
import { mayachainApi } from './swappers/MayachainSwapper/endpoints'
import { mayachainSwapper } from './swappers/MayachainSwapper/MayachainSwapper'
import { nearIntentsApi } from './swappers/NearIntentsSwapper/endpoints'
Expand Down Expand Up @@ -116,6 +118,10 @@ export const swappers: Record<SwapperName, (SwapperApi & Swapper) | undefined> =
...debridgeSwapper,
...debridgeApi,
},
[SwapperName.Garden]: {
...gardenSwapper,
...gardenApi,
},
[SwapperName.Test]: undefined,
}

Expand All @@ -135,6 +141,7 @@ const DEFAULT_AVNU_SLIPPAGE_DECIMAL_PERCENTAGE = '0.02'
const DEFAULT_STONFI_SLIPPAGE_DECIMAL_PERCENTAGE = '0.01'
// deBridge API off-chain simulation overestimates output on some chains (e.g. SEI ~2.4%), so auto slippage (1%) is insufficient
const DEFAULT_DEBRIDGE_SLIPPAGE_DECIMAL_PERCENTAGE = '0.03'
const DEFAULT_GARDEN_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005'

export const getDefaultSlippageDecimalPercentageForSwapper = (
swapperName: SwapperName | undefined,
Expand Down Expand Up @@ -175,6 +182,8 @@ export const getDefaultSlippageDecimalPercentageForSwapper = (
return DEFAULT_AVNU_SLIPPAGE_DECIMAL_PERCENTAGE
case SwapperName.Stonfi:
return DEFAULT_STONFI_SLIPPAGE_DECIMAL_PERCENTAGE
case SwapperName.Garden:
return DEFAULT_GARDEN_SLIPPAGE_DECIMAL_PERCENTAGE
default:
return assertUnreachable(swapperName)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/swapper/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './constants'
export * from './cowswap-utils'
export * from './safe-utils'
export * from './starknet-utils'
export * from './swapper'
export * from './swappers/ArbitrumBridgeSwapper'
export * from './swappers/AvnuSwapper'
Expand All @@ -10,6 +11,7 @@ export * from './swappers/ChainflipSwapper'
export * from './swappers/SunioSwapper'
export * from './swappers/CowSwapper'
export * from './swappers/DebridgeSwapper'
export * from './swappers/GardenSwapper'
export * from './swappers/PortalsSwapper'
export * from './swappers/ThorchainSwapper'
export * from './swappers/MayachainSwapper'
Expand Down
168 changes: 168 additions & 0 deletions packages/swapper/src/starknet-utils/buildStarknetInvokeTx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { starknet } from '@shapeshiftoss/chain-adapters'
import { toAddressNList } from '@shapeshiftoss/chain-adapters'
import { hash, num } from 'starknet'

export const toHexString = (value: unknown): string => {
if (typeof value !== 'string') {
throw new Error(`toHexString: expected string, got ${typeof value}`)
}
if (value.startsWith('0x')) return value
if (/^[0-9a-fA-F]+$/.test(value) && /[a-fA-F]/.test(value)) {
return `0x${value}`
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (/^\d+$/.test(value)) {
return num.toHex(value)
}
throw new Error(`toHexString: ambiguous input ${JSON.stringify(value)}`)
}

type StarknetEstimateResult = {
result?: {
l1_gas_consumed?: string
l1_gas_price?: string
l2_gas_consumed?: string
l2_gas_price?: string
l1_data_gas_consumed?: string
l1_data_gas_price?: string
}[]
error?: unknown
}

export const buildStarknetInvokeTx = async ({
formattedCalldata,
normalizedFrom,
accountNumber,
adapter,
}: {
formattedCalldata: string[]
normalizedFrom: string
accountNumber: number
adapter: starknet.ChainAdapter
}) => {
const chainIdHex = await adapter.getStarknetProvider().getChainId()
const nonce = await adapter.getNonce(normalizedFrom)

const version = '0x3' as const
const estimateTx = {
type: 'INVOKE',
version,
sender_address: normalizedFrom,
calldata: formattedCalldata,
signature: [],
nonce,
resource_bounds: {
l1_gas: { max_amount: '0x186a0', max_price_per_unit: '0x5f5e100' },
l2_gas: { max_amount: '0x0', max_price_per_unit: '0x0' },
l1_data_gas: { max_amount: '0x186a0', max_price_per_unit: '0x1' },
},
tip: '0x0',
paymaster_data: [],
account_deployment_data: [],
nonce_data_availability_mode: 'L1',
fee_data_availability_mode: 'L1',
}

const estimateResult: StarknetEstimateResult = await (async () => {
try {
const response = await adapter
.getStarknetProvider()
.fetch('starknet_estimateFee', [[estimateTx], ['SKIP_VALIDATE'], 'latest'])
return (await response.json()) as StarknetEstimateResult
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
throw new Error(`starknet_estimateFee RPC call failed for ${normalizedFrom}: ${message}`)
}
})()

if (estimateResult.error) {
throw new Error(
`starknet_estimateFee returned error for ${normalizedFrom}: ${JSON.stringify(
estimateResult.error,
)}`,
)
}

const feeEstimate = estimateResult.result?.[0]
if (!feeEstimate) {
throw new Error(`starknet_estimateFee returned no estimate for ${normalizedFrom}`)
}
Comment on lines +42 to +88
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve typed swapper errors in the shared RPC path.

This helper now performs the provider calls for both Avnu and Garden, but RPC/json failures and the fee-estimation branches still escape as raw Errors. That makes execution failures harder to classify, localize, and internationalize once they bubble through the swapper layer. Wrap this path in typed error handling with structured context instead of throwing generic exceptions.

As per coding guidelines, "ALWAYS wrap async operations in try-catch blocks and use AsyncResultOf utility for converting promises to Results" and "ALWAYS use custom error classes from @shapeshiftoss/errors with meaningful error codes for internationalization and relevant details in error objects".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/starknet-utils/buildStarknetInvokeTx.ts` around lines 41
- 76, The fee-estimation path (calls to adapter.getStarknetProvider().fetch and
subsequent handling of estimateResult/feeEstimate in buildStarknetInvokeTx) must
be wrapped in a try/catch and converted to an AsyncResultOf promise pattern;
catch RPC/json failures and replace raw Error throws with typed errors from
`@shapeshiftoss/errors` (use a meaningful error code and include context like
method='starknet_estimateFee', request payload=estimateTx, and any response
error/result), and return/propagate the Result type instead of throwing so
callers can distinguish and internationalize errors.


const l1GasConsumed = feeEstimate.l1_gas_consumed
? BigInt(feeEstimate.l1_gas_consumed)
: BigInt('0x186a0')
const l1GasPrice = feeEstimate.l1_gas_price
? BigInt(feeEstimate.l1_gas_price)
: BigInt('0x5f5e100')
const l2GasConsumed = feeEstimate.l2_gas_consumed
? BigInt(feeEstimate.l2_gas_consumed)
: BigInt('0x0')
const l2GasPrice = feeEstimate.l2_gas_price ? BigInt(feeEstimate.l2_gas_price) : BigInt('0x0')
const l1DataGasConsumed = feeEstimate.l1_data_gas_consumed
? BigInt(feeEstimate.l1_data_gas_consumed)
: BigInt('0x186a0')
const l1DataGasPrice = feeEstimate.l1_data_gas_price
? BigInt(feeEstimate.l1_data_gas_price)
: BigInt('0x1')

const resourceBounds = {
l1_gas: {
max_amount: (l1GasConsumed * BigInt(500)) / BigInt(100),
max_price_per_unit: (l1GasPrice * BigInt(200)) / BigInt(100),
},
l2_gas: {
max_amount: (l2GasConsumed * BigInt(500)) / BigInt(100),
max_price_per_unit: (l2GasPrice * BigInt(200)) / BigInt(100),
},
l1_data_gas: {
max_amount: (l1DataGasConsumed * BigInt(500)) / BigInt(100),
max_price_per_unit: (l1DataGasPrice * BigInt(200)) / BigInt(100),
},
}

const invokeHashInputs = {
senderAddress: normalizedFrom,
version,
compiledCalldata: formattedCalldata,
chainId: chainIdHex,
nonce,
nonceDataAvailabilityMode: 0 as const,
feeDataAvailabilityMode: 0 as const,
resourceBounds: {
l1_gas: {
max_amount: resourceBounds.l1_gas.max_amount,
max_price_per_unit: resourceBounds.l1_gas.max_price_per_unit,
},
l2_gas: {
max_amount: resourceBounds.l2_gas.max_amount,
max_price_per_unit: resourceBounds.l2_gas.max_price_per_unit,
},
l1_data_gas: {
max_amount: resourceBounds.l1_data_gas.max_amount,
max_price_per_unit: resourceBounds.l1_data_gas.max_price_per_unit,
},
},
tip: '0x0',
paymasterData: [],
accountDeploymentData: [],
}

const txHash = hash.calculateInvokeTransactionHash(invokeHashInputs)

return {
addressNList: toAddressNList(adapter.getBip44Params({ accountNumber })),
txHash,
_txDetails: {
fromAddress: normalizedFrom,
calldata: formattedCalldata,
nonce,
version,
resourceBounds,
chainId: chainIdHex,
nonceDataAvailabilityMode: 0 as const,
feeDataAvailabilityMode: 0 as const,
tip: '0x0',
paymasterData: [],
accountDeploymentData: [],
},
}
}
1 change: 1 addition & 0 deletions packages/swapper/src/starknet-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './buildStarknetInvokeTx'
Loading