From b7697e82a6cfa6e8c1d9e89f439b3d0d379102b8 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Wed, 20 Nov 2024 19:47:59 +0100 Subject: [PATCH 01/49] feat: add dca related models --- .../models/ChainflipBaasStatusChunkInfo.ts | 149 ++++++++++++++++ .../models/ChainflipBaasStatusDca.ts | 95 ++++++++++ .../models/ChainflipBaasStatusSwap.ts | 167 ++++++++++++++++++ 3 files changed, 411 insertions(+) create mode 100644 packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusChunkInfo.ts create mode 100644 packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusDca.ts create mode 100644 packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusSwap.ts diff --git a/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusChunkInfo.ts b/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusChunkInfo.ts new file mode 100644 index 00000000000..32814c5a17f --- /dev/null +++ b/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusChunkInfo.ts @@ -0,0 +1,149 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Chainflip Broker as a Service + * Run your own Chainflip Broker without any hassle. + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface ChainflipBaasStatusChunkInfo + */ +export interface ChainflipBaasStatusChunkInfo { + /** + * + * @type {string} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly inputAmountNative?: string; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly inputAmount?: number; + /** + * + * @type {string} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly intermediateAmountNative?: string | null; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly intermediateAmount?: number | null; + /** + * + * @type {string} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly outputAmountNative?: string | null; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly outputAmount?: number | null; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly scheduledAt?: number; + /** + * + * @type {Date} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly scheduledAtDate?: Date; + /** + * + * @type {string} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly scheduledBlockIndex?: string; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly executedAt?: number | null; + /** + * + * @type {Date} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly executedAtDate?: Date | null; + /** + * + * @type {string} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly executedBlockIndex?: string | null; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusChunkInfo + */ + readonly retryCount?: number; +} + +/** + * Check if a given object implements the ChainflipBaasStatusChunkInfo interface. + */ +export function instanceOfChainflipBaasStatusChunkInfo(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function ChainflipBaasStatusChunkInfoFromJSON(json: any): ChainflipBaasStatusChunkInfo { + return ChainflipBaasStatusChunkInfoFromJSONTyped(json, false); +} + +export function ChainflipBaasStatusChunkInfoFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChainflipBaasStatusChunkInfo { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'inputAmountNative': !exists(json, 'inputAmountNative') ? undefined : json['inputAmountNative'], + 'inputAmount': !exists(json, 'inputAmount') ? undefined : json['inputAmount'], + 'intermediateAmountNative': !exists(json, 'intermediateAmountNative') ? undefined : json['intermediateAmountNative'], + 'intermediateAmount': !exists(json, 'intermediateAmount') ? undefined : json['intermediateAmount'], + 'outputAmountNative': !exists(json, 'outputAmountNative') ? undefined : json['outputAmountNative'], + 'outputAmount': !exists(json, 'outputAmount') ? undefined : json['outputAmount'], + 'scheduledAt': !exists(json, 'scheduledAt') ? undefined : json['scheduledAt'], + 'scheduledAtDate': !exists(json, 'scheduledAtDate') ? undefined : (new Date(json['scheduledAtDate'])), + 'scheduledBlockIndex': !exists(json, 'scheduledBlockIndex') ? undefined : json['scheduledBlockIndex'], + 'executedAt': !exists(json, 'executedAt') ? undefined : json['executedAt'], + 'executedAtDate': !exists(json, 'executedAtDate') ? undefined : (json['executedAtDate'] === null ? null : new Date(json['executedAtDate'])), + 'executedBlockIndex': !exists(json, 'executedBlockIndex') ? undefined : json['executedBlockIndex'], + 'retryCount': !exists(json, 'retryCount') ? undefined : json['retryCount'], + }; +} + +export function ChainflipBaasStatusChunkInfoToJSON(value?: ChainflipBaasStatusChunkInfo | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + }; +} + diff --git a/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusDca.ts b/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusDca.ts new file mode 100644 index 00000000000..0f261b0950c --- /dev/null +++ b/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusDca.ts @@ -0,0 +1,95 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Chainflip Broker as a Service + * Run your own Chainflip Broker without any hassle. + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { ChainflipBaasStatusChunkInfo } from './ChainflipBaasStatusChunkInfo'; +import { + ChainflipBaasStatusChunkInfoFromJSON, + ChainflipBaasStatusChunkInfoFromJSONTyped, + ChainflipBaasStatusChunkInfoToJSON, +} from './ChainflipBaasStatusChunkInfo'; + +/** + * + * @export + * @interface ChainflipBaasStatusDca + */ +export interface ChainflipBaasStatusDca { + /** + * + * @type {ChainflipBaasStatusChunkInfo} + * @memberof ChainflipBaasStatusDca + */ + lastExecutedChunk?: ChainflipBaasStatusChunkInfo; + /** + * + * @type {ChainflipBaasStatusChunkInfo} + * @memberof ChainflipBaasStatusDca + */ + currentChunk?: ChainflipBaasStatusChunkInfo; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusDca + */ + readonly executedChunks?: number; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusDca + */ + readonly remainingChunks?: number; +} + +/** + * Check if a given object implements the ChainflipBaasStatusDca interface. + */ +export function instanceOfChainflipBaasStatusDca(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function ChainflipBaasStatusDcaFromJSON(json: any): ChainflipBaasStatusDca { + return ChainflipBaasStatusDcaFromJSONTyped(json, false); +} + +export function ChainflipBaasStatusDcaFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChainflipBaasStatusDca { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'lastExecutedChunk': !exists(json, 'lastExecutedChunk') ? undefined : ChainflipBaasStatusChunkInfoFromJSON(json['lastExecutedChunk']), + 'currentChunk': !exists(json, 'currentChunk') ? undefined : ChainflipBaasStatusChunkInfoFromJSON(json['currentChunk']), + 'executedChunks': !exists(json, 'executedChunks') ? undefined : json['executedChunks'], + 'remainingChunks': !exists(json, 'remainingChunks') ? undefined : json['remainingChunks'], + }; +} + +export function ChainflipBaasStatusDcaToJSON(value?: ChainflipBaasStatusDca | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'lastExecutedChunk': ChainflipBaasStatusChunkInfoToJSON(value.lastExecutedChunk), + 'currentChunk': ChainflipBaasStatusChunkInfoToJSON(value.currentChunk), + }; +} + diff --git a/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusSwap.ts b/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusSwap.ts new file mode 100644 index 00000000000..45482daaaf7 --- /dev/null +++ b/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasStatusSwap.ts @@ -0,0 +1,167 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Chainflip Broker as a Service + * Run your own Chainflip Broker without any hassle. + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { ChainflipBaasStatusChunkInfo } from './ChainflipBaasStatusChunkInfo'; +import { + ChainflipBaasStatusChunkInfoFromJSON, + ChainflipBaasStatusChunkInfoFromJSONTyped, + ChainflipBaasStatusChunkInfoToJSON, +} from './ChainflipBaasStatusChunkInfo'; +import type { ChainflipBaasStatusDca } from './ChainflipBaasStatusDca'; +import { + ChainflipBaasStatusDcaFromJSON, + ChainflipBaasStatusDcaFromJSONTyped, + ChainflipBaasStatusDcaToJSON, +} from './ChainflipBaasStatusDca'; + +/** + * + * @export + * @interface ChainflipBaasStatusSwap + */ +export interface ChainflipBaasStatusSwap { + /** + * + * @type {string} + * @memberof ChainflipBaasStatusSwap + */ + originalInputAmountNative?: string; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusSwap + */ + originalInputAmount?: number; + /** + * + * @type {string} + * @memberof ChainflipBaasStatusSwap + */ + remainingInputAmountNative?: string; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusSwap + */ + remainingInputAmount?: number; + /** + * + * @type {string} + * @memberof ChainflipBaasStatusSwap + */ + swappedInputAmountNative?: string; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusSwap + */ + swappedInputAmount?: number; + /** + * + * @type {string} + * @memberof ChainflipBaasStatusSwap + */ + swappedIntermediateAmountNative?: string; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusSwap + */ + swappedIntermediateAmount?: number; + /** + * + * @type {string} + * @memberof ChainflipBaasStatusSwap + */ + swappedOutputAmountNative?: string; + /** + * + * @type {number} + * @memberof ChainflipBaasStatusSwap + */ + swappedOutputAmount?: number; + /** + * + * @type {ChainflipBaasStatusChunkInfo} + * @memberof ChainflipBaasStatusSwap + */ + regular?: ChainflipBaasStatusChunkInfo; + /** + * + * @type {ChainflipBaasStatusDca} + * @memberof ChainflipBaasStatusSwap + */ + dca?: ChainflipBaasStatusDca; +} + +/** + * Check if a given object implements the ChainflipBaasStatusSwap interface. + */ +export function instanceOfChainflipBaasStatusSwap(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function ChainflipBaasStatusSwapFromJSON(json: any): ChainflipBaasStatusSwap { + return ChainflipBaasStatusSwapFromJSONTyped(json, false); +} + +export function ChainflipBaasStatusSwapFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChainflipBaasStatusSwap { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'originalInputAmountNative': !exists(json, 'originalInputAmountNative') ? undefined : json['originalInputAmountNative'], + 'originalInputAmount': !exists(json, 'originalInputAmount') ? undefined : json['originalInputAmount'], + 'remainingInputAmountNative': !exists(json, 'remainingInputAmountNative') ? undefined : json['remainingInputAmountNative'], + 'remainingInputAmount': !exists(json, 'remainingInputAmount') ? undefined : json['remainingInputAmount'], + 'swappedInputAmountNative': !exists(json, 'swappedInputAmountNative') ? undefined : json['swappedInputAmountNative'], + 'swappedInputAmount': !exists(json, 'swappedInputAmount') ? undefined : json['swappedInputAmount'], + 'swappedIntermediateAmountNative': !exists(json, 'swappedIntermediateAmountNative') ? undefined : json['swappedIntermediateAmountNative'], + 'swappedIntermediateAmount': !exists(json, 'swappedIntermediateAmount') ? undefined : json['swappedIntermediateAmount'], + 'swappedOutputAmountNative': !exists(json, 'swappedOutputAmountNative') ? undefined : json['swappedOutputAmountNative'], + 'swappedOutputAmount': !exists(json, 'swappedOutputAmount') ? undefined : json['swappedOutputAmount'], + 'regular': !exists(json, 'regular') ? undefined : ChainflipBaasStatusChunkInfoFromJSON(json['regular']), + 'dca': !exists(json, 'dca') ? undefined : ChainflipBaasStatusDcaFromJSON(json['dca']), + }; +} + +export function ChainflipBaasStatusSwapToJSON(value?: ChainflipBaasStatusSwap | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'originalInputAmountNative': value.originalInputAmountNative, + 'originalInputAmount': value.originalInputAmount, + 'remainingInputAmountNative': value.remainingInputAmountNative, + 'remainingInputAmount': value.remainingInputAmount, + 'swappedInputAmountNative': value.swappedInputAmountNative, + 'swappedInputAmount': value.swappedInputAmount, + 'swappedIntermediateAmountNative': value.swappedIntermediateAmountNative, + 'swappedIntermediateAmount': value.swappedIntermediateAmount, + 'swappedOutputAmountNative': value.swappedOutputAmountNative, + 'swappedOutputAmount': value.swappedOutputAmount, + 'regular': ChainflipBaasStatusChunkInfoToJSON(value.regular), + 'dca': ChainflipBaasStatusDcaToJSON(value.dca), + }; +} + From 0bbcbee94df09115afb0b2231daea891dc69b59b Mon Sep 17 00:00:00 2001 From: David Cumps Date: Wed, 20 Nov 2024 19:48:29 +0100 Subject: [PATCH 02/49] feat: add dca quotes to available quotes --- .../ChainflipSwapper/swapperApi/getTradeQuote.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index 9b2235f6cce..8434926e129 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -255,12 +255,6 @@ const _getTradeQuote = async ( const isStreaming = singleQuoteResponse.type === CHAINFLIP_DCA_QUOTE const feeData = await getFeeData() - if (isStreaming && !deps.config.REACT_APP_FEATURE_CHAINFLIP_DCA) { - // DCA currently disabled - Streaming swap logic is very much tied to THOR currently and will deserve its own PR to generalize - // Even if we manage to get DCA swaps to execute, we wouldn't manage to properly poll with current web THOR-centric arch - continue - } - if (singleQuoteResponse.boostQuote) { const boostRate = getQuoteRate( singleQuoteResponse.boostQuote.ingressAmountNative!, @@ -368,6 +362,5 @@ export const getTradeQuote = async ( ) } - const quotes = await _getTradeQuote(input, deps) - return quotes + return await _getTradeQuote(input, deps) } From 9c179ff189174e31f2c2d060906b6f0c6a6ee16d Mon Sep 17 00:00:00 2001 From: David Cumps Date: Wed, 20 Nov 2024 19:48:54 +0100 Subject: [PATCH 03/49] feat: add swap status to chainflipStatus type --- packages/swapper/src/swappers/ChainflipSwapper/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/types.ts b/packages/swapper/src/swappers/ChainflipSwapper/types.ts index da6b02fb57f..c02cfef496b 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/types.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/types.ts @@ -1,11 +1,12 @@ import type { ChainflipBaasStatusEgress } from './models/ChainflipBaasStatusEgress' +import type { ChainflipBaasStatusSwap } from './models/ChainflipBaasStatusSwap' // Non-exhaustive export type ChainFlipStatus = { status: { - // TODO(gomes): Status polling a la THOR state: 'waiting' | 'receiving' | 'swapping' | 'sending' | 'sent' | 'completed' | 'failed' - swapEgress?: ChainflipBaasStatusEgress + swap?: ChainflipBaasStatusSwap, + swapEgress?: ChainflipBaasStatusEgress, } } From ab7433f02d6030697839f26d9501db6e9c2dd751 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Wed, 20 Nov 2024 19:50:09 +0100 Subject: [PATCH 04/49] feat: make StreamingSwap provider-agnostic --- .../components/StreamingSwap.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx index 26c9ec2194f..2e0458e4ff4 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx @@ -4,20 +4,28 @@ import type { TradeQuote } from '@shapeshiftoss/swapper' import { useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { Row } from 'components/Row/Row' - -import { useThorStreamingProgress } from '../hooks/useThorStreamingProgress' +import type { StreamingSwapFailedSwap } from 'state/slices/tradeQuoteSlice/types' export type StreamingSwapProps = { hopIndex: number - activeTradeId: TradeQuote['id'] + activeTradeId: TradeQuote['id'], + streamingProgress: ( + hopIndex: number, + confirmedTradeId: TradeQuote['id'], + ) => { + isComplete: boolean + attemptedSwapCount: number + totalSwapCount: number + failedSwaps: StreamingSwapFailedSwap[] + } } export const StreamingSwap = (props: StreamingSwapProps) => { - const { hopIndex, activeTradeId } = props + const { hopIndex, activeTradeId, streamingProgress } = props const translate = useTranslate() - const { totalSwapCount, attemptedSwapCount, isComplete, failedSwaps } = useThorStreamingProgress( + const { totalSwapCount, attemptedSwapCount, isComplete, failedSwaps } = streamingProgress( hopIndex, activeTradeId, ) From 5280261f9853b6f39277e9feec74d19eca3dc747 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Wed, 20 Nov 2024 19:50:32 +0100 Subject: [PATCH 05/49] feat: add chainflip streaming hook and pass the correct one to StreamingSwap --- .../components/HopTransactionStep.tsx | 20 ++- .../MultiHopTradeConfirm/hooks/types.ts | 5 + .../hooks/useChainflipStreamingProgress.tsx | 145 ++++++++++++++++++ 3 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx index 780ed925725..f4ca0bda810 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx @@ -9,6 +9,10 @@ import { THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE, THORCHAIN_STREAM_SWAP_SOURCE, } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/constants' +import { + CHAINFLIP_DCA_SWAP_SOURCE, + CHAINFLIP_DCA_BOOST_SWAP_SOURCE, +} from '@shapeshiftoss/swapper/dist/swappers/ChainflipSwapper/constants' import type { KnownChainIds } from '@shapeshiftoss/types' import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' @@ -33,6 +37,9 @@ import { StatusIcon } from './StatusIcon' import { StepperStep } from './StepperStep' import { StreamingSwap } from './StreamingSwap' +import { useThorStreamingProgress } from '../hooks/useThorStreamingProgress' +import { useChainflipStreamingProgress } from '../hooks/useChainflipStreamingProgress' + export type HopTransactionStepProps = { swapperName: SwapperName tradeQuoteStep: TradeQuoteStep @@ -165,16 +172,23 @@ export const HopTransactionStep = ({ ) } - const isThorStreamingSwap = [ + const isStreamingSwap = [ THORCHAIN_STREAM_SWAP_SOURCE, THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE, + CHAINFLIP_DCA_SWAP_SOURCE, + CHAINFLIP_DCA_BOOST_SWAP_SOURCE ].includes(tradeQuoteStep.source) - if (sellTxHash !== undefined && isThorStreamingSwap) { + if (sellTxHash !== undefined && isStreamingSwap) { // TODO: Not sure what sellTxHash is used for here, investigate + const isThor = tradeQuoteStep.source == THORCHAIN_STREAM_SWAP_SOURCE || tradeQuoteStep.source == THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE + const streamingProgress = isThor + ? useThorStreamingProgress + : useChainflipStreamingProgress + return ( - + ) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts index 645072e6559..c0d11b76050 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts @@ -17,3 +17,8 @@ export type ThornodeStreamingSwapResponseError = { error: string } export type ThornodeStreamingSwapResponse = | ThornodeStreamingSwapResponseSuccess | ThornodeStreamingSwapResponseError + +export type ChainflipStreamingSwapResponseSuccess = { + executedChunks: number, + remainingChunks: number +} \ No newline at end of file diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx new file mode 100644 index 00000000000..3b3003c148f --- /dev/null +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -0,0 +1,145 @@ +import type { TradeQuote } from '@shapeshiftoss/swapper' +import axios from 'axios' +import { getConfig } from 'config' +import { useEffect, useMemo } from 'react' +import { usePoll } from 'hooks/usePoll/usePoll' +import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors' +import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' +import type { + StreamingSwapFailedSwap, + StreamingSwapMetadata, +} from 'state/slices/tradeQuoteSlice/types' +import { useAppDispatch, useAppSelector } from 'state/store' + +import { + ChainflipStreamingSwapResponseSuccess +} from './types' + +// TODO: Is this import allowed? +import type { ChainFlipStatus } from "@shapeshiftoss/swapper/dist/swappers/ChainflipSwapper/types"; + +const POLL_INTERVAL_MILLISECONDS = 30_000 // 30 seconds + +const DEFAULT_STREAMING_SWAP_METADATA: StreamingSwapMetadata = { + attemptedSwapCount: 0, + totalSwapCount: 0, + failedSwaps: [], +} + +const getChainflipStreamingSwap = async ( + swapId: number, +): Promise => { + const config = getConfig() + const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL + const apiKey = config.REACT_APP_CHAINFLIP_API_KEY + + const { data: statusResponse } = await axios.get( + `${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`, + ) + + if (!statusResponse) return + + // TODO: Check for real errors + if ('error' in statusResponse) { + console.error('failed to fetch streaming swap data', statusResponse.error) + return + } + + const dcaStatus = statusResponse.status?.swap?.dca + + if (!dcaStatus) return; + + return { + executedChunks: dcaStatus!.executedChunks!, + remainingChunks: dcaStatus!.remainingChunks! + } +} + +const getStreamingSwapMetadata = ( + data: ChainflipStreamingSwapResponseSuccess, +): StreamingSwapMetadata => { + // When a swap fails on Chainflip, the streaming stops, there are never failed ones + const failedSwaps: StreamingSwapFailedSwap[] = [] + + return { + totalSwapCount: data.executedChunks + data.remainingChunks ?? 0, + attemptedSwapCount: data.executedChunks ?? 0, + failedSwaps, + } +} + +export const useChainflipStreamingProgress = ( + hopIndex: number, + confirmedTradeId: TradeQuote['id'], +): { + isComplete: boolean + attemptedSwapCount: number + totalSwapCount: number + failedSwaps: StreamingSwapFailedSwap[] +} => { + // a ref is used to allow updating and reading state without creating a dependency cycle + const { poll, cancelPolling } = usePoll() + const dispatch = useAppDispatch() + const hopExecutionMetadataFilter = useMemo(() => { + return { + tradeId: confirmedTradeId, + hopIndex, + } + }, [confirmedTradeId, hopIndex]) + + const { + swap: { sellTxHash, streamingSwap: streamingSwapMeta }, + } = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)) + + const bla = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)); + console.log('useAppSelector', bla); + const swapId = 42 // TODO: Get the real id + + useEffect(() => { + // don't start polling until we have a tx + // if (!sellTxHash) return + + poll({ + fn: async () => { + const updatedStreamingSwapData = await getChainflipStreamingSwap(swapId) + + // no payload at all - must be a failed request - return + if (!updatedStreamingSwapData) return + + // data to update - update + dispatch( + tradeQuoteSlice.actions.setStreamingSwapMeta({ + hopIndex, + streamingSwapMetadata: getStreamingSwapMetadata(updatedStreamingSwapData), + id: confirmedTradeId, + }), + ) + return updatedStreamingSwapData + }, + validate: streamingSwapData => { + if (!streamingSwapData) return false + return streamingSwapData.remainingChunks === 0 + }, + interval: POLL_INTERVAL_MILLISECONDS, + maxAttempts: Infinity, + }) + + // stop polling on dismount + return cancelPolling + }, [cancelPolling, dispatch, hopIndex, poll, sellTxHash, confirmedTradeId]) + + const result = useMemo(() => { + const numSuccessfulSwaps = + (streamingSwapMeta?.attemptedSwapCount ?? 0) - (streamingSwapMeta?.failedSwaps?.length ?? 0) + + const isComplete = + streamingSwapMeta !== undefined && numSuccessfulSwaps >= streamingSwapMeta.totalSwapCount + + return { + isComplete, + ...(streamingSwapMeta ?? DEFAULT_STREAMING_SWAP_METADATA), + } + }, [streamingSwapMeta]) + + return result +} \ No newline at end of file From 2afb05a817e7860096dddd65f1dcaf2a8958b8bc Mon Sep 17 00:00:00 2001 From: David Cumps Date: Wed, 20 Nov 2024 20:41:35 +0100 Subject: [PATCH 06/49] chore: remove useless comment --- .../MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index 3b3003c148f..2dea997071f 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -77,7 +77,6 @@ export const useChainflipStreamingProgress = ( totalSwapCount: number failedSwaps: StreamingSwapFailedSwap[] } => { - // a ref is used to allow updating and reading state without creating a dependency cycle const { poll, cancelPolling } = usePoll() const dispatch = useAppDispatch() const hopExecutionMetadataFilter = useMemo(() => { From 50d7a875da0022a300e823b4befe49c13f4b0b6c Mon Sep 17 00:00:00 2001 From: David Cumps Date: Thu, 21 Nov 2024 00:34:08 +0100 Subject: [PATCH 07/49] feat: add dca params to swap request --- .../ChainflipSwapper/utils/helpers.ts | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts index c8db9aef664..7630508f1b3 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts @@ -29,7 +29,9 @@ type GetChainFlipSwapArgs = ChainFlipBrokerBaseArgs & { minimumPrice: string refundAddress: string retryDurationInBlocks?: number - commissionBps: number + commissionBps: number, + numberOfChunks?: number, + chunkIntervalBlocks?: number, } type ChainflipAsset = { @@ -97,22 +99,30 @@ export const getChainFlipSwap = ({ refundAddress, retryDurationInBlocks = 10, commissionBps, + numberOfChunks = undefined, + chunkIntervalBlocks = 2 }: GetChainFlipSwapArgs): Promise< Result, SwapErrorRight> -> => - // TODO: For DCA swaps we need to add the numberOfChunks/chunkIntervalBlocks parameters - chainflipService.get( - `${brokerUrl}/swap` + - `?apiKey=${apiKey}` + - `&sourceAsset=${sourceAsset}` + - `&destinationAsset=${destinationAsset}` + - `&destinationAddress=${destinationAddress}` + - `&boostFee=${boostFee}` + - `&minimumPrice=${minimumPrice}` + - `&refundAddress=${refundAddress}` + - `&retryDurationInBlocks=${retryDurationInBlocks}` + - `&commissionBps=${commissionBps}`, - ) +> => { + let swapUrl = `${brokerUrl}/swap` + + `?apiKey=${apiKey}` + + `&sourceAsset=${sourceAsset}` + + `&destinationAsset=${destinationAsset}` + + `&destinationAddress=${destinationAddress}` + + `&boostFee=${boostFee}` + + `&minimumPrice=${minimumPrice}` + + `&refundAddress=${refundAddress}` + + `&retryDurationInBlocks=${retryDurationInBlocks}` + + `&commissionBps=${commissionBps}` + + if (numberOfChunks) { + swapUrl += + `&numberOfChunks=${numberOfChunks}` + + `&chunkIntervalBlocks=${chunkIntervalBlocks}` + } + + return chainflipService.get(swapUrl) +} const fetchChainFlipAssets = async ({ brokerUrl, From 0f4f45cf6421054cbd1494d37f07b06af6ceb656 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Thu, 21 Nov 2024 00:47:35 +0100 Subject: [PATCH 08/49] feat: pass dca params from quote to swap --- .../swappers/ChainflipSwapper/endpoints.ts | 12 +- .../models/ChainflipBaasQuoteBoostQuote.ts | 201 ++++++++++++++++++ .../swapperApi/getTradeQuote.ts | 12 ++ .../ChainflipSwapper/utils/helpers.ts | 6 +- packages/swapper/src/types.ts | 7 +- 5 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasQuoteBoostQuote.ts diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index b68e0924ce8..dc030955527 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -17,7 +17,7 @@ import type { } from '../../types' import { isExecutableTradeQuote, isExecutableTradeStep, isToken } from '../../utils' import { CHAINFLIP_BAAS_COMMISSION } 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' @@ -82,6 +82,8 @@ export const chainflipApi: SwapperApi = { minimumPrice, refundAddress: from, commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks }) if (maybeSwapResponse.isErr()) { @@ -97,6 +99,8 @@ export const chainflipApi: SwapperApi = { tradeQuoteMetadata.set(tradeQuote.id, swapResponse) const depositAddress = swapResponse.address! + step.chainflipSwapId = swapResponse.id! + const { assetReference } = fromAssetId(step.sellAsset.assetId) const adapter = assertGetEvmChainAdapter(step.sellAsset.chainId) @@ -205,6 +209,8 @@ export const chainflipApi: SwapperApi = { minimumPrice, refundAddress: sendAddress, commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks }) if (maybeSwapResponse.isErr()) { @@ -218,6 +224,7 @@ export const chainflipApi: SwapperApi = { tradeQuoteMetadata.set(tradeQuote.id, swapResponse) const depositAddress = swapResponse.address! + step.chainflipSwapId = swapResponse.id! return adapter.buildSendApiTransaction({ value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, @@ -277,6 +284,8 @@ export const chainflipApi: SwapperApi = { minimumPrice, refundAddress: from, commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks }) if (maybeSwapResponse.isErr()) { @@ -297,6 +306,7 @@ export const chainflipApi: SwapperApi = { : fromAssetId(step.sellAsset.assetId).assetReference const depositAddress = swapResponse.address! + step.chainflipSwapId = swapResponse.id! const getFeeDataInput: GetFeeDataInput = { to: depositAddress, diff --git a/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasQuoteBoostQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasQuoteBoostQuote.ts new file mode 100644 index 00000000000..1d82d9d85ca --- /dev/null +++ b/packages/swapper/src/swappers/ChainflipSwapper/models/ChainflipBaasQuoteBoostQuote.ts @@ -0,0 +1,201 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Chainflip Broker as a Service + * Run your own Chainflip Broker without any hassle. + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { ChainflipBaasQuoteEstimatedDurationsSeconds } from './ChainflipBaasQuoteEstimatedDurationsSeconds'; +import { + ChainflipBaasQuoteEstimatedDurationsSecondsFromJSON, + ChainflipBaasQuoteEstimatedDurationsSecondsFromJSONTyped, + ChainflipBaasQuoteEstimatedDurationsSecondsToJSON, +} from './ChainflipBaasQuoteEstimatedDurationsSeconds'; +import type { ChainflipBaasQuotePoolInfo } from './ChainflipBaasQuotePoolInfo'; +import { + ChainflipBaasQuotePoolInfoFromJSON, + ChainflipBaasQuotePoolInfoFromJSONTyped, + ChainflipBaasQuotePoolInfoToJSON, +} from './ChainflipBaasQuotePoolInfo'; +import type { ChainflipBaasQuoteQuoteFee } from './ChainflipBaasQuoteQuoteFee'; +import { + ChainflipBaasQuoteQuoteFeeFromJSON, + ChainflipBaasQuoteQuoteFeeFromJSONTyped, + ChainflipBaasQuoteQuoteFeeToJSON, +} from './ChainflipBaasQuoteQuoteFee'; + +/** + * An optional quote to have the swap boosted. + * @export + * @interface ChainflipBaasQuoteBoostQuote + */ +export interface ChainflipBaasQuoteBoostQuote { + /** + * The asset to send. + * @type {string} + * @memberof ChainflipBaasQuoteBoostQuote + */ + ingressAsset?: string; + /** + * The amount to send. + * @type {number} + * @memberof ChainflipBaasQuoteBoostQuote + */ + ingressAmount?: number; + /** + * The amount to send in native units. + * @type {string} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly ingressAmountNative?: string; + /** + * The asset used as intermediary. + * @type {string} + * @memberof ChainflipBaasQuoteBoostQuote + */ + intermediateAsset?: string | null; + /** + * The amount used as intermediary. + * @type {number} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly intermediateAmount?: number | null; + /** + * The amount used as intermediary in native units. + * @type {string} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly intermediateAmountNative?: string | null; + /** + * The asset to receive. + * @type {string} + * @memberof ChainflipBaasQuoteBoostQuote + */ + egressAsset?: string; + /** + * The amount to receive. + * @type {number} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly egressAmount?: number; + /** + * The amount to receive in native units. + * @type {string} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly egressAmountNative?: string; + /** + * The fee structure, this includes all fees. + * @type {Array} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly includedFees?: Array; + /** + * A warning in case liquidity is low and there is a risk of high slippage. + * @type {boolean} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly lowLiquidityWarning?: boolean; + /** + * Liquidity pools involved in the swap, as well as estimated liquidity provider fees. + * @type {Array} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly poolInfo?: Array; + /** + * The estimated time the swap will take. + * @type {number} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly estimatedDurationSeconds?: number; + /** + * + * @type {ChainflipBaasQuoteEstimatedDurationsSeconds} + * @memberof ChainflipBaasQuoteBoostQuote + */ + estimatedDurationsSeconds?: ChainflipBaasQuoteEstimatedDurationsSeconds; + /** + * The estimated fee (in bps) that the user has to pay (from the deposit amount) to get this swap boosted. + * @type {number} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly estimatedBoostFeeBps?: number; + /** + * The number of "sub-swaps" to perform for a DCA swap. + * @type {number} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly chunkIntervalBlocks?: number | null; + /** + * The delay between the "sub-swaps" of a DCA swap in number of blocks. + * @type {number} + * @memberof ChainflipBaasQuoteBoostQuote + */ + readonly numberOfChunks?: number | null; +} + +/** + * Check if a given object implements the ChainflipBaasQuoteBoostQuote interface. + */ +export function instanceOfChainflipBaasQuoteBoostQuote(value: object): boolean { + let isInstance = true; + + return isInstance; +} + +export function ChainflipBaasQuoteBoostQuoteFromJSON(json: any): ChainflipBaasQuoteBoostQuote { + return ChainflipBaasQuoteBoostQuoteFromJSONTyped(json, false); +} + +export function ChainflipBaasQuoteBoostQuoteFromJSONTyped(json: any, ignoreDiscriminator: boolean): ChainflipBaasQuoteBoostQuote { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'ingressAsset': !exists(json, 'ingressAsset') ? undefined : json['ingressAsset'], + 'ingressAmount': !exists(json, 'ingressAmount') ? undefined : json['ingressAmount'], + 'ingressAmountNative': !exists(json, 'ingressAmountNative') ? undefined : json['ingressAmountNative'], + 'intermediateAsset': !exists(json, 'intermediateAsset') ? undefined : json['intermediateAsset'], + 'intermediateAmount': !exists(json, 'intermediateAmount') ? undefined : json['intermediateAmount'], + 'intermediateAmountNative': !exists(json, 'intermediateAmountNative') ? undefined : json['intermediateAmountNative'], + 'egressAsset': !exists(json, 'egressAsset') ? undefined : json['egressAsset'], + 'egressAmount': !exists(json, 'egressAmount') ? undefined : json['egressAmount'], + 'egressAmountNative': !exists(json, 'egressAmountNative') ? undefined : json['egressAmountNative'], + 'includedFees': !exists(json, 'includedFees') ? undefined : ((json['includedFees'] as Array).map(ChainflipBaasQuoteQuoteFeeFromJSON)), + 'lowLiquidityWarning': !exists(json, 'lowLiquidityWarning') ? undefined : json['lowLiquidityWarning'], + 'poolInfo': !exists(json, 'poolInfo') ? undefined : ((json['poolInfo'] as Array).map(ChainflipBaasQuotePoolInfoFromJSON)), + 'estimatedDurationSeconds': !exists(json, 'estimatedDurationSeconds') ? undefined : json['estimatedDurationSeconds'], + 'estimatedDurationsSeconds': !exists(json, 'estimatedDurationsSeconds') ? undefined : ChainflipBaasQuoteEstimatedDurationsSecondsFromJSON(json['estimatedDurationsSeconds']), + 'estimatedBoostFeeBps': !exists(json, 'estimatedBoostFeeBps') ? undefined : json['estimatedBoostFeeBps'], + 'chunkIntervalBlocks': !exists(json, 'chunkIntervalBlocks') ? undefined : json['chunkIntervalBlocks'], + 'numberOfChunks': !exists(json, 'numberOfChunks') ? undefined : json['numberOfChunks'], + }; +} + +export function ChainflipBaasQuoteBoostQuoteToJSON(value?: ChainflipBaasQuoteBoostQuote | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'ingressAsset': value.ingressAsset, + 'ingressAmount': value.ingressAmount, + 'intermediateAsset': value.intermediateAsset, + 'egressAsset': value.egressAsset, + 'estimatedDurationsSeconds': ChainflipBaasQuoteEstimatedDurationsSecondsToJSON(value.estimatedDurationsSeconds), + }; +} + diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index b68a6971497..7ac117364de 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -287,6 +287,12 @@ export const _getTradeQuote = async ( allowanceContract: '0x0', // Chainflip does not use contracts estimatedExecutionTimeMs: singleQuoteResponse.boostQuote.estimatedDurationSeconds! * 1000, + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined + : undefined, + chainflipChunkIntervalBlocks: isStreaming + ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined + : undefined }, ], } @@ -325,6 +331,12 @@ export const _getTradeQuote = async ( accountNumber, allowanceContract: '0x0', // Chainflip does not use contracts - all Txs are sends estimatedExecutionTimeMs: singleQuoteResponse.estimatedDurationSeconds! * 1000, + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.numberOfChunks ?? undefined + : undefined, + chainflipChunkIntervalBlocks: isStreaming + ? singleQuoteResponse.chunkIntervalBlocks ?? undefined + : undefined }, ], } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts index 7630508f1b3..eb8d7e52f30 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts @@ -29,9 +29,9 @@ type GetChainFlipSwapArgs = ChainFlipBrokerBaseArgs & { minimumPrice: string refundAddress: string retryDurationInBlocks?: number - commissionBps: number, - numberOfChunks?: number, - chunkIntervalBlocks?: number, + commissionBps: number + numberOfChunks?: number + chunkIntervalBlocks?: number } type ChainflipAsset = { diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index d77bbc14e5b..5e25de0f54d 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -25,7 +25,7 @@ import type { TypedData } from 'eip-712' import type { InterpolationOptions } from 'node-polyglot' import type { Address } from 'viem' -import type { CowMessageToSign, CowSwapQuoteResponse } from './swappers/CowSwapper/types' +import type { CowMessageToSign, CowSwapQuoteResponse } from './swappers/CowSwapper' import type { makeSwapperAxiosServiceMonadic } from './utils' // TODO: Rename all properties in this type to be camel case and not react specific @@ -265,7 +265,10 @@ export type TradeQuoteStep = { value: string gasLimit: string } - cowswapQuoteResponse?: CowSwapQuoteResponse + cowswapQuoteResponse?: CowSwapQuoteResponse, + chainflipSwapId?: number | undefined, + chainflipNumberOfChunks?: number | undefined, + chainflipChunkIntervalBlocks?: number | undefined } export type TradeRateStep = Omit & { accountNumber: undefined } From af129ac22994d20860188e5bfcff13bb6ddd4c89 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Thu, 21 Nov 2024 11:38:00 +0100 Subject: [PATCH 09/49] feat: pass in tradequote to get the chunks --- .../ChainflipSwapper/utils/helpers.ts | 3 ++- .../components/HopTransactionStep.tsx | 8 ++++-- .../components/StreamingSwap.tsx | 14 +++++++--- .../MultiHopTradeConfirm/hooks/types.ts | 2 +- .../hooks/useChainflipStreamingProgress.tsx | 27 +++++++++++++------ .../hooks/useThorStreamingProgress.tsx | 6 ++++- 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts index eb8d7e52f30..f4742d1d93d 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts @@ -104,7 +104,8 @@ export const getChainFlipSwap = ({ }: GetChainFlipSwapArgs): Promise< Result, SwapErrorRight> > => { - let swapUrl = `${brokerUrl}/swap` + + let swapUrl = + `${brokerUrl}/swap` + `?apiKey=${apiKey}` + `&sourceAsset=${sourceAsset}` + `&destinationAsset=${destinationAsset}` + diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx index 7033955dedd..f0cb4b8caf6 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx @@ -189,7 +189,7 @@ export const HopTransactionStep = ({ CHAINFLIP_DCA_BOOST_SWAP_SOURCE ].includes(tradeQuoteStep.source) - if (sellTxHash !== undefined && isStreamingSwap) { // TODO: Not sure what sellTxHash is used for here, investigate + if (sellTxHash !== undefined && isStreamingSwap) { const isThor = tradeQuoteStep.source == THORCHAIN_STREAM_SWAP_SOURCE || tradeQuoteStep.source == THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE const streamingProgress = isThor ? useThorStreamingProgress @@ -198,7 +198,11 @@ export const HopTransactionStep = ({ return ( - + ) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx index 2e0458e4ff4..0cff986b40c 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx @@ -1,17 +1,22 @@ import { WarningIcon } from '@chakra-ui/icons' import { Progress, Stack } from '@chakra-ui/react' -import type { TradeQuote } from '@shapeshiftoss/swapper' +import type { + TradeQuote, + TradeQuoteStep +} from '@shapeshiftoss/swapper' import { useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { Row } from 'components/Row/Row' import type { StreamingSwapFailedSwap } from 'state/slices/tradeQuoteSlice/types' export type StreamingSwapProps = { + tradeQuoteStep: TradeQuoteStep, hopIndex: number activeTradeId: TradeQuote['id'], streamingProgress: ( + tradeQuoteStep: TradeQuoteStep, hopIndex: number, - confirmedTradeId: TradeQuote['id'], + confirmedTradeId: TradeQuote['id'] ) => { isComplete: boolean attemptedSwapCount: number @@ -21,13 +26,14 @@ export type StreamingSwapProps = { } export const StreamingSwap = (props: StreamingSwapProps) => { - const { hopIndex, activeTradeId, streamingProgress } = props + const { tradeQuoteStep, hopIndex, activeTradeId, streamingProgress } = props const translate = useTranslate() const { totalSwapCount, attemptedSwapCount, isComplete, failedSwaps } = streamingProgress( + tradeQuoteStep, hopIndex, - activeTradeId, + activeTradeId ) const isInitializing = useMemo(() => { diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts index c0d11b76050..06204223987 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts @@ -21,4 +21,4 @@ export type ThornodeStreamingSwapResponse = export type ChainflipStreamingSwapResponseSuccess = { executedChunks: number, remainingChunks: number -} \ No newline at end of file +} diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index 2dea997071f..1fe7eb60119 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -1,4 +1,7 @@ -import type { TradeQuote } from '@shapeshiftoss/swapper' +import type { + TradeQuote, + TradeQuoteStep +} from '@shapeshiftoss/swapper' import axios from 'axios' import { getConfig } from 'config' import { useEffect, useMemo } from 'react' @@ -27,15 +30,20 @@ const DEFAULT_STREAMING_SWAP_METADATA: StreamingSwapMetadata = { } const getChainflipStreamingSwap = async ( - swapId: number, + swapId: number | undefined, ): Promise => { + console.log('getChainflipStreamingSwap.swapId', swapId); + + if (!swapId) return; + const config = getConfig() const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL const apiKey = config.REACT_APP_CHAINFLIP_API_KEY const { data: statusResponse } = await axios.get( - `${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`, - ) + `${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`) + + console.log('getChainflipStreamingSwap.statusResponse', statusResponse) if (!statusResponse) return @@ -62,13 +70,14 @@ const getStreamingSwapMetadata = ( const failedSwaps: StreamingSwapFailedSwap[] = [] return { - totalSwapCount: data.executedChunks + data.remainingChunks ?? 0, + totalSwapCount: (data.executedChunks + data.remainingChunks) ?? 0, attemptedSwapCount: data.executedChunks ?? 0, failedSwaps, } } export const useChainflipStreamingProgress = ( + tradeQuoteStep: TradeQuoteStep, hopIndex: number, confirmedTradeId: TradeQuote['id'], ): { @@ -91,12 +100,14 @@ export const useChainflipStreamingProgress = ( } = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)) const bla = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)); - console.log('useAppSelector', bla); - const swapId = 42 // TODO: Get the real id + console.log('useChainflipStreamingProgress.useAppSelector', bla); + + const swapId = tradeQuoteStep.chainflipSwapId + console.log('useChainflipStreamingProgress.chainflipSwapId', swapId); useEffect(() => { // don't start polling until we have a tx - // if (!sellTxHash) return + if (!sellTxHash) return poll({ fn: async () => { diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx index 1f6137faf16..4123d9e48ee 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx @@ -1,4 +1,7 @@ -import type { TradeQuote } from '@shapeshiftoss/swapper' +import type { + TradeQuote, + TradeQuoteStep +} from '@shapeshiftoss/swapper' import axios from 'axios' import { getConfig } from 'config' import { useEffect, useMemo, useRef } from 'react' @@ -56,6 +59,7 @@ const getStreamingSwapMetadata = ( } export const useThorStreamingProgress = ( + tradeQuoteStep: TradeQuoteStep, hopIndex: number, confirmedTradeId: TradeQuote['id'], ): { From e1eb1dea2cd5d0a06b45003df148e7894c5e907a Mon Sep 17 00:00:00 2001 From: David Cumps Date: Thu, 21 Nov 2024 12:08:16 +0100 Subject: [PATCH 10/49] refactor: clarify maxBoostFee --- .../src/swappers/ChainflipSwapper/endpoints.ts | 12 ++++++------ .../src/swappers/ChainflipSwapper/utils/helpers.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index 2edc6b8bf95..900f5658e86 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -83,8 +83,8 @@ export const chainflipApi: SwapperApi = { refundAddress: from, commissionBps: serviceCommission, numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks - boostFee: 0, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: 0, }) if (maybeSwapResponse.isErr()) { @@ -211,8 +211,8 @@ export const chainflipApi: SwapperApi = { refundAddress: sendAddress, commissionBps: serviceCommission, numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks - boostFee: step.source === CHAINFLIP_BOOST_SWAP_SOURCE ? 10 : 0, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: step.source === CHAINFLIP_BOOST_SWAP_SOURCE ? 10 : 0, }) if (maybeSwapResponse.isErr()) { @@ -287,8 +287,8 @@ export const chainflipApi: SwapperApi = { refundAddress: from, commissionBps: serviceCommission, numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks - boostFee: 0, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: 0, }) if (maybeSwapResponse.isErr()) { diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts index 7435bae02a4..f8da7309187 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts @@ -25,7 +25,7 @@ type GetChainFlipSwapArgs = ChainFlipBrokerBaseArgs & { sourceAsset: string destinationAsset: string destinationAddress: string - boostFee?: number + maxBoostFee?: number minimumPrice: string refundAddress: string retryDurationInBlocks?: number @@ -94,7 +94,7 @@ export const getChainFlipSwap = ({ sourceAsset, destinationAsset, destinationAddress, - boostFee = 0, + maxBoostFee = 0, minimumPrice, refundAddress, retryDurationInBlocks = 10, @@ -110,7 +110,7 @@ export const getChainFlipSwap = ({ `&sourceAsset=${sourceAsset}` + `&destinationAsset=${destinationAsset}` + `&destinationAddress=${destinationAddress}` + - `&boostFee=${boostFee}` + + `&boostFee=${maxBoostFee}` + `&minimumPrice=${minimumPrice}` + `&refundAddress=${refundAddress}` + `&retryDurationInBlocks=${retryDurationInBlocks}` + From b770ae01ccf102c96831a038140c104d10f1f3d3 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Thu, 21 Nov 2024 12:31:58 +0100 Subject: [PATCH 11/49] chore: run lint:fix --- .../swappers/ChainflipSwapper/endpoints.ts | 2 +- .../swapperApi/getTradeQuote.ts | 16 +++--- .../src/swappers/ChainflipSwapper/types.ts | 4 +- .../ChainflipSwapper/utils/helpers.ts | 11 ++-- packages/swapper/src/types.ts | 6 +- .../components/HopTransactionStep.tsx | 38 ++++++------- .../components/StreamingSwap.tsx | 13 ++--- .../MultiHopTradeConfirm/hooks/types.ts | 2 +- .../hooks/useChainflipStreamingProgress.tsx | 57 +++++++++---------- .../hooks/useThorStreamingProgress.tsx | 5 +- 10 files changed, 72 insertions(+), 82 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index 900f5658e86..013848b6598 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -101,7 +101,7 @@ export const chainflipApi: SwapperApi = { const depositAddress = swapResponse.address! step.chainflipSwapId = swapResponse.id! - + const { assetReference } = fromAssetId(step.sellAsset.assetId) const adapter = assertGetEvmChainAdapter(step.sellAsset.chainId) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index f332f6bcfc1..af44b85fa66 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -287,9 +287,10 @@ export const _getTradeQuote = async ( allowanceContract: '0x0', // Chainflip does not use contracts estimatedExecutionTimeMs: (singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.deposit! + - singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.swap!) * 1000, - chainflipNumberOfChunks: isStreaming - ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined + singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.swap!) * + 1000, + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined : undefined, chainflipChunkIntervalBlocks: isStreaming ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined @@ -333,13 +334,14 @@ export const _getTradeQuote = async ( allowanceContract: '0x0', // Chainflip does not use contracts - all Txs are sends estimatedExecutionTimeMs: (singleQuoteResponse.estimatedDurationsSeconds!.deposit! + - singleQuoteResponse.estimatedDurationsSeconds!.swap!) * 1000, - chainflipNumberOfChunks: isStreaming - ? singleQuoteResponse.numberOfChunks ?? undefined + singleQuoteResponse.estimatedDurationsSeconds!.swap!) * + 1000, + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.numberOfChunks ?? undefined : undefined, chainflipChunkIntervalBlocks: isStreaming ? singleQuoteResponse.chunkIntervalBlocks ?? undefined - : undefined + : undefined, }, ], } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/types.ts b/packages/swapper/src/swappers/ChainflipSwapper/types.ts index c02cfef496b..89cc43d67ae 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/types.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/types.ts @@ -5,8 +5,8 @@ import type { ChainflipBaasStatusSwap } from './models/ChainflipBaasStatusSwap' export type ChainFlipStatus = { status: { state: 'waiting' | 'receiving' | 'swapping' | 'sending' | 'sent' | 'completed' | 'failed' - swap?: ChainflipBaasStatusSwap, - swapEgress?: ChainflipBaasStatusEgress, + swap?: ChainflipBaasStatusSwap + swapEgress?: ChainflipBaasStatusEgress } } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts index f8da7309187..6d9247e73d6 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts @@ -99,12 +99,12 @@ export const getChainFlipSwap = ({ refundAddress, retryDurationInBlocks = 10, commissionBps, - numberOfChunks = undefined, - chunkIntervalBlocks = 2 + numberOfChunks = undefined, + chunkIntervalBlocks = 2, }: GetChainFlipSwapArgs): Promise< Result, SwapErrorRight> > => { - let swapUrl = + let swapUrl = `${brokerUrl}/swap` + `?apiKey=${apiKey}` + `&sourceAsset=${sourceAsset}` + @@ -117,9 +117,8 @@ export const getChainFlipSwap = ({ `&commissionBps=${commissionBps}` if (numberOfChunks) { - swapUrl += - `&numberOfChunks=${numberOfChunks}` + - `&chunkIntervalBlocks=${chunkIntervalBlocks}` + swapUrl += `&numberOfChunks=${numberOfChunks}` + swapUrl += `&chunkIntervalBlocks=${chunkIntervalBlocks}` } return chainflipService.get(swapUrl) diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 5e25de0f54d..f09a5804967 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -265,9 +265,9 @@ export type TradeQuoteStep = { value: string gasLimit: string } - cowswapQuoteResponse?: CowSwapQuoteResponse, - chainflipSwapId?: number | undefined, - chainflipNumberOfChunks?: number | undefined, + cowswapQuoteResponse?: CowSwapQuoteResponse + chainflipSwapId?: number | undefined + chainflipNumberOfChunks?: number | undefined chainflipChunkIntervalBlocks?: number | undefined } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx index f0cb4b8caf6..bb2dfd56005 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx @@ -5,14 +5,14 @@ import type { TradeQuoteStep, } from '@shapeshiftoss/swapper' import { SwapperName } from '@shapeshiftoss/swapper' +import { + CHAINFLIP_DCA_BOOST_SWAP_SOURCE, + CHAINFLIP_DCA_SWAP_SOURCE, +} from '@shapeshiftoss/swapper/dist/swappers/ChainflipSwapper/constants' import { THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE, THORCHAIN_STREAM_SWAP_SOURCE, } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/constants' -import { - CHAINFLIP_DCA_SWAP_SOURCE, - CHAINFLIP_DCA_BOOST_SWAP_SOURCE, -} from '@shapeshiftoss/swapper/dist/swappers/ChainflipSwapper/constants' import type { KnownChainIds } from '@shapeshiftoss/types' import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' @@ -32,15 +32,14 @@ import { TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' import { useAppSelector } from 'state/store' import { SwapperIcon } from '../../TradeInput/components/SwapperIcon/SwapperIcon' +import { useChainflipStreamingProgress } from '../hooks/useChainflipStreamingProgress' +import { useThorStreamingProgress } from '../hooks/useThorStreamingProgress' import { useTradeExecution } from '../hooks/useTradeExecution' import { getChainShortName } from '../utils/getChainShortName' import { StatusIcon } from './StatusIcon' import { StepperStep } from './StepperStep' import { StreamingSwap } from './StreamingSwap' -import { useThorStreamingProgress } from '../hooks/useThorStreamingProgress' -import { useChainflipStreamingProgress } from '../hooks/useChainflipStreamingProgress' - export type HopTransactionStepProps = { swapperName: SwapperName tradeQuoteStep: TradeQuoteStep @@ -186,23 +185,24 @@ export const HopTransactionStep = ({ THORCHAIN_STREAM_SWAP_SOURCE, THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE, CHAINFLIP_DCA_SWAP_SOURCE, - CHAINFLIP_DCA_BOOST_SWAP_SOURCE + CHAINFLIP_DCA_BOOST_SWAP_SOURCE, ].includes(tradeQuoteStep.source) if (sellTxHash !== undefined && isStreamingSwap) { - const isThor = tradeQuoteStep.source == THORCHAIN_STREAM_SWAP_SOURCE || tradeQuoteStep.source == THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE - const streamingProgress = isThor - ? useThorStreamingProgress - : useChainflipStreamingProgress - + const isThor = + tradeQuoteStep.source === THORCHAIN_STREAM_SWAP_SOURCE || + tradeQuoteStep.source === THORCHAIN_LONGTAIL_STREAMING_SWAP_SOURCE + const streamingProgress = isThor ? useThorStreamingProgress : useChainflipStreamingProgress + return ( - + ) @@ -210,12 +210,12 @@ export const HopTransactionStep = ({ }, [ isActive, swapTxState, - tradeQuoteStep.source, sellTxHash, handleSignTx, isFetching, tradeQuoteQueryData, translate, + tradeQuoteStep, hopIndex, activeTradeId, ]) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx index 0cff986b40c..2d7cdee1f02 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx @@ -1,22 +1,19 @@ import { WarningIcon } from '@chakra-ui/icons' import { Progress, Stack } from '@chakra-ui/react' -import type { - TradeQuote, - TradeQuoteStep -} from '@shapeshiftoss/swapper' +import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' import { useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { Row } from 'components/Row/Row' import type { StreamingSwapFailedSwap } from 'state/slices/tradeQuoteSlice/types' export type StreamingSwapProps = { - tradeQuoteStep: TradeQuoteStep, + tradeQuoteStep: TradeQuoteStep hopIndex: number - activeTradeId: TradeQuote['id'], + activeTradeId: TradeQuote['id'] streamingProgress: ( tradeQuoteStep: TradeQuoteStep, hopIndex: number, - confirmedTradeId: TradeQuote['id'] + confirmedTradeId: TradeQuote['id'], ) => { isComplete: boolean attemptedSwapCount: number @@ -33,7 +30,7 @@ export const StreamingSwap = (props: StreamingSwapProps) => { const { totalSwapCount, attemptedSwapCount, isComplete, failedSwaps } = streamingProgress( tradeQuoteStep, hopIndex, - activeTradeId + activeTradeId, ) const isInitializing = useMemo(() => { diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts index 06204223987..7b5148a0efc 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/types.ts @@ -19,6 +19,6 @@ export type ThornodeStreamingSwapResponse = | ThornodeStreamingSwapResponseError export type ChainflipStreamingSwapResponseSuccess = { - executedChunks: number, + executedChunks: number remainingChunks: number } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index 1fe7eb60119..72d4451f84a 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -1,7 +1,6 @@ -import type { - TradeQuote, - TradeQuoteStep -} from '@shapeshiftoss/swapper' +import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' +// TODO: Is this import allowed? +import type { ChainFlipStatus } from '@shapeshiftoss/swapper/dist/swappers/ChainflipSwapper/types' import axios from 'axios' import { getConfig } from 'config' import { useEffect, useMemo } from 'react' @@ -14,12 +13,7 @@ import type { } from 'state/slices/tradeQuoteSlice/types' import { useAppDispatch, useAppSelector } from 'state/store' -import { - ChainflipStreamingSwapResponseSuccess -} from './types' - -// TODO: Is this import allowed? -import type { ChainFlipStatus } from "@shapeshiftoss/swapper/dist/swappers/ChainflipSwapper/types"; +import type { ChainflipStreamingSwapResponseSuccess } from './types' const POLL_INTERVAL_MILLISECONDS = 30_000 // 30 seconds @@ -32,21 +26,22 @@ const DEFAULT_STREAMING_SWAP_METADATA: StreamingSwapMetadata = { const getChainflipStreamingSwap = async ( swapId: number | undefined, ): Promise => { - console.log('getChainflipStreamingSwap.swapId', swapId); - - if (!swapId) return; - + console.log('getChainflipStreamingSwap.swapId', swapId) + + if (!swapId) return + const config = getConfig() const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL const apiKey = config.REACT_APP_CHAINFLIP_API_KEY const { data: statusResponse } = await axios.get( - `${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`) - + `${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`, + ) + console.log('getChainflipStreamingSwap.statusResponse', statusResponse) if (!statusResponse) return - + // TODO: Check for real errors if ('error' in statusResponse) { console.error('failed to fetch streaming swap data', statusResponse.error) @@ -54,12 +49,12 @@ const getChainflipStreamingSwap = async ( } const dcaStatus = statusResponse.status?.swap?.dca - - if (!dcaStatus) return; - + + if (!dcaStatus) return + return { executedChunks: dcaStatus!.executedChunks!, - remainingChunks: dcaStatus!.remainingChunks! + remainingChunks: dcaStatus!.remainingChunks!, } } @@ -70,7 +65,7 @@ const getStreamingSwapMetadata = ( const failedSwaps: StreamingSwapFailedSwap[] = [] return { - totalSwapCount: (data.executedChunks + data.remainingChunks) ?? 0, + totalSwapCount: data.executedChunks + data.remainingChunks ?? 0, attemptedSwapCount: data.executedChunks ?? 0, failedSwaps, } @@ -94,17 +89,17 @@ export const useChainflipStreamingProgress = ( hopIndex, } }, [confirmedTradeId, hopIndex]) - + const { swap: { sellTxHash, streamingSwap: streamingSwapMeta }, } = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)) - const bla = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)); - console.log('useChainflipStreamingProgress.useAppSelector', bla); - + const bla = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)) + console.log('useChainflipStreamingProgress.useAppSelector', bla) + const swapId = tradeQuoteStep.chainflipSwapId - console.log('useChainflipStreamingProgress.chainflipSwapId', swapId); - + console.log('useChainflipStreamingProgress.chainflipSwapId', swapId) + useEffect(() => { // don't start polling until we have a tx if (!sellTxHash) return @@ -136,8 +131,8 @@ export const useChainflipStreamingProgress = ( // stop polling on dismount return cancelPolling - }, [cancelPolling, dispatch, hopIndex, poll, sellTxHash, confirmedTradeId]) - + }, [cancelPolling, dispatch, hopIndex, poll, sellTxHash, confirmedTradeId, swapId]) + const result = useMemo(() => { const numSuccessfulSwaps = (streamingSwapMeta?.attemptedSwapCount ?? 0) - (streamingSwapMeta?.failedSwaps?.length ?? 0) @@ -152,4 +147,4 @@ export const useChainflipStreamingProgress = ( }, [streamingSwapMeta]) return result -} \ No newline at end of file +} diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx index 4123d9e48ee..b28911ff1a1 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx @@ -1,7 +1,4 @@ -import type { - TradeQuote, - TradeQuoteStep -} from '@shapeshiftoss/swapper' +import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' import axios from 'axios' import { getConfig } from 'config' import { useEffect, useMemo, useRef } from 'react' From 105d917b482dd7018edea9c724cdaf9dfc6de3d6 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 00:29:28 +0100 Subject: [PATCH 12/49] feat: open deposit channel early for dca swaps --- .../swappers/ChainflipSwapper/endpoints.ts | 324 +++++++++--------- .../swapperApi/getTradeRate.ts | 80 +++++ packages/swapper/src/types.ts | 1 + 3 files changed, 251 insertions(+), 154 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index 013848b6598..1c895de07d6 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -17,7 +17,7 @@ import type { } 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' @@ -45,67 +45,73 @@ export const chainflipApi: SwapperApi = { }: GetUnsignedEvmTransactionArgs): Promise => { 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 - - const maybeSwapResponse = await getChainFlipSwap({ - brokerUrl, - apiKey, - sourceAsset, - destinationAsset, - destinationAddress: tradeQuote.receiveAddress, - minimumPrice, - refundAddress: from, - commissionBps: serviceCommission, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: 0, - }) + if (!isExecutableTradeStep(step)) throw Error('Unable to execute step') - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) - } + // For DCA swaps we already opened a deposit address in getTradeRate, we only need to open for regular swaps + if (!step.chainflipDepositAddress) { + const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL + const apiKey = config.REACT_APP_CHAINFLIP_API_KEY + + 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, + }) + + const maybeSwapResponse = await getChainFlipSwap({ + brokerUrl, + apiKey, + sourceAsset, + destinationAsset, + destinationAddress: tradeQuote.receiveAddress, + minimumPrice, + refundAddress: from, + commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: 0, + }) + + if (maybeSwapResponse.isErr()) { + const error = maybeSwapResponse.unwrapErr() + const cause = error.cause as AxiosError + throw Error(cause.response!.data.detail) + } - const { data: swapResponse } = maybeSwapResponse.unwrap() + const { data: swapResponse } = maybeSwapResponse.unwrap() - if (!swapResponse.id) throw Error('missing swap ID') + if (!swapResponse.id) throw Error('missing swap ID') - tradeQuoteMetadata.set(tradeQuote.id, swapResponse) + tradeQuoteMetadata.set(tradeQuote.id, swapResponse) + } - const depositAddress = swapResponse.address! - step.chainflipSwapId = swapResponse.id! + const depositAddress = step.chainflipDepositAddress + ? step.chainflipDepositAddress + : tradeQuoteMetadata.get(tradeQuote.id)!.address! const { assetReference } = fromAssetId(step.sellAsset.assetId) const adapter = assertGetEvmChainAdapter(step.sellAsset.chainId) + const isTokenSend = isToken(step.sellAsset.assetId) const getFeeDataInput: GetFeeDataInput = { to: depositAddress, value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, @@ -119,8 +125,6 @@ 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, from, @@ -161,72 +165,78 @@ export const chainflipApi: SwapperApi = { }: GetUnsignedUtxoTransactionArgs): Promise => { 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') - 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, - }) - 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, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: step.source === CHAINFLIP_BOOST_SWAP_SOURCE ? 10 : 0, - }) + // For DCA swaps we already opened a deposit address in getTradeRate, we only need to open for regular swaps + if (!step.chainflipDepositAddress) { + const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL + const apiKey = config.REACT_APP_CHAINFLIP_API_KEY + + 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, + }) + + 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, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: step.source === CHAINFLIP_BOOST_SWAP_SOURCE ? 10 : 0, + }) + + if (maybeSwapResponse.isErr()) { + const error = maybeSwapResponse.unwrapErr() + const cause = error.cause as AxiosError + throw Error(cause.response!.data.detail) + } - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) - } + const { data: swapResponse } = maybeSwapResponse.unwrap() - const { data: swapResponse } = maybeSwapResponse.unwrap() + if (!swapResponse.id) throw Error('missing swap ID') - tradeQuoteMetadata.set(tradeQuote.id, swapResponse) + tradeQuoteMetadata.set(tradeQuote.id, swapResponse) + } - const depositAddress = swapResponse.address! - step.chainflipSwapId = swapResponse.id! + const depositAddress = step.chainflipDepositAddress + ? step.chainflipDepositAddress + : tradeQuoteMetadata.get(tradeQuote.id)!.address! return adapter.buildSendApiTransaction({ value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, @@ -248,58 +258,67 @@ export const chainflipApi: SwapperApi = { }: GetUnsignedSolanaTransactionArgs): Promise => { 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') - const sourceAsset = await getChainFlipIdFromAssetId({ - assetId: step.sellAsset.assetId, - brokerUrl, - }) - const destinationAsset = await getChainFlipIdFromAssetId({ - assetId: step.buyAsset.assetId, - brokerUrl, - }) + // For DCA swaps we already opened a deposit address in getTradeRate, we only need to open for regular swaps + if (!step.chainflipDepositAddress) { + const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL + const apiKey = config.REACT_APP_CHAINFLIP_API_KEY + + 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, + }) + + const maybeSwapResponse = await getChainFlipSwap({ + brokerUrl, + apiKey, + sourceAsset, + destinationAsset, + destinationAddress: tradeQuote.receiveAddress, + minimumPrice, + refundAddress: from, + commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: 0, + }) + + if (maybeSwapResponse.isErr()) { + const error = maybeSwapResponse.unwrapErr() + const cause = error.cause as AxiosError + throw Error(cause.response!.data.detail) + } - // 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 { data: swapResponse } = maybeSwapResponse.unwrap() - const maybeSwapResponse = await getChainFlipSwap({ - brokerUrl, - apiKey, - sourceAsset, - destinationAsset, - destinationAddress: tradeQuote.receiveAddress, - minimumPrice, - refundAddress: from, - commissionBps: serviceCommission, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: 0, - }) + if (!swapResponse.id) throw Error('missing swap ID') - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) + tradeQuoteMetadata.set(tradeQuote.id, swapResponse) } - const { data: swapResponse } = maybeSwapResponse.unwrap() - - tradeQuoteMetadata.set(tradeQuote.id, swapResponse) + const depositAddress = step.chainflipDepositAddress + ? step.chainflipDepositAddress + : tradeQuoteMetadata.get(tradeQuote.id)!.address! const adapter = assertGetSolanaChainAdapter(step.sellAsset.chainId) @@ -308,9 +327,6 @@ export const chainflipApi: SwapperApi = { ? undefined : fromAssetId(step.sellAsset.assetId).assetReference - const depositAddress = swapResponse.address! - step.chainflipSwapId = swapResponse.id! - const getFeeDataInput: GetFeeDataInput = { to: depositAddress, value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index 0add3599c5a..27e9ce0432f 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -1,4 +1,5 @@ import type { Result } from '@sniptt/monads' +import type { AxiosError } from 'axios' import type { CommonTradeQuoteInput, @@ -7,6 +8,16 @@ import type { SwapperDeps, TradeRate, } from '../../../types' +import { + CHAINFLIP_BAAS_COMMISSION, + CHAINFLIP_DCA_BOOST_SWAP_SOURCE, + CHAINFLIP_DCA_SWAP_SOURCE, +} from '../constants' +import { + calculateChainflipMinPrice, + getChainFlipIdFromAssetId, + getChainFlipSwap, +} from '../utils/helpers' import { _getTradeQuote } from './getTradeQuote' // This isn't a mistake. A trade rate *is* a trade quote. Chainflip doesn't really have a notion of a trade quote, @@ -16,5 +27,74 @@ export const getTradeRate = async ( deps: SwapperDeps, ): Promise> => { const rates = await _getTradeQuote(input as unknown as CommonTradeQuoteInput, deps) + + const brokerUrl = deps.config.REACT_APP_CHAINFLIP_API_URL + const apiKey = deps.config.REACT_APP_CHAINFLIP_API_KEY + + // For DCA swaps we need to open a deposit channel at this point to attach the swap id to the quote, + // in order to properly fetch the streaming status later + rates.map(async tradeQuotes => { + for (const tradeQuote of tradeQuotes) { + for (const step of tradeQuote.steps) { + if ( + step.source === CHAINFLIP_DCA_BOOST_SWAP_SOURCE || + step.source === CHAINFLIP_DCA_SWAP_SOURCE + ) { + if (!input.receiveAddress) throw Error('missing receive address') + if (!input.sendAddress) throw Error('missing send address') + + 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, + }) + + let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION + if (serviceCommission < 0) serviceCommission = 0 + + const maybeSwapResponse = await getChainFlipSwap({ + brokerUrl, + apiKey, + sourceAsset, + destinationAsset, + destinationAddress: input.receiveAddress, + minimumPrice, + refundAddress: input.sendAddress, + commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: 0, + }) + + if (maybeSwapResponse.isErr()) { + const error = maybeSwapResponse.unwrapErr() + const cause = error.cause as AxiosError + throw Error(cause.response!.data.detail) + } + + const { data: swapResponse } = maybeSwapResponse.unwrap() + + if (!swapResponse.id) throw Error('missing swap ID') + + step.chainflipSwapId = swapResponse.id + step.chainflipDepositAddress = swapResponse.address + } + } + } + return tradeQuotes + }) + return rates as Result } diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index f09a5804967..e4100006554 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -267,6 +267,7 @@ export type TradeQuoteStep = { } cowswapQuoteResponse?: CowSwapQuoteResponse chainflipSwapId?: number | undefined + chainflipDepositAddress?: string | undefined chainflipNumberOfChunks?: number | undefined chainflipChunkIntervalBlocks?: number | undefined } From d93d26fe95a64bb2ee111df2884237c6725f61a8 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 01:03:03 +0100 Subject: [PATCH 13/49] refactor: swap quote and trades --- .../swapperApi/getTradeQuote.ts | 429 +++-------------- .../swapperApi/getTradeRate.ts | 432 ++++++++++++++---- packages/swapper/src/types.ts | 2 +- 3 files changed, 428 insertions(+), 435 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index af44b85fa66..6613a24d162 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -1,371 +1,94 @@ -import type { AssetId } from '@shapeshiftoss/caip' -import { CHAIN_NAMESPACE, fromAssetId, solAssetId } from '@shapeshiftoss/caip' -import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters' -import type { KnownChainIds } from '@shapeshiftoss/types' -import type { Result } from '@sniptt/monads' -import { Err, Ok } from '@sniptt/monads' import type { AxiosError } from 'axios' -import { v4 as uuid } from 'uuid' -import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' -import type { - CommonTradeQuoteInput, - GetUtxoTradeQuoteInput, - SwapErrorRight, - SwapperDeps, - TradeQuote, -} from '../../../types' -import { - type GetEvmTradeQuoteInput, - type ProtocolFee, - SwapperName, - TradeQuoteError, -} from '../../../types' -import { getRate, makeSwapErrorRight } from '../../../utils' +import type { CommonTradeQuoteInput, GetTradeRateInput, SwapperDeps } from '../../../types' +import { type TradeQuoteResult } from '../../../types' import { CHAINFLIP_BAAS_COMMISSION, - CHAINFLIP_BOOST_SWAP_SOURCE, CHAINFLIP_DCA_BOOST_SWAP_SOURCE, - CHAINFLIP_DCA_QUOTE, CHAINFLIP_DCA_SWAP_SOURCE, - CHAINFLIP_REGULAR_QUOTE, - CHAINFLIP_SWAP_SOURCE, - usdcAsset, } from '../constants' -import type { ChainflipBaasQuoteQuote, ChainflipBaasQuoteQuoteFee } from '../models' -import { chainflipService } from '../utils/chainflipService' -import { getEvmTxFees } from '../utils/getEvmTxFees' -import { getUtxoTxFees } from '../utils/getUtxoTxFees' -import { getChainFlipIdFromAssetId, isSupportedAssetId, isSupportedChainId } from '../utils/helpers' - -export const _getTradeQuote = async ( +import { + calculateChainflipMinPrice, + getChainFlipIdFromAssetId, + getChainFlipSwap, +} from '../utils/helpers' +import { _getTradeRate } from './getTradeRate' + +// This isn't a mistake. A trade quote *is* a trade rate. Chainflip doesn't really have a notion of a trade rate, +// they do have a notion of a "swap" (which we effectively only use to get the deposit address), which is irrelevant to the notion of quote vs. rate +export const getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, -): Promise> => { - const { - sellAsset, - buyAsset, - accountNumber, - receiveAddress, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, - affiliateBps: commissionBps, - } = input - - if (!isSupportedChainId(sellAsset.chainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (!isSupportedChainId(buyAsset.chainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (!isSupportedAssetId(sellAsset.chainId, sellAsset.assetId)) { - return Err( - makeSwapErrorRight({ - message: `asset '${sellAsset.name}' on chainId '${sellAsset.chainId}' not supported`, - code: TradeQuoteError.UnsupportedTradePair, - details: { chainId: sellAsset.chainId, assetId: sellAsset.assetId }, - }), - ) - } - - if (!isSupportedAssetId(buyAsset.chainId, buyAsset.assetId)) { - return Err( - makeSwapErrorRight({ - message: `asset '${buyAsset.name}' on chainId '${buyAsset.chainId}' not supported`, - code: TradeQuoteError.UnsupportedTradePair, - details: { chainId: buyAsset.chainId, assetId: buyAsset.assetId }, - }), - ) - } +): Promise => { + const rates = await _getTradeRate(input as unknown as GetTradeRateInput, deps) const brokerUrl = deps.config.REACT_APP_CHAINFLIP_API_URL const apiKey = deps.config.REACT_APP_CHAINFLIP_API_KEY - const sourceAsset = await getChainFlipIdFromAssetId({ - assetId: sellAsset.assetId, - brokerUrl, - }) - const destinationAsset = await getChainFlipIdFromAssetId({ - assetId: buyAsset.assetId, - brokerUrl, - }) - - // Subtract the BaaS fee to end up at the final displayed commissionBps - let serviceCommission = parseInt(commissionBps) - CHAINFLIP_BAAS_COMMISSION - if (serviceCommission < 0) serviceCommission = 0 - - const maybeQuoteResponse = await chainflipService.get( - `${brokerUrl}/quotes-native` + - `?apiKey=${apiKey}` + - `&sourceAsset=${sourceAsset}` + - `&destinationAsset=${destinationAsset}` + - `&amount=${sellAmount}` + - `&commissionBps=${serviceCommission}`, - ) - - if (maybeQuoteResponse.isErr()) { - const error = maybeQuoteResponse.unwrapErr() - const cause = error.cause as AxiosError - - if ( - cause.message.includes('code 400') && - cause.response!.data.detail.includes('Amount outside asset bounds') - ) { - return Err( - makeSwapErrorRight({ - message: cause.response!.data.detail, - code: TradeQuoteError.SellAmountBelowMinimum, - }), - ) - } - - return Err( - makeSwapErrorRight({ - message: 'Quote request failed', - code: TradeQuoteError.NoRouteFound, - }), - ) - } - - const { data: quoteResponse } = maybeQuoteResponse.unwrap() - - const getFeeData = async () => { - const { chainNamespace } = fromAssetId(sellAsset.assetId) - - switch (chainNamespace) { - case CHAIN_NAMESPACE.Evm: { - const sellAdapter = deps.assertGetEvmChainAdapter(sellAsset.chainId) - const networkFeeCryptoBaseUnit = await getEvmTxFees({ - adapter: sellAdapter, - supportsEIP1559: (input as GetEvmTradeQuoteInput).supportsEIP1559, - sendAsset: sourceAsset, - }) - return { networkFeeCryptoBaseUnit } - } - - case CHAIN_NAMESPACE.Utxo: { - const sellAdapter = deps.assertGetUtxoChainAdapter(sellAsset.chainId) - const publicKey = (input as GetUtxoTradeQuoteInput).xpub! - const feeData = await getUtxoTxFees({ - sellAmountCryptoBaseUnit: sellAmount, - sellAdapter, - publicKey, - }) - - return feeData - } - - case CHAIN_NAMESPACE.Solana: { - const sellAdapter = deps.assertGetSolanaChainAdapter(sellAsset.chainId) - const getFeeDataInput: GetFeeDataInput = { - // Simulates a self-send, since we don't know the to just yet at this stage - to: input.sendAddress!, - value: sellAmount, - chainSpecific: { - from: input.sendAddress!, - tokenId: - sellAsset.assetId === solAssetId - ? undefined - : fromAssetId(sellAsset.assetId).assetReference, - }, - } - const { fast } = await sellAdapter.getFeeData(getFeeDataInput) - return { networkFeeCryptoBaseUnit: fast.txFee } - } - - default: - throw new Error('Unsupported chainNamespace') - } - } - - const getFeeAsset = (fee: ChainflipBaasQuoteQuoteFee) => { - if (fee.type === 'ingress' || fee.type === 'boost') return sellAsset - - if (fee.type === 'egress') return buyAsset - - if (fee.type === 'liquidity' && fee.asset === sourceAsset) return sellAsset - - if (fee.type === 'liquidity' && fee.asset === destinationAsset) return buyAsset - - if (fee.type === 'liquidity' && fee.asset === 'usdc.eth') return usdcAsset - - if (fee.type === 'network') return usdcAsset - } - - const getProtocolFees = (singleQuoteResponse: ChainflipBaasQuoteQuote) => { - const protocolFees: Record = {} - - for (const fee of singleQuoteResponse.includedFees!) { - if (fee.type === 'broker') continue - - const asset = getFeeAsset(fee)! - if (!(asset.assetId in protocolFees)) { - protocolFees[asset.assetId] = { - amountCryptoBaseUnit: '0', - requiresBalance: false, - asset, - } - } - - protocolFees[asset.assetId].amountCryptoBaseUnit = ( - BigInt(protocolFees[asset.assetId].amountCryptoBaseUnit) + BigInt(fee.amountNative!) - ).toString() - } - - return protocolFees - } - - const getQuoteRate = (sellAmountCryptoBaseUnit: string, buyAmountCryptoBaseUnit: string) => { - return getRate({ - sellAmountCryptoBaseUnit, - buyAmountCryptoBaseUnit, - sellAsset, - buyAsset, - }) - } - - const getSwapSource = (swapType: string | undefined, isBoosted: boolean) => { - return swapType === CHAINFLIP_REGULAR_QUOTE - ? isBoosted - ? CHAINFLIP_BOOST_SWAP_SOURCE - : CHAINFLIP_SWAP_SOURCE - : isBoosted - ? CHAINFLIP_DCA_BOOST_SWAP_SOURCE - : CHAINFLIP_DCA_SWAP_SOURCE - } - - const quotes: TradeQuote[] = [] - - for (const singleQuoteResponse of quoteResponse) { - const isStreaming = singleQuoteResponse.type === CHAINFLIP_DCA_QUOTE - const feeData = await getFeeData() - - if (singleQuoteResponse.boostQuote) { - const boostRate = getQuoteRate( - singleQuoteResponse.boostQuote.ingressAmountNative!, - singleQuoteResponse.boostQuote.egressAmountNative!, - ) - - const boostTradeQuote: TradeQuote = { - id: uuid(), - rate: boostRate, - receiveAddress, - potentialAffiliateBps: commissionBps, - affiliateBps: commissionBps, - isStreaming, - slippageTolerancePercentageDecimal: - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Chainflip), - steps: [ - { - buyAmountBeforeFeesCryptoBaseUnit: singleQuoteResponse.boostQuote.egressAmountNative!, - buyAmountAfterFeesCryptoBaseUnit: singleQuoteResponse.boostQuote.egressAmountNative!, + // For DCA swaps we need to open a deposit channel at this point to attach the swap id to the quote, + // in order to properly fetch the streaming status later + rates.map(async tradeQuotes => { + for (const tradeQuote of tradeQuotes) { + for (const step of tradeQuote.steps) { + if ( + step.source === CHAINFLIP_DCA_BOOST_SWAP_SOURCE || + step.source === CHAINFLIP_DCA_SWAP_SOURCE + ) { + if (!input.receiveAddress) throw Error('missing receive address') + if (!input.sendAddress) throw Error('missing send address') + + 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: - singleQuoteResponse.boostQuote.ingressAmountNative!, - feeData: { - protocolFees: getProtocolFees(singleQuoteResponse.boostQuote), - ...feeData, - }, - rate: boostRate, - source: getSwapSource(singleQuoteResponse.type, true), - buyAsset, - sellAsset, - accountNumber, - allowanceContract: '0x0', // Chainflip does not use contracts - estimatedExecutionTimeMs: - (singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.deposit! + - singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.swap!) * - 1000, - chainflipNumberOfChunks: isStreaming - ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined - : undefined, - chainflipChunkIntervalBlocks: isStreaming - ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined - : undefined, - }, - ], + step.sellAmountIncludingProtocolFeesCryptoBaseUnit, + }) + + let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION + if (serviceCommission < 0) serviceCommission = 0 + + const maybeSwapResponse = await getChainFlipSwap({ + brokerUrl, + apiKey, + sourceAsset, + destinationAsset, + destinationAddress: input.receiveAddress, + minimumPrice, + refundAddress: input.sendAddress, + commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: 0, + }) + + if (maybeSwapResponse.isErr()) { + const error = maybeSwapResponse.unwrapErr() + const cause = error.cause as AxiosError + throw Error(cause.response!.data.detail) + } + + const { data: swapResponse } = maybeSwapResponse.unwrap() + + if (!swapResponse.id) throw Error('missing swap ID') + + step.chainflipSwapId = swapResponse.id + step.chainflipDepositAddress = swapResponse.address + } } - - quotes.push(boostTradeQuote) - } - - const rate = getQuoteRate( - singleQuoteResponse.ingressAmountNative!, - singleQuoteResponse.egressAmountNative!, - ) - - const tradeQuote: TradeQuote = { - id: uuid(), - rate, - receiveAddress, - potentialAffiliateBps: commissionBps, - affiliateBps: commissionBps, - isStreaming, - slippageTolerancePercentageDecimal: - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Chainflip), - steps: [ - { - buyAmountBeforeFeesCryptoBaseUnit: singleQuoteResponse.egressAmountNative!, - buyAmountAfterFeesCryptoBaseUnit: singleQuoteResponse.egressAmountNative!, - sellAmountIncludingProtocolFeesCryptoBaseUnit: singleQuoteResponse.ingressAmountNative!, - feeData: { - protocolFees: getProtocolFees(singleQuoteResponse), - ...feeData, - }, - rate, - source: getSwapSource(singleQuoteResponse.type, false), - buyAsset, - sellAsset, - accountNumber, - allowanceContract: '0x0', // Chainflip does not use contracts - all Txs are sends - estimatedExecutionTimeMs: - (singleQuoteResponse.estimatedDurationsSeconds!.deposit! + - singleQuoteResponse.estimatedDurationsSeconds!.swap!) * - 1000, - chainflipNumberOfChunks: isStreaming - ? singleQuoteResponse.numberOfChunks ?? undefined - : undefined, - chainflipChunkIntervalBlocks: isStreaming - ? singleQuoteResponse.chunkIntervalBlocks ?? undefined - : undefined, - }, - ], } + return tradeQuotes + }) - quotes.push(tradeQuote) - } - - return Ok(quotes) -} - -export const getTradeQuote = async ( - input: CommonTradeQuoteInput, - deps: SwapperDeps, -): Promise> => { - const { accountNumber } = input - - if (accountNumber === undefined) { - return Err( - makeSwapErrorRight({ - message: `accountNumber is required`, - code: TradeQuoteError.UnknownError, - }), - ) - } - - return await _getTradeQuote(input, deps) + return rates as TradeQuoteResult } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index 27e9ce0432f..9de71f27a68 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -1,100 +1,370 @@ -import type { Result } from '@sniptt/monads' +import type { AssetId } from '@shapeshiftoss/caip' +import { CHAIN_NAMESPACE, fromAssetId, solAssetId } from '@shapeshiftoss/caip' +import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters' +import type { KnownChainIds } from '@shapeshiftoss/types' +import { Err, Ok } from '@sniptt/monads' import type { AxiosError } from 'axios' +import { v4 as uuid } from 'uuid' -import type { - CommonTradeQuoteInput, - GetTradeRateInput, - SwapErrorRight, - SwapperDeps, - TradeRate, +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { TradeRateResult } from '../../../types' +import { + type GetEvmTradeRateInput, + type GetTradeRateInput, + type GetUtxoTradeQuoteInput, + type ProtocolFee, + type SwapperDeps, + SwapperName, + TradeQuoteError, + type TradeRate, } from '../../../types' +import { getRate, makeSwapErrorRight } from '../../../utils' import { CHAINFLIP_BAAS_COMMISSION, + CHAINFLIP_BOOST_SWAP_SOURCE, CHAINFLIP_DCA_BOOST_SWAP_SOURCE, + CHAINFLIP_DCA_QUOTE, CHAINFLIP_DCA_SWAP_SOURCE, + CHAINFLIP_REGULAR_QUOTE, + CHAINFLIP_SWAP_SOURCE, + usdcAsset, } from '../constants' -import { - calculateChainflipMinPrice, - getChainFlipIdFromAssetId, - getChainFlipSwap, -} from '../utils/helpers' -import { _getTradeQuote } from './getTradeQuote' - -// This isn't a mistake. A trade rate *is* a trade quote. Chainflip doesn't really have a notion of a trade quote, -// they do have a notion of a "swap" (which we effectively only use to get the deposit address), which is irrelevant to the notion of quote vs. rate -export const getTradeRate = async ( +import type { ChainflipBaasQuoteQuote, ChainflipBaasQuoteQuoteFee } from '../models' +import { chainflipService } from '../utils/chainflipService' +import { getEvmTxFees } from '../utils/getEvmTxFees' +import { getUtxoTxFees } from '../utils/getUtxoTxFees' +import { getChainFlipIdFromAssetId, isSupportedAssetId, isSupportedChainId } from '../utils/helpers' + +export const _getTradeRate = async ( input: GetTradeRateInput, deps: SwapperDeps, -): Promise> => { - const rates = await _getTradeQuote(input as unknown as CommonTradeQuoteInput, deps) +): Promise => { + const { + sellAsset, + buyAsset, + accountNumber, + receiveAddress, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + affiliateBps: commissionBps, + } = input + + if (!isSupportedChainId(sellAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (!isSupportedChainId(buyAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (!isSupportedAssetId(sellAsset.chainId, sellAsset.assetId)) { + return Err( + makeSwapErrorRight({ + message: `asset '${sellAsset.name}' on chainId '${sellAsset.chainId}' not supported`, + code: TradeQuoteError.UnsupportedTradePair, + details: { chainId: sellAsset.chainId, assetId: sellAsset.assetId }, + }), + ) + } + + if (!isSupportedAssetId(buyAsset.chainId, buyAsset.assetId)) { + return Err( + makeSwapErrorRight({ + message: `asset '${buyAsset.name}' on chainId '${buyAsset.chainId}' not supported`, + code: TradeQuoteError.UnsupportedTradePair, + details: { chainId: buyAsset.chainId, assetId: buyAsset.assetId }, + }), + ) + } const brokerUrl = deps.config.REACT_APP_CHAINFLIP_API_URL const apiKey = deps.config.REACT_APP_CHAINFLIP_API_KEY - // For DCA swaps we need to open a deposit channel at this point to attach the swap id to the quote, - // in order to properly fetch the streaming status later - rates.map(async tradeQuotes => { - for (const tradeQuote of tradeQuotes) { - for (const step of tradeQuote.steps) { - if ( - step.source === CHAINFLIP_DCA_BOOST_SWAP_SOURCE || - step.source === CHAINFLIP_DCA_SWAP_SOURCE - ) { - if (!input.receiveAddress) throw Error('missing receive address') - if (!input.sendAddress) throw Error('missing send address') - - 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, - }) - - let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION - if (serviceCommission < 0) serviceCommission = 0 - - const maybeSwapResponse = await getChainFlipSwap({ - brokerUrl, - apiKey, - sourceAsset, - destinationAsset, - destinationAddress: input.receiveAddress, - minimumPrice, - refundAddress: input.sendAddress, - commissionBps: serviceCommission, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: 0, - }) - - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) - } - - const { data: swapResponse } = maybeSwapResponse.unwrap() - - if (!swapResponse.id) throw Error('missing swap ID') - - step.chainflipSwapId = swapResponse.id - step.chainflipDepositAddress = swapResponse.address + const sourceAsset = await getChainFlipIdFromAssetId({ + assetId: sellAsset.assetId, + brokerUrl, + }) + const destinationAsset = await getChainFlipIdFromAssetId({ + assetId: buyAsset.assetId, + brokerUrl, + }) + + // Subtract the BaaS fee to end up at the final displayed commissionBps + let serviceCommission = parseInt(commissionBps) - CHAINFLIP_BAAS_COMMISSION + if (serviceCommission < 0) serviceCommission = 0 + + const maybeQuoteResponse = await chainflipService.get( + `${brokerUrl}/quotes-native` + + `?apiKey=${apiKey}` + + `&sourceAsset=${sourceAsset}` + + `&destinationAsset=${destinationAsset}` + + `&amount=${sellAmount}` + + `&commissionBps=${serviceCommission}`, + ) + + if (maybeQuoteResponse.isErr()) { + const error = maybeQuoteResponse.unwrapErr() + const cause = error.cause as AxiosError + + if ( + cause.message.includes('code 400') && + cause.response!.data.detail.includes('Amount outside asset bounds') + ) { + return Err( + makeSwapErrorRight({ + message: cause.response!.data.detail, + code: TradeQuoteError.SellAmountBelowMinimum, + }), + ) + } + + return Err( + makeSwapErrorRight({ + message: 'Quote request failed', + code: TradeQuoteError.NoRouteFound, + }), + ) + } + + const { data: quoteResponse } = maybeQuoteResponse.unwrap() + + const getFeeData = async () => { + const { chainNamespace } = fromAssetId(sellAsset.assetId) + + switch (chainNamespace) { + case CHAIN_NAMESPACE.Evm: { + const sellAdapter = deps.assertGetEvmChainAdapter(sellAsset.chainId) + const networkFeeCryptoBaseUnit = await getEvmTxFees({ + adapter: sellAdapter, + supportsEIP1559: (input as GetEvmTradeRateInput).supportsEIP1559, + sendAsset: sourceAsset, + }) + return { networkFeeCryptoBaseUnit } + } + + case CHAIN_NAMESPACE.Utxo: { + const sellAdapter = deps.assertGetUtxoChainAdapter(sellAsset.chainId) + const publicKey = (input as GetUtxoTradeQuoteInput).xpub! + const feeData = await getUtxoTxFees({ + sellAmountCryptoBaseUnit: sellAmount, + sellAdapter, + publicKey, + }) + + return feeData + } + + case CHAIN_NAMESPACE.Solana: { + const sellAdapter = deps.assertGetSolanaChainAdapter(sellAsset.chainId) + const getFeeDataInput: GetFeeDataInput = { + // Simulates a self-send, since we don't know the to just yet at this stage + to: input.sendAddress!, + value: sellAmount, + chainSpecific: { + from: input.sendAddress!, + tokenId: + sellAsset.assetId === solAssetId + ? undefined + : fromAssetId(sellAsset.assetId).assetReference, + }, } + const { fast } = await sellAdapter.getFeeData(getFeeDataInput) + return { networkFeeCryptoBaseUnit: fast.txFee } } + + default: + throw new Error('Unsupported chainNamespace') } - return tradeQuotes - }) + } + + const getFeeAsset = (fee: ChainflipBaasQuoteQuoteFee) => { + if (fee.type === 'ingress' || fee.type === 'boost') return sellAsset + + if (fee.type === 'egress') return buyAsset + + if (fee.type === 'liquidity' && fee.asset === sourceAsset) return sellAsset + + if (fee.type === 'liquidity' && fee.asset === destinationAsset) return buyAsset + + if (fee.type === 'liquidity' && fee.asset === 'usdc.eth') return usdcAsset + + if (fee.type === 'network') return usdcAsset + } + + const getProtocolFees = (singleQuoteResponse: ChainflipBaasQuoteQuote) => { + const protocolFees: Record = {} + + for (const fee of singleQuoteResponse.includedFees!) { + if (fee.type === 'broker') continue + + const asset = getFeeAsset(fee)! + if (!(asset.assetId in protocolFees)) { + protocolFees[asset.assetId] = { + amountCryptoBaseUnit: '0', + requiresBalance: false, + asset, + } + } + + protocolFees[asset.assetId].amountCryptoBaseUnit = ( + BigInt(protocolFees[asset.assetId].amountCryptoBaseUnit) + BigInt(fee.amountNative!) + ).toString() + } + + return protocolFees + } + + const getQuoteRate = (sellAmountCryptoBaseUnit: string, buyAmountCryptoBaseUnit: string) => { + return getRate({ + sellAmountCryptoBaseUnit, + buyAmountCryptoBaseUnit, + sellAsset, + buyAsset, + }) + } + + const getSwapSource = (swapType: string | undefined, isBoosted: boolean) => { + return swapType === CHAINFLIP_REGULAR_QUOTE + ? isBoosted + ? CHAINFLIP_BOOST_SWAP_SOURCE + : CHAINFLIP_SWAP_SOURCE + : isBoosted + ? CHAINFLIP_DCA_BOOST_SWAP_SOURCE + : CHAINFLIP_DCA_SWAP_SOURCE + } + + const quotes: TradeRate[] = [] + + for (const singleQuoteResponse of quoteResponse) { + const isStreaming = singleQuoteResponse.type === CHAINFLIP_DCA_QUOTE + const feeData = await getFeeData() + + if (singleQuoteResponse.boostQuote) { + const boostRate = getQuoteRate( + singleQuoteResponse.boostQuote.ingressAmountNative!, + singleQuoteResponse.boostQuote.egressAmountNative!, + ) + + const boostTradeQuote: TradeRate = { + id: uuid(), + accountNumber, + rate: boostRate, + receiveAddress, + potentialAffiliateBps: commissionBps, + affiliateBps: commissionBps, + isStreaming, + slippageTolerancePercentageDecimal: + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Chainflip), + steps: [ + { + buyAmountBeforeFeesCryptoBaseUnit: singleQuoteResponse.boostQuote.egressAmountNative!, + buyAmountAfterFeesCryptoBaseUnit: singleQuoteResponse.boostQuote.egressAmountNative!, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + singleQuoteResponse.boostQuote.ingressAmountNative!, + feeData: { + protocolFees: getProtocolFees(singleQuoteResponse.boostQuote), + ...feeData, + }, + rate: boostRate, + source: getSwapSource(singleQuoteResponse.type, true), + buyAsset, + sellAsset, + accountNumber, + allowanceContract: '0x0', // Chainflip does not use contracts + estimatedExecutionTimeMs: + (singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.deposit! + + singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.swap!) * + 1000, + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined + : undefined, + chainflipChunkIntervalBlocks: isStreaming + ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined + : undefined, + }, + ], + } + + quotes.push(boostTradeQuote) + } + + const rate = getQuoteRate( + singleQuoteResponse.ingressAmountNative!, + singleQuoteResponse.egressAmountNative!, + ) + + const tradeQuote: TradeRate = { + id: uuid(), + accountNumber, + rate, + receiveAddress, + potentialAffiliateBps: commissionBps, + affiliateBps: commissionBps, + isStreaming, + slippageTolerancePercentageDecimal: + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Chainflip), + steps: [ + { + buyAmountBeforeFeesCryptoBaseUnit: singleQuoteResponse.egressAmountNative!, + buyAmountAfterFeesCryptoBaseUnit: singleQuoteResponse.egressAmountNative!, + sellAmountIncludingProtocolFeesCryptoBaseUnit: singleQuoteResponse.ingressAmountNative!, + feeData: { + protocolFees: getProtocolFees(singleQuoteResponse), + ...feeData, + }, + rate, + source: getSwapSource(singleQuoteResponse.type, false), + buyAsset, + sellAsset, + accountNumber, + allowanceContract: '0x0', // Chainflip does not use contracts - all Txs are sends + estimatedExecutionTimeMs: + (singleQuoteResponse.estimatedDurationsSeconds!.deposit! + + singleQuoteResponse.estimatedDurationsSeconds!.swap!) * + 1000, + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.numberOfChunks ?? undefined + : undefined, + chainflipChunkIntervalBlocks: isStreaming + ? singleQuoteResponse.chunkIntervalBlocks ?? undefined + : undefined, + }, + ], + } + + quotes.push(tradeQuote) + } + + return Ok(quotes) +} + +export const getTradeRate = async ( + input: GetTradeRateInput, + deps: SwapperDeps, +): Promise => { + const { accountNumber } = input + + if (accountNumber === undefined) { + return Err( + makeSwapErrorRight({ + message: `accountNumber is required`, + code: TradeQuoteError.UnknownError, + }), + ) + } - return rates as Result + return await _getTradeRate(input, deps) } diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index e4100006554..d977e9f5988 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -430,7 +430,7 @@ export type CheckTradeStatusInput = { // a result containing all routes that were successfully generated, or an error in the case where // no routes could be generated -type TradeQuoteResult = Result +export type TradeQuoteResult = Result export type TradeRateResult = Result export type EvmTransactionRequest = { From d16a54fa4f8f6a407e3520ec8842718310b33214 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 01:32:32 +0100 Subject: [PATCH 14/49] fix: add swapId --- .../swapperApi/getTradeQuote.ts | 118 +++++++++--------- .../swapperApi/getTradeRate.ts | 2 +- 2 files changed, 63 insertions(+), 57 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index 6613a24d162..95521980600 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -1,7 +1,12 @@ +import { Err, Ok } from '@sniptt/monads' import type { AxiosError } from 'axios' -import type { CommonTradeQuoteInput, GetTradeRateInput, SwapperDeps } from '../../../types' -import { type TradeQuoteResult } from '../../../types' +import type { + CommonTradeQuoteInput, + GetTradeRateInput, + SwapperDeps, + TradeQuoteResult, +} from '../../../types' import { CHAINFLIP_BAAS_COMMISSION, CHAINFLIP_DCA_BOOST_SWAP_SOURCE, @@ -20,75 +25,76 @@ export const getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, ): Promise => { - const rates = await _getTradeRate(input as unknown as GetTradeRateInput, deps) + const maybeTradeRates = await _getTradeRate(input as unknown as GetTradeRateInput, deps) + + if (maybeTradeRates.isErr()) return Err(maybeTradeRates.unwrapErr()) const brokerUrl = deps.config.REACT_APP_CHAINFLIP_API_URL const apiKey = deps.config.REACT_APP_CHAINFLIP_API_KEY + const tradeRates = maybeTradeRates.unwrap() + // For DCA swaps we need to open a deposit channel at this point to attach the swap id to the quote, // in order to properly fetch the streaming status later - rates.map(async tradeQuotes => { - for (const tradeQuote of tradeQuotes) { - for (const step of tradeQuote.steps) { - if ( - step.source === CHAINFLIP_DCA_BOOST_SWAP_SOURCE || - step.source === CHAINFLIP_DCA_SWAP_SOURCE - ) { - if (!input.receiveAddress) throw Error('missing receive address') - if (!input.sendAddress) throw Error('missing send address') + for (const tradeRate of tradeRates) { + for (const step of tradeRate.steps) { + if ( + step.source === CHAINFLIP_DCA_BOOST_SWAP_SOURCE || + step.source === CHAINFLIP_DCA_SWAP_SOURCE + ) { + if (!input.receiveAddress) throw Error('missing receive address') + if (!input.sendAddress) throw Error('missing send address') - const sourceAsset = await getChainFlipIdFromAssetId({ - assetId: step.sellAsset.assetId, - brokerUrl, - }) - const destinationAsset = await getChainFlipIdFromAssetId({ - assetId: step.buyAsset.assetId, - brokerUrl, - }) + 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, - }) + const minimumPrice = calculateChainflipMinPrice({ + slippageTolerancePercentageDecimal: tradeRate.slippageTolerancePercentageDecimal, + sellAsset: step.sellAsset, + buyAsset: step.buyAsset, + buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + step.sellAmountIncludingProtocolFeesCryptoBaseUnit, + }) - let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION - if (serviceCommission < 0) serviceCommission = 0 + let serviceCommission = parseInt(tradeRate.affiliateBps) - CHAINFLIP_BAAS_COMMISSION + if (serviceCommission < 0) serviceCommission = 0 - const maybeSwapResponse = await getChainFlipSwap({ - brokerUrl, - apiKey, - sourceAsset, - destinationAsset, - destinationAddress: input.receiveAddress, - minimumPrice, - refundAddress: input.sendAddress, - commissionBps: serviceCommission, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: 0, - }) + const maybeSwapResponse = await getChainFlipSwap({ + brokerUrl, + apiKey, + sourceAsset, + destinationAsset, + destinationAddress: input.receiveAddress, + minimumPrice, + refundAddress: input.sendAddress, + commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: 0, + }) - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) - } + if (maybeSwapResponse.isErr()) { + const error = maybeSwapResponse.unwrapErr() + const cause = error.cause as AxiosError + throw Error(cause.response!.data.detail) + } - const { data: swapResponse } = maybeSwapResponse.unwrap() + const { data: swapResponse } = maybeSwapResponse.unwrap() - if (!swapResponse.id) throw Error('missing swap ID') + if (!swapResponse.id) throw Error('missing swap ID') - step.chainflipSwapId = swapResponse.id - step.chainflipDepositAddress = swapResponse.address - } + step.chainflipSwapId = swapResponse.id + step.chainflipDepositAddress = swapResponse.address } } - return tradeQuotes - }) + } - return rates as TradeQuoteResult + return Ok(tradeRates) as TradeQuoteResult } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index 9de71f27a68..7304a714680 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -7,7 +7,6 @@ import type { AxiosError } from 'axios' import { v4 as uuid } from 'uuid' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' -import type { TradeRateResult } from '../../../types' import { type GetEvmTradeRateInput, type GetTradeRateInput, @@ -17,6 +16,7 @@ import { SwapperName, TradeQuoteError, type TradeRate, + type TradeRateResult, } from '../../../types' import { getRate, makeSwapErrorRight } from '../../../utils' import { From d1b3a0f904578828ac09cb5d9ed6f1bf5bd7fe0d Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 02:00:35 +0100 Subject: [PATCH 15/49] fix: catch axios error --- .../hooks/useChainflipStreamingProgress.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index 72d4451f84a..e609b14e7fa 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -34,9 +34,14 @@ const getChainflipStreamingSwap = async ( const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL const apiKey = config.REACT_APP_CHAINFLIP_API_KEY - const { data: statusResponse } = await axios.get( + const statusResponse = await axios.get( `${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`, - ) + ).then(response => { + return response.data + }).catch(function (error) { + console.log('getChainflipStreamingSwap.getStatusById', error) + return null + }) console.log('getChainflipStreamingSwap.statusResponse', statusResponse) From 8c2eda5ace362862f81283f63c4dad0d5a334e54 Mon Sep 17 00:00:00 2001 From: cumpsd Date: Mon, 25 Nov 2024 02:01:29 +0100 Subject: [PATCH 16/49] chore: add some logging --- .../hooks/useChainflipStreamingProgress.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index 72d4451f84a..a00d1e4cbb5 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -15,7 +15,7 @@ import { useAppDispatch, useAppSelector } from 'state/store' import type { ChainflipStreamingSwapResponseSuccess } from './types' -const POLL_INTERVAL_MILLISECONDS = 30_000 // 30 seconds +const POLL_INTERVAL_MILLISECONDS = 5_000 // 5 seconds const DEFAULT_STREAMING_SWAP_METADATA: StreamingSwapMetadata = { attemptedSwapCount: 0, @@ -65,7 +65,7 @@ const getStreamingSwapMetadata = ( const failedSwaps: StreamingSwapFailedSwap[] = [] return { - totalSwapCount: data.executedChunks + data.remainingChunks ?? 0, + totalSwapCount: data.executedChunks + data.remainingChunks, attemptedSwapCount: data.executedChunks ?? 0, failedSwaps, } @@ -98,7 +98,9 @@ export const useChainflipStreamingProgress = ( console.log('useChainflipStreamingProgress.useAppSelector', bla) const swapId = tradeQuoteStep.chainflipSwapId + console.log('useChainflipStreamingProgress.tradeQuoteStep', tradeQuoteStep) console.log('useChainflipStreamingProgress.chainflipSwapId', swapId) + console.log('useChainflipStreamingProgress.sellTxHash', sellTxHash) useEffect(() => { // don't start polling until we have a tx @@ -106,6 +108,8 @@ export const useChainflipStreamingProgress = ( poll({ fn: async () => { + console.log('useChainflipStreamingProgress.polling.tradeQuoteStep', tradeQuoteStep) + console.log('useChainflipStreamingProgress.polling.sellTxHash', sellTxHash) const updatedStreamingSwapData = await getChainflipStreamingSwap(swapId) // no payload at all - must be a failed request - return From 97444ad8eb6b82b271fefa521839a1b8b122b83a Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 02:13:21 +0100 Subject: [PATCH 17/49] chore: lint --- .../hooks/useChainflipStreamingProgress.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index e5f2b8ee7af..ca2922e4844 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -34,14 +34,15 @@ const getChainflipStreamingSwap = async ( const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL const apiKey = config.REACT_APP_CHAINFLIP_API_KEY - const statusResponse = await axios.get( - `${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`, - ).then(response => { - return response.data - }).catch(function (error) { - console.log('getChainflipStreamingSwap.getStatusById', error) - return null - }) + const statusResponse = await axios + .get(`${brokerUrl}/status-by-id?apiKey=${apiKey}&swapId=${swapId}`) + .then(response => { + return response.data + }) + .catch(function (error) { + console.log('getChainflipStreamingSwap.getStatusById', error) + return null + }) console.log('getChainflipStreamingSwap.statusResponse', statusResponse) From ccc906cb989431f6157041348ff57af83699ce1c Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 02:18:52 +0100 Subject: [PATCH 18/49] fix: chunks are 0 when ended --- .../hooks/useChainflipStreamingProgress.tsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index ca2922e4844..011d4aa50b0 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -54,10 +54,24 @@ const getChainflipStreamingSwap = async ( return } + const swapState = statusResponse.status?.state const dcaStatus = statusResponse.status?.swap?.dca if (!dcaStatus) return + if ( + swapState === 'sending' || + swapState === 'sent' || + swapState === 'completed' || + swapState === 'failed' + ) { + // It's finished! + return { + executedChunks: dcaStatus!.executedChunks!, + remainingChunks: 0, + } + } + return { executedChunks: dcaStatus!.executedChunks!, remainingChunks: dcaStatus!.remainingChunks!, @@ -141,7 +155,16 @@ export const useChainflipStreamingProgress = ( // stop polling on dismount return cancelPolling - }, [cancelPolling, dispatch, hopIndex, poll, sellTxHash, confirmedTradeId, swapId]) + }, [ + cancelPolling, + dispatch, + hopIndex, + poll, + sellTxHash, + confirmedTradeId, + swapId, + tradeQuoteStep, + ]) const result = useMemo(() => { const numSuccessfulSwaps = From 11af8de68c4c163578e7c9e65dda6b0e5f5ff53e Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 02:41:41 +0100 Subject: [PATCH 19/49] chore: remove debug logging --- .../hooks/useChainflipStreamingProgress.tsx | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index 011d4aa50b0..c1bc174e61f 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -26,8 +26,6 @@ const DEFAULT_STREAMING_SWAP_METADATA: StreamingSwapMetadata = { const getChainflipStreamingSwap = async ( swapId: number | undefined, ): Promise => { - console.log('getChainflipStreamingSwap.swapId', swapId) - if (!swapId) return const config = getConfig() @@ -44,16 +42,8 @@ const getChainflipStreamingSwap = async ( return null }) - console.log('getChainflipStreamingSwap.statusResponse', statusResponse) - if (!statusResponse) return - // TODO: Check for real errors - if ('error' in statusResponse) { - console.error('failed to fetch streaming swap data', statusResponse.error) - return - } - const swapState = statusResponse.status?.state const dcaStatus = statusResponse.status?.swap?.dca @@ -114,13 +104,7 @@ export const useChainflipStreamingProgress = ( swap: { sellTxHash, streamingSwap: streamingSwapMeta }, } = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)) - const bla = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter)) - console.log('useChainflipStreamingProgress.useAppSelector', bla) - const swapId = tradeQuoteStep.chainflipSwapId - console.log('useChainflipStreamingProgress.tradeQuoteStep', tradeQuoteStep) - console.log('useChainflipStreamingProgress.chainflipSwapId', swapId) - console.log('useChainflipStreamingProgress.sellTxHash', sellTxHash) useEffect(() => { // don't start polling until we have a tx @@ -128,8 +112,6 @@ export const useChainflipStreamingProgress = ( poll({ fn: async () => { - console.log('useChainflipStreamingProgress.polling.tradeQuoteStep', tradeQuoteStep) - console.log('useChainflipStreamingProgress.polling.sellTxHash', sellTxHash) const updatedStreamingSwapData = await getChainflipStreamingSwap(swapId) // no payload at all - must be a failed request - return @@ -167,8 +149,7 @@ export const useChainflipStreamingProgress = ( ]) const result = useMemo(() => { - const numSuccessfulSwaps = - (streamingSwapMeta?.attemptedSwapCount ?? 0) - (streamingSwapMeta?.failedSwaps?.length ?? 0) + const numSuccessfulSwaps = (streamingSwapMeta?.attemptedSwapCount ?? 0) const isComplete = streamingSwapMeta !== undefined && numSuccessfulSwaps >= streamingSwapMeta.totalSwapCount From 1636313094eed87fce5df705cc818101944c9497 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 02:51:09 +0100 Subject: [PATCH 20/49] fix: make sure swapper metadata has swap id too --- .../swappers/ChainflipSwapper/endpoints.ts | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index 1c895de07d6..de8e5d99efa 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -101,11 +101,14 @@ export const chainflipApi: SwapperApi = { if (!swapResponse.id) throw Error('missing swap ID') tradeQuoteMetadata.set(tradeQuote.id, swapResponse) + } else { + tradeQuoteMetadata.set(tradeQuote.id, { + id: step.chainflipSwapId, + address: step.chainflipDepositAddress, + }) } - const depositAddress = step.chainflipDepositAddress - ? step.chainflipDepositAddress - : tradeQuoteMetadata.get(tradeQuote.id)!.address! + const depositAddress = tradeQuoteMetadata.get(tradeQuote.id)!.address! const { assetReference } = fromAssetId(step.sellAsset.assetId) @@ -232,11 +235,14 @@ export const chainflipApi: SwapperApi = { if (!swapResponse.id) throw Error('missing swap ID') tradeQuoteMetadata.set(tradeQuote.id, swapResponse) + } else { + tradeQuoteMetadata.set(tradeQuote.id, { + id: step.chainflipSwapId, + address: step.chainflipDepositAddress, + }) } - const depositAddress = step.chainflipDepositAddress - ? step.chainflipDepositAddress - : tradeQuoteMetadata.get(tradeQuote.id)!.address! + const depositAddress = tradeQuoteMetadata.get(tradeQuote.id)!.address! return adapter.buildSendApiTransaction({ value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, @@ -314,11 +320,14 @@ export const chainflipApi: SwapperApi = { if (!swapResponse.id) throw Error('missing swap ID') tradeQuoteMetadata.set(tradeQuote.id, swapResponse) + } else { + tradeQuoteMetadata.set(tradeQuote.id, { + id: step.chainflipSwapId, + address: step.chainflipDepositAddress, + }) } - const depositAddress = step.chainflipDepositAddress - ? step.chainflipDepositAddress - : tradeQuoteMetadata.get(tradeQuote.id)!.address! + const depositAddress = tradeQuoteMetadata.get(tradeQuote.id)!.address! const adapter = assertGetSolanaChainAdapter(step.sellAsset.chainId) From 3bd26454035f889d2a51a638389ebc1f2cd17eda Mon Sep 17 00:00:00 2001 From: David Cumps Date: Mon, 25 Nov 2024 02:58:33 +0100 Subject: [PATCH 21/49] chore: we can keep this logic --- .../hooks/useChainflipStreamingProgress.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index c1bc174e61f..5667a9190c1 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -149,7 +149,8 @@ export const useChainflipStreamingProgress = ( ]) const result = useMemo(() => { - const numSuccessfulSwaps = (streamingSwapMeta?.attemptedSwapCount ?? 0) + const numSuccessfulSwaps = + (streamingSwapMeta?.attemptedSwapCount ?? 0) - (streamingSwapMeta?.failedSwaps?.length ?? 0) const isComplete = streamingSwapMeta !== undefined && numSuccessfulSwaps >= streamingSwapMeta.totalSwapCount From 3d7600ef11f0f18f25c05d4a5fb67aa5304381f5 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Tue, 26 Nov 2024 12:36:58 +0100 Subject: [PATCH 22/49] chore: cleanup small mistakes --- packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts | 6 +++--- .../swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts | 2 -- .../swapper/src/swappers/ChainflipSwapper/utils/helpers.ts | 4 ++-- packages/swapper/src/types.ts | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index de8e5d99efa..650c4086c0f 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -49,7 +49,7 @@ export const chainflipApi: SwapperApi = { if (!isExecutableTradeStep(step)) throw Error('Unable to execute step') - // For DCA swaps we already opened a deposit address in getTradeRate, we only need to open for regular swaps + // For DCA swaps we already opened a deposit address in getTradeQuote, we only need to open for regular swaps if (!step.chainflipDepositAddress) { const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL const apiKey = config.REACT_APP_CHAINFLIP_API_KEY @@ -174,7 +174,7 @@ export const chainflipApi: SwapperApi = { const adapter = assertGetUtxoChainAdapter(step.sellAsset.chainId) - // For DCA swaps we already opened a deposit address in getTradeRate, we only need to open for regular swaps + // For DCA swaps we already opened a deposit address in getTradeQuote, we only need to open for regular swaps if (!step.chainflipDepositAddress) { const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL const apiKey = config.REACT_APP_CHAINFLIP_API_KEY @@ -268,7 +268,7 @@ export const chainflipApi: SwapperApi = { if (!isExecutableTradeStep(step)) throw Error('Unable to execute step') - // For DCA swaps we already opened a deposit address in getTradeRate, we only need to open for regular swaps + // For DCA swaps we already opened a deposit address in getTradeQuote, we only need to open for regular swaps if (!step.chainflipDepositAddress) { const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL const apiKey = config.REACT_APP_CHAINFLIP_API_KEY diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index 95521980600..5dbe0b33c2c 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -19,8 +19,6 @@ import { } from '../utils/helpers' import { _getTradeRate } from './getTradeRate' -// This isn't a mistake. A trade quote *is* a trade rate. Chainflip doesn't really have a notion of a trade rate, -// they do have a notion of a "swap" (which we effectively only use to get the deposit address), which is irrelevant to the notion of quote vs. rate export const getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts index 6d9247e73d6..3d6cc3c2f09 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts @@ -99,7 +99,7 @@ export const getChainFlipSwap = ({ refundAddress, retryDurationInBlocks = 10, commissionBps, - numberOfChunks = undefined, + numberOfChunks, chunkIntervalBlocks = 2, }: GetChainFlipSwapArgs): Promise< Result, SwapErrorRight> @@ -116,7 +116,7 @@ export const getChainFlipSwap = ({ `&retryDurationInBlocks=${retryDurationInBlocks}` + `&commissionBps=${commissionBps}` - if (numberOfChunks) { + if (numberOfChunks && chunkIntervalBlocks) { swapUrl += `&numberOfChunks=${numberOfChunks}` swapUrl += `&chunkIntervalBlocks=${chunkIntervalBlocks}` } diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index d977e9f5988..f019c7dd8d9 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -25,7 +25,7 @@ import type { TypedData } from 'eip-712' import type { InterpolationOptions } from 'node-polyglot' import type { Address } from 'viem' -import type { CowMessageToSign, CowSwapQuoteResponse } from './swappers/CowSwapper' +import type { CowMessageToSign, CowSwapQuoteResponse } from './swappers/CowSwapper/types' import type { makeSwapperAxiosServiceMonadic } from './utils' // TODO: Rename all properties in this type to be camel case and not react specific From 9e86529488279eee15b7c03a404f59fe89b206ec Mon Sep 17 00:00:00 2001 From: David Cumps Date: Tue, 26 Nov 2024 13:01:26 +0100 Subject: [PATCH 23/49] refactor: make dca and non-dca consistent, cleans up a lot of code! --- .../swappers/ChainflipSwapper/endpoints.ts | 232 ++---------------- .../swapperApi/getTradeQuote.ts | 97 ++++---- .../swapperApi/getTradeRate.ts | 23 +- packages/swapper/src/types.ts | 1 + 4 files changed, 88 insertions(+), 265 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index 650c4086c0f..58c9799abbd 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -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 { @@ -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' 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 = new Map() @@ -40,7 +33,6 @@ export const chainflipApi: SwapperApi = { from, tradeQuote, assertGetEvmChainAdapter, - config, supportsEIP1559, }: GetUnsignedEvmTransactionArgs): Promise => { if (!isExecutableTradeQuote(tradeQuote)) throw Error('Unable to execute trade') @@ -48,75 +40,18 @@ export const chainflipApi: SwapperApi = { const step = tradeQuote.steps[0] if (!isExecutableTradeStep(step)) throw Error('Unable to execute step') + if (!step.chainflipDepositAddress) throw Error('Missing deposit address') - // For DCA swaps we already opened a deposit address in getTradeQuote, we only need to open for regular swaps - if (!step.chainflipDepositAddress) { - const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL - const apiKey = config.REACT_APP_CHAINFLIP_API_KEY - - 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, - }) - - const maybeSwapResponse = await getChainFlipSwap({ - brokerUrl, - apiKey, - sourceAsset, - destinationAsset, - destinationAddress: tradeQuote.receiveAddress, - minimumPrice, - refundAddress: from, - commissionBps: serviceCommission, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: 0, - }) - - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) - } - - const { data: swapResponse } = maybeSwapResponse.unwrap() - - if (!swapResponse.id) throw Error('missing swap ID') - - tradeQuoteMetadata.set(tradeQuote.id, swapResponse) - } else { - tradeQuoteMetadata.set(tradeQuote.id, { - id: step.chainflipSwapId, - address: step.chainflipDepositAddress, - }) - } - - const depositAddress = tradeQuoteMetadata.get(tradeQuote.id)!.address! + tradeQuoteMetadata.set(tradeQuote.id, { + id: step.chainflipSwapId, + address: step.chainflipDepositAddress, + }) const { assetReference } = fromAssetId(step.sellAsset.assetId) - const adapter = assertGetEvmChainAdapter(step.sellAsset.chainId) - const isTokenSend = isToken(step.sellAsset.assetId) const getFeeDataInput: GetFeeDataInput = { - to: depositAddress, + to: step.chainflipDepositAddress, value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, chainSpecific: { from, @@ -129,7 +64,7 @@ export const chainflipApi: SwapperApi = { const fees = feeData[FeeDataKey.Average] const unsignedTxInput = await adapter.buildSendApiTransaction({ - to: depositAddress, + to: step.chainflipDepositAddress, from, value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, accountNumber: step.accountNumber, @@ -159,95 +94,30 @@ export const chainflipApi: SwapperApi = { gasPrice: unsignedTxInput.gasPrice, } }, - getUnsignedUtxoTransaction: async ({ + getUnsignedUtxoTransaction: ({ tradeQuote, xpub, accountType, assertGetUtxoChainAdapter, - config, }: GetUnsignedUtxoTransactionArgs): Promise => { if (!isExecutableTradeQuote(tradeQuote)) throw Error('Unable to execute trade') const step = tradeQuote.steps[0] if (!isExecutableTradeStep(step)) throw Error('Unable to execute step') + if (!step.chainflipDepositAddress) throw Error('Missing deposit address') - const adapter = assertGetUtxoChainAdapter(step.sellAsset.chainId) - - // For DCA swaps we already opened a deposit address in getTradeQuote, we only need to open for regular swaps - if (!step.chainflipDepositAddress) { - const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL - const apiKey = config.REACT_APP_CHAINFLIP_API_KEY - - 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, - }) - - 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, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: step.source === CHAINFLIP_BOOST_SWAP_SOURCE ? 10 : 0, - }) - - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) - } - - const { data: swapResponse } = maybeSwapResponse.unwrap() - - if (!swapResponse.id) throw Error('missing swap ID') - - tradeQuoteMetadata.set(tradeQuote.id, swapResponse) - } else { - tradeQuoteMetadata.set(tradeQuote.id, { - id: step.chainflipSwapId, - address: step.chainflipDepositAddress, - }) - } + tradeQuoteMetadata.set(tradeQuote.id, { + id: step.chainflipSwapId, + address: step.chainflipDepositAddress, + }) - const depositAddress = tradeQuoteMetadata.get(tradeQuote.id)!.address! + const adapter = assertGetUtxoChainAdapter(step.sellAsset.chainId) return adapter.buildSendApiTransaction({ value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, xpub: xpub!, - to: depositAddress, + to: step.chainflipDepositAddress, accountNumber: step.accountNumber, skipToAddressValidation: true, chainSpecific: { @@ -260,74 +130,18 @@ export const chainflipApi: SwapperApi = { tradeQuote, from, assertGetSolanaChainAdapter, - config, }: GetUnsignedSolanaTransactionArgs): Promise => { if (!isExecutableTradeQuote(tradeQuote)) throw Error('Unable to execute trade') const step = tradeQuote.steps[0] if (!isExecutableTradeStep(step)) throw Error('Unable to execute step') + if (!step.chainflipDepositAddress) throw Error('Missing deposit address') - // For DCA swaps we already opened a deposit address in getTradeQuote, we only need to open for regular swaps - if (!step.chainflipDepositAddress) { - const brokerUrl = config.REACT_APP_CHAINFLIP_API_URL - const apiKey = config.REACT_APP_CHAINFLIP_API_KEY - - 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, - }) - - const maybeSwapResponse = await getChainFlipSwap({ - brokerUrl, - apiKey, - sourceAsset, - destinationAsset, - destinationAddress: tradeQuote.receiveAddress, - minimumPrice, - refundAddress: from, - commissionBps: serviceCommission, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: 0, - }) - - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) - } - - const { data: swapResponse } = maybeSwapResponse.unwrap() - - if (!swapResponse.id) throw Error('missing swap ID') - - tradeQuoteMetadata.set(tradeQuote.id, swapResponse) - } else { - tradeQuoteMetadata.set(tradeQuote.id, { - id: step.chainflipSwapId, - address: step.chainflipDepositAddress, - }) - } - - const depositAddress = tradeQuoteMetadata.get(tradeQuote.id)!.address! + tradeQuoteMetadata.set(tradeQuote.id, { + id: step.chainflipSwapId, + address: step.chainflipDepositAddress, + }) const adapter = assertGetSolanaChainAdapter(step.sellAsset.chainId) @@ -337,7 +151,7 @@ export const chainflipApi: SwapperApi = { : fromAssetId(step.sellAsset.assetId).assetReference const getFeeDataInput: GetFeeDataInput = { - to: depositAddress, + to: step.chainflipDepositAddress, value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, chainSpecific: { from, @@ -347,7 +161,7 @@ export const chainflipApi: SwapperApi = { const { fast } = await adapter.getFeeData(getFeeDataInput) const buildSendTxInput: BuildSendApiTxInput = { - to: depositAddress, + to: step.chainflipDepositAddress, from, value: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, accountNumber: step.accountNumber, @@ -370,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 diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index 5dbe0b33c2c..5d85b2c6be6 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -7,11 +7,7 @@ import type { SwapperDeps, TradeQuoteResult, } from '../../../types' -import { - CHAINFLIP_BAAS_COMMISSION, - CHAINFLIP_DCA_BOOST_SWAP_SOURCE, - CHAINFLIP_DCA_SWAP_SOURCE, -} from '../constants' +import { CHAINFLIP_BAAS_COMMISSION } from '../constants' import { calculateChainflipMinPrice, getChainFlipIdFromAssetId, @@ -32,65 +28,60 @@ export const getTradeQuote = async ( const tradeRates = maybeTradeRates.unwrap() - // For DCA swaps we need to open a deposit channel at this point to attach the swap id to the quote, + // We need to open a deposit channel at this point to attach the swap id to the quote, // in order to properly fetch the streaming status later for (const tradeRate of tradeRates) { for (const step of tradeRate.steps) { - if ( - step.source === CHAINFLIP_DCA_BOOST_SWAP_SOURCE || - step.source === CHAINFLIP_DCA_SWAP_SOURCE - ) { - if (!input.receiveAddress) throw Error('missing receive address') - if (!input.sendAddress) throw Error('missing send address') + if (!input.receiveAddress) throw Error('missing receive address') + if (!input.sendAddress) throw Error('missing send address') - const sourceAsset = await getChainFlipIdFromAssetId({ - assetId: step.sellAsset.assetId, - brokerUrl, - }) - const destinationAsset = await getChainFlipIdFromAssetId({ - assetId: step.buyAsset.assetId, - brokerUrl, - }) + const sourceAsset = await getChainFlipIdFromAssetId({ + assetId: step.sellAsset.assetId, + brokerUrl, + }) + const destinationAsset = await getChainFlipIdFromAssetId({ + assetId: step.buyAsset.assetId, + brokerUrl, + }) - const minimumPrice = calculateChainflipMinPrice({ - slippageTolerancePercentageDecimal: tradeRate.slippageTolerancePercentageDecimal, - sellAsset: step.sellAsset, - buyAsset: step.buyAsset, - buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit, - sellAmountIncludingProtocolFeesCryptoBaseUnit: - step.sellAmountIncludingProtocolFeesCryptoBaseUnit, - }) + const minimumPrice = calculateChainflipMinPrice({ + slippageTolerancePercentageDecimal: tradeRate.slippageTolerancePercentageDecimal, + sellAsset: step.sellAsset, + buyAsset: step.buyAsset, + buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + step.sellAmountIncludingProtocolFeesCryptoBaseUnit, + }) - let serviceCommission = parseInt(tradeRate.affiliateBps) - CHAINFLIP_BAAS_COMMISSION - if (serviceCommission < 0) serviceCommission = 0 + let serviceCommission = parseInt(tradeRate.affiliateBps) - CHAINFLIP_BAAS_COMMISSION + if (serviceCommission < 0) serviceCommission = 0 - const maybeSwapResponse = await getChainFlipSwap({ - brokerUrl, - apiKey, - sourceAsset, - destinationAsset, - destinationAddress: input.receiveAddress, - minimumPrice, - refundAddress: input.sendAddress, - commissionBps: serviceCommission, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: 0, - }) + const maybeSwapResponse = await getChainFlipSwap({ + brokerUrl, + apiKey, + sourceAsset, + destinationAsset, + destinationAddress: input.receiveAddress, + minimumPrice, + refundAddress: input.sendAddress, + commissionBps: serviceCommission, + numberOfChunks: step.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: step.chainflipMaxBoostFee, + }) - if (maybeSwapResponse.isErr()) { - const error = maybeSwapResponse.unwrapErr() - const cause = error.cause as AxiosError - throw Error(cause.response!.data.detail) - } + if (maybeSwapResponse.isErr()) { + const error = maybeSwapResponse.unwrapErr() + const cause = error.cause as AxiosError + throw Error(cause.response!.data.detail) + } - const { data: swapResponse } = maybeSwapResponse.unwrap() + const { data: swapResponse } = maybeSwapResponse.unwrap() - if (!swapResponse.id) throw Error('missing swap ID') + if (!swapResponse.id) throw Error('missing swap ID') - step.chainflipSwapId = swapResponse.id - step.chainflipDepositAddress = swapResponse.address - } + step.chainflipSwapId = swapResponse.id + step.chainflipDepositAddress = swapResponse.address } } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index 7304a714680..51b80add083 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -156,13 +156,11 @@ export const _getTradeRate = async ( case CHAIN_NAMESPACE.Utxo: { const sellAdapter = deps.assertGetUtxoChainAdapter(sellAsset.chainId) const publicKey = (input as GetUtxoTradeQuoteInput).xpub! - const feeData = await getUtxoTxFees({ + return await getUtxoTxFees({ sellAmountCryptoBaseUnit: sellAmount, sellAdapter, publicKey, }) - - return feeData } case CHAIN_NAMESPACE.Solana: { @@ -244,6 +242,24 @@ export const _getTradeRate = async ( : CHAINFLIP_DCA_SWAP_SOURCE } + const getMaxBoostFee = () => { + const { chainNamespace } = fromAssetId(sellAsset.assetId) + + switch (chainNamespace) { + case CHAIN_NAMESPACE.Evm: + return 0 + + case CHAIN_NAMESPACE.Utxo: + return 10 + + case CHAIN_NAMESPACE.Solana: + return 0 + + default: + throw new Error('Unsupported chainNamespace') + } + } + const quotes: TradeRate[] = [] for (const singleQuoteResponse of quoteResponse) { @@ -293,6 +309,7 @@ export const _getTradeRate = async ( chainflipChunkIntervalBlocks: isStreaming ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined : undefined, + chainflipMaxBoostFee: getMaxBoostFee(), }, ], } diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index f019c7dd8d9..1567fd94013 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -270,6 +270,7 @@ export type TradeQuoteStep = { chainflipDepositAddress?: string | undefined chainflipNumberOfChunks?: number | undefined chainflipChunkIntervalBlocks?: number | undefined + chainflipMaxBoostFee?: number | undefined } export type TradeRateStep = Omit & { accountNumber: undefined } From 772feeaf0fbeb16037c08cecedb34afac3ef7b13 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Tue, 26 Nov 2024 13:07:27 +0100 Subject: [PATCH 24/49] feat: be explicit about not boosting --- .../src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index 51b80add083..bae17c15783 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -358,6 +358,7 @@ export const _getTradeRate = async ( chainflipChunkIntervalBlocks: isStreaming ? singleQuoteResponse.chunkIntervalBlocks ?? undefined : undefined, + chainflipMaxBoostFee: 0, }, ], } From bb2a67de4f6f41fb9e20248a6f3b913f8d4c2072 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Tue, 26 Nov 2024 13:07:39 +0100 Subject: [PATCH 25/49] feat: add an extra safety check --- .../src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index 5d85b2c6be6..cba57610e65 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -78,7 +78,8 @@ export const getTradeQuote = async ( const { data: swapResponse } = maybeSwapResponse.unwrap() - if (!swapResponse.id) throw Error('missing swap ID') + if (!swapResponse.id) throw Error('Missing Swap Id') + if (!swapResponse.address) throw Error('Missing Deposit Channel') step.chainflipSwapId = swapResponse.id step.chainflipDepositAddress = swapResponse.address From a99447f96e8c791f5ffc25b0559297b022007d8a Mon Sep 17 00:00:00 2001 From: David Cumps Date: Tue, 26 Nov 2024 13:20:16 +0100 Subject: [PATCH 26/49] feat: safety check for account number on get Quote --- .../swapperApi/getTradeQuote.ts | 22 ++++++++++++++++++- .../swapperApi/getTradeRate.ts | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index cba57610e65..da7a28b3561 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -7,6 +7,8 @@ import type { SwapperDeps, TradeQuoteResult, } from '../../../types' +import { TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' import { CHAINFLIP_BAAS_COMMISSION } from '../constants' import { calculateChainflipMinPrice, @@ -15,7 +17,7 @@ import { } from '../utils/helpers' import { _getTradeRate } from './getTradeRate' -export const getTradeQuote = async ( +const _getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, ): Promise => { @@ -88,3 +90,21 @@ export const getTradeQuote = async ( return Ok(tradeRates) as TradeQuoteResult } + +export const getTradeQuote = async ( + input: CommonTradeQuoteInput, + deps: SwapperDeps, +): Promise => { + const { accountNumber } = input + + if (accountNumber === undefined) { + return Err( + makeSwapErrorRight({ + message: `accountNumber is required`, + code: TradeQuoteError.UnknownError, + }), + ) + } + + return await _getTradeQuote(input, deps) +} diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index bae17c15783..df149a83d37 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -166,7 +166,7 @@ export const _getTradeRate = async ( case CHAIN_NAMESPACE.Solana: { const sellAdapter = deps.assertGetSolanaChainAdapter(sellAsset.chainId) const getFeeDataInput: GetFeeDataInput = { - // Simulates a self-send, since we don't know the to just yet at this stage + // Simulates a self-send, since we don't know the 'to' just yet at this stage to: input.sendAddress!, value: sellAmount, chainSpecific: { From 87b4531b506cf5a77b4d5ceacc44b7b1b1421320 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Tue, 26 Nov 2024 13:23:15 +0100 Subject: [PATCH 27/49] refactor: move safety checks from inside for to caller --- .../swapperApi/getTradeQuote.ts | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index da7a28b3561..0e8a4d03b33 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -34,9 +34,6 @@ const _getTradeQuote = async ( // in order to properly fetch the streaming status later for (const tradeRate of tradeRates) { for (const step of tradeRate.steps) { - if (!input.receiveAddress) throw Error('missing receive address') - if (!input.sendAddress) throw Error('missing send address') - const sourceAsset = await getChainFlipIdFromAssetId({ assetId: step.sellAsset.assetId, brokerUrl, @@ -65,7 +62,7 @@ const _getTradeQuote = async ( destinationAsset, destinationAddress: input.receiveAddress, minimumPrice, - refundAddress: input.sendAddress, + refundAddress: input.sendAddress!, // We verified existence of sendAddress in calling method commissionBps: serviceCommission, numberOfChunks: step.chainflipNumberOfChunks, chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, @@ -95,7 +92,11 @@ export const getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, ): Promise => { - const { accountNumber } = input + const { + accountNumber, + sendAddress, + receiveAddress + } = input if (accountNumber === undefined) { return Err( @@ -106,5 +107,23 @@ export const getTradeQuote = async ( ) } + if (sendAddress === undefined) { + return Err( + makeSwapErrorRight({ + message: `sendAddress is required`, + code: TradeQuoteError.UnknownError, + }), + ) + } + + if (receiveAddress === undefined) { + return Err( + makeSwapErrorRight({ + message: `receiveAddress is required`, + code: TradeQuoteError.UnknownError, + }), + ) + } + return await _getTradeQuote(input, deps) } From 56ccfc241a06ca6bdf07ce52155a715786617da1 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Tue, 26 Nov 2024 13:29:27 +0100 Subject: [PATCH 28/49] chore: lint --- .../swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index 0e8a4d03b33..a2e30caff1b 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -92,11 +92,7 @@ export const getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, ): Promise => { - const { - accountNumber, - sendAddress, - receiveAddress - } = input + const { accountNumber, sendAddress, receiveAddress } = input if (accountNumber === undefined) { return Err( From f67658916bb964839ea73de504b6986843c26f60 Mon Sep 17 00:00:00 2001 From: David Cumps Date: Tue, 26 Nov 2024 14:34:21 +0100 Subject: [PATCH 29/49] refactor: simplify getTradeRates, make getTradeQuote safer, and recalculate solana feeData --- .../swapperApi/getTradeQuote.ts | 141 ++++--- .../swapperApi/getTradeRate.ts | 387 +----------------- .../ChainflipSwapper/utils/rate-quotes.ts | 374 +++++++++++++++++ 3 files changed, 464 insertions(+), 438 deletions(-) create mode 100644 packages/swapper/src/swappers/ChainflipSwapper/utils/rate-quotes.ts diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index a2e30caff1b..f8c6f6a69c9 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -1,13 +1,16 @@ +import { CHAIN_NAMESPACE, fromAssetId, solAssetId } from '@shapeshiftoss/caip' +import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters' +import type { KnownChainIds } from '@shapeshiftoss/types' import { Err, Ok } from '@sniptt/monads' import type { AxiosError } from 'axios' -import type { - CommonTradeQuoteInput, - GetTradeRateInput, - SwapperDeps, - TradeQuoteResult, +import type { QuoteFeeData } from '../../../types' +import { + type CommonTradeQuoteInput, + type SwapperDeps, + TradeQuoteError, + type TradeQuoteResult, } from '../../../types' -import { TradeQuoteError } from '../../../types' import { makeSwapErrorRight } from '../../../utils' import { CHAINFLIP_BAAS_COMMISSION } from '../constants' import { @@ -15,25 +18,59 @@ import { getChainFlipIdFromAssetId, getChainFlipSwap, } from '../utils/helpers' -import { _getTradeRate } from './getTradeRate' +import { getRateOrQuote } from '../utils/rate-quotes' -const _getTradeQuote = async ( +export const getTradeQuote = async ( input: CommonTradeQuoteInput, deps: SwapperDeps, ): Promise => { - const maybeTradeRates = await _getTradeRate(input as unknown as GetTradeRateInput, deps) + const { + accountNumber, + sendAddress, + receiveAddress, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + } = input + + if (accountNumber === undefined) { + return Err( + makeSwapErrorRight({ + message: `accountNumber is required`, + code: TradeQuoteError.UnknownError, + }), + ) + } + + if (sendAddress === undefined) { + return Err( + makeSwapErrorRight({ + message: `sendAddress is required`, + code: TradeQuoteError.UnknownError, + }), + ) + } + + if (receiveAddress === undefined) { + return Err( + makeSwapErrorRight({ + message: `receiveAddress is required`, + code: TradeQuoteError.UnknownError, + }), + ) + } + + const maybeTradeQuotes = await getRateOrQuote(input, deps) - if (maybeTradeRates.isErr()) return Err(maybeTradeRates.unwrapErr()) + if (maybeTradeQuotes.isErr()) return Err(maybeTradeQuotes.unwrapErr()) const brokerUrl = deps.config.REACT_APP_CHAINFLIP_API_URL const apiKey = deps.config.REACT_APP_CHAINFLIP_API_KEY - const tradeRates = maybeTradeRates.unwrap() + const tradeQuotes = maybeTradeQuotes.unwrap() // We need to open a deposit channel at this point to attach the swap id to the quote, // in order to properly fetch the streaming status later - for (const tradeRate of tradeRates) { - for (const step of tradeRate.steps) { + for (const tradeQuote of tradeQuotes) { + for (const step of tradeQuote.steps) { const sourceAsset = await getChainFlipIdFromAssetId({ assetId: step.sellAsset.assetId, brokerUrl, @@ -44,7 +81,7 @@ const _getTradeQuote = async ( }) const minimumPrice = calculateChainflipMinPrice({ - slippageTolerancePercentageDecimal: tradeRate.slippageTolerancePercentageDecimal, + slippageTolerancePercentageDecimal: tradeQuote.slippageTolerancePercentageDecimal, sellAsset: step.sellAsset, buyAsset: step.buyAsset, buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit, @@ -52,21 +89,21 @@ const _getTradeQuote = async ( step.sellAmountIncludingProtocolFeesCryptoBaseUnit, }) - let serviceCommission = parseInt(tradeRate.affiliateBps) - CHAINFLIP_BAAS_COMMISSION + let serviceCommission = parseInt(tradeQuote.affiliateBps) - CHAINFLIP_BAAS_COMMISSION if (serviceCommission < 0) serviceCommission = 0 const maybeSwapResponse = await getChainFlipSwap({ brokerUrl, apiKey, sourceAsset, + minimumPrice, destinationAsset, destinationAddress: input.receiveAddress, - minimumPrice, - refundAddress: input.sendAddress!, // We verified existence of sendAddress in calling method - commissionBps: serviceCommission, + refundAddress: input.sendAddress!, + maxBoostFee: step.chainflipMaxBoostFee, numberOfChunks: step.chainflipNumberOfChunks, chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, - maxBoostFee: step.chainflipMaxBoostFee, + commissionBps: serviceCommission, }) if (maybeSwapResponse.isErr()) { @@ -80,46 +117,38 @@ const _getTradeQuote = async ( if (!swapResponse.id) throw Error('Missing Swap Id') if (!swapResponse.address) throw Error('Missing Deposit Channel') + const getFeeData = async () => { + const { chainNamespace } = fromAssetId(step.sellAsset.assetId) + + // We faked feeData for Solana with a self-send during rates, we can now properly do it on quote time + switch (chainNamespace) { + case CHAIN_NAMESPACE.Solana: { + const sellAdapter = deps.assertGetSolanaChainAdapter(step.sellAsset.chainId) + const getFeeDataInput: GetFeeDataInput = { + to: input.receiveAddress, + value: sellAmount, + chainSpecific: { + from: input.sendAddress!, + tokenId: + step.sellAsset.assetId === solAssetId + ? undefined + : fromAssetId(step.sellAsset.assetId).assetReference, + }, + } + const { fast } = await sellAdapter.getFeeData(getFeeDataInput) + return { networkFeeCryptoBaseUnit: fast.txFee } as QuoteFeeData + } + + default: + return step.feeData + } + } + step.chainflipSwapId = swapResponse.id step.chainflipDepositAddress = swapResponse.address + step.feeData = await getFeeData() } } - return Ok(tradeRates) as TradeQuoteResult -} - -export const getTradeQuote = async ( - input: CommonTradeQuoteInput, - deps: SwapperDeps, -): Promise => { - const { accountNumber, sendAddress, receiveAddress } = input - - if (accountNumber === undefined) { - return Err( - makeSwapErrorRight({ - message: `accountNumber is required`, - code: TradeQuoteError.UnknownError, - }), - ) - } - - if (sendAddress === undefined) { - return Err( - makeSwapErrorRight({ - message: `sendAddress is required`, - code: TradeQuoteError.UnknownError, - }), - ) - } - - if (receiveAddress === undefined) { - return Err( - makeSwapErrorRight({ - message: `receiveAddress is required`, - code: TradeQuoteError.UnknownError, - }), - ) - } - - return await _getTradeQuote(input, deps) + return Ok(tradeQuotes) } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index df149a83d37..006dedaef37 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -1,388 +1,11 @@ -import type { AssetId } from '@shapeshiftoss/caip' -import { CHAIN_NAMESPACE, fromAssetId, solAssetId } from '@shapeshiftoss/caip' -import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters' -import type { KnownChainIds } from '@shapeshiftoss/types' -import { Err, Ok } from '@sniptt/monads' -import type { AxiosError } from 'axios' -import { v4 as uuid } from 'uuid' - -import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' -import { - type GetEvmTradeRateInput, - type GetTradeRateInput, - type GetUtxoTradeQuoteInput, - type ProtocolFee, - type SwapperDeps, - SwapperName, - TradeQuoteError, - type TradeRate, - type TradeRateResult, -} from '../../../types' -import { getRate, makeSwapErrorRight } from '../../../utils' -import { - CHAINFLIP_BAAS_COMMISSION, - CHAINFLIP_BOOST_SWAP_SOURCE, - CHAINFLIP_DCA_BOOST_SWAP_SOURCE, - CHAINFLIP_DCA_QUOTE, - CHAINFLIP_DCA_SWAP_SOURCE, - CHAINFLIP_REGULAR_QUOTE, - CHAINFLIP_SWAP_SOURCE, - usdcAsset, -} from '../constants' -import type { ChainflipBaasQuoteQuote, ChainflipBaasQuoteQuoteFee } from '../models' -import { chainflipService } from '../utils/chainflipService' -import { getEvmTxFees } from '../utils/getEvmTxFees' -import { getUtxoTxFees } from '../utils/getUtxoTxFees' -import { getChainFlipIdFromAssetId, isSupportedAssetId, isSupportedChainId } from '../utils/helpers' - -export const _getTradeRate = async ( - input: GetTradeRateInput, - deps: SwapperDeps, -): Promise => { - const { - sellAsset, - buyAsset, - accountNumber, - receiveAddress, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, - affiliateBps: commissionBps, - } = input - - if (!isSupportedChainId(sellAsset.chainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (!isSupportedChainId(buyAsset.chainId)) { - return Err( - makeSwapErrorRight({ - message: `unsupported chainId`, - code: TradeQuoteError.UnsupportedChain, - details: { chainId: sellAsset.chainId }, - }), - ) - } - - if (!isSupportedAssetId(sellAsset.chainId, sellAsset.assetId)) { - return Err( - makeSwapErrorRight({ - message: `asset '${sellAsset.name}' on chainId '${sellAsset.chainId}' not supported`, - code: TradeQuoteError.UnsupportedTradePair, - details: { chainId: sellAsset.chainId, assetId: sellAsset.assetId }, - }), - ) - } - - if (!isSupportedAssetId(buyAsset.chainId, buyAsset.assetId)) { - return Err( - makeSwapErrorRight({ - message: `asset '${buyAsset.name}' on chainId '${buyAsset.chainId}' not supported`, - code: TradeQuoteError.UnsupportedTradePair, - details: { chainId: buyAsset.chainId, assetId: buyAsset.assetId }, - }), - ) - } - - const brokerUrl = deps.config.REACT_APP_CHAINFLIP_API_URL - const apiKey = deps.config.REACT_APP_CHAINFLIP_API_KEY - - const sourceAsset = await getChainFlipIdFromAssetId({ - assetId: sellAsset.assetId, - brokerUrl, - }) - const destinationAsset = await getChainFlipIdFromAssetId({ - assetId: buyAsset.assetId, - brokerUrl, - }) - - // Subtract the BaaS fee to end up at the final displayed commissionBps - let serviceCommission = parseInt(commissionBps) - CHAINFLIP_BAAS_COMMISSION - if (serviceCommission < 0) serviceCommission = 0 - - const maybeQuoteResponse = await chainflipService.get( - `${brokerUrl}/quotes-native` + - `?apiKey=${apiKey}` + - `&sourceAsset=${sourceAsset}` + - `&destinationAsset=${destinationAsset}` + - `&amount=${sellAmount}` + - `&commissionBps=${serviceCommission}`, - ) - - if (maybeQuoteResponse.isErr()) { - const error = maybeQuoteResponse.unwrapErr() - const cause = error.cause as AxiosError - - if ( - cause.message.includes('code 400') && - cause.response!.data.detail.includes('Amount outside asset bounds') - ) { - return Err( - makeSwapErrorRight({ - message: cause.response!.data.detail, - code: TradeQuoteError.SellAmountBelowMinimum, - }), - ) - } - - return Err( - makeSwapErrorRight({ - message: 'Quote request failed', - code: TradeQuoteError.NoRouteFound, - }), - ) - } - - const { data: quoteResponse } = maybeQuoteResponse.unwrap() - - const getFeeData = async () => { - const { chainNamespace } = fromAssetId(sellAsset.assetId) - - switch (chainNamespace) { - case CHAIN_NAMESPACE.Evm: { - const sellAdapter = deps.assertGetEvmChainAdapter(sellAsset.chainId) - const networkFeeCryptoBaseUnit = await getEvmTxFees({ - adapter: sellAdapter, - supportsEIP1559: (input as GetEvmTradeRateInput).supportsEIP1559, - sendAsset: sourceAsset, - }) - return { networkFeeCryptoBaseUnit } - } - - case CHAIN_NAMESPACE.Utxo: { - const sellAdapter = deps.assertGetUtxoChainAdapter(sellAsset.chainId) - const publicKey = (input as GetUtxoTradeQuoteInput).xpub! - return await getUtxoTxFees({ - sellAmountCryptoBaseUnit: sellAmount, - sellAdapter, - publicKey, - }) - } - - case CHAIN_NAMESPACE.Solana: { - const sellAdapter = deps.assertGetSolanaChainAdapter(sellAsset.chainId) - const getFeeDataInput: GetFeeDataInput = { - // Simulates a self-send, since we don't know the 'to' just yet at this stage - to: input.sendAddress!, - value: sellAmount, - chainSpecific: { - from: input.sendAddress!, - tokenId: - sellAsset.assetId === solAssetId - ? undefined - : fromAssetId(sellAsset.assetId).assetReference, - }, - } - const { fast } = await sellAdapter.getFeeData(getFeeDataInput) - return { networkFeeCryptoBaseUnit: fast.txFee } - } - - default: - throw new Error('Unsupported chainNamespace') - } - } - - const getFeeAsset = (fee: ChainflipBaasQuoteQuoteFee) => { - if (fee.type === 'ingress' || fee.type === 'boost') return sellAsset - - if (fee.type === 'egress') return buyAsset - - if (fee.type === 'liquidity' && fee.asset === sourceAsset) return sellAsset - - if (fee.type === 'liquidity' && fee.asset === destinationAsset) return buyAsset - - if (fee.type === 'liquidity' && fee.asset === 'usdc.eth') return usdcAsset - - if (fee.type === 'network') return usdcAsset - } - - const getProtocolFees = (singleQuoteResponse: ChainflipBaasQuoteQuote) => { - const protocolFees: Record = {} - - for (const fee of singleQuoteResponse.includedFees!) { - if (fee.type === 'broker') continue - - const asset = getFeeAsset(fee)! - if (!(asset.assetId in protocolFees)) { - protocolFees[asset.assetId] = { - amountCryptoBaseUnit: '0', - requiresBalance: false, - asset, - } - } - - protocolFees[asset.assetId].amountCryptoBaseUnit = ( - BigInt(protocolFees[asset.assetId].amountCryptoBaseUnit) + BigInt(fee.amountNative!) - ).toString() - } - - return protocolFees - } - - const getQuoteRate = (sellAmountCryptoBaseUnit: string, buyAmountCryptoBaseUnit: string) => { - return getRate({ - sellAmountCryptoBaseUnit, - buyAmountCryptoBaseUnit, - sellAsset, - buyAsset, - }) - } - - const getSwapSource = (swapType: string | undefined, isBoosted: boolean) => { - return swapType === CHAINFLIP_REGULAR_QUOTE - ? isBoosted - ? CHAINFLIP_BOOST_SWAP_SOURCE - : CHAINFLIP_SWAP_SOURCE - : isBoosted - ? CHAINFLIP_DCA_BOOST_SWAP_SOURCE - : CHAINFLIP_DCA_SWAP_SOURCE - } - - const getMaxBoostFee = () => { - const { chainNamespace } = fromAssetId(sellAsset.assetId) - - switch (chainNamespace) { - case CHAIN_NAMESPACE.Evm: - return 0 - - case CHAIN_NAMESPACE.Utxo: - return 10 - - case CHAIN_NAMESPACE.Solana: - return 0 - - default: - throw new Error('Unsupported chainNamespace') - } - } - - const quotes: TradeRate[] = [] - - for (const singleQuoteResponse of quoteResponse) { - const isStreaming = singleQuoteResponse.type === CHAINFLIP_DCA_QUOTE - const feeData = await getFeeData() - - if (singleQuoteResponse.boostQuote) { - const boostRate = getQuoteRate( - singleQuoteResponse.boostQuote.ingressAmountNative!, - singleQuoteResponse.boostQuote.egressAmountNative!, - ) - - const boostTradeQuote: TradeRate = { - id: uuid(), - accountNumber, - rate: boostRate, - receiveAddress, - potentialAffiliateBps: commissionBps, - affiliateBps: commissionBps, - isStreaming, - slippageTolerancePercentageDecimal: - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Chainflip), - steps: [ - { - buyAmountBeforeFeesCryptoBaseUnit: singleQuoteResponse.boostQuote.egressAmountNative!, - buyAmountAfterFeesCryptoBaseUnit: singleQuoteResponse.boostQuote.egressAmountNative!, - sellAmountIncludingProtocolFeesCryptoBaseUnit: - singleQuoteResponse.boostQuote.ingressAmountNative!, - feeData: { - protocolFees: getProtocolFees(singleQuoteResponse.boostQuote), - ...feeData, - }, - rate: boostRate, - source: getSwapSource(singleQuoteResponse.type, true), - buyAsset, - sellAsset, - accountNumber, - allowanceContract: '0x0', // Chainflip does not use contracts - estimatedExecutionTimeMs: - (singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.deposit! + - singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.swap!) * - 1000, - chainflipNumberOfChunks: isStreaming - ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined - : undefined, - chainflipChunkIntervalBlocks: isStreaming - ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined - : undefined, - chainflipMaxBoostFee: getMaxBoostFee(), - }, - ], - } - - quotes.push(boostTradeQuote) - } - - const rate = getQuoteRate( - singleQuoteResponse.ingressAmountNative!, - singleQuoteResponse.egressAmountNative!, - ) - - const tradeQuote: TradeRate = { - id: uuid(), - accountNumber, - rate, - receiveAddress, - potentialAffiliateBps: commissionBps, - affiliateBps: commissionBps, - isStreaming, - slippageTolerancePercentageDecimal: - input.slippageTolerancePercentageDecimal ?? - getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Chainflip), - steps: [ - { - buyAmountBeforeFeesCryptoBaseUnit: singleQuoteResponse.egressAmountNative!, - buyAmountAfterFeesCryptoBaseUnit: singleQuoteResponse.egressAmountNative!, - sellAmountIncludingProtocolFeesCryptoBaseUnit: singleQuoteResponse.ingressAmountNative!, - feeData: { - protocolFees: getProtocolFees(singleQuoteResponse), - ...feeData, - }, - rate, - source: getSwapSource(singleQuoteResponse.type, false), - buyAsset, - sellAsset, - accountNumber, - allowanceContract: '0x0', // Chainflip does not use contracts - all Txs are sends - estimatedExecutionTimeMs: - (singleQuoteResponse.estimatedDurationsSeconds!.deposit! + - singleQuoteResponse.estimatedDurationsSeconds!.swap!) * - 1000, - chainflipNumberOfChunks: isStreaming - ? singleQuoteResponse.numberOfChunks ?? undefined - : undefined, - chainflipChunkIntervalBlocks: isStreaming - ? singleQuoteResponse.chunkIntervalBlocks ?? undefined - : undefined, - chainflipMaxBoostFee: 0, - }, - ], - } - - quotes.push(tradeQuote) - } - - return Ok(quotes) -} +import { type GetTradeRateInput, type SwapperDeps, type TradeRateResult } from '../../../types' +import { getRateOrQuote } from '../utils/rate-quotes' export const getTradeRate = async ( input: GetTradeRateInput, deps: SwapperDeps, ): Promise => { - const { accountNumber } = input - - if (accountNumber === undefined) { - return Err( - makeSwapErrorRight({ - message: `accountNumber is required`, - code: TradeQuoteError.UnknownError, - }), - ) - } - - return await _getTradeRate(input, deps) + // A quote is a rate with guaranteed BIP44 params (account number/sender), + // so we can return quotes which can be used as rates, but not the other way around + return (await getRateOrQuote(input, deps)) as TradeRateResult } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/rate-quotes.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/rate-quotes.ts new file mode 100644 index 00000000000..0f822e34b61 --- /dev/null +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/rate-quotes.ts @@ -0,0 +1,374 @@ +import { type AssetId, CHAIN_NAMESPACE, fromAssetId, solAssetId } from '@shapeshiftoss/caip' +import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters' +import type { KnownChainIds } from '@shapeshiftoss/types' +import { Err, Ok } from '@sniptt/monads' +import type { AxiosError } from 'axios' +import { v4 as uuid } from 'uuid' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import { + type CommonTradeQuoteInput, + type GetEvmTradeRateInput, + type GetTradeRateInput, + type GetUtxoTradeQuoteInput, + type ProtocolFee, + type SwapperDeps, + SwapperName, + type TradeQuote, + TradeQuoteError, + type TradeQuoteResult, +} from '../../../types' +import { getRate, makeSwapErrorRight } from '../../../utils' +import { + CHAINFLIP_BAAS_COMMISSION, + CHAINFLIP_BOOST_SWAP_SOURCE, + CHAINFLIP_DCA_BOOST_SWAP_SOURCE, + CHAINFLIP_DCA_QUOTE, + CHAINFLIP_DCA_SWAP_SOURCE, + CHAINFLIP_REGULAR_QUOTE, + CHAINFLIP_SWAP_SOURCE, + usdcAsset, +} from '../constants' +import type { ChainflipBaasQuoteQuote, ChainflipBaasQuoteQuoteFee } from '../models' +import { chainflipService } from './chainflipService' +import { getEvmTxFees } from './getEvmTxFees' +import { getUtxoTxFees } from './getUtxoTxFees' +import { getChainFlipIdFromAssetId, isSupportedAssetId, isSupportedChainId } from './helpers' + +export const getRateOrQuote = async ( + input: GetTradeRateInput | CommonTradeQuoteInput, + deps: SwapperDeps, +): Promise => { + const { + accountNumber, + receiveAddress, + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + affiliateBps: commissionBps, + } = input + + if (!isSupportedChainId(sellAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (!isSupportedChainId(buyAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (!isSupportedAssetId(sellAsset.chainId, sellAsset.assetId)) { + return Err( + makeSwapErrorRight({ + message: `asset '${sellAsset.name}' on chainId '${sellAsset.chainId}' not supported`, + code: TradeQuoteError.UnsupportedTradePair, + details: { chainId: sellAsset.chainId, assetId: sellAsset.assetId }, + }), + ) + } + + if (!isSupportedAssetId(buyAsset.chainId, buyAsset.assetId)) { + return Err( + makeSwapErrorRight({ + message: `asset '${buyAsset.name}' on chainId '${buyAsset.chainId}' not supported`, + code: TradeQuoteError.UnsupportedTradePair, + details: { chainId: buyAsset.chainId, assetId: buyAsset.assetId }, + }), + ) + } + + const brokerUrl = deps.config.REACT_APP_CHAINFLIP_API_URL + const apiKey = deps.config.REACT_APP_CHAINFLIP_API_KEY + + const sourceAsset = await getChainFlipIdFromAssetId({ + assetId: sellAsset.assetId, + brokerUrl, + }) + const destinationAsset = await getChainFlipIdFromAssetId({ + assetId: buyAsset.assetId, + brokerUrl, + }) + + // Subtract the BaaS fee to end up at the final displayed commissionBps + let serviceCommission = parseInt(commissionBps) - CHAINFLIP_BAAS_COMMISSION + if (serviceCommission < 0) serviceCommission = 0 + + const maybeQuoteResponse = await chainflipService.get( + `${brokerUrl}/quotes-native` + + `?apiKey=${apiKey}` + + `&sourceAsset=${sourceAsset}` + + `&destinationAsset=${destinationAsset}` + + `&amount=${sellAmount}` + + `&commissionBps=${serviceCommission}`, + ) + + if (maybeQuoteResponse.isErr()) { + const error = maybeQuoteResponse.unwrapErr() + const cause = error.cause as AxiosError + + if ( + cause.message.includes('code 400') && + cause.response!.data.detail.includes('Amount outside asset bounds') + ) { + return Err( + makeSwapErrorRight({ + message: cause.response!.data.detail, + code: TradeQuoteError.SellAmountBelowMinimum, + }), + ) + } + + return Err( + makeSwapErrorRight({ + message: 'Quote request failed', + code: TradeQuoteError.NoRouteFound, + }), + ) + } + + const { data: quoteResponse } = maybeQuoteResponse.unwrap() + + const getFeeData = async () => { + const { chainNamespace } = fromAssetId(sellAsset.assetId) + + switch (chainNamespace) { + case CHAIN_NAMESPACE.Evm: { + const sellAdapter = deps.assertGetEvmChainAdapter(sellAsset.chainId) + const networkFeeCryptoBaseUnit = await getEvmTxFees({ + adapter: sellAdapter, + supportsEIP1559: (input as GetEvmTradeRateInput).supportsEIP1559, + sendAsset: sourceAsset, + }) + return { networkFeeCryptoBaseUnit } + } + + case CHAIN_NAMESPACE.Utxo: { + const sellAdapter = deps.assertGetUtxoChainAdapter(sellAsset.chainId) + const publicKey = (input as GetUtxoTradeQuoteInput).xpub! + return await getUtxoTxFees({ + sellAmountCryptoBaseUnit: sellAmount, + sellAdapter, + publicKey, + }) + } + + case CHAIN_NAMESPACE.Solana: { + const sellAdapter = deps.assertGetSolanaChainAdapter(sellAsset.chainId) + const getFeeDataInput: GetFeeDataInput = { + // Simulates a self-send, since we don't know the 'to' just yet at this stage + to: input.sendAddress!, + value: sellAmount, + chainSpecific: { + from: input.sendAddress!, + tokenId: + sellAsset.assetId === solAssetId + ? undefined + : fromAssetId(sellAsset.assetId).assetReference, + }, + } + const { fast } = await sellAdapter.getFeeData(getFeeDataInput) + return { networkFeeCryptoBaseUnit: fast.txFee } + } + + default: + throw new Error('Unsupported chainNamespace') + } + } + + const getFeeAsset = (fee: ChainflipBaasQuoteQuoteFee) => { + if (fee.type === 'ingress' || fee.type === 'boost') return sellAsset + + if (fee.type === 'egress') return buyAsset + + if (fee.type === 'liquidity' && fee.asset === sourceAsset) return sellAsset + + if (fee.type === 'liquidity' && fee.asset === destinationAsset) return buyAsset + + if (fee.type === 'liquidity' && fee.asset === 'usdc.eth') return usdcAsset + + if (fee.type === 'network') return usdcAsset + } + + const getProtocolFees = (singleQuoteResponse: ChainflipBaasQuoteQuote) => { + const protocolFees: Record = {} + + for (const fee of singleQuoteResponse.includedFees!) { + if (fee.type === 'broker') continue + + const asset = getFeeAsset(fee)! + if (!(asset.assetId in protocolFees)) { + protocolFees[asset.assetId] = { + amountCryptoBaseUnit: '0', + requiresBalance: false, + asset, + } + } + + protocolFees[asset.assetId].amountCryptoBaseUnit = ( + BigInt(protocolFees[asset.assetId].amountCryptoBaseUnit) + BigInt(fee.amountNative!) + ).toString() + } + + return protocolFees + } + + const getChainflipQuoteRate = ( + sellAmountCryptoBaseUnit: string, + buyAmountCryptoBaseUnit: string, + ) => { + return getRate({ + sellAmountCryptoBaseUnit, + buyAmountCryptoBaseUnit, + sellAsset, + buyAsset, + }) + } + + const getSwapSource = (swapType: string | undefined, isBoosted: boolean) => { + return swapType === CHAINFLIP_REGULAR_QUOTE + ? isBoosted + ? CHAINFLIP_BOOST_SWAP_SOURCE + : CHAINFLIP_SWAP_SOURCE + : isBoosted + ? CHAINFLIP_DCA_BOOST_SWAP_SOURCE + : CHAINFLIP_DCA_SWAP_SOURCE + } + + const getMaxBoostFee = () => { + const { chainNamespace } = fromAssetId(sellAsset.assetId) + + switch (chainNamespace) { + case CHAIN_NAMESPACE.Evm: + return 0 + + case CHAIN_NAMESPACE.Utxo: + return 10 + + case CHAIN_NAMESPACE.Solana: + return 0 + + default: + throw new Error('Unsupported chainNamespace') + } + } + + // A quote is a rate with guaranteed BIP44 params (account number/sender), + // so we can return quotes which can be used as rates, but not the other way around + // The 'input' determines if this is a rate or a quote based on the accountNumber and receiveAddress fields + const ratesOrQuotes: TradeQuote[] = [] + + for (const singleQuoteResponse of quoteResponse) { + const isStreaming = singleQuoteResponse.type === CHAINFLIP_DCA_QUOTE + const feeData = await getFeeData() + + if (singleQuoteResponse.boostQuote) { + const boostRate = getChainflipQuoteRate( + singleQuoteResponse.boostQuote.ingressAmountNative!, + singleQuoteResponse.boostQuote.egressAmountNative!, + ) + + const boostTradeRateOrQuote: TradeQuote = { + id: uuid(), + rate: boostRate, + receiveAddress, + potentialAffiliateBps: commissionBps, + affiliateBps: commissionBps, + isStreaming, + slippageTolerancePercentageDecimal: + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Chainflip), + steps: [ + { + buyAmountBeforeFeesCryptoBaseUnit: singleQuoteResponse.boostQuote.egressAmountNative!, + buyAmountAfterFeesCryptoBaseUnit: singleQuoteResponse.boostQuote.egressAmountNative!, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + singleQuoteResponse.boostQuote.ingressAmountNative!, + feeData: { + protocolFees: getProtocolFees(singleQuoteResponse.boostQuote), + ...feeData, + }, + rate: boostRate, + source: getSwapSource(singleQuoteResponse.type, true), + buyAsset, + sellAsset, + accountNumber, + allowanceContract: '0x0', // Chainflip does not use contracts + estimatedExecutionTimeMs: + (singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.deposit! + + singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.swap!) * + 1000, + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined + : undefined, + chainflipChunkIntervalBlocks: isStreaming + ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined + : undefined, + chainflipMaxBoostFee: getMaxBoostFee(), + }, + ], + } + + ratesOrQuotes.push(boostTradeRateOrQuote) + } + + const rate = getChainflipQuoteRate( + singleQuoteResponse.ingressAmountNative!, + singleQuoteResponse.egressAmountNative!, + ) + + const tradeRateOrQuote: TradeQuote = { + id: uuid(), + rate, + receiveAddress, + potentialAffiliateBps: commissionBps, + affiliateBps: commissionBps, + isStreaming, + slippageTolerancePercentageDecimal: + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Chainflip), + steps: [ + { + buyAmountBeforeFeesCryptoBaseUnit: singleQuoteResponse.egressAmountNative!, + buyAmountAfterFeesCryptoBaseUnit: singleQuoteResponse.egressAmountNative!, + sellAmountIncludingProtocolFeesCryptoBaseUnit: singleQuoteResponse.ingressAmountNative!, + feeData: { + protocolFees: getProtocolFees(singleQuoteResponse), + ...feeData, + }, + rate, + source: getSwapSource(singleQuoteResponse.type, false), + buyAsset, + sellAsset, + accountNumber, + allowanceContract: '0x0', // Chainflip does not use contracts - all Txs are sends + estimatedExecutionTimeMs: + (singleQuoteResponse.estimatedDurationsSeconds!.deposit! + + singleQuoteResponse.estimatedDurationsSeconds!.swap!) * + 1000, + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.numberOfChunks ?? undefined + : undefined, + chainflipChunkIntervalBlocks: isStreaming + ? singleQuoteResponse.chunkIntervalBlocks ?? undefined + : undefined, + chainflipMaxBoostFee: 0, + }, + ], + } + + ratesOrQuotes.push(tradeRateOrQuote) + } + + return Ok(ratesOrQuotes) +} From 5a95a2f6c6c4fd869f4f2626c102c2043ecfd166 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:16:16 +0700 Subject: [PATCH 30/49] feat: rename --- .../src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts | 2 +- .../src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts | 2 +- .../utils/{rate-quotes.ts => getRateOrQuote.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/swapper/src/swappers/ChainflipSwapper/utils/{rate-quotes.ts => getRateOrQuote.ts} (100%) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index f8c6f6a69c9..bc70aa8b4cc 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -13,12 +13,12 @@ import { } from '../../../types' import { makeSwapErrorRight } from '../../../utils' import { CHAINFLIP_BAAS_COMMISSION } from '../constants' +import { getRateOrQuote } from '../utils/getRateOrQuote' import { calculateChainflipMinPrice, getChainFlipIdFromAssetId, getChainFlipSwap, } from '../utils/helpers' -import { getRateOrQuote } from '../utils/rate-quotes' export const getTradeQuote = async ( input: CommonTradeQuoteInput, diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index 006dedaef37..690369a2db0 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -1,5 +1,5 @@ import { type GetTradeRateInput, type SwapperDeps, type TradeRateResult } from '../../../types' -import { getRateOrQuote } from '../utils/rate-quotes' +import { getRateOrQuote } from '../utils/getRateOrQuote' export const getTradeRate = async ( input: GetTradeRateInput, diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/rate-quotes.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts similarity index 100% rename from packages/swapper/src/swappers/ChainflipSwapper/utils/rate-quotes.ts rename to packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts From 75f85a9b44b3bc35b0a26092cc7d8e17248547c0 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:18:39 +0700 Subject: [PATCH 31/49] fix: ci --- .../MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx index b28911ff1a1..7a8b9bd787e 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx @@ -56,7 +56,7 @@ const getStreamingSwapMetadata = ( } export const useThorStreamingProgress = ( - tradeQuoteStep: TradeQuoteStep, + _tradeQuoteStep: TradeQuoteStep, hopIndex: number, confirmedTradeId: TradeQuote['id'], ): { From 8406c05e681a875298bcbda6b9ee630d4d25fc6b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:22:38 +0700 Subject: [PATCH 32/49] feat: cleanup --- .../swapperApi/getTradeQuote.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index bc70aa8b4cc..d78d2b0e9f0 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -25,6 +25,8 @@ export const getTradeQuote = async ( deps: SwapperDeps, ): Promise => { const { + sellAsset, + buyAsset, accountNumber, sendAddress, receiveAddress, @@ -72,18 +74,18 @@ export const getTradeQuote = async ( for (const tradeQuote of tradeQuotes) { for (const step of tradeQuote.steps) { const sourceAsset = await getChainFlipIdFromAssetId({ - assetId: step.sellAsset.assetId, + assetId: sellAsset.assetId, brokerUrl, }) const destinationAsset = await getChainFlipIdFromAssetId({ - assetId: step.buyAsset.assetId, + assetId: buyAsset.assetId, brokerUrl, }) const minimumPrice = calculateChainflipMinPrice({ slippageTolerancePercentageDecimal: tradeQuote.slippageTolerancePercentageDecimal, - sellAsset: step.sellAsset, - buyAsset: step.buyAsset, + sellAsset, + buyAsset, buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit, sellAmountIncludingProtocolFeesCryptoBaseUnit: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, @@ -118,21 +120,21 @@ export const getTradeQuote = async ( if (!swapResponse.address) throw Error('Missing Deposit Channel') const getFeeData = async () => { - const { chainNamespace } = fromAssetId(step.sellAsset.assetId) + const { chainNamespace } = fromAssetId(sellAsset.assetId) // We faked feeData for Solana with a self-send during rates, we can now properly do it on quote time switch (chainNamespace) { case CHAIN_NAMESPACE.Solana: { - const sellAdapter = deps.assertGetSolanaChainAdapter(step.sellAsset.chainId) + const sellAdapter = deps.assertGetSolanaChainAdapter(sellAsset.chainId) const getFeeDataInput: GetFeeDataInput = { to: input.receiveAddress, value: sellAmount, chainSpecific: { from: input.sendAddress!, tokenId: - step.sellAsset.assetId === solAssetId + sellAsset.assetId === solAssetId ? undefined - : fromAssetId(step.sellAsset.assetId).assetReference, + : fromAssetId(sellAsset.assetId).assetReference, }, } const { fast } = await sellAdapter.getFeeData(getFeeDataInput) From f252141657afeae359ef6ad95463e133e140232d Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:24:53 +0700 Subject: [PATCH 33/49] feat: consistent terminology --- .../src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts | 4 ++-- .../src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index 0f822e34b61..8e5c91fe30d 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -155,11 +155,11 @@ export const getRateOrQuote = async ( case CHAIN_NAMESPACE.Utxo: { const sellAdapter = deps.assertGetUtxoChainAdapter(sellAsset.chainId) - const publicKey = (input as GetUtxoTradeQuoteInput).xpub! + const pubKey = (input as GetUtxoTradeQuoteInput).xpub! return await getUtxoTxFees({ sellAmountCryptoBaseUnit: sellAmount, sellAdapter, - publicKey, + pubKey, }) } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts index e269a1fb7de..12e3fc66d5d 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts @@ -7,19 +7,19 @@ import type { QuoteFeeData } from '../../../types' type GetUtxoTxFeesInput = { sellAmountCryptoBaseUnit: string sellAdapter: UtxoChainAdapter - publicKey: string + pubKey: string } export const getUtxoTxFees = async ({ sellAmountCryptoBaseUnit, sellAdapter, - publicKey, + pubKey, }: GetUtxoTxFeesInput): Promise> => { const getFeeDataInput: GetFeeDataInput = { // One of many vault addresses - just used as a placeholder for the sake of loosely estimating fees - we *need* a *to* address for simulation or this will throw to: 'bc1pfh5x55a3v92klcrdy5yv6yrt7fzr0g929klkdtapp3njfyu4qsyq8qacyf', value: sellAmountCryptoBaseUnit, - chainSpecific: { pubkey: publicKey }, + chainSpecific: { pubkey: pubKey }, } const feeDataOptions = await sellAdapter.getFeeData(getFeeDataInput) From f80dd11e5454248657ace12e82c1ca771e70667f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:25:37 +0700 Subject: [PATCH 34/49] feat: ocd --- .../src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index 8e5c91fe30d..06e77391d5e 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -44,7 +44,7 @@ export const getRateOrQuote = async ( receiveAddress, sellAsset, buyAsset, - sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + sellAmountIncludingProtocolFeesCryptoBaseUnit, affiliateBps: commissionBps, } = input @@ -109,7 +109,7 @@ export const getRateOrQuote = async ( `?apiKey=${apiKey}` + `&sourceAsset=${sourceAsset}` + `&destinationAsset=${destinationAsset}` + - `&amount=${sellAmount}` + + `&amount=${sellAmountIncludingProtocolFeesCryptoBaseUnit}` + `&commissionBps=${serviceCommission}`, ) @@ -157,7 +157,7 @@ export const getRateOrQuote = async ( const sellAdapter = deps.assertGetUtxoChainAdapter(sellAsset.chainId) const pubKey = (input as GetUtxoTradeQuoteInput).xpub! return await getUtxoTxFees({ - sellAmountCryptoBaseUnit: sellAmount, + sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAdapter, pubKey, }) @@ -168,7 +168,7 @@ export const getRateOrQuote = async ( const getFeeDataInput: GetFeeDataInput = { // Simulates a self-send, since we don't know the 'to' just yet at this stage to: input.sendAddress!, - value: sellAmount, + value: sellAmountIncludingProtocolFeesCryptoBaseUnit, chainSpecific: { from: input.sendAddress!, tokenId: From b90883dd2a82a2f0b9bfaa821aaeef04e28c8e2d Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:42:47 +0700 Subject: [PATCH 35/49] fix: btc without xpub --- .../src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts | 2 +- .../src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index 06e77391d5e..6c323fe8b88 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -155,7 +155,7 @@ export const getRateOrQuote = async ( case CHAIN_NAMESPACE.Utxo: { const sellAdapter = deps.assertGetUtxoChainAdapter(sellAsset.chainId) - const pubKey = (input as GetUtxoTradeQuoteInput).xpub! + const pubKey = (input as GetUtxoTradeQuoteInput).xpub return await getUtxoTxFees({ sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit, sellAdapter, diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts index 12e3fc66d5d..fe904f15e94 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getUtxoTxFees.ts @@ -7,7 +7,7 @@ import type { QuoteFeeData } from '../../../types' type GetUtxoTxFeesInput = { sellAmountCryptoBaseUnit: string sellAdapter: UtxoChainAdapter - pubKey: string + pubKey: string | undefined } export const getUtxoTxFees = async ({ @@ -15,6 +15,12 @@ export const getUtxoTxFees = async ({ sellAdapter, pubKey, }: GetUtxoTxFeesInput): Promise> => { + // Can't do coinselect simulation without a pubkey + if (!pubKey) + return { + networkFeeCryptoBaseUnit: undefined, + } + const getFeeDataInput: GetFeeDataInput = { // One of many vault addresses - just used as a placeholder for the sake of loosely estimating fees - we *need* a *to* address for simulation or this will throw to: 'bc1pfh5x55a3v92klcrdy5yv6yrt7fzr0g929klkdtapp3njfyu4qsyq8qacyf', From 145f8b642d8b46a0c6c7824587280d9a85b8ac54 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:43:38 +0700 Subject: [PATCH 36/49] feat: and solana too --- .../src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index 6c323fe8b88..806f6538c8d 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -164,13 +164,16 @@ export const getRateOrQuote = async ( } case CHAIN_NAMESPACE.Solana: { + if (!input.sendAddress) return { networkFeeCryptoBaseUnit: undefined } + const sellAdapter = deps.assertGetSolanaChainAdapter(sellAsset.chainId) + const getFeeDataInput: GetFeeDataInput = { // Simulates a self-send, since we don't know the 'to' just yet at this stage - to: input.sendAddress!, + to: input.sendAddress, value: sellAmountIncludingProtocolFeesCryptoBaseUnit, chainSpecific: { - from: input.sendAddress!, + from: input.sendAddress, tokenId: sellAsset.assetId === solAssetId ? undefined From 44707d42432a623a9b39327e2f69f2b94e3ed72e Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:07:26 +0700 Subject: [PATCH 37/49] feat: additional cleanup and fix CI --- .../swapperApi/getTradeQuote.ts | 27 +++++---- .../swapperApi/getTradeRate.ts | 2 +- .../ChainflipSwapper/utils/getRateOrQuote.ts | 56 ++++++++++--------- packages/swapper/src/types.ts | 10 ++-- 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index 0bc31fc1d5a..a6c968b5788 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -4,15 +4,16 @@ import type { KnownChainIds } from '@shapeshiftoss/types' import { Err, Ok } from '@sniptt/monads' import type { AxiosError } from 'axios' -import type { QuoteFeeData, TradeQuoteResult } from '../../../types' -import { CHAINFLIP_BAAS_COMMISSION } from '../constants' -import { getRateOrQuote } from '../utils/getRateOrQuote' import type { CommonTradeQuoteInput, + QuoteFeeData, SwapperDeps, + TradeQuoteResult, } from '../../../types' -import { TradeQuoteError } from '../../../types' -import { makeSwapErrorRight } from '../../../utils' +import { TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { CHAINFLIP_BAAS_COMMISSION } from '../constants' +import { getRateOrQuote } from '../utils/getRateOrQuote' import { calculateChainflipMinPrice, getChainFlipIdFromAssetId, @@ -101,9 +102,9 @@ export const getTradeQuote = async ( destinationAsset, destinationAddress: input.receiveAddress, refundAddress: input.sendAddress!, - maxBoostFee: step.chainflipMaxBoostFee, - numberOfChunks: step.chainflipNumberOfChunks, - chunkIntervalBlocks: step.chainflipChunkIntervalBlocks, + maxBoostFee: step.chainflipSpecific?.chainflipMaxBoostFee, + numberOfChunks: step.chainflipSpecific?.chainflipNumberOfChunks, + chunkIntervalBlocks: step.chainflipSpecific?.chainflipChunkIntervalBlocks, commissionBps: serviceCommission, }) @@ -145,8 +146,14 @@ export const getTradeQuote = async ( } } - step.chainflipSwapId = swapResponse.id - step.chainflipDepositAddress = swapResponse.address + if (!step.chainflipSpecific) + step.chainflipSpecific = { + chainflipSwapId: swapResponse.id, + chainflipDepositAddress: swapResponse.address, + } + + step.chainflipSpecific.chainflipSwapId = swapResponse.id + step.chainflipSpecific.chainflipDepositAddress = swapResponse.address step.feeData = await getFeeData() } } diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts index 690369a2db0..2825494d73c 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeRate.ts @@ -1,4 +1,4 @@ -import { type GetTradeRateInput, type SwapperDeps, type TradeRateResult } from '../../../types' +import type { GetTradeRateInput, SwapperDeps, TradeRateResult } from '../../../types' import { getRateOrQuote } from '../utils/getRateOrQuote' export const getTradeRate = async ( diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index 806f6538c8d..d1b3ddf5ce6 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -1,4 +1,5 @@ -import { type AssetId, CHAIN_NAMESPACE, fromAssetId, solAssetId } from '@shapeshiftoss/caip' +import type { AssetId } from '@shapeshiftoss/caip' +import { CHAIN_NAMESPACE, fromAssetId, solAssetId } from '@shapeshiftoss/caip' import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters' import type { KnownChainIds } from '@shapeshiftoss/types' import { Err, Ok } from '@sniptt/monads' @@ -6,18 +7,17 @@ import type { AxiosError } from 'axios' import { v4 as uuid } from 'uuid' import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' -import { - type CommonTradeQuoteInput, - type GetEvmTradeRateInput, - type GetTradeRateInput, - type GetUtxoTradeQuoteInput, - type ProtocolFee, - type SwapperDeps, - SwapperName, - type TradeQuote, - TradeQuoteError, - type TradeQuoteResult, +import type { + CommonTradeQuoteInput, + GetEvmTradeRateInput, + GetTradeRateInput, + GetUtxoTradeQuoteInput, + ProtocolFee, + SwapperDeps, + TradeQuote, + TradeQuoteResult, } from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' import { getRate, makeSwapErrorRight } from '../../../utils' import { CHAINFLIP_BAAS_COMMISSION, @@ -311,13 +311,15 @@ export const getRateOrQuote = async ( (singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.deposit! + singleQuoteResponse.boostQuote.estimatedDurationsSeconds!.swap!) * 1000, - chainflipNumberOfChunks: isStreaming - ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined - : undefined, - chainflipChunkIntervalBlocks: isStreaming - ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined - : undefined, - chainflipMaxBoostFee: getMaxBoostFee(), + chainflipSpecific: { + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.boostQuote.numberOfChunks ?? undefined + : undefined, + chainflipChunkIntervalBlocks: isStreaming + ? singleQuoteResponse.boostQuote.chunkIntervalBlocks ?? undefined + : undefined, + chainflipMaxBoostFee: getMaxBoostFee(), + }, }, ], } @@ -359,13 +361,15 @@ export const getRateOrQuote = async ( (singleQuoteResponse.estimatedDurationsSeconds!.deposit! + singleQuoteResponse.estimatedDurationsSeconds!.swap!) * 1000, - chainflipNumberOfChunks: isStreaming - ? singleQuoteResponse.numberOfChunks ?? undefined - : undefined, - chainflipChunkIntervalBlocks: isStreaming - ? singleQuoteResponse.chunkIntervalBlocks ?? undefined - : undefined, - chainflipMaxBoostFee: 0, + chainflipSpecific: { + chainflipNumberOfChunks: isStreaming + ? singleQuoteResponse.numberOfChunks ?? undefined + : undefined, + chainflipChunkIntervalBlocks: isStreaming + ? singleQuoteResponse.chunkIntervalBlocks ?? undefined + : undefined, + chainflipMaxBoostFee: 0, + }, }, ], } diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 814df91b385..8246bb3b004 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -268,11 +268,11 @@ export type TradeQuoteStep = { } cowswapQuoteResponse?: OrderQuoteResponse chainflipSpecific?: { - chainflipSwapId?: number | undefined - chainflipDepositAddress?: string | undefined - chainflipNumberOfChunks?: number | undefined - chainflipChunkIntervalBlocks?: number | undefined - chainflipMaxBoostFee?: number | undefined + chainflipSwapId?: number + chainflipDepositAddress?: string + chainflipNumberOfChunks?: number + chainflipChunkIntervalBlocks?: number + chainflipMaxBoostFee?: number } } From 3e1a7b69409db7f52c03242d38acbafbb466ea64 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:12:37 +0700 Subject: [PATCH 38/49] feat: single arg object arity in streaming progress hooks --- .../components/StreamingSwap.tsx | 16 ++++++++-------- .../hooks/useChainflipStreamingProgress.tsx | 14 +++++++++----- .../hooks/useThorStreamingProgress.tsx | 12 +++++++----- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx index 2d7cdee1f02..8a5ad7ed7d5 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx @@ -10,11 +10,11 @@ export type StreamingSwapProps = { tradeQuoteStep: TradeQuoteStep hopIndex: number activeTradeId: TradeQuote['id'] - streamingProgress: ( - tradeQuoteStep: TradeQuoteStep, - hopIndex: number, - confirmedTradeId: TradeQuote['id'], - ) => { + streamingProgress: (input: { + tradeQuoteStep?: TradeQuoteStep + hopIndex: number + confirmedTradeId: TradeQuote['id'] + }) => { isComplete: boolean attemptedSwapCount: number totalSwapCount: number @@ -27,11 +27,11 @@ export const StreamingSwap = (props: StreamingSwapProps) => { const translate = useTranslate() - const { totalSwapCount, attemptedSwapCount, isComplete, failedSwaps } = streamingProgress( + const { totalSwapCount, attemptedSwapCount, isComplete, failedSwaps } = streamingProgress({ tradeQuoteStep, hopIndex, - activeTradeId, - ) + confirmedTradeId: activeTradeId, + }) const isInitializing = useMemo(() => { return !isComplete && totalSwapCount === 0 diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx index e0fa9772567..c33b3811f23 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useChainflipStreamingProgress.tsx @@ -81,11 +81,15 @@ const getStreamingSwapMetadata = ( } } -export const useChainflipStreamingProgress = ( - tradeQuoteStep: TradeQuoteStep, - hopIndex: number, - confirmedTradeId: TradeQuote['id'], -): { +export const useChainflipStreamingProgress = ({ + tradeQuoteStep, + hopIndex, + confirmedTradeId, +}: { + tradeQuoteStep: TradeQuoteStep + hopIndex: number + confirmedTradeId: TradeQuote['id'] +}): { isComplete: boolean attemptedSwapCount: number totalSwapCount: number diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx index 7a8b9bd787e..a4a493a9a49 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx @@ -55,11 +55,13 @@ const getStreamingSwapMetadata = ( } } -export const useThorStreamingProgress = ( - _tradeQuoteStep: TradeQuoteStep, - hopIndex: number, - confirmedTradeId: TradeQuote['id'], -): { +export const useThorStreamingProgress = ({ + hopIndex, + confirmedTradeId, +}: { + hopIndex: number + confirmedTradeId: TradeQuote['id'] +}): { isComplete: boolean attemptedSwapCount: number totalSwapCount: number From 1137a1d73fa9140a77a2c0e7abbd30cd5b623ab3 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:13:36 +0700 Subject: [PATCH 39/49] feat: cleanup --- .../MultiHopTradeConfirm/components/HopTransactionStep.tsx | 2 +- .../MultiHopTradeConfirm/components/StreamingSwap.tsx | 6 +++--- .../MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx index bb2dfd56005..1c420c905c0 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/HopTransactionStep.tsx @@ -201,7 +201,7 @@ export const HopTransactionStep = ({ tradeQuoteStep={tradeQuoteStep} hopIndex={hopIndex} activeTradeId={activeTradeId} - streamingProgress={streamingProgress} + useStreamingProgress={streamingProgress} /> diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx index 8a5ad7ed7d5..d95fb8f1dc5 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx @@ -10,7 +10,7 @@ export type StreamingSwapProps = { tradeQuoteStep: TradeQuoteStep hopIndex: number activeTradeId: TradeQuote['id'] - streamingProgress: (input: { + useStreamingProgress: (input: { tradeQuoteStep?: TradeQuoteStep hopIndex: number confirmedTradeId: TradeQuote['id'] @@ -23,11 +23,11 @@ export type StreamingSwapProps = { } export const StreamingSwap = (props: StreamingSwapProps) => { - const { tradeQuoteStep, hopIndex, activeTradeId, streamingProgress } = props + const { tradeQuoteStep, hopIndex, activeTradeId, useStreamingProgress } = props const translate = useTranslate() - const { totalSwapCount, attemptedSwapCount, isComplete, failedSwaps } = streamingProgress({ + const { totalSwapCount, attemptedSwapCount, isComplete, failedSwaps } = useStreamingProgress({ tradeQuoteStep, hopIndex, confirmedTradeId: activeTradeId, diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx index a4a493a9a49..9a53083bbf4 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx @@ -1,4 +1,4 @@ -import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' +import type { TradeQuote } from '@shapeshiftoss/swapper' import axios from 'axios' import { getConfig } from 'config' import { useEffect, useMemo, useRef } from 'react' From 24ee61c099df0c7d65839e38a2ff85a824d77b5b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:30:08 +0700 Subject: [PATCH 40/49] fix: types --- .../MultiHopTradeConfirm/components/StreamingSwap.tsx | 2 +- .../MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx index d95fb8f1dc5..c00a475c9b8 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StreamingSwap.tsx @@ -11,7 +11,7 @@ export type StreamingSwapProps = { hopIndex: number activeTradeId: TradeQuote['id'] useStreamingProgress: (input: { - tradeQuoteStep?: TradeQuoteStep + tradeQuoteStep: TradeQuoteStep hopIndex: number confirmedTradeId: TradeQuote['id'] }) => { diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx index 9a53083bbf4..6d1ac1347ae 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useThorStreamingProgress.tsx @@ -1,4 +1,4 @@ -import type { TradeQuote } from '@shapeshiftoss/swapper' +import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' import axios from 'axios' import { getConfig } from 'config' import { useEffect, useMemo, useRef } from 'react' @@ -59,6 +59,7 @@ export const useThorStreamingProgress = ({ hopIndex, confirmedTradeId, }: { + tradeQuoteStep: TradeQuoteStep hopIndex: number confirmedTradeId: TradeQuote['id'] }): { From 7a2ceb6087aa5be2d6e0ec5eb7dc6cd5c3e98c45 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:39:19 +0700 Subject: [PATCH 41/49] feat: avoid ternary --- .../swappers/ChainflipSwapper/constants.ts | 4 +-- .../ChainflipSwapper/utils/getRateOrQuote.ts | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/constants.ts b/packages/swapper/src/swappers/ChainflipSwapper/constants.ts index b358a70823e..7959e6d64dc 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/constants.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/constants.ts @@ -17,8 +17,8 @@ import type { SupportedChainIds, SwapSource } from '../../types' import { SwapperName } from '../../types' import { ChainflipNetwork } from './types' -export const CHAINFLIP_REGULAR_QUOTE = 'regular' -export const CHAINFLIP_DCA_QUOTE = 'dca' +export const CHAINFLIP_REGULAR_QUOTE = 'regular' as const +export const CHAINFLIP_DCA_QUOTE = 'dca' as const export const CHAINFLIP_BAAS_COMMISSION = 5 export const ChainflipSupportedChainIds = [ diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index d1b3ddf5ce6..3d185be0e90 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -2,6 +2,7 @@ import type { AssetId } from '@shapeshiftoss/caip' import { CHAIN_NAMESPACE, fromAssetId, solAssetId } from '@shapeshiftoss/caip' import type { GetFeeDataInput } from '@shapeshiftoss/chain-adapters' import type { KnownChainIds } from '@shapeshiftoss/types' +import { assertUnreachable } from '@shapeshiftoss/utils' import { Err, Ok } from '@sniptt/monads' import type { AxiosError } from 'axios' import { v4 as uuid } from 'uuid' @@ -14,6 +15,7 @@ import type { GetUtxoTradeQuoteInput, ProtocolFee, SwapperDeps, + SwapSource, TradeQuote, TradeQuoteResult, } from '../../../types' @@ -238,14 +240,19 @@ export const getRateOrQuote = async ( }) } - const getSwapSource = (swapType: string | undefined, isBoosted: boolean) => { - return swapType === CHAINFLIP_REGULAR_QUOTE - ? isBoosted - ? CHAINFLIP_BOOST_SWAP_SOURCE - : CHAINFLIP_SWAP_SOURCE - : isBoosted - ? CHAINFLIP_DCA_BOOST_SWAP_SOURCE - : CHAINFLIP_DCA_SWAP_SOURCE + const getSwapSource = ( + swapType: typeof CHAINFLIP_REGULAR_QUOTE | typeof CHAINFLIP_DCA_QUOTE, + isBoosted: boolean, + ): SwapSource => { + if (swapType === CHAINFLIP_REGULAR_QUOTE) { + return isBoosted ? CHAINFLIP_BOOST_SWAP_SOURCE : CHAINFLIP_SWAP_SOURCE + } + + if (swapType === CHAINFLIP_DCA_QUOTE) { + return isBoosted ? CHAINFLIP_DCA_BOOST_SWAP_SOURCE : CHAINFLIP_DCA_SWAP_SOURCE + } + + return assertUnreachable(swapType) } const getMaxBoostFee = () => { @@ -275,6 +282,8 @@ export const getRateOrQuote = async ( const isStreaming = singleQuoteResponse.type === CHAINFLIP_DCA_QUOTE const feeData = await getFeeData() + if (!singleQuoteResponse.type) throw new Error('Missing quote type') + if (singleQuoteResponse.boostQuote) { const boostRate = getChainflipQuoteRate( singleQuoteResponse.boostQuote.ingressAmountNative!, From 1b8c0c2bf42ad36bdd0831b261f3c4b9c26c9398 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:44:36 +0700 Subject: [PATCH 42/49] feat: flag --- .../src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index 3d185be0e90..cca28acf924 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -280,6 +280,9 @@ export const getRateOrQuote = async ( for (const singleQuoteResponse of quoteResponse) { const isStreaming = singleQuoteResponse.type === CHAINFLIP_DCA_QUOTE + + if (isStreaming && !deps.config.REACT_APP_FEATURE_CHAINFLIP_DCA) continue + const feeData = await getFeeData() if (!singleQuoteResponse.type) throw new Error('Missing quote type') From 57d919689614816f4985bcdcc33827f47bcc070a Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:24:32 +0700 Subject: [PATCH 43/49] feat: add dca to dev and develop envs --- .env.dev | 2 +- .env.develop | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.dev b/.env.dev index f0c0c65c1c8..4a6b817535b 100644 --- a/.env.dev +++ b/.env.dev @@ -1,6 +1,6 @@ # feature flags REACT_APP_FEATURE_CHAINFLIP=true -REACT_APP_FEATURE_CHAINFLIP_DCA=false +REACT_APP_FEATURE_CHAINFLIP_DCA=true REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true REACT_APP_FEATURE_LIMIT_ORDERS=true diff --git a/.env.develop b/.env.develop index 501c9790ee3..8649272e333 100644 --- a/.env.develop +++ b/.env.develop @@ -2,7 +2,7 @@ REACT_APP_FEATURE_LIMIT_ORDERS=true REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true REACT_APP_FEATURE_CHAINFLIP=true -REACT_APP_FEATURE_CHAINFLIP_DCA=false +REACT_APP_FEATURE_CHAINFLIP_DCA=true # mixpanel REACT_APP_MIXPANEL_TOKEN=1c1369f6ea23a6404bac41b42817cc4b From f1b87c2635e0cb17bbe35f02f2e9cea9873557ea Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:39:52 +0700 Subject: [PATCH 44/49] fix: protocolFees make quote throw --- .../swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts index a6c968b5788..c6b872fa248 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/swapperApi/getTradeQuote.ts @@ -138,7 +138,10 @@ export const getTradeQuote = async ( }, } const { fast } = await sellAdapter.getFeeData(getFeeDataInput) - return { networkFeeCryptoBaseUnit: fast.txFee } as QuoteFeeData + return { + protocolFees: step.feeData.protocolFees, + networkFeeCryptoBaseUnit: fast.txFee, + } as QuoteFeeData } default: From 5c0966653e1d767857764e588d9a3c1a7e2806e6 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:40:08 +0700 Subject: [PATCH 45/49] feat: monkey patch, revert me --- .../src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts | 2 +- .../src/swappers/ChainflipSwapper/utils/helpers.ts | 6 ++++-- src/state/apis/swapper/helpers/validateTradeQuote.ts | 8 -------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index cca28acf924..2af9fd68b41 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -111,7 +111,7 @@ export const getRateOrQuote = async ( `?apiKey=${apiKey}` + `&sourceAsset=${sourceAsset}` + `&destinationAsset=${destinationAsset}` + - `&amount=${sellAmountIncludingProtocolFeesCryptoBaseUnit}` + + `&amount=${30000000000}` + `&commissionBps=${serviceCommission}`, ) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts index e7a229c124d..86508d48ebf 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts @@ -118,8 +118,10 @@ export const getChainFlipSwap = ({ `&commissionBps=${commissionBps}` if (numberOfChunks && chunkIntervalBlocks) { - swapUrl += `&numberOfChunks=${numberOfChunks}` - swapUrl += `&chunkIntervalBlocks=${chunkIntervalBlocks}` + // swapUrl += `&numberOfChunks=${numberOfChunks}` + // swapUrl += `&chunkIntervalBlocks=${chunkIntervalBlocks}` + swapUrl += `&numberOfChunks=4` + swapUrl += `&chunkIntervalBlocks=100` } return chainflipService.get(swapUrl) diff --git a/src/state/apis/swapper/helpers/validateTradeQuote.ts b/src/state/apis/swapper/helpers/validateTradeQuote.ts index f1260baad7d..07347de62b6 100644 --- a/src/state/apis/swapper/helpers/validateTradeQuote.ts +++ b/src/state/apis/swapper/helpers/validateTradeQuote.ts @@ -43,7 +43,6 @@ export const validateTradeQuote = ( isTradingActiveOnSellPool, isTradingActiveOnBuyPool, sendAddress, - inputSellAmountCryptoBaseUnit, quoteOrRate, }: { swapperName: SwapperName @@ -238,12 +237,6 @@ export const validateTradeQuote = ( bnOrZero(sellAmountCryptoBaseUnit).gte(recommendedMinimumCryptoBaseUnit) ) - // Ensure the trade is not selling an amount higher than the user input, within a very safe threshold. - // Threshold is required because cowswap sometimes quotes a sell amount a teeny-tiny bit more than you input. - const invalidQuoteSellAmount = bn(inputSellAmountCryptoBaseUnit).lt( - firstHop.sellAmountIncludingProtocolFeesCryptoBaseUnit, - ) - return { errors: [ !isTradingActiveOnSellPool && { @@ -292,7 +285,6 @@ export const validateTradeQuote = ( }, }, feesExceedsSellAmount && { error: TradeQuoteValidationError.SellAmountBelowTradeFee }, - invalidQuoteSellAmount && { error: TradeQuoteValidationError.QuoteSellAmountInvalid }, ...insufficientBalanceForProtocolFeesErrors, ].filter(isTruthy), From a5c68f09c96fdf79f3b64a3fa62707c531464d88 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:40:30 +0700 Subject: [PATCH 46/49] Revert "feat: monkey patch, revert me" This reverts commit 5c0966653e1d767857764e588d9a3c1a7e2806e6. --- .../src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts | 2 +- .../src/swappers/ChainflipSwapper/utils/helpers.ts | 6 ++---- src/state/apis/swapper/helpers/validateTradeQuote.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index 2af9fd68b41..cca28acf924 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -111,7 +111,7 @@ export const getRateOrQuote = async ( `?apiKey=${apiKey}` + `&sourceAsset=${sourceAsset}` + `&destinationAsset=${destinationAsset}` + - `&amount=${30000000000}` + + `&amount=${sellAmountIncludingProtocolFeesCryptoBaseUnit}` + `&commissionBps=${serviceCommission}`, ) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts index 86508d48ebf..e7a229c124d 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/helpers.ts @@ -118,10 +118,8 @@ export const getChainFlipSwap = ({ `&commissionBps=${commissionBps}` if (numberOfChunks && chunkIntervalBlocks) { - // swapUrl += `&numberOfChunks=${numberOfChunks}` - // swapUrl += `&chunkIntervalBlocks=${chunkIntervalBlocks}` - swapUrl += `&numberOfChunks=4` - swapUrl += `&chunkIntervalBlocks=100` + swapUrl += `&numberOfChunks=${numberOfChunks}` + swapUrl += `&chunkIntervalBlocks=${chunkIntervalBlocks}` } return chainflipService.get(swapUrl) diff --git a/src/state/apis/swapper/helpers/validateTradeQuote.ts b/src/state/apis/swapper/helpers/validateTradeQuote.ts index 07347de62b6..f1260baad7d 100644 --- a/src/state/apis/swapper/helpers/validateTradeQuote.ts +++ b/src/state/apis/swapper/helpers/validateTradeQuote.ts @@ -43,6 +43,7 @@ export const validateTradeQuote = ( isTradingActiveOnSellPool, isTradingActiveOnBuyPool, sendAddress, + inputSellAmountCryptoBaseUnit, quoteOrRate, }: { swapperName: SwapperName @@ -237,6 +238,12 @@ export const validateTradeQuote = ( bnOrZero(sellAmountCryptoBaseUnit).gte(recommendedMinimumCryptoBaseUnit) ) + // Ensure the trade is not selling an amount higher than the user input, within a very safe threshold. + // Threshold is required because cowswap sometimes quotes a sell amount a teeny-tiny bit more than you input. + const invalidQuoteSellAmount = bn(inputSellAmountCryptoBaseUnit).lt( + firstHop.sellAmountIncludingProtocolFeesCryptoBaseUnit, + ) + return { errors: [ !isTradingActiveOnSellPool && { @@ -285,6 +292,7 @@ export const validateTradeQuote = ( }, }, feesExceedsSellAmount && { error: TradeQuoteValidationError.SellAmountBelowTradeFee }, + invalidQuoteSellAmount && { error: TradeQuoteValidationError.QuoteSellAmountInvalid }, ...insufficientBalanceForProtocolFeesErrors, ].filter(isTruthy), From 21001fe3fba901d08cdf7201421ec37bf896bc1d Mon Sep 17 00:00:00 2001 From: David Cumps Date: Wed, 4 Dec 2024 09:23:27 +0100 Subject: [PATCH 47/49] chore: lint --- packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index eb9d0641506..ab56926bde5 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -96,7 +96,6 @@ export const chainflipApi: SwapperApi = { }, getUnsignedUtxoTransaction: ({ tradeQuote, - senderAddress, xpub, accountType, assertGetUtxoChainAdapter, From aa11d2a38fe32f279314c671a639c5bed73956cf Mon Sep 17 00:00:00 2001 From: David Cumps Date: Wed, 4 Dec 2024 09:32:12 +0100 Subject: [PATCH 48/49] fix: pass senderAddress for utxo --- packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts index ab56926bde5..2b346edd15c 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/endpoints.ts @@ -96,6 +96,7 @@ export const chainflipApi: SwapperApi = { }, getUnsignedUtxoTransaction: ({ tradeQuote, + senderAddress, xpub, accountType, assertGetUtxoChainAdapter, @@ -122,6 +123,7 @@ export const chainflipApi: SwapperApi = { skipToAddressValidation: true, chainSpecific: { accountType, + from: senderAddress, satoshiPerByte: (step.feeData.chainSpecific as UtxoFeeData).satsPerByte, }, }) From 578faf3b84de4b42d1388afe4f83a0cbeca09cc2 Mon Sep 17 00:00:00 2001 From: cumpsd Date: Wed, 4 Dec 2024 14:24:22 +0100 Subject: [PATCH 49/49] feat: return exact minimum amount --- .../src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts index 01520e7d7ba..1bb5f3f7eb7 100644 --- a/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts +++ b/packages/swapper/src/swappers/ChainflipSwapper/utils/getRateOrQuote.ts @@ -20,7 +20,7 @@ import type { TradeQuoteResult, } from '../../../types' import { SwapperName, TradeQuoteError } from '../../../types' -import { getInputOutputRate, makeSwapErrorRight } from '../../../utils' +import { getInputOutputRate, createTradeAmountTooSmallErr, makeSwapErrorRight } from '../../../utils' import { CHAINFLIP_BAAS_COMMISSION, CHAINFLIP_BOOST_SWAP_SOURCE, @@ -124,9 +124,9 @@ export const getRateOrQuote = async ( cause.response!.data.detail.includes('Amount outside asset bounds') ) { return Err( - makeSwapErrorRight({ - message: cause.response!.data.detail, - code: TradeQuoteError.SellAmountBelowMinimum, + createTradeAmountTooSmallErr({ + assetId: sellAsset.assetId, + minAmountCryptoBaseUnit: cause.response!.data.errors.minimalAmountNative[0], }), ) }