Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add chainflip dca #8152

Open
wants to merge 57 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b7697e8
feat: add dca related models
CumpsD Nov 20, 2024
0bbcbee
feat: add dca quotes to available quotes
CumpsD Nov 20, 2024
9c179ff
feat: add swap status to chainflipStatus type
CumpsD Nov 20, 2024
ab7433f
feat: make StreamingSwap provider-agnostic
CumpsD Nov 20, 2024
5280261
feat: add chainflip streaming hook and pass the correct one to Stream…
CumpsD Nov 20, 2024
1094347
Merge branch 'develop' into chainflip-dca
CumpsD Nov 20, 2024
2afb05a
chore: remove useless comment
CumpsD Nov 20, 2024
50d7a87
feat: add dca params to swap request
CumpsD Nov 20, 2024
0f4f45c
feat: pass dca params from quote to swap
CumpsD Nov 20, 2024
af129ac
feat: pass in tradequote to get the chunks
CumpsD Nov 21, 2024
dd90aa3
Merge branch 'develop' into chainflip-dca
CumpsD Nov 21, 2024
e1eb1de
refactor: clarify maxBoostFee
CumpsD Nov 21, 2024
b770ae0
chore: run lint:fix
CumpsD Nov 21, 2024
77dca75
Merge branch 'shapeshift:develop' into chainflip-dca
CumpsD Nov 24, 2024
105d917
feat: open deposit channel early for dca swaps
CumpsD Nov 24, 2024
d93d26f
refactor: swap quote and trades
CumpsD Nov 25, 2024
d16a54f
fix: add swapId
CumpsD Nov 25, 2024
d1b3a0f
fix: catch axios error
CumpsD Nov 25, 2024
8c2eda5
chore: add some logging
CumpsD Nov 25, 2024
6494ff8
Merge branch 'chainflip-dca' of https://github.com/CumpsD/web into ch…
CumpsD Nov 25, 2024
97444ad
chore: lint
CumpsD Nov 25, 2024
ccc906c
fix: chunks are 0 when ended
CumpsD Nov 25, 2024
11af8de
chore: remove debug logging
CumpsD Nov 25, 2024
1636313
fix: make sure swapper metadata has swap id too
CumpsD Nov 25, 2024
3bd2645
chore: we can keep this logic
CumpsD Nov 25, 2024
f147a1f
Merge branch 'shapeshift:develop' into chainflip-dca
CumpsD Nov 25, 2024
3d7600e
chore: cleanup small mistakes
CumpsD Nov 26, 2024
3cbdaa1
Merge branch 'develop' into chainflip-dca
CumpsD Nov 26, 2024
9e86529
refactor: make dca and non-dca consistent, cleans up a lot of code!
CumpsD Nov 26, 2024
772feea
feat: be explicit about not boosting
CumpsD Nov 26, 2024
bb2a67d
feat: add an extra safety check
CumpsD Nov 26, 2024
a99447f
feat: safety check for account number on get Quote
CumpsD Nov 26, 2024
87b4531
refactor: move safety checks from inside for to caller
CumpsD Nov 26, 2024
56ccfc2
chore: lint
CumpsD Nov 26, 2024
f676589
refactor: simplify getTradeRates, make getTradeQuote safer, and recal…
CumpsD Nov 26, 2024
5a95a2f
feat: rename
gomesalexandre Nov 27, 2024
75f85a9
fix: ci
gomesalexandre Nov 27, 2024
8406c05
feat: cleanup
gomesalexandre Nov 27, 2024
f252141
feat: consistent terminology
gomesalexandre Nov 27, 2024
f80dd11
feat: ocd
gomesalexandre Nov 27, 2024
b90883d
fix: btc without xpub
gomesalexandre Nov 27, 2024
145f8b6
feat: and solana too
gomesalexandre Nov 27, 2024
44b8544
Merge remote-tracking branch 'origin/develop' into chainflip-dca
gomesalexandre Nov 27, 2024
44707d4
feat: additional cleanup and fix CI
gomesalexandre Nov 27, 2024
3e1a7b6
feat: single arg object arity in streaming progress hooks
gomesalexandre Nov 27, 2024
1137a1d
feat: cleanup
gomesalexandre Nov 27, 2024
24ee61c
fix: types
gomesalexandre Nov 27, 2024
7a2ceb6
feat: avoid ternary
gomesalexandre Nov 27, 2024
1b8c0c2
feat: flag
gomesalexandre Nov 27, 2024
57d9196
feat: add dca to dev and develop envs
gomesalexandre Nov 27, 2024
f1b87c2
fix: protocolFees make quote throw
gomesalexandre Nov 27, 2024
5c09666
feat: monkey patch, revert me
gomesalexandre Nov 27, 2024
a5c68f0
Revert "feat: monkey patch, revert me"
gomesalexandre Nov 27, 2024
2f74bf7
Merge branch 'develop' into chainflip-dca
CumpsD Dec 4, 2024
21001fe
chore: lint
CumpsD Dec 4, 2024
aa11d2a
fix: pass senderAddress for utxo
CumpsD Dec 4, 2024
578faf3
feat: return exact minimum amount
CumpsD Dec 4, 2024
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
195 changes: 22 additions & 173 deletions packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { FeeDataKey } from '@shapeshiftoss/chain-adapters'
import type { BTCSignTx, SolanaSignTx } from '@shapeshiftoss/hdwallet-core'
import type { EvmChainId, KnownChainIds } from '@shapeshiftoss/types'
import { TxStatus } from '@shapeshiftoss/unchained-client'
import type { AxiosError } from 'axios'
import type { InterpolationOptions } from 'node-polyglot'

