Skip to content
Closed
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
5 changes: 5 additions & 0 deletions packages/advanced-logic/src/advanced-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import NativeToken from './extensions/payment-network/native-token';
import AnyToNative from './extensions/payment-network/any-to-native';
import Erc20TransferableReceivablePaymentNetwork from './extensions/payment-network/erc20/transferable-receivable';
import MetaPaymentNetwork from './extensions/payment-network/meta';
import AnyToHinkalWalletErc20ProxyPaymentNetwork from './extensions/payment-network/any-to-hinkal-wallet-erc20-proxy';

/**
* Module to manage Advanced logic extensions
Expand All @@ -51,6 +52,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
anyToNativeToken: AnyToNative[];
erc20TransferableReceivable: Erc20TransferableReceivablePaymentNetwork;
metaPn: MetaPaymentNetwork;
anyToHinkalWalletErc20Proxy: AnyToHinkalWalletErc20ProxyPaymentNetwork;
};

private currencyManager: CurrencyTypes.ICurrencyManager;
Expand All @@ -74,6 +76,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
anyToNativeToken: [new AnyToNear(currencyManager), new AnyToNearTestnet(currencyManager)],
erc20TransferableReceivable: new Erc20TransferableReceivablePaymentNetwork(currencyManager),
metaPn: new MetaPaymentNetwork(currencyManager),
anyToHinkalWalletErc20Proxy: new AnyToHinkalWalletErc20ProxyPaymentNetwork(currencyManager),
};
}

Expand Down Expand Up @@ -135,6 +138,8 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]:
this.extensions.erc20TransferableReceivable,
[ExtensionTypes.PAYMENT_NETWORK_ID.META]: this.extensions.metaPn,
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_HINKAL_WALLET]:
this.extensions.anyToHinkalWalletErc20Proxy,
}[id];

if (!extension) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { ExtensionTypes, CurrencyTypes, RequestLogicTypes } from '@requestnetwork/types';
import { conversionSupportedNetworks, UnsupportedCurrencyError } from '@requestnetwork/currency';
import Erc20FeeProxyPaymentNetwork from './erc20/fee-proxy-contract';

const CURRENT_VERSION = '0.0.1';

export default class AnyToHinkalWalletErc20ProxyPaymentNetwork extends Erc20FeeProxyPaymentNetwork<ExtensionTypes.PnAnyToHinkalWalletErc20.ICreationParameters> {
public constructor(
currencyManager: CurrencyTypes.ICurrencyManager,
extensionId: ExtensionTypes.PAYMENT_NETWORK_ID = ExtensionTypes.PAYMENT_NETWORK_ID
.ERC20_HINKAL_WALLET,
currentVersion: string = CURRENT_VERSION,
) {
super(currencyManager, extensionId, currentVersion);
}
/**
* Creates the extensionsData to create the extension ERC20 fee proxy contract payment detection
*
* @param creationParameters extensions parameters to create
*
* @returns IExtensionCreationAction the extensionsData to be stored in the request
*/
public createCreationAction(
creationParameters: ExtensionTypes.PnAnyToHinkalWalletErc20.ICreationParameters,
): ExtensionTypes.IAction {
if (!creationParameters.acceptedTokens) {
throw Error('acceptedTokens is required');
}
if (creationParameters.acceptedTokens.length === 0) {
throw Error('acceptedTokens cannot be empty');
}
if (creationParameters.acceptedTokens.some((address) => !this.isValidAddress(address))) {
throw Error('acceptedTokens must contains only valid ethereum addresses');
}
const network = creationParameters.network;
this.throwIfInvalidNetwork(network);

for (const address of creationParameters.acceptedTokens) {
const acceptedCurrency = this.currencyManager.fromAddress(address, network);
if (!acceptedCurrency) {
throw new UnsupportedCurrencyError({
value: address,
network,
});
}
if (!this.currencyManager.supportsConversion(acceptedCurrency, network)) {
throw Error(
`acceptedTokens must contain only supported token addresses (ERC20 only). ${address} is not supported for ${network}.`,
);
}
}

return super.createCreationAction(creationParameters);
}

