Skip to content

feat: adding hinkal private batch payments #1627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/payment-processor/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
],
},
};
4 changes: 2 additions & 2 deletions packages/payment-processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.15",
"@openzeppelin/contracts": "4.9.6",
"@requestnetwork/currency": "0.28.0",
"@requestnetwork/payment-detection": "0.54.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<ContractTransaction> {
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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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)

Expand Down Expand Up @@ -129,7 +131,7 @@ const getTokenShieldedBalance = async (
address: string,
tokenAddress = currencyAddress,
): Promise<bigint> => {
const balances = await hinkalStore[address].getBalances();
const balances = await hinkalStore[address].getTotalBalance();
const tokenBalance = balances.find(
(balance) => balance.token.erc20TokenAddress === tokenAddress,
)?.balance;
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
});
});
});
39 changes: 32 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected].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/[email protected].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"
Expand Down Expand Up @@ -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==
Expand All @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -26998,7 +27014,7 @@ [email protected]:
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==
Expand Down Expand Up @@ -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"
Expand Down