import type {
Expand All @@ -16,18 +15,12 @@ import type {
UtxoFeeData,
} from '../../types'
import { isExecutableTradeQuote, isExecutableTradeStep, isToken } from '../../utils'
import { CHAINFLIP_BAAS_COMMISSION, CHAINFLIP_BOOST_SWAP_SOURCE } from './constants'
import type { ChainflipBaasSwapDepositAddress } from './models/ChainflipBaasSwapDepositAddress'
import type { ChainflipBaasSwapDepositAddress } from './models'
import { getTradeQuote } from './swapperApi/getTradeQuote'
import { getTradeRate } from './swapperApi/getTradeRate'
import type { ChainFlipStatus } from './types'
import { chainflipService } from './utils/chainflipService'
import { getLatestChainflipStatusMessage } from './utils/getLatestChainflipStatusMessage'
import {
calculateChainflipMinPrice,
getChainFlipIdFromAssetId,
getChainFlipSwap,
} from './utils/helpers'

// Persists the ID so we can look it up later when checking the status
const tradeQuoteMetadata: Map<string, ChainflipBaasSwapDepositAddress> = new Map()
Expand All @@ -40,70 +33,25 @@ export const chainflipApi: SwapperApi = {
from,
tradeQuote,
assertGetEvmChainAdapter,
config,
supportsEIP1559,
}: GetUnsignedEvmTransactionArgs): Promise<EvmTransactionRequest> => {
if (!isExecutableTradeQuote(tradeQuote)) throw Error('Unable to execute trade')

const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL
const apiKey = config.REACT_APP_CHAINFLIP_API_KEY

const step = tradeQuote.steps[0]

const isTokenSend = isToken(step.sellAsset.assetId)
const sourceAsset = await getChainFlipIdFromAssetId({
assetId: step.sellAsset.assetId,
brokerUrl,
})
const destinationAsset = await getChainFlipIdFromAssetId({
assetId: step.buyAsset.assetId,
brokerUrl,
})

const minimumPrice = calculateChainflipMinPrice({
slippageTolerancePercentageDecimal: tradeQuote.slippageTolerancePercentageDecimal,
sellAsset: step.sellAsset,
buyAsset: step.buyAsset,
buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit,
sellAmountIncludingProtocolFeesCryptoBaseUnit:
step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
})

// Subtract the BaaS fee to end up at the final displayed commissionBps
let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION
if (serviceCommission < 0) serviceCommission = 0
if (!isExecutableTradeStep(step)) throw Error('Unable to execute step')
if (!step.chainflipDepositAddress) throw Error('Missing deposit address')

const maybeSwapResponse = await getChainFlipSwap({
brokerUrl,
apiKey,
sourceAsset,
destinationAsset,
destinationAddress: tradeQuote.receiveAddress,
minimumPrice,
refundAddress: from,
commissionBps: serviceCommission,
boostFee: 0,
tradeQuoteMetadata.set(tradeQuote.id, {
id: step.chainflipSwapId,
address: step.chainflipDepositAddress,
})

if (maybeSwapResponse.isErr()) {
const error = maybeSwapResponse.unwrapErr()
const cause = error.cause as AxiosError<any, any>
throw Error(cause.response!.data.detail)
}

const { data: swapResponse } = maybeSwapResponse.unwrap()

if (!swapResponse.id) throw Error('missing swap ID')

tradeQuoteMetadata.set(tradeQuote.id, swapResponse)

const depositAddress = swapResponse.address!
const { assetReference } = fromAssetId(step.sellAsset.assetId)

const adapter = assertGetEvmChainAdapter(step.sellAsset.chainId)

const isTokenSend = isToken(step.sellAsset.assetId)
const getFeeDataInput: GetFeeDataInput<EvmChainId> = {
to: depositAddress,
to: step.chainflipDepositAddress,
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
chainSpecific: {
from,
Expand All @@ -115,10 +63,8 @@ export const chainflipApi: SwapperApi = {
const feeData = await adapter.getFeeData(getFeeDataInput)
const fees = feeData[FeeDataKey.Average]

if (!isExecutableTradeStep(step)) throw Error('Unable to execute trade step')

const unsignedTxInput = await adapter.buildSendApiTransaction({
to: depositAddress,
to: step.chainflipDepositAddress,
from,
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
accountNumber: step.accountNumber,
Expand Down Expand Up @@ -148,83 +94,30 @@ export const chainflipApi: SwapperApi = {
gasPrice: unsignedTxInput.gasPrice,
}
},
getUnsignedUtxoTransaction: async ({
getUnsignedUtxoTransaction: ({
tradeQuote,
xpub,
accountType,
assertGetUtxoChainAdapter,
config,
}: GetUnsignedUtxoTransactionArgs): Promise<BTCSignTx> => {
if (!isExecutableTradeQuote(tradeQuote)) throw Error('Unable to execute trade')

const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL
const apiKey = config.REACT_APP_CHAINFLIP_API_KEY

const step = tradeQuote.steps[0]

if (!isExecutableTradeStep(step)) throw Error('Unable to execute step')
if (!step.chainflipDepositAddress) throw Error('Missing deposit address')

const sourceAsset = await getChainFlipIdFromAssetId({
assetId: step.sellAsset.assetId,
brokerUrl,
})
const destinationAsset = await getChainFlipIdFromAssetId({
assetId: step.buyAsset.assetId,
brokerUrl,
})

// Subtract the BaaS fee to end up at the final displayed commissionBps
let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION
if (serviceCommission < 0) serviceCommission = 0

const minimumPrice = calculateChainflipMinPrice({
slippageTolerancePercentageDecimal: tradeQuote.slippageTolerancePercentageDecimal,
sellAsset: step.sellAsset,
buyAsset: step.buyAsset,
buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit,
sellAmountIncludingProtocolFeesCryptoBaseUnit:
step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
tradeQuoteMetadata.set(tradeQuote.id, {
id: step.chainflipSwapId,
address: step.chainflipDepositAddress,
})

const adapter = assertGetUtxoChainAdapter(step.sellAsset.chainId)

const sendAddress = await adapter.getAddress({
accountNumber: step.accountNumber,
// @ts-ignore this is a rare occurence of wallet not being passed but this being fine as we pass a pubKey instead
// types are stricter than they should for the sake of paranoia
wallet,
accountType,
pubKey: xpub,
})

const maybeSwapResponse = await getChainFlipSwap({
brokerUrl,
apiKey,
sourceAsset,
destinationAsset,
destinationAddress: tradeQuote.receiveAddress,
minimumPrice,
refundAddress: sendAddress,
commissionBps: serviceCommission,
boostFee: step.source === CHAINFLIP_BOOST_SWAP_SOURCE ? 10 : 0,
})

if (maybeSwapResponse.isErr()) {
const error = maybeSwapResponse.unwrapErr()
const cause = error.cause as AxiosError<any, any>
throw Error(cause.response!.data.detail)
}

const { data: swapResponse } = maybeSwapResponse.unwrap()

tradeQuoteMetadata.set(tradeQuote.id, swapResponse)

const depositAddress = swapResponse.address!

return adapter.buildSendApiTransaction({
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
xpub: xpub!,
to: depositAddress,
to: step.chainflipDepositAddress,
accountNumber: step.accountNumber,
skipToAddressValidation: true,
chainSpecific: {
Expand All @@ -237,60 +130,18 @@ export const chainflipApi: SwapperApi = {
tradeQuote,
from,
assertGetSolanaChainAdapter,
config,
}: GetUnsignedSolanaTransactionArgs): Promise<SolanaSignTx> => {
if (!isExecutableTradeQuote(tradeQuote)) throw Error('Unable to execute trade')

const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL
const apiKey = config.REACT_APP_CHAINFLIP_API_KEY

const step = tradeQuote.steps[0]

if (!isExecutableTradeStep(step)) throw Error('Unable to execute step')
if (!step.chainflipDepositAddress) throw Error('Missing deposit address')

const sourceAsset = await getChainFlipIdFromAssetId({
assetId: step.sellAsset.assetId,
brokerUrl,
tradeQuoteMetadata.set(tradeQuote.id, {
id: step.chainflipSwapId,
address: step.chainflipDepositAddress,
})
const destinationAsset = await getChainFlipIdFromAssetId({
assetId: step.buyAsset.assetId,
brokerUrl,
})

// Subtract the BaaS fee to end up at the final displayed commissionBps
let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION
if (serviceCommission < 0) serviceCommission = 0

const minimumPrice = calculateChainflipMinPrice({
slippageTolerancePercentageDecimal: tradeQuote.slippageTolerancePercentageDecimal,
sellAsset: step.sellAsset,
buyAsset: step.buyAsset,
buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit,
sellAmountIncludingProtocolFeesCryptoBaseUnit:
step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
})

const maybeSwapResponse = await getChainFlipSwap({
brokerUrl,
apiKey,
sourceAsset,
destinationAsset,
destinationAddress: tradeQuote.receiveAddress,
minimumPrice,
refundAddress: from,
commissionBps: serviceCommission,
boostFee: 0,
})

if (maybeSwapResponse.isErr()) {
const error = maybeSwapResponse.unwrapErr()
const cause = error.cause as AxiosError<any, any>
throw Error(cause.response!.data.detail)
}

const { data: swapResponse } = maybeSwapResponse.unwrap()

tradeQuoteMetadata.set(tradeQuote.id, swapResponse)

const adapter = assertGetSolanaChainAdapter(step.sellAsset.chainId)

Expand All @@ -299,10 +150,8 @@ export const chainflipApi: SwapperApi = {
? undefined
: fromAssetId(step.sellAsset.assetId).assetReference

const depositAddress = swapResponse.address!

const getFeeDataInput: GetFeeDataInput<KnownChainIds.SolanaMainnet> = {
to: depositAddress,
to: step.chainflipDepositAddress,
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
chainSpecific: {
from,
Expand All @@ -312,7 +161,7 @@ export const chainflipApi: SwapperApi = {
const { fast } = await adapter.getFeeData(getFeeDataInput)

const buildSendTxInput: BuildSendApiTxInput<KnownChainIds.SolanaMainnet> = {
to: depositAddress,
to: step.chainflipDepositAddress,
from,
value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit,
accountNumber: step.accountNumber,
Expand All @@ -335,7 +184,7 @@ export const chainflipApi: SwapperApi = {
message: string | [string, InterpolationOptions] | undefined
}> => {
const swap = tradeQuoteMetadata.get(quoteId)
if (!swap) throw Error(`missing trade quote metadata for quoteId ${quoteId}`)
if (!swap) throw Error(`Missing trade quote metadata for quoteId ${quoteId}`)
// Note, the swapId isn't the quoteId - we set the swapId at pre-execution time, when getting the receive addy and instantiating a flip swap
const swapId = swap.id

Expand Down
Loading