From 6ee11ed2d78ae63605bfcc74512c98e05ee4b617 Mon Sep 17 00:00:00 2001 From: vbasiuk Date: Mon, 2 Dec 2024 19:36:44 +0200 Subject: [PATCH] add amount convertion and fix expirationDate (unix not ISO string) --- src/iden3comm/handlers/payment.ts | 63 ++++++--- src/storage/blockchain/abi/ERC20.json | 1 + .../{erc20-permit-sig.ts => erc20-helper.ts} | 20 ++- src/storage/blockchain/index.ts | 2 +- src/verifiable/constants.ts | 1 + tests/handlers/payment.test.ts | 120 ++++++++++++------ 6 files changed, 147 insertions(+), 60 deletions(-) create mode 100644 src/storage/blockchain/abi/ERC20.json rename src/storage/blockchain/{erc20-permit-sig.ts => erc20-helper.ts} (69%) diff --git a/src/iden3comm/handlers/payment.ts b/src/iden3comm/handlers/payment.ts index 03154e99..dc2f8d10 100644 --- a/src/iden3comm/handlers/payment.ts +++ b/src/iden3comm/handlers/payment.ts @@ -33,7 +33,7 @@ import { SupportedCurrencies, SupportedPaymentProofType } from '../../verifiable'; -import { Signer, ethers } from 'ethers'; +import { Signer, ethers, parseEther, parseUnits } from 'ethers'; import { Resolvable } from 'did-resolver'; import { verifyExpiresTime } from './common'; @@ -85,20 +85,24 @@ export async function verifyEIP712TypedData( data: Iden3PaymentRailsRequestV1 | Iden3PaymentRailsERC20RequestV1, resolver: Resolvable ): Promise { + const convertedAmount = await convertPaymentAmount( + data.amount, + data.currency as SupportedCurrencies + ); const paymentData = data.type === PaymentRequestDataType.Iden3PaymentRailsRequestV1 ? { recipient: data.recipient, - amount: data.amount, - expirationDate: new Date(data.expirationDate).getTime(), + amount: convertedAmount, + expirationDate: data.expirationDate, nonce: data.nonce, metadata: '0x' } : { tokenAddress: data.tokenAddress, recipient: data.recipient, - amount: data.amount, - expirationDate: new Date(data.expirationDate).getTime(), + amount: convertedAmount, + expirationDate: data.expirationDate, nonce: data.nonce, metadata: '0x' }; @@ -130,6 +134,34 @@ export async function verifyEIP712TypedData( throw new Error(`failed request. no matching verificationMethod`); } +export async function convertPaymentAmount( + amount: string, + currency: SupportedCurrencies +): Promise { + let convertedAmount = 0n; + switch (currency) { + case SupportedCurrencies.ETH: + case SupportedCurrencies.POL: + case SupportedCurrencies.MATIC: + convertedAmount = parseEther(amount.toString()); + break; + case SupportedCurrencies.ETH_WEI: + convertedAmount = parseUnits(amount.toString(), 'wei'); + break; + case SupportedCurrencies.ETH_GWEI: + convertedAmount = parseUnits(amount.toString(), 'gwei'); + break; + case SupportedCurrencies.USDC: + case SupportedCurrencies.USDT: { + convertedAmount = parseUnits(amount.toString(), 6); + break; + } + default: + throw new Error(`failed request. unsupported currency ${currency}`); + } + return convertedAmount; +} + /** * @beta * PaymentRailsInfo represents payment info for payment rails @@ -149,10 +181,10 @@ export type PaymentRailsInfo = { */ export type PaymentRailsChainInfo = { nonce: bigint; - amount: bigint; + amount: string; currency: SupportedCurrencies | string; chainId: string; - expirationDate?: string; + expirationDate?: Date; features?: PaymentFeatures[]; type: | PaymentRequestDataType.Iden3PaymentRailsRequestV1 @@ -514,27 +546,28 @@ export class PaymentHandler if (type === PaymentRequestDataType.Iden3PaymentRailsERC20RequestV1 && !tokenAddress) { throw new Error(`failed request. no token address for currency ${currency}`); } - const expirationTime = expirationDate - ? new Date(expirationDate).getTime() - : new Date(new Date().setHours(new Date().getHours() + 1)).getTime(); + const expirationDateRequired = + expirationDate ?? new Date(new Date().setHours(new Date().getHours() + 1)); const typeUrl = `https://schema.iden3.io/core/json/${type}.json`; const typesFetchResult = await fetch(typeUrl); const types = await typesFetchResult.json(); delete types.EIP712Domain; + + const convertedAmount = await convertPaymentAmount(amount, currency as SupportedCurrencies); const paymentData = type === PaymentRequestDataType.Iden3PaymentRailsRequestV1 ? { recipient, - amount, - expirationDate: expirationTime, + amount: convertedAmount, + expirationDate: getUnixTimestamp(expirationDateRequired), nonce, metadata: '0x' } : { tokenAddress, recipient, - amount, - expirationDate: expirationTime, + amount: convertedAmount, + expirationDate: getUnixTimestamp(expirationDateRequired), nonce, metadata: '0x' }; @@ -570,7 +603,7 @@ export class PaymentHandler recipient, amount: amount.toString(), currency, - expirationDate: new Date(expirationTime).toISOString(), + expirationDate: getUnixTimestamp(expirationDateRequired).toString(), nonce: nonce.toString(), metadata: '0x', proof diff --git a/src/storage/blockchain/abi/ERC20.json b/src/storage/blockchain/abi/ERC20.json new file mode 100644 index 00000000..5beab314 --- /dev/null +++ b/src/storage/blockchain/abi/ERC20.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"uint256","name":"initialSupply","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"allowance","type":"uint256"},{"internalType":"uint256","name":"needed","type":"uint256"}],"name":"ERC20InsufficientAllowance","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"balance","type":"uint256"},{"internalType":"uint256","name":"needed","type":"uint256"}],"name":"ERC20InsufficientBalance","type":"error"},{"inputs":[{"internalType":"address","name":"approver","type":"address"}],"name":"ERC20InvalidApprover","type":"error"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"}],"name":"ERC20InvalidReceiver","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"ERC20InvalidSender","type":"error"},{"inputs":[{"internalType":"address","name":"spender","type":"address"}],"name":"ERC20InvalidSpender","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/src/storage/blockchain/erc20-permit-sig.ts b/src/storage/blockchain/erc20-helper.ts similarity index 69% rename from src/storage/blockchain/erc20-permit-sig.ts rename to src/storage/blockchain/erc20-helper.ts index 86b9ff7b..f848f9e5 100644 --- a/src/storage/blockchain/erc20-permit-sig.ts +++ b/src/storage/blockchain/erc20-helper.ts @@ -1,6 +1,7 @@ -import { Contract, Signer } from 'ethers'; +import { Contract, Signer, ethers } from 'ethers'; -import abi from './abi/ERC20Permit.json'; +import permitAbi from './abi/ERC20Permit.json'; +import erc20Abi from './abi/ERC20.json'; /** * @beta @@ -19,7 +20,7 @@ export async function getPermitSignature( value: bigint, deadline: number ) { - const erc20PermitContract = new Contract(tokenAddress, abi, signer); + const erc20PermitContract = new Contract(tokenAddress, permitAbi, signer); const nonce = await erc20PermitContract.nonces(await signer.getAddress()); const domainData = await erc20PermitContract.eip712Domain(); const domain = { @@ -49,3 +50,16 @@ export async function getPermitSignature( return signer.signTypedData(domain, types, message); } + +/** + * @beta + * getERC20Decimals is a function to retrieve the number of decimals of an ERC20 token + * @param {string} tokenAddress - Token address + */ +export async function getERC20Decimals( + tokenAddress: string, + runner: ethers.ContractRunner +): Promise { + const erc20Contract = new Contract(tokenAddress, erc20Abi, runner); + return erc20Contract.decimals(); +} diff --git a/src/storage/blockchain/index.ts b/src/storage/blockchain/index.ts index 48ea4b4d..4f59682a 100644 --- a/src/storage/blockchain/index.ts +++ b/src/storage/blockchain/index.ts @@ -2,4 +2,4 @@ export * from './state'; export * from './onchain-zkp-verifier'; export * from './onchain-revocation'; export * from './did-resolver-readonly-storage'; -export * from './erc20-permit-sig'; +export * from './erc20-helper'; diff --git a/src/verifiable/constants.ts b/src/verifiable/constants.ts index 3efd35ab..1ed6eb8f 100644 --- a/src/verifiable/constants.ts +++ b/src/verifiable/constants.ts @@ -154,6 +154,7 @@ export enum SupportedCurrencies { ETH_WEI = 'ETHWEI', ETH_GWEI = 'ETHGWEI', MATIC = 'MATIC', + POL = 'POL', USDT = 'USDT', USDC = 'USDC' } diff --git a/tests/handlers/payment.test.ts b/tests/handlers/payment.test.ts index af08c630..656b8dde 100644 --- a/tests/handlers/payment.test.ts +++ b/tests/handlers/payment.test.ts @@ -37,6 +37,7 @@ import path from 'path'; import { MediaType, PROTOCOL_MESSAGE_TYPE } from '../../src/iden3comm/constants'; import { DID } from '@iden3/js-iden3-core'; import { + convertPaymentAmount, createPayment, createPaymentRequest, IPaymentHandler, @@ -334,15 +335,19 @@ describe('payment-request handler', () => { mcPayContractAbi, ethSigner ); + const convertedAmount = await convertPaymentAmount( + data.amount, + data.currency as SupportedCurrencies + ); const paymentData = { recipient: data.recipient, - amount: data.amount, - expirationDate: new Date(data.expirationDate).getTime(), + amount: convertedAmount, + expirationDate: data.expirationDate, nonce: data.nonce, metadata: data.metadata }; - const options = { value: data.amount }; + const options = { value: convertedAmount }; const txData = await payContract.pay(paymentData, data.proof[0].proofValue, options); return txData.hash; } else if (data.type == PaymentRequestDataType.Iden3PaymentRailsERC20RequestV1) { @@ -351,11 +356,15 @@ describe('payment-request handler', () => { mcPayContractAbi, ethSigner ); + const convertedAmount = await convertPaymentAmount( + data.amount, + data.currency as SupportedCurrencies + ); const paymentData = { tokenAddress: data.tokenAddress, recipient: data.recipient, - amount: data.amount, - expirationDate: new Date(data.expirationDate).getTime(), + amount: convertedAmount, + expirationDate: data.expirationDate, nonce: data.nonce, metadata: data.metadata }; @@ -365,8 +374,8 @@ describe('payment-request handler', () => { ethSigner, data.tokenAddress, await payContract.getAddress(), - BigInt(data.amount), - new Date(data.expirationDate).getTime() + convertedAmount, + +data.expirationDate ); const txData = await payContract.payERC20Permit( permitSignature, @@ -456,17 +465,17 @@ describe('payment-request handler', () => { 'https://w3id.org/security/suites/eip712sig-2021/v1' ], recipient: '0xE9D7fCDf32dF4772A7EF7C24c76aB40E4A42274a', - amount: '40', - currency: SupportedCurrencies.ETH_WEI, - expirationDate: '2044-11-07T12:45:00.000Z', - nonce: '40024', + amount: '0.00003', + currency: SupportedCurrencies.ETH_GWEI, + expirationDate: '2362135500', + nonce: '411390', metadata: '0x', proof: [ { type: SupportedPaymentProofType.EthereumEip712Signature2021, proofPurpose: 'assertionMethod', proofValue: - '0x756e11c55fe8f4d2867c2e14e52a06baba29e4b789b4521aafa1623ad96c67aa23dc042bfebd4711ed2db5f145c853a5487b878d8e113e1ede0c41553f6318dd1c', + '0xb93fb0ddd4d89c7ae7fcdc7a675f7a949766bc385804adc63f7f5340de68625b21dd046e9d8698d78b3c67ca84f640de1b8eae865092f633c0e6e198d0d5cc971b', verificationMethod: 'did:pkh:eip155:80002:0xE9D7fCDf32dF4772A7EF7C24c76aB40E4A42274a', created: new Date().toISOString(), eip712: { @@ -476,7 +485,7 @@ describe('payment-request handler', () => { name: 'MCPayment', version: '1.0.0', chainId: '80002', - verifyingContract: '0x380dd90852d3Fe75B4f08D0c47416D6c4E0dC774' + verifyingContract: '0xF8E49b922D5Fb00d3EdD12bd14064f275726D339' } } } @@ -502,17 +511,17 @@ describe('payment-request handler', () => { ], tokenAddress: '0x2FE40749812FAC39a0F380649eF59E01bccf3a1A', recipient: '0xE9D7fCDf32dF4772A7EF7C24c76aB40E4A42274a', - amount: '40', - currency: 'TST', - expirationDate: '2044-11-07T12:45:00.000Z', - nonce: '40024', + amount: '0.00003', + currency: 'USDT', + expirationDate: '2362135500', + nonce: '411390', metadata: '0x', proof: [ { type: SupportedPaymentProofType.EthereumEip712Signature2021, proofPurpose: 'assertionMethod', proofValue: - '0xcb5a8d39a536768fabaafbf17f24954acf7c7d7a6f9a8b75ad5f9c29d324cdaf63de8cebfde508a5a03b60e1a4b765b21f7f3cd60dfed27ce5208432e3fd4c481b', + '0xfcc7eef264932f2a1a388a1479299f1614f11ffcb7d885f5d369b5ad202b03023f1a2fa85c70b4e08d8a0e5c97464f31c593bcc3bf9a92ada79ea8a653f199681c', verificationMethod: 'did:pkh:eip155:80002:0xE9D7fCDf32dF4772A7EF7C24c76aB40E4A42274a', created: new Date().toISOString(), eip712: { @@ -522,7 +531,7 @@ describe('payment-request handler', () => { name: 'MCPayment', version: '1.0.0', chainId: '80002', - verifyingContract: '0x380dd90852d3Fe75B4f08D0c47416D6c4E0dC774' + verifyingContract: '0xF8E49b922D5Fb00d3EdD12bd14064f275726D339' } } } @@ -592,10 +601,10 @@ describe('payment-request handler', () => { multiChainPaymentConfig: [ { chainId: '80002', - paymentContract: '0x380dd90852d3Fe75B4f08D0c47416D6c4E0dC774', + paymentContract: '0xF8E49b922D5Fb00d3EdD12bd14064f275726D339', recipient: '0xE9D7fCDf32dF4772A7EF7C24c76aB40E4A42274a', erc20TokenAddressArr: [ - { symbol: 'TST', address: '0x2FE40749812FAC39a0F380649eF59E01bccf3a1A' } + { symbol: 'USDT', address: '0x2FE40749812FAC39a0F380649eF59E01bccf3a1A' } ] }, { @@ -658,7 +667,7 @@ describe('payment-request handler', () => { ); const agentMessageBytes = await paymentHandler.handlePaymentRequest(msgBytesRequest, { paymentHandler: paymentHandlerFuncMock, - nonce: '40024' + nonce: '411390' }); if (!agentMessageBytes) { fail('handlePaymentRequest is not expected null response'); @@ -681,7 +690,7 @@ describe('payment-request handler', () => { ); const agentMessageBytes = await paymentHandler.handlePaymentRequest(msgBytesRequest, { paymentHandler: paymentHandlerFuncMock, - nonce: '40024', + nonce: '411390', erc20TokenApproveHandler: () => Promise.resolve('0x312312334') }); if (!agentMessageBytes) { @@ -826,15 +835,15 @@ describe('payment-request handler', () => { description: 'Iden3PaymentRailsRequestV1 payment-request integration test', chains: [ { - nonce: 100046n, - amount: 100n, - currency: SupportedCurrencies.ETH_WEI, + nonce: 1000412n, + amount: '0.001', + currency: SupportedCurrencies.ETH_GWEI, chainId: '80002', type: PaymentRequestDataType.Iden3PaymentRailsRequestV1 }, { - nonce: 10001112n, - amount: 10000n, + nonce: 10001123n, + amount: '2233', currency: SupportedCurrencies.ETH_WEI, chainId: '1101', type: PaymentRequestDataType.Iden3PaymentRailsRequestV1 @@ -851,7 +860,7 @@ describe('payment-request handler', () => { ); const agentMessageBytes = await paymentHandler.handlePaymentRequest(msgBytesRequest, { paymentHandler: paymentIntegrationHandlerFunc('', ''), - nonce: '100046' + nonce: '1000412' }); if (!agentMessageBytes) { fail('handlePaymentRequest is not expected null response'); @@ -882,15 +891,15 @@ describe('payment-request handler', () => { description: 'Iden3PaymentRailsERC20RequestV1 payment-request integration test', chains: [ { - nonce: 22005n, - amount: 30n, - currency: 'TST', + nonce: 220011n, + amount: '0.000001', + currency: 'USDT', chainId: '80002', type: PaymentRequestDataType.Iden3PaymentRailsERC20RequestV1 }, { - nonce: 220011122n, - amount: 30n, + nonce: 220011124n, + amount: '0.000001', currency: SupportedCurrencies.USDT, chainId: '1101', type: PaymentRequestDataType.Iden3PaymentRailsERC20RequestV1 @@ -900,6 +909,8 @@ describe('payment-request handler', () => { ] ); + console.log(JSON.stringify(paymentRequest)); + const msgBytesRequest = await packageManager.pack( MediaType.PlainMessage, byteEncoder.encode(JSON.stringify(paymentRequest)), @@ -907,12 +918,16 @@ describe('payment-request handler', () => { ); const agentMessageBytes = await paymentHandler.handlePaymentRequest(msgBytesRequest, { paymentHandler: paymentIntegrationHandlerFunc('', ''), - nonce: '22005', + nonce: '220011', erc20TokenApproveHandler: async (data: Iden3PaymentRailsERC20RequestV1) => { const token = new Contract(data.tokenAddress, erc20Abi, ethSigner); + const convertedAmount = await convertPaymentAmount( + data.amount, + data.currency as SupportedCurrencies + ); const txData = await token.approve( data.proof[0].eip712.domain.verifyingContract, - data.amount + convertedAmount ); await txData.wait(1); return txData.hash; @@ -948,15 +963,15 @@ describe('payment-request handler', () => { chains: [ { features: [PaymentFeatures.EIP_2612], - nonce: 330003n, - amount: 30n, - currency: 'TST', + nonce: 330004n, + amount: '0.000002', + currency: 'USDT', chainId: '80002', type: PaymentRequestDataType.Iden3PaymentRailsERC20RequestV1 }, { nonce: 330001122n, - amount: 30n, + amount: '0.000002', currency: SupportedCurrencies.USDT, chainId: '1101', type: PaymentRequestDataType.Iden3PaymentRailsERC20RequestV1 @@ -973,7 +988,7 @@ describe('payment-request handler', () => { ); const agentMessageBytes = await paymentHandler.handlePaymentRequest(msgBytesRequest, { paymentHandler: paymentIntegrationHandlerFunc('', ''), - nonce: '330003' + nonce: '330004' }); if (!agentMessageBytes) { fail('handlePaymentRequest is not expected null response'); @@ -1054,4 +1069,27 @@ describe('payment-request handler', () => { paymentValidationHandler: paymentValidationIntegrationHandlerFunc }); }); + + it('convertPaymentAmount tests', async () => { + const convertedWei = await convertPaymentAmount('100', SupportedCurrencies.ETH_WEI); + expect(convertedWei).to.be.eq(BigInt(100)); + + const convertedGwei = await convertPaymentAmount('100', SupportedCurrencies.ETH_GWEI); + expect(convertedGwei).to.be.eq(BigInt(100000000000)); + + const convertedEth = await convertPaymentAmount('100', SupportedCurrencies.ETH); + expect(convertedEth).to.be.eq(BigInt(100000000000000000000)); + + const convertedMatic = await convertPaymentAmount('100', SupportedCurrencies.MATIC); + expect(convertedMatic).to.be.eq(BigInt(100000000000000000000)); + + const convertedPol = await convertPaymentAmount('100', SupportedCurrencies.POL); + expect(convertedPol).to.be.eq(BigInt(100000000000000000000)); + + const convertedUsdt = await convertPaymentAmount('100', SupportedCurrencies.USDT); + expect(convertedUsdt).to.be.eq(BigInt(100000000)); + + const convertedUsdc = await convertPaymentAmount('100', SupportedCurrencies.USDC); + expect(convertedUsdc).to.be.eq(BigInt(100000000)); + }); });