diff --git a/frontend/tests/unit/actions/transfers/subsidized-transfer.spec.ts b/frontend/tests/unit/actions/transfers/subsidized-transfer.spec.ts new file mode 100644 index 000000000..597c76649 --- /dev/null +++ b/frontend/tests/unit/actions/transfers/subsidized-transfer.spec.ts @@ -0,0 +1,241 @@ +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; +import { flushPromises } from '@vue/test-utils'; + +import type { SubsidizedTransferData } from '@/actions/transfers'; +import { SubsidizedTransfer } from '@/actions/transfers'; +import * as requestManager from '@/services/transactions/request-manager'; +import * as tokenUtils from '@/services/transactions/token'; +import * as transactionUtils from '@/services/transactions/utils'; +import type { IEthereumProvider } from '@/services/web3-provider'; +import { UInt256 } from '@/types/uint-256'; +import { + generateChain, + generateRequestInformationData, + generateStepData, + generateSubsidizedTransferData, + generateToken, + generateTokenAmountData, + generateUInt256Data, + getRandomEthereumAddress, + getRandomNumber, + getRandomString, +} from '~/utils/data_generators'; +import { MockedEthereumProvider } from '~/utils/mocks/ethereum-provider'; + +vi.mock('@ethersproject/providers'); +vi.mock('@/services/transactions/token'); +vi.mock('@/services/transactions/fill-manager'); +vi.mock('@/services/transactions/request-manager'); + +class TestSubsidizedTransfer extends SubsidizedTransfer { + public ensureTokenAllowance(provider: IEthereumProvider) { + return super.ensureTokenAllowance(provider); + } + + public sendRequestTransaction(provider: IEthereumProvider) { + return super.sendRequestTransaction(provider); + } +} + +describe('SubsidizedTransfer', () => { + beforeEach(() => { + global.Date.now = vi.fn(); + + Object.defineProperties(tokenUtils, { + ensureTokenAllowance: { value: vi.fn().mockResolvedValue(undefined) }, + isAllowanceApproved: { value: vi.fn().mockResolvedValue(true) }, + }); + + Object.defineProperties(requestManager, { + sendRequestTransaction: { value: vi.fn().mockResolvedValue('0xHash') }, + getRequestData: { + value: vi.fn().mockResolvedValue({ + withdrawn: false, + }), + }, + }); + + Object.defineProperties(transactionUtils, { + getCurrentBlockNumber: { value: vi.fn().mockResolvedValue(0) }, + }); + }); + + afterEach(() => { + flushPromises(); + }); + + describe('ensureTokenAllowance()', async () => { + it('makes a call to set the allowance for the FeeSub contract', async () => { + const feeSubAddress = '0xFeeSub'; + const data = generateSubsidizedTransferData({ + fees: generateTokenAmountData({ amount: '2' }), + sourceChain: generateChain({ requestManagerAddress: '0xRequestManager', feeSubAddress }), + sourceAmount: generateTokenAmountData({ + token: generateToken({ address: '0xSourceToken' }), + amount: '1', + }), + feeSubAddress, + }); + const transfer = new TestSubsidizedTransfer(data); + const signer = new JsonRpcSigner(undefined, new JsonRpcProvider()); + const provider = new MockedEthereumProvider({ + signer: signer, + signerAddress: data.requestInformation?.requestAccount, + }); + + await transfer.ensureTokenAllowance(provider); + + expect(tokenUtils.ensureTokenAllowance).toHaveBeenCalledTimes(1); + expect(tokenUtils.ensureTokenAllowance).toHaveBeenLastCalledWith( + provider, + '0xSourceToken', + feeSubAddress, + new UInt256('1'), + ); + expect(tokenUtils.isAllowanceApproved).toHaveBeenCalledTimes(1); + expect(tokenUtils.isAllowanceApproved).toHaveBeenLastCalledWith( + provider, + '0xSourceToken', + data.requestInformation?.requestAccount, + feeSubAddress, + new UInt256('1'), + ); + }); + }); + + describe('sendRequestTransaction()', () => { + it('calls the request function on the FeeSub contract', async () => { + const feeSubAddress = '0xFeeSub'; + const data = generateSubsidizedTransferData({ + sourceChain: generateChain({ requestManagerAddress: '0xRequestManager', feeSubAddress }), + sourceAmount: generateTokenAmountData({ + token: generateToken({ address: '0xSourceToken' }), + amount: '1', + }), + targetChain: generateChain({ identifier: 2 }), + targetAmount: generateTokenAmountData({ + token: generateToken({ address: '0xTargetToken' }), + amount: '1', + }), + targetAccount: '0xTargetAccount', + validityPeriod: generateUInt256Data('3'), + fees: generateTokenAmountData({ amount: '4' }), + feeSubAddress, + }); + const transfer = new TestSubsidizedTransfer(data); + const signer = new JsonRpcSigner(undefined, new JsonRpcProvider()); + const provider = new MockedEthereumProvider({ + signer: signer, + signerAddress: data.requestInformation?.requestAccount, + }); + + await transfer.sendRequestTransaction(provider); + + expect(requestManager.sendRequestTransaction).toHaveBeenCalledTimes(1); + expect(requestManager.sendRequestTransaction).toHaveBeenLastCalledWith( + provider, + new UInt256('1'), + 2, + feeSubAddress, + '0xSourceToken', + '0xTargetToken', + '0xTargetAccount', + new UInt256('3'), + ); + }); + }); + + describe('withdraw', () => { + it('uses the FeeSub contract to withdraw', async () => { + const feeSubAddress = '0xFeeSub'; + const identifier = getRandomString(); + const data = generateSubsidizedTransferData({ + expired: true, + requestInformation: generateRequestInformationData({ + identifier, + }), + sourceChain: generateChain({ + identifier: 1, + requestManagerAddress: '0xRequestManager', + feeSubAddress, + }), + }); + const transfer = new TestSubsidizedTransfer(data); + const signer = new JsonRpcSigner(undefined, new JsonRpcProvider()); + const provider = new MockedEthereumProvider({ chainId: 1, signer }); + + await transfer.withdraw(provider); + + expect(requestManager.withdrawRequest).toHaveBeenCalledOnce(); + expect(requestManager.withdrawRequest).toHaveBeenLastCalledWith( + provider, + feeSubAddress, + identifier, + ); + }); + }); + + describe('encode()', () => { + it('serializes all data to persist the whole transfer', () => { + const feeSubAddress = getRandomEthereumAddress(); + const sourceChain = generateChain({ feeSubAddress }); + const sourceAmount = generateTokenAmountData(); + const targetChain = generateChain(); + const targetAmount = generateTokenAmountData(); + const targetAccount = getRandomEthereumAddress(); + const validityPeriod = generateUInt256Data(); + const fees = generateTokenAmountData({ amount: '0' }); + const date = 1652688517448; + const requestInformation = generateRequestInformationData(); + const expired = true; + const withdrawn = true; + const claimCount = getRandomNumber(); + const steps = [generateStepData()]; + const data: SubsidizedTransferData = { + sourceChain, + sourceAmount, + targetChain, + targetAmount, + targetAccount, + validityPeriod, + fees, + date, + requestInformation, + expired, + withdrawn, + claimCount, + steps, + feeSubAddress, + }; + const transfer = new TestSubsidizedTransfer(data); + + const encodedData = transfer.encode(); + + expect(encodedData.sourceChain).toMatchObject(sourceChain); + expect(encodedData.sourceAmount).toMatchObject(sourceAmount); + expect(encodedData.targetChain).toMatchObject(targetChain); + expect(encodedData.targetAmount).toMatchObject(targetAmount); + expect(encodedData.targetAccount).toMatchObject(targetAccount); + expect(encodedData.validityPeriod).toMatchObject(validityPeriod); + expect(encodedData.fees).toMatchObject(fees); + expect(encodedData.date).toMatchObject(date); + expect(encodedData.requestInformation).toMatchObject(requestInformation); + expect(encodedData.expired).toBe(true); + expect(encodedData.withdrawn).toBe(true); + expect(encodedData.steps).toMatchObject(steps); + expect(encodedData.claimCount).toBe(claimCount); + expect(encodedData.feeSubAddress).toBe(feeSubAddress); + }); + + it('can be used to re-instantiate subsidized transfer again', () => { + const data = generateSubsidizedTransferData(); + const transfer = new TestSubsidizedTransfer(data); + + const encodedData = transfer.encode(); + const newTransfer = new TestSubsidizedTransfer(encodedData); + const newEncodedData = newTransfer.encode(); + + expect(encodedData).toMatchObject(newEncodedData); + }); + }); +}); diff --git a/frontend/tests/unit/actions/transfers/transfer.spec.ts b/frontend/tests/unit/actions/transfers/transfer.spec.ts index 71fa34a95..c7a8033d9 100644 --- a/frontend/tests/unit/actions/transfers/transfer.spec.ts +++ b/frontend/tests/unit/actions/transfers/transfer.spec.ts @@ -85,7 +85,7 @@ function define(object: unknown, property: string, value: unknown): void { Object.defineProperty(object, property, { value }); } -describe('transfer', () => { +describe('Transfer', () => { beforeEach(() => { global.Date.now = vi.fn(); diff --git a/frontend/tests/unit/components/ShareTweet.spec.ts b/frontend/tests/unit/components/ShareTweet.spec.ts index c3f5e3ba9..213ccb378 100644 --- a/frontend/tests/unit/components/ShareTweet.spec.ts +++ b/frontend/tests/unit/components/ShareTweet.spec.ts @@ -6,6 +6,7 @@ import { generateChain, generateRequestFulfillmentData, generateRequestInformationData, + generateSubsidizedTransferData, generateTokenAmountData, generateTransfer, generateTransferData, @@ -83,7 +84,7 @@ https://app.beamerbridge.com/`; it('uses the zebra campaign text when the transfer was subsidized', () => { const feeSubAddress = getRandomEthereumAddress(); const transfer = generateTransfer({ - transferData: generateTransferData({ + transferData: generateSubsidizedTransferData({ requestInformation: generateRequestInformationData({ timestamp: 1 }), requestFulfillment: generateRequestFulfillmentData({ timestamp: 100 }), sourceChain: generateChain({ feeSubAddress }), diff --git a/frontend/tests/unit/stores/transfer-history/serializer.spec.ts b/frontend/tests/unit/stores/transfer-history/serializer.spec.ts index 31c00d8e0..15814f131 100644 --- a/frontend/tests/unit/stores/transfer-history/serializer.spec.ts +++ b/frontend/tests/unit/stores/transfer-history/serializer.spec.ts @@ -1,6 +1,11 @@ import { SubsidizedTransfer, Transfer } from '@/actions/transfers'; import { transferHistorySerializer } from '@/stores/transfer-history/serializer'; -import { generateChain, generateStepData, generateTransferData } from '~/utils/data_generators'; +import { + generateChain, + generateStepData, + generateSubsidizedTransferData, + generateTransferData, +} from '~/utils/data_generators'; vi.mock('@/actions/transfers', async (importOriginal) => { const mod: object = await importOriginal(); @@ -66,7 +71,7 @@ describe('transfer history serializer', () => { }); it('is able to detect and create instances of subsidized and non-subsidized transfers accordingly', () => { - const subsidizedTransferData = generateTransferData({ + const subsidizedTransferData = generateSubsidizedTransferData({ sourceChain: generateChain({ feeSubAddress: '0x123' }), feeSubAddress: '0x123', }); diff --git a/frontend/tests/utils/data_generators.ts b/frontend/tests/utils/data_generators.ts index 2b99126d5..2f61d98ba 100644 --- a/frontend/tests/utils/data_generators.ts +++ b/frontend/tests/utils/data_generators.ts @@ -152,9 +152,7 @@ export function generateAllowanceInformationData( }; } -export function generateTransferData( - partialTransferData?: Partial, -): TransferData { +export function generateTransferData(partialTransferData?: Partial): TransferData { return { sourceChain: generateChain(), sourceAmount: generateTokenAmountData(), @@ -170,6 +168,17 @@ export function generateTransferData( }; } +export function generateSubsidizedTransferData( + partialTransferData?: Partial, +): SubsidizedTransferData { + const feeSubAddress = partialTransferData?.feeSubAddress ?? getRandomEthereumAddress(); + return { + ...generateTransferData({ sourceChain: generateChain({ feeSubAddress }) }), + feeSubAddress: feeSubAddress, + ...partialTransferData, + }; +} + export function generateTransfer(options?: { transferData?: Partial; active?: boolean;