From 6cb57020a6bfba4e7b81e3e455643a82334c08fe Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 3 Apr 2024 14:27:59 +0200 Subject: [PATCH] chore(jumpstart): support for reclaim from retired jumpstart contracts (#5200) ### Description This PR adds a `retiredContractAddresses` to the jumpstart dynamic config. If we need to change the contract for whatever reason, we should still allow users to reclaim their funds that were sent to previous versions of the contract. The only place we need to do this is in the transaction classification stage - we should mark transactions to _any_ jumpstart contract appropriately, from there the jumpstart transaction details screen will allow reclaim from that contract directly. Of course this logic relies on the abi of the jumpstart contracts staying the same, since a single abi is baked into the app. ### Test plan Unit tests ### Related issues n/a ### Backwards compatibility Y ### Network scalability Y --- ...JumpstartTransactionDetailsScreen.test.tsx | 40 ++++++++++++++++++- src/statsig/constants.ts | 6 ++- .../feed/TransferFeedItem.test.tsx | 13 ++++-- src/transactions/feed/TransferFeedItem.tsx | 12 ++++-- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/jumpstart/JumpstartTransactionDetailsScreen.test.tsx b/src/jumpstart/JumpstartTransactionDetailsScreen.test.tsx index 24876023f8b..16ec04a0f65 100644 --- a/src/jumpstart/JumpstartTransactionDetailsScreen.test.tsx +++ b/src/jumpstart/JumpstartTransactionDetailsScreen.test.tsx @@ -45,6 +45,8 @@ const mockReclaimTx: TransactionRequest = { maxFeePerGas: BigInt(1), maxPriorityFeePerGas: undefined, } +const mockRetiredContractAddress = '0xretired' +const mockTransactionHash = '0x544367eaf2b01622dd1c7b75a6b19bf278d72127aecfb2e5106424c40c268e8b' describe('JumpstartTransactionDetailsScreen', () => { beforeEach(() => { @@ -52,7 +54,10 @@ describe('JumpstartTransactionDetailsScreen', () => { jest.mocked(getDynamicConfigParams).mockReturnValue({ jumpstartContracts: { - [NetworkId['celo-alfajores']]: { contractAddress: mockJumpstartAdddress }, + [NetworkId['celo-alfajores']]: { + contractAddress: mockJumpstartAdddress, + retiredContractAddresses: [mockRetiredContractAddress], + }, }, }) jest.mocked(prepareTransactions).mockResolvedValue({ @@ -115,7 +120,7 @@ describe('JumpstartTransactionDetailsScreen', () => { __typename: 'TokenTransferV3', networkId: NetworkId['celo-alfajores'], type, - transactionHash: '0x544367eaf2b01622dd1c7b75a6b19bf278d72127aecfb2e5106424c40c268e8b', + transactionHash: mockTransactionHash, timestamp: 1542306118, block: '8648978', address, @@ -139,6 +144,11 @@ describe('JumpstartTransactionDetailsScreen', () => { ) ) expect(getByTestId('JumpstartContent/AmountValue')).toHaveTextContent('10.00 cUSD') + expect(fetchClaimStatus).toHaveBeenCalledWith( + mockJumpstartAdddress, + 'celo-alfajores', + mockTransactionHash + ) }) it('shows the correct amount and no reclaim button for jumpstart received transactions', async () => { @@ -234,6 +244,32 @@ describe('JumpstartTransactionDetailsScreen', () => { ]) }) + it('uses the relevant jumpstart contract to prepare the reclaim transaction', async () => { + jest.mocked(fetchClaimStatus).mockResolvedValue({ + beneficiary: mockAccount, + index: 0, + claimed: false, + }) + renderScreen({ + transaction: tokenTransfer({ + type: TokenTransactionTypeV2.Sent, + address: mockRetiredContractAddress, + }), + }) + + await waitFor(() => + expect(fetchClaimStatus).toHaveBeenCalledWith( + mockRetiredContractAddress, + 'celo-alfajores', + mockTransactionHash + ) + ) + expect(prepareTransactions).toHaveBeenCalledWith({ + baseTransactions: [expect.objectContaining({ to: mockRetiredContractAddress })], + feeCurrencies: expect.any(Array), + }) + }) + it('shows an error if the reclaim failed', async () => { jest.mocked(fetchClaimStatus).mockResolvedValue({ beneficiary: mockAccount, diff --git a/src/statsig/constants.ts b/src/statsig/constants.ts index 9d8c9f28e4c..9e47dc1dc2c 100644 --- a/src/statsig/constants.ts +++ b/src/statsig/constants.ts @@ -102,7 +102,11 @@ export const DynamicConfigs = { configName: StatsigDynamicConfigs.WALLET_JUMPSTART_CONFIG, defaultValues: { jumpstartContracts: {} as { - [key in NetworkId]?: { contractAddress?: string; depositERC20GasEstimate: string } + [key in NetworkId]?: { + contractAddress?: string + depositERC20GasEstimate: string + retiredContractAddresses?: string[] + } }, maxAllowedSendAmountUsd: 100, }, diff --git a/src/transactions/feed/TransferFeedItem.test.tsx b/src/transactions/feed/TransferFeedItem.test.tsx index 9969325fe68..6062ddc202f 100644 --- a/src/transactions/feed/TransferFeedItem.test.tsx +++ b/src/transactions/feed/TransferFeedItem.test.tsx @@ -43,6 +43,7 @@ const MOCK_CONTACT = { contactId: 'contactId', address: MOCK_ADDRESS, } +const mockRetiredJumpstartAdddress = '0xabc' jest.mock('src/statsig') @@ -68,7 +69,10 @@ describe('TransferFeedItem', () => { jest.mocked(getFeatureGate).mockReturnValue(true) jest.mocked(getDynamicConfigParams).mockReturnValue({ jumpstartContracts: { - [NetworkId['celo-alfajores']]: { contractAddress: mockJumpstartAdddress }, + [NetworkId['celo-alfajores']]: { + contractAddress: mockJumpstartAdddress, + retiredContractAddresses: [mockRetiredJumpstartAdddress], + }, }, }) }) @@ -654,10 +658,13 @@ describe('TransferFeedItem', () => { expect(queryByTestId('TransferFeedItem/tokenAmount')).toBeNull() }) - it('renders correctly for jumpstart deposit', async () => { + it.each([ + { address: mockJumpstartAdddress, addressType: 'current' }, + { address: mockRetiredJumpstartAdddress, addressType: 'retired' }, + ])('renders correctly for jumpstart deposit to $addressType contract', async ({ address }) => { const { getByTestId } = renderScreen({ type: TokenTransactionTypeV2.Sent, - address: mockJumpstartAdddress, + address, amount: { tokenAddress: mockCusdAddress, tokenId: mockCusdTokenId, diff --git a/src/transactions/feed/TransferFeedItem.tsx b/src/transactions/feed/TransferFeedItem.tsx index 45d739a0856..3cdd99abd18 100644 --- a/src/transactions/feed/TransferFeedItem.tsx +++ b/src/transactions/feed/TransferFeedItem.tsx @@ -21,6 +21,7 @@ import { useTokenInfo } from 'src/tokens/hooks' import TransactionFeedItemImage from 'src/transactions/feed/TransactionFeedItemImage' import { useTransferFeedDetails } from 'src/transactions/transferFeedUtils' import { TokenTransfer } from 'src/transactions/types' +import { isPresent } from 'src/utils/typescript' interface Props { transfer: TokenTransfer } @@ -99,10 +100,15 @@ function TransferFeedItem({ transfer }: Props) { } function isJumpstartTransaction(tx: TokenTransfer) { - const jumpstartAddress = getDynamicConfigParams( + const jumpstartConfig = getDynamicConfigParams( DynamicConfigs[StatsigDynamicConfigs.WALLET_JUMPSTART_CONFIG] - ).jumpstartContracts[tx.networkId]?.contractAddress - return tx.address === jumpstartAddress + ).jumpstartContracts[tx.networkId] + const jumpstartAddresses = [ + jumpstartConfig?.contractAddress, + ...(jumpstartConfig?.retiredContractAddresses ?? []), + ].filter(isPresent) + + return jumpstartAddresses.includes(tx.address) } const styles = StyleSheet.create({