/**
* Applies a creation extension action
*
* @param extensionAction action to apply
* @param timestamp action timestamp
*
* @returns state of the extension created
*/
protected applyCreation(
extensionAction: ExtensionTypes.IAction,
timestamp: number,
): ExtensionTypes.IState {
if (!extensionAction.parameters.network || extensionAction.parameters.network.length === 0) {
throw Error('network is required');
}

if (
!extensionAction.parameters.acceptedTokens ||
extensionAction.parameters.acceptedTokens.length === 0
) {
throw Error('acceptedTokens is required and cannot be empty');
}
if (
extensionAction.parameters.acceptedTokens.some(
(address: string) => !this.isValidAddress(address),
)
) {
throw Error('acceptedTokens must contains only valid ethereum addresses');
}

const feePNCreationAction = super.applyCreation(extensionAction, timestamp);

return {
...feePNCreationAction,
events: [
{
name: 'create',
parameters: {
feeAddress: extensionAction.parameters.feeAddress,
feeAmount: extensionAction.parameters.feeAmount,
paymentAddress: extensionAction.parameters.paymentAddress,
refundAddress: extensionAction.parameters.refundAddress,
salt: extensionAction.parameters.salt,
network: extensionAction.parameters.network,
acceptedTokens: extensionAction.parameters.acceptedTokens,
maxRateTimespan: extensionAction.parameters.maxRateTimespan,
},
timestamp,
},
],
values: {
...feePNCreationAction.values,
network: extensionAction.parameters.network,
acceptedTokens: extensionAction.parameters.acceptedTokens,
maxRateTimespan: extensionAction.parameters.maxRateTimespan,
},
};
}

/**
* Validate the extension action regarding the currency and network
* It must throw in case of error
*/
protected validate(
request: RequestLogicTypes.IRequest,
extensionAction: ExtensionTypes.IAction,
): void {
//TODO: add hinkal network validataion
const network =
extensionAction.action === ExtensionTypes.PnFeeReferenceBased.ACTION.CREATE
? extensionAction.parameters.network
: request.extensions[this.extensionId]?.values.network;
if (!network) {
return;
}

// Nothing can be validated if the network has not been given yet
if (!network) {
return;
}

if (!conversionSupportedNetworks.includes(network)) {
throw new Error(`The network (${network}) is not supported for this payment network.`);
}

const currency = this.currencyManager.fromStorageCurrency(request.currency);
if (!currency) {
throw new UnsupportedCurrencyError(request.currency);
}
if (!this.currencyManager.supportsConversion(currency, network)) {
throw new Error(
`The currency (${currency.id}, ${currency.hash}) of the request is not supported for this payment network.`,
);
}
}
}
2 changes: 2 additions & 0 deletions packages/payment-processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"test:watch": "yarn test --watch"
},
"dependencies": {
"@hinkal/client": "^0.1.8",
"@hinkal/crypto": "^0.1.6",
"@openzeppelin/contracts": "4.9.6",
"@requestnetwork/currency": "0.18.0",
"@requestnetwork/payment-detection": "0.45.0",
Expand Down
84 changes: 84 additions & 0 deletions packages/payment-processor/src/payment/erc20-hinkal-wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { providers } from 'ethers';
import { BigNumberish } from 'ethers';
import { Signer } from 'ethers';
import { ClientTypes, ExtensionTypes } from '@requestnetwork/types';
import {
getAmountToPay,
getProvider,
getRequestPaymentValues,
getSigner,
validateRequest,
} from './utils';
import { EthersProviderAdapter, Hinkal } from '@hinkal/client';
import { ERC20__factory, ERC20Proxy__factory } from '@requestnetwork/smart-contracts/types';
import { RelayerTransaction } from '@hinkal/client/dist/types/relay';
import { emporiumOp } from '@hinkal/crypto';
import { erc20ProxyArtifact } from '@requestnetwork/smart-contracts';
import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection';
import { EvmChains } from '@requestnetwork/currency';

export async function payErc20HinkalWalletProxyRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
amount: BigNumberish,
): Promise<providers.TransactionResponse> {
const signer = getSigner(signerOrProvider);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential issues with 'getSigner' utility.

Ensure that the getSigner utility function properly handles all possible inputs for signerOrProvider. Passing a provider when a signer is required could lead to runtime errors.

Consider adding validation or error handling within getSigner to confirm that a valid signer is returned.


const { transactionHash } = await constructAndSendTransferOp(request, signer, amount);

return await signer.provider!.getTransaction(transactionHash);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid using the non-null assertion operator '!'.

Using the non-null assertion operator ! can lead to runtime errors if signer.provider is null or undefined. It's safer to use optional chaining and handle the potential undefined value appropriately.

Apply this diff to fix the issue:

-return await signer.provider!.getTransaction(transactionHash);
+if (signer.provider) {
+  return await signer.provider.getTransaction(transactionHash);
+} else {
+  throw new Error('Provider is not available on the signer');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return await signer.provider!.getTransaction(transactionHash);
if (signer.provider) {
return await signer.provider.getTransaction(transactionHash);
} else {
throw new Error('Provider is not available on the signer');
}
🧰 Tools
🪛 Biome

[error] 20-20: Forbidden non-null assertion.

Unsafe fix: Replace with optional chain operator ?. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator

(lint/style/noNonNullAssertion)

}

export async function constructAndSendTransferOp(
request: ClientTypes.IRequestData,
signer: Signer,
amount: BigNumberish,
): Promise<RelayerTransaction> {
validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_HINKAL_WALLET);
const hinkalProviderAdapter = new EthersProviderAdapter(signer, await signer.getChainId());

const tokenAddress = request.currencyInfo.value;
const { network } = request.currencyInfo;

const tokenInstance = ERC20__factory.connect(tokenAddress, signer.provider!);

const { paymentReference, paymentAddress } = getRequestPaymentValues(request);

const amountToPay = getAmountToPay(request, amount);

// TODO: calculate/get Hinkal fee

const pn = getPaymentNetworkExtension(request);
// TODO: add hinkal instance support check also
EvmChains.assertChainSupported(network!);

const proxyAddress = erc20ProxyArtifact.getAddress(network, pn?.version);

const proxyContract = ERC20Proxy__factory.connect(proxyAddress, signer.provider!);

const hinkal = new Hinkal();

await hinkal.initProviderAdapter(undefined, hinkalProviderAdapter);

await hinkal.initUserKeys();

await hinkal.resetMerkle();

const approveOp = emporiumOp(tokenInstance, 'approve', [proxyAddress, amountToPay]);

const transferOp = emporiumOp(proxyContract, 'transferFromWithReference', [
tokenAddress,
paymentAddress,
amountToPay,
`0x${paymentReference}`,
]);

return (await hinkal.actionPrivateWallet(
[tokenAddress],
[BigInt(amount.toString())],
[false],
[approveOp, transferOp],
undefined,
false,
)) as RelayerTransaction;
}
4 changes: 4 additions & 0 deletions packages/payment-processor/src/payment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { encodeRequestErc20Approval } from './encoder-approval';
import { encodeRequestPayment } from './encoder-payment';
import { IPreparedTransaction } from './prepared-transaction';
import { IRequestPaymentOptions } from '../types';
import { payErc20HinkalWalletProxyRequest } from './erc20-hinkal-wallet';
export { INearTransactionCallback } from './utils-near';

