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
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_BOB_GATEWAY_SWAP=false
VITE_FEATURE_USERBACK=true
VITE_FEATURE_AGENTIC_CHAT=false
VITE_FEATURE_MM_NATIVE_MULTICHAIN=false
Expand Down Expand Up @@ -365,3 +366,6 @@ VITE_USERBACK_TOKEN=A-3gHopRTd55QqxXGsJd0XLVVG3

# agentic chat
VITE_AGENTIC_SERVER_BASE_URL=https://api.agent.shapeshift.com

# bob gateway
VITE_BOB_GATEWAY_API_KEY=fc969b70f1767095a1b188c92e30d7d9
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ VITE_FEATURE_HEMI=true
VITE_FEATURE_SONIC=true
VITE_FEATURE_UNICHAIN=true
VITE_FEATURE_BOB=true
VITE_FEATURE_BOB_GATEWAY_SWAP=true
VITE_FEATURE_MODE=true
VITE_FEATURE_SONEIUM=true
VITE_FEATURE_TON=true
Expand Down
5 changes: 5 additions & 0 deletions headers/csps/defi/swappers/BobGateway.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://gateway-api-mainnet.gobob.xyz'],
}
2 changes: 2 additions & 0 deletions headers/csps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { csp as safe } from './defi/safe'
import { csp as zeroX } from './defi/swappers/0x'
import { csp as avnu } from './defi/swappers/Avnu'
import { csp as bebop } from './defi/swappers/Bebop'
import { csp as bobGateway } from './defi/swappers/BobGateway'
import { csp as butterSwap } from './defi/swappers/ButterSwap'
import { csp as cowSwap } from './defi/swappers/CowSwap'
import { csp as nearIntents } from './defi/swappers/NearIntents'
Expand Down Expand Up @@ -184,6 +185,7 @@ export const csps = [
safe,
zeroX,
avnu,
bobGateway,
bebop,
cowSwap,
nearIntents,
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 @@ -37,4 +37,5 @@ export const getServerConfig = (): SwapperConfig => ({
VITE_ACROSS_API_URL: env.ACROSS_API_URL,
VITE_ACROSS_INTEGRATOR_ID: env.ACROSS_INTEGRATOR_ID,
VITE_DEBRIDGE_API_URL: env.DEBRIDGE_API_URL,
VITE_BOB_GATEWAY_API_KEY: env.BOB_GATEWAY_API_KEY,
})
14 changes: 14 additions & 0 deletions packages/public-api/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SwapperName } from '@shapeshiftoss/swapper'

