Skip to content

Commit

Permalink
feat(swap): add cross chain swap transaction type and store pending tx (
Browse files Browse the repository at this point in the history
#5668)

### Description

This PR:
- add cross chain swap item to the transactions query. the existing
infrastructure will store these fetched items in redux / remove any
pending transactions of the same hash.
- add pending transaction of cross chain swap type on successful swap
- prevent the transaction watcher from marking the whole cross chain
swap as successful
- add swap type (same or cross chain) to analytics events 
- render no UI for cross chain swap items 

### Test plan

Tested using redux debugger that the transactions are created as pending
and updated when blockchain-api responses are recieved.

### Related issues

- Fixes RET-1120

### Backwards compatibility

Y

### Network scalability

If a new NetworkId and/or Network are added in the future, the changes
in this PR will:

- [x] Continue to work without code changes, OR trigger a compilation
error (guaranteeing we find it when a new network is added)
  • Loading branch information
kathaypacific authored Jul 26, 2024
1 parent 1dcc3b9 commit 8f686e4
Show file tree
Hide file tree
Showing 13 changed files with 164 additions and 18 deletions.
3 changes: 2 additions & 1 deletion src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -1179,6 +1179,7 @@ type SwapQuoteEvent = SwapEvent & {
price: string
appFeePercentageIncludedInPrice: string | null | undefined
provider: string
swapType: SwapType
}

export interface SwapTimeMetrics {
Expand Down
4 changes: 4 additions & 0 deletions src/swap/SwapScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,7 @@ describe('SwapScreen', () => {
provider: defaultQuote.details.swapProvider,
estimatedPriceImpact: defaultQuote.unvalidatedSwapTransaction.estimatedPriceImpact,
allowanceTarget: defaultQuote.unvalidatedSwapTransaction.allowanceTarget,
swapType: 'same-chain',
},
userInput: {
toTokenId: mockCusdTokenId,
Expand Down Expand Up @@ -1088,6 +1089,7 @@ describe('SwapScreen', () => {
provider: defaultQuote.details.swapProvider,
estimatedPriceImpact: defaultQuote.unvalidatedSwapTransaction.estimatedPriceImpact,
allowanceTarget: defaultQuote.unvalidatedSwapTransaction.allowanceTarget,
swapType: 'same-chain',
},
userInput: {
toTokenId: mockCeloTokenId,
Expand Down Expand Up @@ -1135,6 +1137,7 @@ describe('SwapScreen', () => {
provider: defaultQuote.details.swapProvider,
estimatedPriceImpact: defaultQuote.unvalidatedSwapTransaction.estimatedPriceImpact,
allowanceTarget: defaultQuote.unvalidatedSwapTransaction.allowanceTarget,
swapType: 'same-chain',
},
userInput: {
toTokenId: mockCusdTokenId,
Expand Down Expand Up @@ -1192,6 +1195,7 @@ describe('SwapScreen', () => {
feeCurrency: undefined,
feeCurrencySymbol: 'CELO',
txCount: 2,
swapType: 'same-chain',
})
})

Expand Down
2 changes: 2 additions & 0 deletions src/swap/SwapScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ export function SwapScreen({ route }: Props) {
price,
appFeePercentageIncludedInPrice,
provider: quote.provider,
swapType: quote.swapType,
web3Library: 'viem',
...getSwapTxsAnalyticsProperties(
quote.preparedTransactions.transactions,
Expand All @@ -434,6 +435,7 @@ export function SwapScreen({ route }: Props) {
provider: quote.provider,
estimatedPriceImpact,
allowanceTarget,
swapType: quote.swapType,
},
userInput,
areSwapTokensShuffled,
Expand Down
63 changes: 63 additions & 0 deletions src/swap/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const mockSwapFromParams = (toTokenId: string, feeCurrency?: Address): PayloadAc
estimatedPriceImpact: '0.1',
allowanceTarget: mockAllowanceTarget,
receivedAt: mockQuoteReceivedTimestamp,
swapType: 'same-chain',
},
areSwapTokensShuffled: false,
},
Expand Down Expand Up @@ -151,6 +152,7 @@ const mockSwapEthereum: PayloadAction<SwapInfo> = {
estimatedPriceImpact: '0.1',
allowanceTarget: mockAllowanceTarget,
receivedAt: mockQuoteReceivedTimestamp,
swapType: 'same-chain',
},
areSwapTokensShuffled: false,
},
Expand Down Expand Up @@ -187,6 +189,20 @@ const mockSwapWithWBTCBuyToken: PayloadAction<SwapInfo> = {
},
},
}
const mockCrossChainSwap: PayloadAction<SwapInfo> = {
...mockSwap,
payload: {
...mockSwap.payload,
quote: {
...mockSwap.payload.quote,
swapType: 'cross-chain',
},
userInput: {
...mockSwap.payload.userInput,
toTokenId: mockEthTokenId,
},
},
}

const store = createMockStore({
tokens: {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 7 additions & 2 deletions src/swap/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function* swapSubmitSaga(action: PayloadAction<SwapInfo>) {
estimatedPriceImpact,
preparedTransactions: serializablePreparedTransactions,
receivedAt: quoteReceivedAt,
swapType,
} = quote
const amountType = updatedField === Field.TO ? ('buyAmount' as const) : ('sellAmount' as const)
const amount = swapAmount[updatedField]
Expand Down Expand Up @@ -147,6 +148,7 @@ export function* swapSubmitSaga(action: PayloadAction<SwapInfo>) {
web3Library: 'viem' as const,
areSwapTokensShuffled,
...getSwapTxsAnalyticsProperties(preparedTransactions, fromToken.networkId, tokensById),
swapType,
}

let quoteToTransactionElapsedTimeInMs: number | undefined
Expand Down Expand Up @@ -225,9 +227,12 @@ export function* swapSubmitSaga(action: PayloadAction<SwapInfo>) {
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,
Expand Down
5 changes: 4 additions & 1 deletion src/swap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -25,7 +27,7 @@ interface SwapUserInput {
}

interface BaseSwapTransaction {
swapType: 'same-chain' | 'cross-chain'
swapType: SwapType
chainId: number
buyAmount: string
sellAmount: string
Expand Down Expand Up @@ -73,6 +75,7 @@ export interface SwapInfo {
provider: string
estimatedPriceImpact: string | null
allowanceTarget: string
swapType: SwapType
}
areSwapTokensShuffled: boolean
}
Expand Down
10 changes: 8 additions & 2 deletions src/swap/useSwapQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/transactions/feed/TransactionFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ function TransactionFeed() {
case 'EarnWithdraw':
case 'EarnClaimReward':
return <EarnFeedItem key={tx.transactionHash} transaction={tx} />
case 'CrossChainTokenExchange':
return null // TODO
}
}

Expand Down
14 changes: 7 additions & 7 deletions src/transactions/feed/TransactionFeedItemImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
43 changes: 43 additions & 0 deletions src/transactions/feed/queryHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ export const TRANSACTIONS_QUERY = gql`
...EarnWithdrawItem
...EarnClaimRewardItem
...TokenApprovalItem
...CrossChainTokenExchangeItem
}
}
}
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/transactions/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/transactions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 8f686e4

Please sign in to comment.