export const noConversionNetworks = [
Expand Down Expand Up @@ -71,6 +72,7 @@ export class UnsupportedPaymentChain extends Error {
* Processes a transaction to pay a Request.
* Supported networks:
* - ERC20_PROXY_CONTRACT
* - ERC20_HINKAL_WALLET
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider enhancing documentation for privacy features

While the addition is correct, consider adding a brief note about Hinkal Wallet's privacy features and its specific use case, as it differs from other payment networks in this aspect.

Example addition:

 * Supported networks:
 * - ERC20_PROXY_CONTRACT
- * - ERC20_HINKAL_WALLET
+ * - ERC20_HINKAL_WALLET (Privacy-focused wallet using ZK-proofs for confidential transactions)
 * - ETH_INPUT_DATA
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* - ERC20_HINKAL_WALLET
* - ERC20_PROXY_CONTRACT
* - ERC20_HINKAL_WALLET (Privacy-focused wallet using ZK-proofs for confidential transactions)
* - ETH_INPUT_DATA

* - ETH_INPUT_DATA
* - ERC20_FEE_PROXY_CONTRACT
* - ANY_TO_ERC20_PROXY
Expand Down Expand Up @@ -99,6 +101,8 @@ export async function payRequest(
case ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT:
case ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE:
return payErc20Request(request, signer, amount, undefined, overrides);
case ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_HINKAL_WALLET:
return payErc20HinkalWalletProxyRequest(request, signer, amount!);
case ExtensionTypes.PAYMENT_NETWORK_ID.ERC777_STREAM:
return payErc777StreamRequest(request, signer);
case ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY: {
Expand Down
3 changes: 3 additions & 0 deletions packages/payment-processor/src/payment/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ const {
ANY_TO_ERC20_PROXY,
NATIVE_TOKEN,
ERC20_TRANSFERABLE_RECEIVABLE,
ERC20_HINKAL_WALLET,
} = ExtensionTypes.PAYMENT_NETWORK_ID;
const currenciesMap: any = {
[ERC777_STREAM]: RequestLogicTypes.CURRENCY.ERC777,
Expand Down Expand Up @@ -200,6 +201,7 @@ export function validateRequest(
ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE_TOKEN,
ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY,
ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_HINKAL_WALLET,
].includes(paymentNetworkId)
? // Any currency type is valid with Any to ERC20 / ETH / Native conversion
true
Expand All @@ -214,6 +216,7 @@ export function validateRequest(
ERC20_FEE_PROXY_CONTRACT,
ERC777_STREAM,
ERC20_TRANSFERABLE_RECEIVABLE,
ERC20_HINKAL_WALLET,
].includes(paymentNetworkId) || request.currencyInfo.value;

// Payment network with fees should have both or none of fee address and fee amount
Expand Down
1 change: 1 addition & 0 deletions packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"prepare": "yarn run build"
},
"dependencies": {
"@hinkal/data": "^0.1.3",
"ethers": "5.5.1"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/currency-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,6 @@ export type CurrencyPairs = Record<string, Record<string, number>>;
* Network => currencyFrom => currencyTo => cost
*/
export type AggregatorsMap<T extends ChainName = ChainName> = Partial<Record<T, CurrencyPairs>>;

//For now only optimism is supported to work with Hinkal Wallet
export type HinkalSupportedNetworks = 'optimism';
Comment on lines +210 to +212
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider relocating the HinkalSupportedNetworks type.

For better code organization, consider moving this type definition near other network-related types at the beginning of the file (around line 4 where EvmChainName is defined).

3 changes: 3 additions & 0 deletions packages/types/src/extension-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as PnMeta from './extensions/pn-meta';
import * as PnAnyToAnyConversion from './extensions/pn-any-to-any-conversion-types';
import * as Identity from './identity-types';
import * as RequestLogic from './request-logic-types';
import * as PnAnyToHinkalWalletErc20 from './extensions/pn-any-hinkal-wallet-based-types';

export {
ContentData,
Expand All @@ -22,6 +23,7 @@ export {
PnAnyToEth,
PnAnyToAnyConversion,
PnMeta,
PnAnyToHinkalWalletErc20,
};

/** Extension interface is extended by the extensions implementation */
Expand Down Expand Up @@ -89,6 +91,7 @@ export enum PAYMENT_NETWORK_ID {
BITCOIN_ADDRESS_BASED = 'pn-bitcoin-address-based',
TESTNET_BITCOIN_ADDRESS_BASED = 'pn-testnet-bitcoin-address-based',
ERC20_ADDRESS_BASED = 'pn-erc20-address-based',
ERC20_HINKAL_WALLET = 'pn-erc20-hinkal-wallet',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider adding type constraints for security-critical operations.

Given that this is part of a private wallet integration using zero-knowledge proofs, consider:

  1. Adding JSDoc comments to document security assumptions and requirements.
  2. Using strict type constraints where possible to prevent misuse.
  3. Ensuring type definitions align with the security properties of Hinkal's ZK-proof system.

ERC20_PROXY_CONTRACT = 'pn-erc20-proxy-contract',
ERC20_FEE_PROXY_CONTRACT = 'pn-erc20-fee-proxy-contract',
ERC777_STREAM = 'pn-erc777-stream',
Expand Down
12 changes: 12 additions & 0 deletions packages/types/src/extensions/pn-any-hinkal-wallet-based-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as PnAnyToAnyConversion from './pn-any-to-any-conversion-types';
import { HinkalSupportedNetworks } from '../currency-types';
Comment on lines +1 to +2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

HinkalSupportedNetworks type needs to be expanded

The current implementation only supports 'optimism' network, but the PR objectives mention support for multiple networks (Polygon, Arbitrum, Ethereum). Update the HinkalSupportedNetworks type to include all required networks:

export type HinkalSupportedNetworks = 'optimism' | 'polygon' | 'arbitrum' | 'ethereum';
🔗 Analysis chain

Verify completeness of supported networks

The PR objectives mention support for multiple networks (Polygon, Arbitrum, Ethereum, etc.), but HinkalSupportedNetworks might be limited. Please ensure all required networks are included in the type definition.


Let me try another search to find the definition of HinkalSupportedNetworks:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check the definition of HinkalSupportedNetworks
rg -A 5 "type HinkalSupportedNetworks" "packages/types/src/currency-types.ts"

Length of output: 28780


Script:

#!/bin/bash
# Search for HinkalSupportedNetworks definition
rg -A 5 "type HinkalSupportedNetworks|enum HinkalSupportedNetworks" "packages/types/src/currency-types.ts"

Length of output: 156


/** Any to Hinkal Wallet reference-based payment network extension interface */
export type IAnyToHinkalWalletErc20 =
PnAnyToAnyConversion.IConversionReferenceBased<ICreationParameters>;

/** Parameters for the creation action */
export interface ICreationParameters extends PnAnyToAnyConversion.ICreationParameters {
network: HinkalSupportedNetworks;
acceptedTokens: string[];
}
Loading