export const ENABLED_SWAPPER_NAMES: readonly SwapperName[] = [
SwapperName.Bebop,
SwapperName.ButterSwap,
SwapperName.Chainflip,
SwapperName.CowSwap,
SwapperName.Mayachain,
SwapperName.NearIntents,
SwapperName.Portals,
SwapperName.Relay,
SwapperName.Thorchain,
SwapperName.Zrx,
]
1 change: 1 addition & 0 deletions packages/public-api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const envSchema = z.object({
// Swapper API keys
ACROSS_INTEGRATOR_ID: z.string().default(''),
BEBOP_API_KEY: z.string().min(1),
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),
Expand Down
3 changes: 2 additions & 1 deletion packages/public-api/src/routes/quote/getQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { Request, Response } from 'express'
import { v4 as uuidv4 } from 'uuid'

import { getAsset } from '../../assets'
import { ENABLED_SWAPPER_NAMES } from '../../constants'
import { env } from '../../env'
import { QuoteStore, quoteStore } from '../../lib/quoteStore'
import { registry } from '../../registry'
Expand Down Expand Up @@ -78,7 +79,7 @@ export const getQuote = async (req: Request, res: Response): Promise<void> => {
}

const swapper = swappers[validSwapperName]
if (!swapper) {
if (!swapper || !ENABLED_SWAPPER_NAMES.includes(validSwapperName)) {
res.status(400).json({
error: `Swapper not available: ${swapperName}`,
} satisfies ErrorResponse)
Expand Down
16 changes: 2 additions & 14 deletions packages/public-api/src/routes/rates/getRates.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { GetTradeRateInput } from '@shapeshiftoss/swapper'
import { getTradeRates, SwapperName, swappers, TradeQuoteError } from '@shapeshiftoss/swapper'
import { getTradeRates, swappers, TradeQuoteError } from '@shapeshiftoss/swapper'
import type { Request, Response } from 'express'

import { getAsset } from '../../assets'
import { ENABLED_SWAPPER_NAMES } from '../../constants'
import { env } from '../../env'
import { registry } from '../../registry'
import { getSwapperDeps } from '../../swapperDeps'
Expand All @@ -11,19 +12,6 @@ import { PartnerCodeHeaderSchema, rateLimitResponse } from '../../types'
import type { ApiRate, RateResponse } from './types'
import { RateResponseSchema, RatesRequestSchema } from './types'

const ENABLED_SWAPPER_NAMES = [
SwapperName.Bebop,
SwapperName.ButterSwap,
SwapperName.Chainflip,
SwapperName.CowSwap,
SwapperName.Mayachain,
SwapperName.NearIntents,
SwapperName.Portals,
SwapperName.Relay,
SwapperName.Thorchain,
SwapperName.Zrx,
] as const

// Rate timeout per swapper (10 seconds)
const RATE_TIMEOUT_MS = 10_000

Expand Down
1 change: 1 addition & 0 deletions packages/swapper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@coral-xyz/anchor": "0.29.0",
"@cowprotocol/app-data": "^2.3.0",
"@defuse-protocol/one-click-sdk-typescript": "^0.1.1-0.2",
"@gobob/bob-sdk": "5.6.0",
"@mysten/sui": "^1.45.2",
"@shapeshiftoss/bitcoinjs-lib": "7.0.0-shapeshift.0",
"@shapeshiftoss/caip": "workspace:^",
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 @@ -8,6 +8,8 @@ import { avnuSwapper } from './swappers/AvnuSwapper/AvnuSwapper'
import { avnuApi } from './swappers/AvnuSwapper/endpoints'
import { bebopSwapper } from './swappers/BebopSwapper/BebopSwapper'
import { bebopApi } from './swappers/BebopSwapper/endpoints'
import { bobGatewaySwapper } from './swappers/BobGatewaySwapper/BobGatewaySwapper'
import { bobGatewayApi } from './swappers/BobGatewaySwapper/endpoints'
import { butterSwap } from './swappers/ButterSwap/ButterSwap'
import { butterSwapApi } from './swappers/ButterSwap/endpoints'
import { cetusSwapper } from './swappers/CetusSwapper/CetusSwapper'
Expand Down Expand Up @@ -116,6 +118,10 @@ export const swappers: Record<SwapperName, (SwapperApi & Swapper) | undefined> =
...debridgeSwapper,
...debridgeApi,
},
[SwapperName.BobGateway]: {
...bobGatewaySwapper,
...bobGatewayApi,
},
[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_BOB_GATEWAY_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.BobGateway:
return DEFAULT_BOB_GATEWAY_SLIPPAGE_DECIMAL_PERCENTAGE
default:
return assertUnreachable(swapperName)
}
Expand Down
1 change: 1 addition & 0 deletions packages/swapper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './cowswap-utils'
export * from './safe-utils'
export * from './swapper'
export * from './swappers/ArbitrumBridgeSwapper'
export * from './swappers/BobGatewaySwapper'
export * from './swappers/AvnuSwapper'
export * from './swappers/BebopSwapper'
export * from './swappers/CetusSwapper'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Swapper } from '../../types'
import { executeEvmTransaction } from '../../utils'

export const bobGatewaySwapper: Swapper = {
executeEvmTransaction,
executeUtxoTransaction: (txToSign, { signAndBroadcastTransaction }) =>
signAndBroadcastTransaction(txToSign),
}
184 changes: 184 additions & 0 deletions packages/swapper/src/swappers/BobGatewaySwapper/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { isGatewayError } from '@gobob/bob-sdk'
import { evm } from '@shapeshiftoss/chain-adapters'
import { TxStatus } from '@shapeshiftoss/unchained-client'

import type { SwapperApi, UtxoFeeData } from '../../types'
import { getExecutableTradeStep, isExecutableTradeQuote } from '../../utils'
import { getBobGatewayTradeQuote } from './swapperApi/getTradeQuote'
import { getBobGatewayTradeRate } from './swapperApi/getTradeRate'
import {
getBobGatewayClient,
mapBobGatewayOrderStatusToTxStatus,
registerBobGatewayTx,
} from './utils/helpers'

const registeredSwapIds = new Set<string>()

export const bobGatewayApi: SwapperApi = {
getTradeRate: async (input, deps) => {
return (await getBobGatewayTradeRate(input, deps)).map(tradeRate => [tradeRate])
},
getTradeQuote: async (input, deps) => {
return (await getBobGatewayTradeQuote(input, deps)).map(tradeQuote => [tradeQuote])
},
getUnsignedUtxoTransaction: ({
stepIndex,
tradeQuote,
xpub,
accountType,
assertGetUtxoChainAdapter,
}) => {
if (!isExecutableTradeQuote(tradeQuote)) {
throw new Error('[BobGateway] unable to execute a trade rate')
}

const step = getExecutableTradeStep(tradeQuote, stepIndex)
const { bobSpecific, sellAsset } = step

const adapter = assertGetUtxoChainAdapter(sellAsset.chainId)

if (!bobSpecific?.utxoTx) throw new Error('[BobGateway] invalid utxo transaction')
const { depositAddress, opReturnData, sender } = bobSpecific.utxoTx

return adapter.buildSendApiTransaction({
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
xpub,
to: depositAddress,
accountNumber: step.accountNumber,
skipToAddressValidation: true,
chainSpecific: {
from: sender,
accountType,
opReturnData,
satoshiPerByte: (step.feeData.chainSpecific as UtxoFeeData).satsPerByte,
},
})
},
getUtxoTransactionFees: async ({ stepIndex, tradeQuote, xpub, assertGetUtxoChainAdapter }) => {
if (!isExecutableTradeQuote(tradeQuote)) {
throw new Error('[BobGateway] unable to execute a trade rate')
}

const step = getExecutableTradeStep(tradeQuote, stepIndex)
const { bobSpecific, sellAsset } = step

const adapter = assertGetUtxoChainAdapter(sellAsset.chainId)

if (!bobSpecific?.utxoTx) throw new Error('[BobGateway] invalid utxo transaction')
const { depositAddress, opReturnData, sender } = bobSpecific.utxoTx

const { fast } = await adapter.getFeeData({
to: depositAddress,
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
chainSpecific: { pubkey: xpub, opReturnData, from: sender },
sendMax: false,
})

return fast.txFee
},
getUnsignedEvmTransaction: async ({
from,
stepIndex,
tradeQuote,
assertGetEvmChainAdapter,
supportsEIP1559,
}) => {
if (!isExecutableTradeQuote(tradeQuote)) {
throw new Error('[BobGateway] unable to execute a trade rate')
}

const step = getExecutableTradeStep(tradeQuote, stepIndex)
const { accountNumber, bobSpecific, sellAsset } = step

const adapter = assertGetEvmChainAdapter(sellAsset.chainId)

if (!bobSpecific?.evmTx) throw new Error('[BobGateway] invalid evm transaction')
const { to, data, value } = bobSpecific.evmTx

const feeData = await evm.getFees({ adapter, data, to, value, from, supportsEIP1559 })

return adapter.buildCustomApiTx({
accountNumber,
from,
to,
value,
data,
...feeData,
})
},
getEvmTransactionFees: async ({
from,
stepIndex,
tradeQuote,
supportsEIP1559,
assertGetEvmChainAdapter,
}) => {
if (!isExecutableTradeQuote(tradeQuote)) {
throw new Error('[BobGateway] unable to execute a trade rate')
}

const step = getExecutableTradeStep(tradeQuote, stepIndex)
const { bobSpecific, sellAsset } = step

const adapter = assertGetEvmChainAdapter(sellAsset.chainId)

if (!bobSpecific?.evmTx) throw new Error('[BobGateway] invalid evm transaction')
const { to, data, value } = bobSpecific.evmTx

const feeData = await evm.getFees({ adapter, data, to, value, from, supportsEIP1559 })

return feeData.networkFeeCryptoBaseUnit
},
checkTradeStatus: async ({ swap, config, txHash }) => {
if (!swap) throw new Error('[BobGateway] swap is required for status check')

const orderId = swap.metadata.bobSpecific?.orderId
if (!orderId) throw new Error('[BobGateway] orderId is required for status check')

if (txHash && !registeredSwapIds.has(swap.id)) {
try {
await registerBobGatewayTx({
config,
orderId,
txHash,
sellAsset: swap.sellAsset,
buyAsset: swap.buyAsset,
})
registeredSwapIds.add(swap.id)
} catch {}
}

let orderInfo
try {
orderInfo = await getBobGatewayClient(config).getOrder(orderId)
} catch (err) {
if (isGatewayError(err)) {
if (err.code === 'ORDER_NOT_FOUND') {
return {
buyTxHash: undefined,
status: TxStatus.Unknown,
message: 'Waiting for deposit...',
}
}
}

throw err
}
Comment thread
kaladinlight marked this conversation as resolved.

const status = mapBobGatewayOrderStatusToTxStatus(orderInfo.status)

const buyTxHash =
'success' in orderInfo.status ? orderInfo.status.success.receivedTokens[0]?.txHash : undefined

const refundTxHash =
'refunded' in orderInfo.status
? orderInfo.status.refunded.refundedTokens[0]?.txHash
: undefined

return {
status,
buyTxHash,
message: refundTxHash ? 'Trade refunded' : undefined,
}
},
}
3 changes: 3 additions & 0 deletions packages/swapper/src/swappers/BobGatewaySwapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { bobGatewayApi } from './endpoints'
export { bobGatewaySwapper } from './BobGatewaySwapper'
export * from './utils/constants'
Loading
Loading