From 9e882e639a69a6cd9bbf0ef01ac78c360a42c27a Mon Sep 17 00:00:00 2001 From: nkoreli Date: Sun, 25 May 2025 11:41:24 -0700 Subject: [PATCH 1/2] adding-hinkal-batch-function-with-test --- packages/payment-processor/jest.config.js | 10 +++ packages/payment-processor/package.json | 4 +- .../payment/erc-20-private-payment-hinkal.ts | 82 ++++++++++++++----- .../erc-20-private-payment-hinkal.test.ts | 50 ++++++++++- yarn.lock | 39 +++++++-- 5 files changed, 153 insertions(+), 32 deletions(-) diff --git a/packages/payment-processor/jest.config.js b/packages/payment-processor/jest.config.js index be20f1cb6c..f387ce714a 100644 --- a/packages/payment-processor/jest.config.js +++ b/packages/payment-processor/jest.config.js @@ -3,4 +3,14 @@ const jestCommonConfig = require('../../jest.config'); /** @type {import('jest').Config} */ module.exports = { ...jestCommonConfig, + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + tsconfig: { + isolatedModules: process.env.ISOLATED_MODULES !== 'false', + }, + }, + ], + }, }; diff --git a/packages/payment-processor/package.json b/packages/payment-processor/package.json index 1fd38dd584..16b774b6be 100644 --- a/packages/payment-processor/package.json +++ b/packages/payment-processor/package.json @@ -36,11 +36,11 @@ "lint:check": "eslint .", "prepare": "yarn run build", "test": "jest --testPathIgnorePatterns test/payment/erc-20-private-payment-hinkal.test.ts --runInBand", - "test:hinkal": "jest test/payment/erc-20-private-payment-hinkal.test.ts --runInBand", + "test:hinkal": "ISOLATED_MODULES=false jest test/payment/erc-20-private-payment-hinkal.test.ts --runInBand", "test:watch": "yarn test --watch" }, "dependencies": { - "@hinkal/common": "0.2.12", + "@hinkal/common": "0.2.14", "@openzeppelin/contracts": "4.9.6", "@requestnetwork/currency": "0.28.0", "@requestnetwork/payment-detection": "0.54.0", diff --git a/packages/payment-processor/src/payment/erc-20-private-payment-hinkal.ts b/packages/payment-processor/src/payment/erc-20-private-payment-hinkal.ts index 97fd1ae811..1b77a976b4 100644 --- a/packages/payment-processor/src/payment/erc-20-private-payment-hinkal.ts +++ b/packages/payment-processor/src/payment/erc-20-private-payment-hinkal.ts @@ -18,7 +18,7 @@ import { validateRequest, } from './utils'; import { IPreparedPrivateTransaction } from './prepared-transaction'; -import type { IHinkal, RelayerTransaction } from '@hinkal/common'; +import { getERC20Token, type IHinkal, type RelayerTransaction } from '@hinkal/common'; /** * This is a globally accessible state variable exported for use in other parts of the application or tests. @@ -62,14 +62,47 @@ export async function sendToHinkalShieldedAddressFromPublic( const signer = getSigner(signerOrProvider); const hinkalObject = await addToHinkalStore(signer); + const token = getERC20Token(tokenAddress, await signer.getChainId()); + if (!token) throw Error(); + const amountToPay = BigNumber.from(amount).toBigInt(); if (recipientInfo) { - return hinkalObject.depositForOther([tokenAddress], [amountToPay], recipientInfo); + return hinkalObject.depositForOther([token], [amountToPay], recipientInfo); } else { - return hinkalObject.deposit([tokenAddress], [amountToPay]); + return hinkalObject.deposit([token], [amountToPay]); } } +/** + * Sends a batch of payments to Hinkal shielded addresses from a public address. + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param tokenAddresses the addresses of the ERC20 tokens to send. + * @param amounts the amounts of tokens to send. + * @param recipientInfos include the shielded addresses of the recipients. + */ +export async function sendBatchPaymentsToHinkalShieldedAddressesFromPublic( + signerOrProvider: providers.Provider | Signer = getProvider(), + tokenAddresses: string[], + amounts: BigNumberish[], + recipientInfos: string[], +): Promise { + const signer = getSigner(signerOrProvider); + const hinkalObject = await addToHinkalStore(signer); + const chainId = await signer.getChainId(); + + const tokens = tokenAddresses.map((tokenAddress) => { + const token = getERC20Token(tokenAddress, chainId); + if (!token) throw Error('Token cannot be found'); + return token; + }); + const amountsToPay = amounts.map((amount) => BigNumber.from(amount).toBigInt()); + + console.log({ tokens, amountsToPay, recipientInfos }); + + const tx = await hinkalObject.multiSendPrivateRecipients(tokens, amountsToPay, recipientInfos); + return tx; +} + /** * Processes a transaction to pay privately a request through the ERC20 fee proxy contract. * @param request request to pay. @@ -147,13 +180,16 @@ export async function prepareErc20ProxyPaymentFromHinkalShieldedAddress( const { emporiumOp } = await import('@hinkal/common'); const ops = [ - emporiumOp(tokenContract, 'approve', [proxyContract.address, amountToPay]), - emporiumOp(proxyContract, 'transferFromWithReference', [ - tokenAddress, - paymentAddress, - amountToPay, - `0x${paymentReference}`, - ]), + emporiumOp({ + contract: tokenContract, + func: 'approve', + args: [proxyContract.address, amountToPay], + }), + emporiumOp({ + contract: proxyContract, + func: 'transferFromWithReference', + args: [tokenAddress, paymentAddress, amountToPay, `0x${paymentReference}`], + }), ]; return { @@ -192,15 +228,23 @@ export async function prepareErc20FeeProxyPaymentFromHinkalShieldedAddress( const { emporiumOp } = await import('@hinkal/common'); const ops = [ - emporiumOp(tokenContract, 'approve', [proxyContract.address, totalAmount]), - emporiumOp(proxyContract, 'transferFromWithReferenceAndFee', [ - tokenAddress, - paymentAddress, - amountToPay, - `0x${paymentReference}`, - feeToPay, - feeAddress, - ]), + emporiumOp({ + contract: tokenContract, + func: 'approve', + args: [proxyContract.address, totalAmount], + }), + emporiumOp({ + contract: proxyContract, + func: 'transferFromWithReferenceAndFee', + args: [ + tokenAddress, + paymentAddress, + amountToPay, + `0x${paymentReference}`, + feeToPay, + feeAddress, + ], + }), ]; return { diff --git a/packages/payment-processor/test/payment/erc-20-private-payment-hinkal.test.ts b/packages/payment-processor/test/payment/erc-20-private-payment-hinkal.test.ts index 88ae0b6ccf..ee6df1bde4 100644 --- a/packages/payment-processor/test/payment/erc-20-private-payment-hinkal.test.ts +++ b/packages/payment-processor/test/payment/erc-20-private-payment-hinkal.test.ts @@ -7,13 +7,14 @@ import { hinkalStore, payErc20FeeProxyRequestFromHinkalShieldedAddress, payErc20ProxyRequestFromHinkalShieldedAddress, + sendBatchPaymentsToHinkalShieldedAddressesFromPublic, sendToHinkalShieldedAddressFromPublic, } from '../../src/payment/erc-20-private-payment-hinkal'; import { getErc20Balance } from '../../src/payment/erc20'; // Constants to configure the tests -const currentNetwork: CurrencyTypes.ChainName = 'base'; -const currencyAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC +const currentNetwork: CurrencyTypes.ChainName = 'optimism'; +const currencyAddress = '0x7F5c764cBc14f9669B88837ca1490cCa17c31607'; // USDC const currentCurrenyType = Types.RequestLogic.CURRENCY.ERC20; const currencyAmount = ethers.utils.parseUnits('0.000001', 6).toBigInt(); const currentGateway = 'https://sepolia.gateway.request.network'; @@ -30,8 +31,9 @@ const payerPrivateKey = process.env.HINKAL_TEST_PAYER_PRIVATE_KEY as string; // 2) receives funds on her shielded address // The private key of a public address grant ownership of the corresponding shielded address. In @hinkal/common, a single public address can have only one shielded address. const payeePrivateKey = process.env.HINKAL_TEST_PAYEE_PRIVATE_KEY as string; +const payee2PrivateKey = process.env.HINKAL_TEST_PAYEE2_PRIVATE_KEY as string; -const RPC_URL = 'https://mainnet.base.org'; // Blockchain RPC endpoint for the Base network +const RPC_URL = 'https://mainnet.optimism.io'; // Blockchain RPC endpoint for the Base network jest.setTimeout(1000000); // Set Jest timeout for asynchronous operations (e.g., blockchain calls) @@ -129,7 +131,7 @@ const getTokenShieldedBalance = async ( address: string, tokenAddress = currencyAddress, ): Promise => { - const balances = await hinkalStore[address].getBalances(); + const balances = await hinkalStore[address].getTotalBalance(); const tokenBalance = balances.find( (balance) => balance.token.erc20TokenAddress === tokenAddress, )?.balance; @@ -144,14 +146,22 @@ describe('ERC-20 Private Payments With Hinkal', () => { let provider: providers.Provider; let payerWallet: ethers.Wallet; let payeeWallet: ethers.Wallet; + let payee2Wallet: ethers.Wallet; let payeeShieldedAddress: string; + let recipientInfos: string[]; beforeAll(async () => { provider = new ethers.providers.JsonRpcProvider(RPC_URL); payerWallet = new Wallet(payerPrivateKey, provider); payeeWallet = new Wallet(payeePrivateKey, provider); + payee2Wallet = new Wallet(payee2PrivateKey, provider); await addToHinkalStore(payerWallet); await addToHinkalStore(payeeWallet); + await addToHinkalStore(payee2Wallet); payeeShieldedAddress = hinkalStore[payeeWallet.address].getRecipientInfo(); + recipientInfos = [ + hinkalStore[payeeWallet.address].getRecipientInfo(true), + hinkalStore[payee2Wallet.address].getRecipientInfo(true), + ]; }); afterAll(async () => { for (const key in hinkalStore) { @@ -255,5 +265,37 @@ describe('ERC-20 Private Payments With Hinkal', () => { expect(payeeShieldedAddress).not.toBe(payeeWallet.address); // trivial check (satisfies 2nd condition) expect(postUsdcBalance - preUsdcBalance).toBe(currencyAmount); // The payee received funds in their shielded account. }); + + it('The payer sends a batch of payments from the EOA to the shielded addresses of two payees', async () => { + // Objectives of this test: + // 1. The payees' addresses should never appear on-chain. + // 2. The payees should successfully receive the funds. + // 3. The transaction should complete successfully. + + const preUsdcBalance1 = await getTokenShieldedBalance(payeeWallet.address); + const preUsdcBalance2 = await getTokenShieldedBalance(payee2Wallet.address); + + // sending the same tokens here + const erc20Array = [currencyAddress, currencyAddress]; + const amounts = [currencyAmount, currencyAmount]; + + const tx = await sendBatchPaymentsToHinkalShieldedAddressesFromPublic( + payerWallet, + erc20Array, + amounts, + recipientInfos, + ); + + const waitedTx = await tx.wait(2); + await waitLittle(1); // wait before balance is increased + + const postUsdcBalance1 = await getTokenShieldedBalance(payeeWallet.address); + const postUsdcBalance2 = await getTokenShieldedBalance(payee2Wallet.address); + + expect(waitedTx.status).toBe(1); + expect(payeeShieldedAddress).not.toBe(payeeWallet.address); // trivial check (satisfies 2nd condition) + expect(postUsdcBalance1 - preUsdcBalance1).toBe(currencyAmount); // The payee received funds in their shielded account. + expect(postUsdcBalance2 - preUsdcBalance2).toBe(currencyAmount); // The payee received funds in their shielded account. + }); }); }); diff --git a/yarn.lock b/yarn.lock index f4d203cf90..0a4bc5fbbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3789,10 +3789,10 @@ resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== -"@hinkal/common@0.2.12": - version "0.2.12" - resolved "https://registry.yarnpkg.com/@hinkal/common/-/common-0.2.12.tgz#27ced11251cc8926187f582333d7fb13d7d028fe" - integrity sha512-kpaS8E6jn9/m0OEQU6zcIXbfxqolycVSCNAHwBWadTQm6OGf9DfUq/Fwjdnr+fHI+aRI8EYBZeyB4+sG7SquQw== +"@hinkal/common@0.2.14": + version "0.2.14" + resolved "https://registry.yarnpkg.com/@hinkal/common/-/common-0.2.14.tgz#6642f8b65500e9870e6aab73981aee8124d85e74" + integrity sha512-EKXwedlYmx97YwwmgSvQ34T1JKnEwZ3+0Hq//I4bo0JllM8AEayDDv0BLnhtOlqZydK7fXS1L/VSg566JT6cew== dependencies: async-mutex "^0.4.0" axios "^1.6.8" @@ -23611,7 +23611,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -23629,6 +23629,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" @@ -23763,7 +23772,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -23798,6 +23807,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -26998,7 +27014,7 @@ workerpool@6.2.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -27033,6 +27049,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" From 3098ac9102eb32792fabc03335fb4f7571f4e26b Mon Sep 17 00:00:00 2001 From: nkoreli Date: Mon, 26 May 2025 16:28:35 -0700 Subject: [PATCH 2/2] increment-hinkal-version --- packages/payment-processor/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/payment-processor/package.json b/packages/payment-processor/package.json index 16b774b6be..179d5b0f1c 100644 --- a/packages/payment-processor/package.json +++ b/packages/payment-processor/package.json @@ -40,7 +40,7 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@hinkal/common": "0.2.14", + "@hinkal/common": "0.2.15", "@openzeppelin/contracts": "4.9.6", "@requestnetwork/currency": "0.28.0", "@requestnetwork/payment-detection": "0.54.0", diff --git a/yarn.lock b/yarn.lock index 0a4bc5fbbe..d2c3139c45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3789,10 +3789,10 @@ resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== -"@hinkal/common@0.2.14": - version "0.2.14" - resolved "https://registry.yarnpkg.com/@hinkal/common/-/common-0.2.14.tgz#6642f8b65500e9870e6aab73981aee8124d85e74" - integrity sha512-EKXwedlYmx97YwwmgSvQ34T1JKnEwZ3+0Hq//I4bo0JllM8AEayDDv0BLnhtOlqZydK7fXS1L/VSg566JT6cew== +"@hinkal/common@0.2.15": + version "0.2.15" + resolved "https://registry.yarnpkg.com/@hinkal/common/-/common-0.2.15.tgz#4eb49f36feb8d93f000adfcb38ad8ab9f36b585f" + integrity sha512-lZQPHBVFSLpAZDF1o3sS9viNjRYQ/TiZLvLveyDsB7k0zMkvKIqT8CDstH7hzHqnnRwV5j2gymvYSY47U+6Hng== dependencies: async-mutex "^0.4.0" axios "^1.6.8"