diff --git a/src/analytics/Properties.tsx b/src/analytics/Properties.tsx index 743c55c3f70..cf9c4aec4a2 100644 --- a/src/analytics/Properties.tsx +++ b/src/analytics/Properties.tsx @@ -76,7 +76,7 @@ import { AdventureCardName } from 'src/onboarding/types' import { PointsActivityId } from 'src/points/types' import { RecipientType } from 'src/recipients/recipient' import { AmountEnteredIn, QrCode } from 'src/send/types' -import { Field } from 'src/swap/types' +import { Field, SwapType } from 'src/swap/types' import { TokenActionName } from 'src/tokens/types' import { NetworkId, TokenTransactionTypeV2, TransactionStatus } from 'src/transactions/types' @@ -1179,6 +1179,7 @@ type SwapQuoteEvent = SwapEvent & { price: string appFeePercentageIncludedInPrice: string | null | undefined provider: string + swapType: SwapType } export interface SwapTimeMetrics { diff --git a/src/swap/SwapScreen.test.tsx b/src/swap/SwapScreen.test.tsx index 369897a9ef8..7f3a7cd6535 100644 --- a/src/swap/SwapScreen.test.tsx +++ b/src/swap/SwapScreen.test.tsx @@ -1035,6 +1035,7 @@ describe('SwapScreen', () => { provider: defaultQuote.details.swapProvider, estimatedPriceImpact: defaultQuote.unvalidatedSwapTransaction.estimatedPriceImpact, allowanceTarget: defaultQuote.unvalidatedSwapTransaction.allowanceTarget, + swapType: 'same-chain', }, userInput: { toTokenId: mockCusdTokenId, @@ -1088,6 +1089,7 @@ describe('SwapScreen', () => { provider: defaultQuote.details.swapProvider, estimatedPriceImpact: defaultQuote.unvalidatedSwapTransaction.estimatedPriceImpact, allowanceTarget: defaultQuote.unvalidatedSwapTransaction.allowanceTarget, + swapType: 'same-chain', }, userInput: { toTokenId: mockCeloTokenId, @@ -1135,6 +1137,7 @@ describe('SwapScreen', () => { provider: defaultQuote.details.swapProvider, estimatedPriceImpact: defaultQuote.unvalidatedSwapTransaction.estimatedPriceImpact, allowanceTarget: defaultQuote.unvalidatedSwapTransaction.allowanceTarget, + swapType: 'same-chain', }, userInput: { toTokenId: mockCusdTokenId, @@ -1192,6 +1195,7 @@ describe('SwapScreen', () => { feeCurrency: undefined, feeCurrencySymbol: 'CELO', txCount: 2, + swapType: 'same-chain', }) }) diff --git a/src/swap/SwapScreen.tsx b/src/swap/SwapScreen.tsx index 8924383ac49..4ca3d60a84a 100644 --- a/src/swap/SwapScreen.tsx +++ b/src/swap/SwapScreen.tsx @@ -411,6 +411,7 @@ export function SwapScreen({ route }: Props) { price, appFeePercentageIncludedInPrice, provider: quote.provider, + swapType: quote.swapType, web3Library: 'viem', ...getSwapTxsAnalyticsProperties( quote.preparedTransactions.transactions, @@ -434,6 +435,7 @@ export function SwapScreen({ route }: Props) { provider: quote.provider, estimatedPriceImpact, allowanceTarget, + swapType: quote.swapType, }, userInput, areSwapTokensShuffled, diff --git a/src/swap/saga.test.ts b/src/swap/saga.test.ts index c5b98d60084..5f10c6dfe94 100644 --- a/src/swap/saga.test.ts +++ b/src/swap/saga.test.ts @@ -102,6 +102,7 @@ const mockSwapFromParams = (toTokenId: string, feeCurrency?: Address): PayloadAc estimatedPriceImpact: '0.1', allowanceTarget: mockAllowanceTarget, receivedAt: mockQuoteReceivedTimestamp, + swapType: 'same-chain', }, areSwapTokensShuffled: false, }, @@ -151,6 +152,7 @@ const mockSwapEthereum: PayloadAction = { estimatedPriceImpact: '0.1', allowanceTarget: mockAllowanceTarget, receivedAt: mockQuoteReceivedTimestamp, + swapType: 'same-chain', }, areSwapTokensShuffled: false, }, @@ -187,6 +189,20 @@ const mockSwapWithWBTCBuyToken: PayloadAction = { }, }, } +const mockCrossChainSwap: PayloadAction = { + ...mockSwap, + payload: { + ...mockSwap.payload, + quote: { + ...mockSwap.payload.quote, + swapType: 'cross-chain', + }, + userInput: { + ...mockSwap.payload.userInput, + toTokenId: mockEthTokenId, + }, + }, +} const store = createMockStore({ tokens: { @@ -491,6 +507,7 @@ describe(swapSubmitSaga, () => { swapTxGasFeeUsd: 0.000929185, swapTxHash: '0x2', areSwapTokensShuffled: false, + swapType: 'same-chain', }) const analyticsProps = (ValoraAnalytics.track as jest.Mock).mock.calls[0][1] @@ -618,6 +635,51 @@ describe(swapSubmitSaga, () => { .run() }) + it('should display the correct standby values for a cross chain swap', async () => { + await expectSaga(swapSubmitSaga, mockCrossChainSwap) + .withState(store.getState()) + .provide(createDefaultProviders(Network.Celo)) + .put( + addStandbyTransaction({ + context: { + id: 'id-swap/saga-Swap/Approve', + tag: 'swap/saga', + description: 'Swap/Approve', + }, + __typename: 'TokenApproval', + networkId: NetworkId['celo-alfajores'], + type: TokenTransactionTypeV2.Approval, + transactionHash: mockApproveTxReceipt.transactionHash, + tokenId: mockCeurTokenId, + approvedAmount: '1', + feeCurrencyId: mockCeloTokenId, + }) + ) + .put( + addStandbyTransaction({ + context: { + id: 'id-swap/saga-Swap/Execute', + tag: 'swap/saga', + description: 'Swap/Execute', + }, + __typename: 'CrossChainTokenExchange', + networkId: NetworkId['celo-alfajores'], + type: TokenTransactionTypeV2.CrossChainSwapTransaction, + inAmount: { + value: mockSwap.payload.userInput.swapAmount[Field.TO], + tokenId: mockEthTokenId, + }, + outAmount: { + value: mockSwap.payload.userInput.swapAmount[Field.FROM], + tokenId: mockCeurTokenId, + }, + transactionHash: mockSwapTxReceipt.transactionHash, + feeCurrencyId: mockCeloTokenId, + }) + ) + .run() + }) + it('should track correctly the imported tokens', async () => { jest .spyOn(Date, 'now') @@ -731,6 +793,7 @@ describe(swapSubmitSaga, () => { swapTxGasFeeUsd: undefined, swapTxHash: undefined, areSwapTokensShuffled: false, + swapType: 'same-chain', }) const analyticsProps = (ValoraAnalytics.track as jest.Mock).mock.calls[0][1] expect(analyticsProps.gas).toBeCloseTo( diff --git a/src/swap/saga.ts b/src/swap/saga.ts index 9ec25e8c9d0..42ecf211c70 100644 --- a/src/swap/saga.ts +++ b/src/swap/saga.ts @@ -88,6 +88,7 @@ export function* swapSubmitSaga(action: PayloadAction) { estimatedPriceImpact, preparedTransactions: serializablePreparedTransactions, receivedAt: quoteReceivedAt, + swapType, } = quote const amountType = updatedField === Field.TO ? ('buyAmount' as const) : ('sellAmount' as const) const amount = swapAmount[updatedField] @@ -147,6 +148,7 @@ export function* swapSubmitSaga(action: PayloadAction) { web3Library: 'viem' as const, areSwapTokensShuffled, ...getSwapTxsAnalyticsProperties(preparedTransactions, fromToken.networkId, tokensById), + swapType, } let quoteToTransactionElapsedTimeInMs: number | undefined @@ -225,9 +227,12 @@ export function* swapSubmitSaga(action: PayloadAction) { feeCurrencyId?: string ): BaseStandbyTransaction => ({ context: swapExecuteContext, - __typename: 'TokenExchangeV3', + __typename: swapType === 'same-chain' ? 'TokenExchangeV3' : 'CrossChainTokenExchange', networkId, - type: TokenTransactionTypeV2.SwapTransaction, + type: + swapType === 'same-chain' + ? TokenTransactionTypeV2.SwapTransaction + : TokenTransactionTypeV2.CrossChainSwapTransaction, inAmount: { value: swapAmount[Field.TO], tokenId: toToken.tokenId, diff --git a/src/swap/types.ts b/src/swap/types.ts index b265342d23e..9d58f31b357 100644 --- a/src/swap/types.ts +++ b/src/swap/types.ts @@ -2,6 +2,8 @@ import BigNumber from 'bignumber.js' import { TokenBalance } from 'src/tokens/slice' import { SerializableTransactionRequest } from 'src/viem/preparedTransactionSerialization' +export type SwapType = 'same-chain' | 'cross-chain' + export enum Field { FROM = 'FROM', TO = 'TO', @@ -25,7 +27,7 @@ interface SwapUserInput { } interface BaseSwapTransaction { - swapType: 'same-chain' | 'cross-chain' + swapType: SwapType chainId: number buyAmount: string sellAmount: string @@ -73,6 +75,7 @@ export interface SwapInfo { provider: string estimatedPriceImpact: string | null allowanceTarget: string + swapType: SwapType } areSwapTokensShuffled: boolean } diff --git a/src/swap/useSwapQuote.ts b/src/swap/useSwapQuote.ts index e1c791f723e..5f2a26b11a7 100644 --- a/src/swap/useSwapQuote.ts +++ b/src/swap/useSwapQuote.ts @@ -2,7 +2,13 @@ import BigNumber from 'bignumber.js' import { useAsyncCallback } from 'react-async-hook' import erc20 from 'src/abis/IERC20' import { useSelector } from 'src/redux/hooks' -import { FetchQuoteResponse, Field, ParsedSwapAmount, SwapTransaction } from 'src/swap/types' +import { + FetchQuoteResponse, + Field, + ParsedSwapAmount, + SwapTransaction, + SwapType, +} from 'src/swap/types' import { feeCurrenciesSelector } from 'src/tokens/selectors' import { TokenBalance } from 'src/tokens/slice' import { NetworkId } from 'src/transactions/types' @@ -24,7 +30,7 @@ const DECREASED_SWAP_AMOUNT_GAS_FEE_MULTIPLIER = 1.2 export const NO_QUOTE_ERROR_MESSAGE = 'No quote available' interface BaseQuoteResult { - swapType: 'same-chain' | 'cross-chain' + swapType: SwapType toTokenId: string fromTokenId: string swapAmount: BigNumber diff --git a/src/transactions/feed/TransactionFeed.tsx b/src/transactions/feed/TransactionFeed.tsx index 5d6cd3dacfe..8ae177ca181 100644 --- a/src/transactions/feed/TransactionFeed.tsx +++ b/src/transactions/feed/TransactionFeed.tsx @@ -83,6 +83,8 @@ function TransactionFeed() { case 'EarnWithdraw': case 'EarnClaimReward': return + case 'CrossChainTokenExchange': + return null // TODO } } diff --git a/src/transactions/feed/TransactionFeedItemImage.tsx b/src/transactions/feed/TransactionFeedItemImage.tsx index 33d16ac813a..48c58fcae90 100644 --- a/src/transactions/feed/TransactionFeedItemImage.tsx +++ b/src/transactions/feed/TransactionFeedItemImage.tsx @@ -17,19 +17,19 @@ const AVATAR_SIZE = 40 type Props = { networkId: NetworkId; status: TransactionStatus } & ( | { - transactionType: 'TokenExchangeV3' + transactionType: + | 'TokenExchangeV3' + | 'CrossChainTokenExchange' + | 'TokenApproval' + | 'EarnDeposit' + | 'EarnWithdraw' + | 'EarnClaimReward' } | { transactionType: 'TokenTransferV3' recipient: Recipient isJumpstart: boolean } - | { - transactionType: 'TokenApproval' - } - | { - transactionType: 'EarnDeposit' | 'EarnWithdraw' | 'EarnClaimReward' - } ) function TransactionFeedItemBaseImage(props: Props) { diff --git a/src/transactions/feed/queryHelper.ts b/src/transactions/feed/queryHelper.ts index 1b2d6fd18fa..7a3ffddd470 100644 --- a/src/transactions/feed/queryHelper.ts +++ b/src/transactions/feed/queryHelper.ts @@ -441,6 +441,7 @@ export const TRANSACTIONS_QUERY = gql` ...EarnWithdrawItem ...EarnClaimRewardItem ...TokenApprovalItem + ...CrossChainTokenExchangeItem } } } @@ -561,6 +562,48 @@ export const TRANSACTIONS_QUERY = gql` } } + fragment CrossChainTokenExchangeItem on CrossChainTokenExchange { + __typename + type + transactionHash + status + timestamp + block + outAmount { + value + tokenAddress + tokenId + localAmount { + value + currencyCode + exchangeRate + } + } + inAmount { + value + tokenAddress + tokenId + localAmount { + value + currencyCode + exchangeRate + } + } + fees { + type + amount { + value + tokenAddress + tokenId + localAmount { + value + currencyCode + exchangeRate + } + } + } + } + fragment EarnDepositItem on EarnDeposit { __typename type diff --git a/src/transactions/saga.ts b/src/transactions/saga.ts index 3d7def1a983..56e269ea2d9 100644 --- a/src/transactions/saga.ts +++ b/src/transactions/saga.ts @@ -264,7 +264,13 @@ export function* getTransactionReceipt( hash: transactionHash as Hash, }) - if (receipt) { + if (transaction.__typename === 'CrossChainTokenExchange' && receipt.status === 'success') { + // Do nothing for a cross chain swap that has a successful receipt because + // it is for the source network only, and we'll need to rely on + // blockchain-api to tell us when the whole cross chain swap has + // succeeded. However, we still want to mark the swap as failed if this + // source chain transaction has been reverted. + } else { yield* call( handleTransactionReceiptReceived, transaction.context.id, diff --git a/src/transactions/types.ts b/src/transactions/types.ts index cb7541b4d9a..d0340069b0d 100644 --- a/src/transactions/types.ts +++ b/src/transactions/types.ts @@ -124,6 +124,7 @@ export enum TokenTransactionTypeV2 { NftReceived = 'NFT_RECEIVED', NftSent = 'NFT_SENT', SwapTransaction = 'SWAP_TRANSACTION', + CrossChainSwapTransaction = 'CROSS_CHAIN_SWAP_TRANSACTION', Approval = 'APPROVAL', EarnDeposit = 'EARN_DEPOSIT', EarnWithdraw = 'EARN_WITHDRAW', @@ -166,7 +167,7 @@ export interface NftTransfer { // Can we optional the fields `transactionHash` and `block`? export interface TokenExchange { - __typename: 'TokenExchangeV3' + __typename: 'TokenExchangeV3' | 'CrossChainTokenExchange' networkId: NetworkId type: TokenTransactionTypeV2 transactionHash: string diff --git a/test/RootStateSchema.json b/test/RootStateSchema.json index ef7d4868053..6fc3fbcdfcf 100644 --- a/test/RootStateSchema.json +++ b/test/RootStateSchema.json @@ -2814,7 +2814,10 @@ "additionalProperties": false, "properties": { "__typename": { - "const": "TokenExchangeV3", + "enum": [ + "CrossChainTokenExchange", + "TokenExchangeV3" + ], "type": "string" }, "context": { @@ -3689,7 +3692,10 @@ "additionalProperties": false, "properties": { "__typename": { - "const": "TokenExchangeV3", + "enum": [ + "CrossChainTokenExchange", + "TokenExchangeV3" + ], "type": "string" }, "block": { @@ -5955,7 +5961,10 @@ "additionalProperties": false, "properties": { "__typename": { - "const": "TokenExchangeV3", + "enum": [ + "CrossChainTokenExchange", + "TokenExchangeV3" + ], "type": "string" }, "block": { @@ -6046,6 +6055,7 @@ "TokenTransactionTypeV2": { "enum": [ "APPROVAL", + "CROSS_CHAIN_SWAP_TRANSACTION", "EARN_CLAIM_REWARD", "EARN_DEPOSIT", "EARN_WITHDRAW",