From 328debd11e6035db117b17af1dd1c0b0db531178 Mon Sep 17 00:00:00 2001 From: Ivo G <41997352+Dadogg80@users.noreply.github.com> Date: Mon, 6 Dec 2021 10:29:08 +0100 Subject: [PATCH] feat: Erc20 escrow payment processor (complete w/tests). (#693) * feat: Payment-processing for erc20EscrowToPay smart-contract interactions --- packages/payment-processor/src/index.ts | 1 + .../src/payment/erc20-escrow-payment.ts | 396 ++++++++++++++++++ .../test/payment/erc20-escrow-payment.test.ts | 316 ++++++++++++++ .../lib/artifacts/ERC20EscrowToPay/0.1.0.json | 297 +++++++++++++ .../lib/artifacts/ERC20EscrowToPay/index.ts | 28 ++ .../src/lib/artifacts/index.ts | 2 +- 6 files changed, 1039 insertions(+), 1 deletion(-) create mode 100644 packages/payment-processor/src/payment/erc20-escrow-payment.ts create mode 100644 packages/payment-processor/test/payment/erc20-escrow-payment.test.ts create mode 100644 packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/0.1.0.json create mode 100644 packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/index.ts diff --git a/packages/payment-processor/src/index.ts b/packages/payment-processor/src/index.ts index c35c0e7ef2..efc332f39a 100644 --- a/packages/payment-processor/src/index.ts +++ b/packages/payment-processor/src/index.ts @@ -12,6 +12,7 @@ export * from './payment/swap-any-to-erc20'; export * from './payment/swap-erc20'; export * from './payment/swap-erc20-fee-proxy'; export * from './payment/conversion-erc20'; +export * as Escrow from './payment/erc20-escrow-payment'; import * as utils from './payment/utils'; export { utils }; diff --git a/packages/payment-processor/src/payment/erc20-escrow-payment.ts b/packages/payment-processor/src/payment/erc20-escrow-payment.ts new file mode 100644 index 0000000000..d4e2b1e55b --- /dev/null +++ b/packages/payment-processor/src/payment/erc20-escrow-payment.ts @@ -0,0 +1,396 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { BigNumber, BigNumberish, constants, ContractTransaction, providers, Signer } from 'ethers'; +import { erc20EscrowToPayArtifact } from '@requestnetwork/smart-contracts'; +import { ERC20EscrowToPay__factory } from '@requestnetwork/smart-contracts/types/'; +import { ClientTypes, PaymentTypes } from '@requestnetwork/types'; +import { + getAmountToPay, + getProvider, + getRequestPaymentValues, + getSigner, + validateRequest, +} from './utils'; +import { ITransactionOverrides } from './transaction-overrides'; +import { encodeApproveAnyErc20 } from './erc20'; + +/** + * Processes the approval transaction of the payment ERC20 to be spent by the erc20EscrowToPay + * contract during the fee proxy delegate call. + * @param request request to pay, used to know the network + * @param paymentTokenAddress picked currency to pay + * @param signerOrProvider the web3 provider. Defaults to Etherscan. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function approveErc20ForEscrow( + request: ClientTypes.IRequestData, + paymentTokenAddress: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + const encodedTx = encodeApproveAnyErc20(paymentTokenAddress, contractAddress, signerOrProvider); + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction({ + data: encodedTx, + to: paymentTokenAddress, + value: 0, + ...overrides, + }); + return tx; +} + +/** + * Processes a transaction to payEscrow(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function payEscrow( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), + amount?: BigNumberish, + feeAmount?: BigNumberish, +): Promise { + const encodedTx = encodePayEscrow(request, signerOrProvider, amount, feeAmount); + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + const signer = getSigner(signerOrProvider); + + const tx = await signer.sendTransaction({ + data: encodedTx, + to: contractAddress, + value: 0, + }); + return tx; +} + +/** + * Processes a transaction to freeze request. + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function freezeRequest( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const encodedTx = encodeFreezeRequest(request, signerOrProvider); + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction({ + data: encodedTx, + to: contractAddress, + value: 0, + ...overrides, + }); + return tx; +} + +/** + * Processes a transaction to payRequestFromEscrow(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function payRequestFromEscrow( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const encodedTx = encodePayRequestFromEscrow(request, signerOrProvider); + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction({ + data: encodedTx, + to: contractAddress, + value: 0, + ...overrides, + }); + return tx; +} + +/** + * Processes a transaction to initiateEmergencyClaim(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function initiateEmergencyClaim( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const encodedTx = encodeInitiateEmergencyClaim(request, signerOrProvider); + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction({ + data: encodedTx, + to: contractAddress, + value: 0, + ...overrides, + }); + return tx; +} + +/** + * Processes a transaction to completeEmergencyClaim(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function completeEmergencyClaim( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const encodedTx = encodeCompleteEmergencyClaim(request, signerOrProvider); + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction({ + data: encodedTx, + to: contractAddress, + value: 0, + ...overrides, + }); + return tx; +} + +/** + * Processes a transaction to revertEmergencyClaim(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function revertEmergencyClaim( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const encodedTx = encodeRevertEmergencyClaim(request, signerOrProvider); + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction({ + data: encodedTx, + to: contractAddress, + value: 0, + ...overrides, + }); + return tx; +} + +/** + * Processes a transaction to refundFrozenFunds(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function refundFrozenFunds( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const encodedTx = encodeRefundFrozenFunds(request, signerOrProvider); + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction({ + data: encodedTx, + to: contractAddress, + value: 0, + ...overrides, + }); + return tx; +} + +/** + * Encodes the call to payEscrow(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param amount optionally, the amount to pay. Defaults to remaining amount of the request. + */ +export function encodePayEscrow( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), + amount?: BigNumberish, + feeAmountOverride?: BigNumberish, +): string { + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); + const signer = getSigner(signerOrProvider); + + const tokenAddress = request.currencyInfo.value; + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + + // collects the parameters to be used, from the request + const { paymentReference, paymentAddress, feeAmount, feeAddress } = getRequestPaymentValues( + request, + ); + + const amountToPay = getAmountToPay(request, amount); + const feeToPay = BigNumber.from(feeAmountOverride || feeAmount || 0); + + const erc20EscrowContract = ERC20EscrowToPay__factory.connect(contractAddress, signer); + + return erc20EscrowContract.interface.encodeFunctionData('payEscrow', [ + tokenAddress, + paymentAddress, + amountToPay, + `0x${paymentReference}`, + feeToPay, + feeAddress || constants.AddressZero, + ]); +} + +/** + * Returns the encoded data to freezeRequest(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export function encodeFreezeRequest( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), +): string { + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); + const signer = getSigner(signerOrProvider); + + // collects the parameters to be used from the request + const { paymentReference } = getRequestPaymentValues(request); + + // connections to the escrow contract + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + const erc20EscrowToPayContract = ERC20EscrowToPay__factory.connect(contractAddress, signer); + + // encodes the function data and returns them + return erc20EscrowToPayContract.interface.encodeFunctionData('freezeRequest', [ + `0x${paymentReference}`, + ]); +} + +/** + * Returns the encoded data to payRequestFromEscrow(). + * @param request request for pay + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export function encodePayRequestFromEscrow( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), +): string { + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); + const signer = getSigner(signerOrProvider); + + // collects the parameters to be used from the request + const { paymentReference } = getRequestPaymentValues(request); + + // connections to the escrow contract + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + const erc20EscrowToPayContract = ERC20EscrowToPay__factory.connect(contractAddress, signer); + + // encodes the function data and returns them + return erc20EscrowToPayContract.interface.encodeFunctionData('payRequestFromEscrow', [ + `0x${paymentReference}`, + ]); +} + +/** + * Returns the encoded data to initiateEmergencyClaim(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export function encodeInitiateEmergencyClaim( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), +): string { + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); + const signer = getSigner(signerOrProvider); + + // collects the parameters to be used from the request + const { paymentReference } = getRequestPaymentValues(request); + + // connections to the escrow contract + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + const erc20EscrowToPayContract = ERC20EscrowToPay__factory.connect(contractAddress, signer); + + // encodes the function data and returns them + return erc20EscrowToPayContract.interface.encodeFunctionData('initiateEmergencyClaim', [ + `0x${paymentReference}`, + ]); +} + +/** + * Returns the encoded data to completeEmergencyClaim(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export function encodeCompleteEmergencyClaim( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), +): string { + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); + const signer = getSigner(signerOrProvider); + + // collects the parameters to be used from the request + const { paymentReference } = getRequestPaymentValues(request); + + // connections to the escrow contract + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + const erc20EscrowToPayContract = ERC20EscrowToPay__factory.connect(contractAddress, signer); + + // encodes the function data and returns them + return erc20EscrowToPayContract.interface.encodeFunctionData('completeEmergencyClaim', [ + `0x${paymentReference}`, + ]); +} + +/** + * Returns the encoded data to revertEmergencyClaim(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export function encodeRevertEmergencyClaim( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), +): string { + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); + const signer = getSigner(signerOrProvider); + + // collects the parameters to be used from the request + const { paymentReference } = getRequestPaymentValues(request); + + // connections to the escrow contract + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + const erc20EscrowToPayContract = ERC20EscrowToPay__factory.connect(contractAddress, signer); + + // encodes the function data and returns them + return erc20EscrowToPayContract.interface.encodeFunctionData('revertEmergencyClaim', [ + `0x${paymentReference}`, + ]); +} + +/** + * Returns the encoded data to refundFrozenFunds(). + * @param request request to pay. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export function encodeRefundFrozenFunds( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Web3Provider | Signer = getProvider(), +): string { + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); + const signer = getSigner(signerOrProvider); + + // collects the parameters to be used from the request + const { paymentReference } = getRequestPaymentValues(request); + + // connections to the escrow contract + const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!); + const erc20EscrowToPayContract = ERC20EscrowToPay__factory.connect(contractAddress, signer); + + // encodes the function data and returns them + return erc20EscrowToPayContract.interface.encodeFunctionData('refundFrozenFunds', [ + `0x${paymentReference}`, + ]); +} diff --git a/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts b/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts new file mode 100644 index 0000000000..af08a612d6 --- /dev/null +++ b/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts @@ -0,0 +1,316 @@ +import { Wallet, providers, BigNumber } from 'ethers'; +import { + ClientTypes, + ExtensionTypes, + IdentityTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import Utils from '@requestnetwork/utils'; +import { Escrow } from '@requestnetwork/payment-processor'; +import { getRequestPaymentValues, getSigner } from '../../src/payment/utils'; + +import { erc20EscrowToPayArtifact } from '@requestnetwork/smart-contracts'; +import { getErc20Balance } from '../../src/payment/erc20'; + +/* eslint-disable no-magic-numbers */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +const erc20ContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; +const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; +const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; +const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +const provider = new providers.JsonRpcProvider('http://localhost:8545'); +const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); + +const validRequest: ClientTypes.IRequestData = { + balance: { + balance: '0', + events: [], + }, + contentData: {}, + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: wallet.address, + }, + currency: 'DAI', + currencyInfo: { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20, + value: erc20ContractAddress, + }, + events: [], + expectedAmount: '100', + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress, + salt: 'salt' + Math.floor(Math.random() * 10000000), + }, + version: '0.1.0', + }, + }, + extensionsData: [], + meta: { + transactionManagerMeta: {}, + }, + pending: null, + requestId: 'abcd', + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, + version: '1.0', +}; + +const escrowAddress = erc20EscrowToPayArtifact.getAddress(validRequest.currencyInfo.network!); +const payerAddress = wallet.address; + +describe('erc20-escrow-payment tests:', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + describe('Test sanity checks:', () => { + const { paymentReference } = getRequestPaymentValues(validRequest); + + it('Should pass with correct values.', () => { + const values = getRequestPaymentValues(validRequest); + + expect(values.feeAddress).toBe(feeAddress); + expect(values.feeAmount).toBe('2'); + expect(values.paymentAddress).toBe(paymentAddress); + expect(values.paymentReference).toBe(paymentReference); + }); + it('Should throw an error if the request is not erc20', async () => { + const request = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; + request.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + + await expect(Escrow.payEscrow(request, wallet)).rejects.toThrowError( + "request cannot be processed, or is not an pn-erc20-fee-proxy-contract request", + ); + }); + it('Should throw an error if the currencyInfo has no value', async () => { + const request = Utils.deepCopy(validRequest); + request.currencyInfo.value = ''; + await expect(Escrow.payEscrow(request, wallet)).rejects.toThrowError( + "request cannot be processed, or is not an pn-erc20-fee-proxy-contract request", + ); + }); + it('Should throw an error if currencyInfo has no network', async () => { + const request = Utils.deepCopy(validRequest); + request.currencyInfo.network = ''; + await expect(Escrow.payEscrow(request, wallet)).rejects.toThrowError( + "request cannot be processed, or is not an pn-erc20-fee-proxy-contract request", + ); + }); + it('Should throw an error if request has no extension', async () => { + const request = Utils.deepCopy(validRequest); + request.extensions = [] as any; + + await expect(Escrow.payEscrow(request, wallet)).rejects.toThrowError( + 'no payment network found', + ); + }); + it('Should consider override parameters', async () => { + const spy = jest.fn(); + const originalSendTransaction = wallet.sendTransaction.bind(wallet); + wallet.sendTransaction = spy; + + const values = getRequestPaymentValues(validRequest); + + await Escrow.payEscrow(validRequest, wallet, undefined); + + expect(spy).toHaveBeenCalledWith({ + data: `0x325a00f00000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000008${values.paymentReference}000000000000000000000000000000000000000000000000`, + to: '0xF08dF3eFDD854FEDE77Ed3b2E515090EEe765154', + value: 0, + }); + wallet.sendTransaction = originalSendTransaction; + }); + }); + + describe('Test encoded function data:', () => { + const values = getRequestPaymentValues(validRequest); + + it('Should encode data to execute payEscrow().', () => { + expect(Escrow.encodePayEscrow(validRequest, wallet)).toBe( + `0x325a00f00000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000008${values.paymentReference}000000000000000000000000000000000000000000000000`, + ); + }); + it('Should encode data to execute payRequestFromEscrow().', () => { + expect(Escrow.encodePayRequestFromEscrow(validRequest, wallet)).toBe( + `0x2a16f4c300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008${values.paymentReference}000000000000000000000000000000000000000000000000`, + ); + }); + it('Should encode data to execute freezeRequest().', () => { + expect(Escrow.encodeFreezeRequest(validRequest, wallet)).toBe( + `0x82865e9d00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008${values.paymentReference}000000000000000000000000000000000000000000000000`, + ); + }); + it('Should encode data to execute initiateEmergencyClaim().', () => { + expect(Escrow.encodeInitiateEmergencyClaim(validRequest, wallet)).toBe( + `0x3a322d4500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008${values.paymentReference}000000000000000000000000000000000000000000000000`, + ); + }); + it('Should encode data to execute completeEmergencyClaim().', () => { + expect(Escrow.encodeCompleteEmergencyClaim(validRequest, wallet)).toBe( + `0x6662e1e000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008${values.paymentReference}000000000000000000000000000000000000000000000000`, + ); + }); + it('Should encode data to execute revertEmergencyClaim().', () => { + expect(Escrow.encodeRevertEmergencyClaim(validRequest, wallet)).toBe( + `0x0797560800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008${values.paymentReference}000000000000000000000000000000000000000000000000`, + ); + }); + it('Should encode data to execute refundFrozenFunds().', () => { + expect(Escrow.encodeRefundFrozenFunds(validRequest, wallet)).toBe( + `0x1a77f53a00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008${values.paymentReference}000000000000000000000000000000000000000000000000`, + ); + }); + }); + + describe('Main use cases:', () => { + beforeEach(async () => { + await Escrow.approveErc20ForEscrow(validRequest, erc20ContractAddress, wallet); + }); + + describe('Normal Flow:', () => { + it('Should pay the amount and fee from payers account', async () => { + const request = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; + + const payerBeforeBalance = await getErc20Balance(request, payerAddress); + const escrowBeforeBalance = await getErc20Balance(request, escrowAddress); + const feeBeforeBalance = await getErc20Balance(request, feeAddress); + + await Escrow.payEscrow(request, wallet, undefined, undefined); + + const payerAfterBalance = await getErc20Balance(request, payerAddress); + const escrowAfterBalance = await getErc20Balance(request, escrowAddress); + const feeAfterBalance = await getErc20Balance(request, feeAddress); + + // Expect payer ERC20 balance should be lower. + expect( + BigNumber.from(payerAfterBalance).eq(BigNumber.from(payerBeforeBalance).sub(102)), + ).toBeTruthy(); + // Expect fee ERC20 balance should be higher. + expect( + BigNumber.from(feeAfterBalance).eq(BigNumber.from(feeBeforeBalance).add(2)), + ).toBeTruthy(); + // Expect escrow Erc20 balance should be higher. + expect( + BigNumber.from(escrowAfterBalance).eq(BigNumber.from(escrowBeforeBalance).add(100)), + ).toBeTruthy(); + }); + it('Should withdraw funds and pay funds from escrow to payee', async () => { + // Set a new requestID to test independent unit-tests. + const request = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = 'aabb'; + + // Execute payEscrow + await Escrow.payEscrow(request, wallet, undefined, undefined); + + // Stores balance after payEscrow(), and before withdraws. + const payeeBeforeBalance = await getErc20Balance(request, paymentAddress); + const escrowBeforeBalance = await getErc20Balance(request, escrowAddress); + + await Escrow.payRequestFromEscrow(request, wallet); + + // Stores balances after withdraws to compare before balance with after balance. + const payeeAfterBalance = await getErc20Balance(request, paymentAddress); + const escrowAfterBalance = await getErc20Balance(request, escrowAddress); + + // Expect escrow Erc20 balance should be lower. + expect( + BigNumber.from(escrowAfterBalance).eq(BigNumber.from(escrowBeforeBalance).sub(100)), + ).toBeTruthy(); + // Expect payee ERC20 balance should be higher. + expect( + BigNumber.from(payeeAfterBalance).eq(BigNumber.from(payeeBeforeBalance).add(100)), + ).toBeTruthy(); + }); + }); + + describe('Emergency Flow:', () => { + it('Should initiate emergency claim', async () => { + // Set a new requestID to test independent unit-tests. + const request = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = 'aacc'; + + // Assign the paymentAddress as the payee. + const payee = getSigner(provider, paymentAddress); + + // Execute payEscrow. + expect( + await (await Escrow.payEscrow(request, wallet, undefined, undefined)).wait(1), + ).toBeTruthy(); + + // Payer initiate emergency claim. + const tx = await Escrow.initiateEmergencyClaim(request, payee); + const confirmedTx = await tx.wait(1); + + // Checks the status and tx.hash. + expect(confirmedTx.status).toBe(1); + expect(tx.hash).toBeDefined(); + }); + it('Should revert emergency claim', async () => { + // Set a new requestID to test independent unit-tests. + const request = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = 'aadd'; + + // Assign the paymentAddress as the payee. + const payee = getSigner(provider, paymentAddress); + + // Execute payEscrow. + await (await Escrow.payEscrow(request, wallet, undefined, undefined)).wait(1); + + // Payer initiate emergency claim. + await (await Escrow.initiateEmergencyClaim(request, payee)).wait(1); + + // Payer reverts the emergency claim. + const tx = await Escrow.revertEmergencyClaim(request, wallet); + const confirmedTx = await tx.wait(1); + + // Checks the status and tx.hash. + expect(confirmedTx.status).toBe(1); + expect(tx.hash).toBeDefined(); + }); + }); + + describe('Freeze Request Flow:', () => { + it('Should freeze funds:', async () => { + // Set a new requestID to test independent unit-tests. + const request = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = 'aaee'; + + // Execute payEscrow function on smart contract. + await (await Escrow.payEscrow(request, wallet, undefined, undefined)).wait(1); + + // Payer freeze escrow funds. + const tx = await Escrow.freezeRequest(request, wallet); + const confirmedTx = await tx.wait(1); + + // Checks the status and tx.hash. + expect(confirmedTx.status).toBe(1); + expect(tx.hash).toBeDefined(); + }); + it('Should revert if tried to withdraw to early:', async () => { + // Set a new requestID to test independent unit-tests. + const request = Utils.deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = 'aaff'; + + // Execute payEscrow. + await (await Escrow.payEscrow(request, wallet, undefined, undefined)).wait(1); + + // Payer executes a freeze of escrow funds. + await (await Escrow.freezeRequest(request, wallet)).wait(1); + + // Payer tries to withdraw frozen funds before unlock date. + await expect(Escrow.refundFrozenFunds(request, wallet)).rejects.toThrowError('Not Yet!',); + }); + }); + }); +}); diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/0.1.0.json new file mode 100644 index 0000000000..b072dae8b7 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/0.1.0.json @@ -0,0 +1,297 @@ +{ +"abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentProxyAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + } + ], + "name": "InitiatedEmergencyClaim", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + } + ], + "name": "RequestFrozen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + } + ], + "name": "RevertedEmergencyClaim", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "feeAddress", + "type": "address" + } + ], + "name": "TransferWithReferenceAndFee", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_erc20Address", + "type": "address" + } + ], + "name": "approvePaymentProxyToSpend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_paymentRef", + "type": "bytes" + } + ], + "name": "completeEmergencyClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_paymentRef", + "type": "bytes" + } + ], + "name": "freezeRequest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_paymentRef", + "type": "bytes" + } + ], + "name": "initiateEmergencyClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_paymentRef", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "_feeAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "payEscrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_paymentRef", + "type": "bytes" + } + ], + "name": "payRequestFromEscrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paymentProxy", + "outputs": [ + { + "internalType": "contract IERC20FeeProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_paymentRef", + "type": "bytes" + } + ], + "name": "refundFrozenFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "requestMapping", + "outputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "payee", + "type": "address" + }, + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "unlockDate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "emergencyClaimDate", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "emergencyState", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isFrozen", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_paymentRef", + "type": "bytes" + } + ], + "name": "revertEmergencyClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] +} \ No newline at end of file diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/index.ts new file mode 100644 index 0000000000..5c12818e80 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/ERC20EscrowToPay/index.ts @@ -0,0 +1,28 @@ +import { ContractArtifact } from '../../ContractArtifact'; + +import { abi as ABI_0_1_0 } from './0.1.0.json'; +// @ts-ignore Cannot find module +import type { ERC20EscrowToPay } from '../../../types/ERC2EscrowToPay'; + +export const erc20EscrowToPayArtifact = new ContractArtifact( + { + '0.1.0': { + abi: ABI_0_1_0, + deployment: { + private: { + address: '0xF08dF3eFDD854FEDE77Ed3b2E515090EEe765154', + creationBlockNumber: 0, + }, + mainnet: { + address: '', + creationBlockNumber: 0, + }, + rinkeby: { + address: '0x8230e703B1c4467A4543422b2cC3284133B9AB5e', + creationBlockNumber: 9669613, + }, + }, + }, + }, + '0.1.0', +); diff --git a/packages/smart-contracts/src/lib/artifacts/index.ts b/packages/smart-contracts/src/lib/artifacts/index.ts index b20aa2919a..83666a933c 100644 --- a/packages/smart-contracts/src/lib/artifacts/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/index.ts @@ -10,7 +10,7 @@ export * from './Erc20SwapConversion'; export * from './EthereumProxy'; export * from './EthereumFeeProxy'; export * from './EthConversionProxy'; - +export * from './ERC20EscrowToPay'; /** * Request Storage */