Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
The content of an encrypted request can be shared to additional third parties using the addStakeholder feature.
Calling request.addStakeholder() allows any stakeholder of a request to add the public key of a third party as a stakeholder on a request. The third party can now read the content of the request.
Feature exists. Docs coming soon...
Request payments can be detected easily, thanks to the integration of The Graph.
Our payment-subgraphs indexes Request's proxy smart contracts and allow you to query payment data easily.
Other methods are available to detect a payment by simply watching the Proxy Smart Contract used for payment. Payment transactions include a paymentReference that links them to the request.
A "conversion" request is one that is denominated in one currency but paid in another currency. This is facilitated by on-chain price feeds provided by oracles. The typical use case is to denominate a request in fiat like USD and pay the request in stablecoins like USDC or DAI.
Request Network is a protocol for creating payment requests and reconciling payments.
Examples to quickly integrate Request Network
The Request Network Templates are examples of how to integrate the Request Network Components into various web frameworks.
The Request Network Templates are distributed as "Template Repositories" in Github. The Github Docs explain how to .
The Request Network API provides an interface for creating and paying requests within your application.
At its core, the Request Network API empowers you to:
Create Requests: Define payment requests with information such as payee, payer (optional), amount, currency, and recurrence (optional).
Facilitate Payments: Return transaction calldata, ready to be signed by end-users and sent to the blockchain for secure and transparent value transfer.
Deliver Webhook Notifications: Receive instant updates on payment status changes, enabling your application to react dynamically to completed transactions.
See
For detailed information on all available endpoints and their parameters, please refer to the full
The following diagram illustrates the typical flow for creating and paying requests using the Request Network API:
A CLI tool to help inject Request Network functionality into projects
The Request Network Foundation operates several Request Node "Gateways" that are free for anyone to use. These gateways offer endpoints for creating and retrieving requests. They also pay the protocol fee for creating requests, about USD $0.10 per request, paid in XDAI.
Request Network allows you to support any currency. Head out to the package to see how we identify and manage currencies in the CurrencyManager. You don't need to add new currencies in this repository.
Instead, when instanciating the CurrencyManager, you can feed it with a list of supported currencies for your dapp:
To implement new types of currencies (aside fiat, BTC, ETH, ERC20), .
After a request is created, it can be updated:
Instructions to pay a request from a Safe Multisig
The Request Network Templates and Example Apps support paying a request from a Safe multisig wallet.
When connecting your wallet, select WalletConnect.
Copy the WalletConnect Pairing Code.

Web Components for integrating Request Network. Usable in any framework.
increaseExpectedAmount
increase the expected amount
payer
addStakeholders
grant 1 or more third parties access to view an encrypted request
payee, payer, third party
Feature exists. More docs coming soon...
accept
accept a request, indicating that it will be paid
payer
cancel
cancel a request
payee, payer
reduceExpectedAmount
reduce the expected amount
payee
const list: CurrencyInput[] = [
{ type: RequestLogicTypes.CURRENCY.ETH, decimals: 18, network: 'anything', symbol: 'ANY' },
];
const currencyManager = new CurrencyManager(list);/** Create request parameters */
export interface ICreateRequestParameters {
requestInfo: RequestLogic.ICreateParameters | IRequestInfo;
signer: Identity.IIdentity;
paymentNetwork?: Payment.PaymentNetworkCreateParameters;
topics?: any[];
contentData?: any;
disablePaymentDetection?: boolean;
disableEvents?: boolean;
}
/**
* Gets the ID of a request without creating it.
*
* @param requestParameters Parameters to create a request
* @returns The requestId
*/
public async computeRequestId(
parameters: Types.ICreateRequestParameters,
): Promise<RequestLogicTypes.RequestId>Use the web3-signature package to create requests using a web3 wallet like Metamask.
To create mock storage requests, where the request is stored in memory on the local machine and cleared as soon as the script is finished running, set the useMockStorage argument to true when instantiating the RequestNetworkobject.
const web3SignatureProvider = new Web3SignatureProvider(provider);
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: 'https://xdai.gateway.request.network/'
},
signatureProvider: web3SignatureProvider,
});const requestClient = new RequestNetwork({
useMockStorage: true,
});Partial Payment Support: Pay a portion of a request instead of the full amount at once. This unlocks powerful use cases such as:
Split payment: split a payment 50% USDC on Base and 50% with USDT on Optimism.
Gradual payment plans: Allow users to pay large invoices in smaller chunks.
Risk mitigation: Test with small amounts before completing large payments.
The API automatically tracks payment progress, showing partially_paid status until the request is fully paid, and prevents overpayment by capping amounts to the remaining balance.
Support for both TypeScript and JavaScript projects
Customizable function selection
Automatic package installation
Support for various package managers (npm, yarn, pnpm, bun)
The CLI analyzes your project structure
You select the functions you want to inject
You choose your preferred language (TypeScript or JavaScript)
The tool injects the selected functions into your project
Necessary dependencies are automatically installed

Gnosis Gateway
real
Gnosis Gateway (deprecated alias)
real
Sepolia Gateway
test
Overall, the Request Network API supports 500+ currencies across 10 EVM chains.
10 EVM Chains:
Ethereum
Arbitrum One
OP Mainnet
Base
Polygon
BSC
Avalanche
Fantom
zkSync Era
Sepolia
For ERC20 and Native Payments, the Request Network API supports 500+ tokens, mostly on Ethereum. See Request Network Token List for the full list.
For Conversion Payments, the Request Network API supports the following invoice currencies:
USD
EUR
CNY
GBP
JPY
For Conversion Payments, the Request Network API supports the following payment currencies:
USDC
USDT
DAI
FAU on Sepolia
To get a list of supported payment currencies for a given invoice currency, use the Supported Chains and Currencies endpoint.
See
The currencies API endpoints provide access to the complete Request Network token list, enabling you to discover and filter available tokens across multiple blockchain networks.
Payment Request Integration: Get the exact currency IDs needed for creating payment requests
Payment Integration: Get accurate token information for payment processing
Currency Validation: Verify supported tokens before creating payment requests
Multi-Chain Support: Access tokens across Ethereum, Polygon, Arbitrum, and more
Developer-Friendly: Simple filtering options for easy integration
Each token in the response includes:
id: Unique Request Network token identifier (e.g., USDC-mainnet)
name: Human-readable token name (e.g., USD Coin)
symbol: Token symbol (e.g., USDC)
decimals: Number of decimal places for the token
address: Contract address on the specified network
network: Blockchain network name
type: Token type (ERC20, ETH, etc.)
hash: Contract address hash
chainId: Blockchain chain ID

Pay requests using stablecoins from any supported network, without manual bridging or token swaps.
Crosschain payments allow users to pay a request using a stablecoin from a different blockchain network than the one specified on the request. For example, a payer can pay a request for USDC on Base using USDT from their Optimism wallet.
Flexibility: Payers can pay with their preferred currency.
Cost-Effective: Automated routing balances cost and speed.
Time-Saving: Payers don't need to swap or bridge tokens manually.
Simplified UX: Payment settlement requires only 1 or 2 signatures from the Payer.
For Crosschain (and Samechain) Payments, the Request Network API supports 12 stablecoins: USDC/USDT/DAI on 4 chains (Ethereum, Arbitrum One, Base, OP Mainnet).
Crosschain payments are supported on the following blockchain networks:
Ethereum
Arbitrum One
Base
OP Mainnet
Warning: Crosschain payments work only with mainnet funds (real money). Test networks are not supported.
The following stablecoins are supported for crosschain payments on both the sending and receiving networks.
USDC
USDT
DAI
To enable crosschain payments, the request must be created with the following parameters:
paymentCurrency included in the and .
amount greater than 1 - executing crosschain payments under 1 stablecoins is not allowed, even though creating requests has no restrictions on amount .
For more details about creating requests, please see the endpoint.
To display a list of possible routes for a given request and payer address, use the endpoint. It returns all of the possible routes based on the payer's token balances.
The API automatically ranks available payment routes based on the following factors:
Transaction fees
Processing speed
Routes that offer a balanced combination of lower fees and faster processing times are ranked higher in the results.
When fetching payment routes, each route displays the total estimated fees in the payment currency. This fee represents the combined costs associated with processing the transaction, including:
Gas Fees:
The total fee includes all gas costs incurred by the payment processor wallet for processing the transaction. This covers:
- Transferring tokens from the payer's wallet.
- Approving the payment execution smart contract.
- Executing the crosschain payment transaction.
For tokens supporting EIP-2612:
- The payment processor wallet also covers for the onchain permit transaction.
For tokens that do not support EIP-2612:
- The payer must perform an onchain approval transaction and pay for the gas fee directly. This fee is
The API may return samechain routes if the payer address has supported currencies on the same chain as the paymentCurrency .
Example: paymentCurrency is USDC on Base, and the payer has USDT on Base
Gasless transactions - the transaction fees are added on top of the request amount
No native token (ETH, etc..) needed for gas
Once the route is selected, the payer needs to fetch the unsigned payment calldata or intents.
If the selected route is a crosschain payment, the endpoint returns an unsigned payment intent. It will also return an unsigned approval permit or unsigned approval calldata, depending on whether the paymentCurrency supports . For crosschain payments, this endpoint is NOT approval aware - it will return an approval permit or approval calldata even if approval has already been granted.
If the selected route is a direct payment, the returns an unsigned payment calldata. It may also return an approval calldata. For direct payments, this endpoint IS approval aware - it will omit the approval calldata if sufficient approval has already been granted.
The intents and calldata returned by the endpoint in the previous step must be signed by the payer's wallet to authorize the crosschain payment. The process for signing the approval varies depending on whether the paymentCurrency supports , indicated by the metadata response parameter.
If the token does not support EIP-2612 Permit, the payer must sign and submit a standard ERC20 approval transaction.
Finally, the signed payment intent (and possibly the signed approval permit) are sent back to execute the crosschain payment via the endpoint. It will handle all the necessary steps to complete the payment. A payment.complete event will be sent to the platform's webhooks when the payment is completed.
It will be possible in the future to add a custom fee to the payment, this is currently under development.
The Request Network Token List is a curated list of tokens supported by Request Network products. The token list follows a standardized format and includes essential information about each token, such as address, symbol, name, decimals, and chainId.
The token list is available at: https://requestnetwork.github.io/request-token-list/latest.json
You can fetch the token list directly in your application:
Each token in the list contains the following information:
We welcome community contributions! To add a new token to the list:
Fork the repository on Github
Add your token information to tokens/token-list.json
Make sure your token meets our requirements (see )
Run tests locally: npm test
A dialog box for granting third-party access to an encrypted invoice created via Request Finance
The add-stakeholder component allows Builders to quickly integrate the Request Finance Add Stakeholder widget into their applications.
The Request Finance Add Stakeholder widget provides a dialog box for end-users to grant third-party access to one of their encrypted payment requests created via Request Finance. Then, under the hood, Request Finance calls request.addStakeholders() to add the third party as a stakeholder to the encrypted payment request.
The template comes in the form of a and a native component, provided by the package. The Web Component can be used anywhere including, but not limited to, React, Next.js, Vue, Svelte, or as a browser script.
A high level overview on how our API does payment detection
The Request API uses a reference-based payment detection system that automatically monitors blockchain transactions to detect when payments are made to your requests. This system works across multiple blockchains and handles various payment scenarios.
When you create a payment request, the API automatically generates a unique, a 16-character identifier that acts as a fingerprint for your request. This reference is what connects blockchain transactions back to your specific request.
The API continuously monitors supported blockchains using subgraphs that scan for transactions containing payment references. This happens automatically in the background, no action required from you.
When someone makes a payment and includes the payment reference in their transaction, our system:
Detects the transaction within minutes
Validates the payment details (amount, currency, recipient)
Updates the request status (partially paid, fully paid, etc.)
Triggers your configured webhooks
Once a payment is detected, your request status is immediately updated and you can get the latest information via:
GET requests to check payment status using the request's id
Automatically receive updates to your
All crosschain payments done using the use our as the last leg of payment, so payment detection works out of the box.
For real-time integration, you can configure webhooks to be notified for the following events:
Payment Confirmed: Full payment received
Payment Partial: Partial payment received
Payment Failed: Transaction failed
Payment Refunded: Payment was refunded
This allows your application to react immediately to payment events without constantly polling the API.
Zero Configuration: Payment detection happens automatically
Multi-blockchain: Works across all supported networks
Real-time: Fast detection and status updates
Flexible: Handles various payment scenarios
The system is designed to be completely transparent to your application, simply create requests and let the API handle all the complexity of monitoring blockchains for payments.
In the Reference-based Payment Networks, Payments are linked to Requests via a paymentReference which is derived from the requestId and payment recipient address.
This paymentReference consists of the last 8 bytes of a salted hash of the requestId and payment recipient address, concatenated :
last8Bytes(hash(lowercase(requestId + salt + address)))requestId is the id of the request
salt is a random number with at least 8 bytes of randomness. It must be unique to each request
address is the payment address for payments, the refund address for refunds
lowercase() transforms all characters to lowercase
hash() is a keccak256 hash function
last8Bytes() take the last 8 bytes
Use the to calculate the payment reference.
An explorer app for viewing requests, payments, and addresses in Request Network.
const tokenList = await fetch(
"https://requestnetwork.github.io/request-token-list/latest.json"
).then((res) => res.json());Promise<IRequestDataWithEvents>
Reliable: Built on proven blockchain indexing infrastructure
The request can be optionally encrypted such that only the payee, payer, and approved 3rd parties can view the request contents.
The request is persisted in IPFS.
The IPFS Content-addressable ID (CID) is stored in a smart contract on Gnosis chain
The payee can optionally cancel the request or increase/decrease the expected amount.
The payer can optionally accept the request, indicating that they intend to pay it.
Both payee and payer can add 3rd party stakeholders if the request is encrypted.
The payer derives a paymentReference from the request contents.
The payer calls a function on the payment network smart contract, passing in the token address, to address, amount, and paymentReference.
An event is emitted containing the token address, to address, amount, and paymentReference.
The event is indexed by the payments subgraph
An app can retrieve the request contents from IPFS and calculate the balance based on events from the payments subgraph.
All of these steps are facilitated by the Request Network JavaScript SDK such that the developer needs only make a few function calls. See the Quickstart to learn more.



Create a Pull Request
Create and Manage API Keys: Users can create new API keys for authentication.
Toggle and Delete API Keys: API keys can be toggled on and off, or deleted if no longer needed, enhancing control over API access.
Security Guidelines: API keys are sensitive and should never be shared publicly. In case of compromise, users are advised to create a new key, update their code, and delete the compromised key.
Multiple Keys: Allows the creation of multiple API keys for different environments or applications.
Create and Manage Webhooks: App developers can configure webhook endpoints to receive real-time notifications for payment events.
Security Guidelines: Each webhook request includes a signature in the `x-request-network-signature` header to ensure authenticity.
Signature Verification: The signature is a SHA-256 HMAC of the request body, signed using the webhook secret.
Example Verification Code:
Navigate to the "API Keys" section.
Click on "Create new key."
Store the key securely and never share it publicly.
Navigate to the "Webhooks" section.
Click on "Add webhook."
Enter the endpoint URL and ensure the endpoint is secure and can handle incoming JSON payloads.
Keep API keys and webhook secrets secure. Never expose them in public repositories or client-side code.
Verify all webhook signatures to ensure authenticity and integrity.
Use HTTPS for all endpoints to encrypt communication.

{
"id": "TKN-mainnet"
"name": "Token Name",
"address": "0x...",
"symbol": "TKN",
"decimals": 18,
"chainId": 1,
"logoURI": "https://..."
}npm install @requestnetwork/add-stakeholderimport '@requestnetwork/add-stakeholder'
export default function App() {
return (
<add-stakeholder builderKey="..." webhookUrl="..."/>
)
}import { AddStakeholder } from '@requestnetwork/add-stakeholder'<AddStakeholder builderKey="..." webhookUrl=".."/> import '@requestnetwork/add-stakeholder'<add-stakeholder builderKey="..." webhookUrl="..."/><script src="./node_modules/add-stakeholder/dist/web-component.umd.cjs" defer></script>
<!-- or -->
<script src="//unpkg.com/@requestnetwork/add-stakeholder" defer></script>
<add-stakeholder builderKey="..." webhookUrl="..."/>import express from 'express';
import crypto from 'node:crypto';
const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret';
app.post('/payment', async (req, res) => {
const signature = req.headers['x-request-network-signature'];
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).json({
success: false,
message: 'Invalid signature'
});
}
// Business logic here
return res.status(200).json({ success: true });
});Service Fees:
The total fees also include any service fees charged by the crosschain infrastructure for facilitating transfers or swaps between different blockchains.
Accountants: Audit and verify financial data on the request network.
Developers: Easily access Request Network data for troubleshooting your applications.
Analysts: Gain deep insights into network activity and trends.
Researchers: Conduct in-depth studies on blockchain data.
Enthusiasts: Stay informed about the latest happenings on the Request Network.
Search Bar: Located at the top, allows you to search for specific requests or addresses.
Dashboard: Provides an overview of network statistics and recent activity.
Requests: View a list of recent requests with details like payee, payer, amount, and timestamp.
Payments: View a list of recent payments with details like blockchain transactions, amounts, fees, and timestamps.
Address: View information about individual addresses, including their requests and payments.
Request: View information about individual requests, including their details and table with actions and payments.
Request: Enter a request ID in the search bar to view its details.
Address: Enter an address to see its requests and payment history.

An Invoicing Template for creating, viewing, and paying requests in Request Network. Built using the Invoice Dashboard and Create Invoice Form Web Components.
Next.js
Invoices can be denominated in the following fiat currencies, in addition to the listed above.
USD
EUR
CNY
GBP
In-memory requests allow for creating and managing requests without immediately persisting them to storage. This enables faster payment workflows and deferred persistence.
Faster payment flow: In-memory requests are helpful when payment is the priority, such as in e-commerce cases. In this scenario, the request is a receipt rather than an invoice.
Deferred Persistence: With in-memory requests, a request can be created on the front end with a user's signature and passed on to the backend for persistence.
The flow of creating and paying an in-memory request is similar to a regular request with the following key differences:
Create an in-memory request by passing the argument skipPeristence: true when instantiating the RequestNetwork instance.
An in-memory request is not persisted immediately like normal requests. Instead, it is stored in memory on the device where it was created. It can be persisted at a later time using the persistTransaction()function.
An in-memory request has the inMemoryInfo
An app for creating and paying requests using the Request Network API.
EasyInvoice is a web application built with Next.js that allows users to create and manage invoices, and accept crypto payments via the Request Network API. It mimics Web2 apps in its functionalities, providing a user-friendly experience with Google login and real-time updates.
A dashboard for viewing and paying invoices in Request Network
Pay a series of requests with a stream of ERC777 Super Tokens from Superfluid.
The first request of a series is very similar to , it defines the salt, paymentAddress and requestId to compute the paymentReference used for the whole series.
Other requests must define a previousRequestId and cannot redefine any of the payment properties (paymentAddress, feeAddress or salt).
Multiple requests can be paid with the same stream, typically recurring requests of fixed amounts paid continuously. A group of requests payable with the same stream are called a request series, they must all have the same currency.
This guide demonstrates how to integrate Request Network into a React Native application using Expo. Due to the differences between Node.js and React Native environments, several polyfills and configurations are necessary to make Request Network work properly. Following this guide will set up your Expo project to use Request Network, handle polyfills for missing modules, and ensure smooth integration. A full Github repository with Request Network support can be found .
An identity object that is used to uniquely identify a stakeholder in a request. Today, a stakeholder's IIdentity is expressed as an Ethereum address, but it is conceivable that future implementations may include alternative identity formats, perhaps W3C DIDs.
Examples of IIdentity are: payee, payer, signer, creator, and any 3rd party that can view an encrypted request's contents.
"metadata": {
"supportsEIP2612": true
}import { ethers } from "ethers";
const ethersProvider = new ethers.providers.Web3Provider(
// Connected wallet provider
walletProvider as ethers.providers.ExternalProvider,
);
const signer = await ethersProvider.getSigner();
// Response from the `GET /request/{requestId}/pay` endpoint
const response = ...
const paymentIntent = JSON.parse(paymentData.paymentIntent);
const supportsEIP2612 = paymentData.metadata.supportsEIP2612;
let approvalSignature = undefined;
let approval = undefined;
if (supportsEIP2612) {
approval = JSON.parse(paymentData.approvalPermitPayload);
approvalSignature = await signer._signTypedData(
approval.domain,
approval.types,
approval.values,
);
} else {
const tx = await signer.sendTransaction(paymentData.approvalCalldata);
await tx.wait();
}
const paymentIntentSignature = await signer._signTypedData(
paymentIntent.domain,
paymentIntent.types,
paymentIntent.values,
);
const signedData = {
signedPaymentIntent: {
signature: paymentIntentSignature,
nonce: paymentIntent.values.nonce.toString(),
deadline: paymentIntent.values.deadline.toString(),
},
signedApprovalPermit: approvalSignature
? {
signature: approvalSignature,
nonce: approval.values.nonce.toString(),
deadline: approval?.values?.deadline
? approval.values.deadline.toString()
: approval.values.expiry.toString(),
}
: undefined,
};Using the request-client.js package, the payer creates a request with the ERC20_FEE_PROXY_CONTRACT payment network.
Using the payment-processor package, payer:
Approves the escrow contract using approveErc20ForEscrow()
Pays the escrow contract using payEscrow()
Waits until the work is complete
Pays the payee from the Escrow contract using payRequestFromEscrow()
These steps are shown by our unit tests:
Constructor
Sign using a private key inside of a wallet
Constructor
Function
Pay a request
requestId
string
The ID of the request
Object
Options
disablePaymentDetection
boolean
Disable payment detection
disableEvents
boolean
Disable events
requestAndMeta
IReturnGetRequestFromId
The value returned by getRequestFromId()
npm install @requestnetwork/epk-decryptionAvoid calling getData() on an in-memory request because it will fail silently by returning an empty EventEmitter object.
Retrieving an in-memory request with requestClient.fromRequestId() will fail because the request has not been persisted yet so it is not possible to read it from the Request Node.
Create an in-memory request by passing the argument skipPeristence: true when instantiating the RequestNetwork instance.
import { Contract, ContractTransaction, Signer } from 'ethers';
import {
encodeApproveErc20,
encodePayErc20Request,
} from '@requestnetwork/payment-processor/dist/payment/erc20-proxy';
import { getRequestPaymentValues } from '@requestnetwork/payment-processor/dist/payment/utils';
import { ClientTypes } from '@requestnetwork/types';const multisigAbi = [
'function submitTransaction(address _destination, uint _value, bytes _data) returns (uint)',
];import {
payRequest
} from "@requestnetwork/payment-processor";
const paymentTx = await payRequest(
inMemoryRequest.inMemoryInfo.requestData,
signer
);
await paymentTx.wait(confirmationBlocks);
const persistingRequestNetwork = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://gnosis.gateway.request.network",
},
});
await persistingRequestNetwork.persistRequest(inMemoryRequest);npm install @requestnetwork/request-client.jsnpm install @requestnetwork/payment-processor// Request parameters
const requestParameters = {...}
const web3SignatureProvider = new Web3SignatureProvider(
ethersProvider!.provider
);
const inMemoryRequestNetwork = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://gnosis.gateway.request.network",
},
signatureProvider: web3SignatureProvider,
skipPersistence: true,
});
let inMemoryRequest =
await inMemoryRequestNetwork.createRequest(requestParameters);
export const payEthWithMultisig = async (
request: ClientTypes.IRequestData,
multisigAddress: string,
signer: Signer,
): Promise<ContractTransaction> => {
const multisigContract = new Contract(multisigAddress, multisigAbi, signer);
const { paymentAddress, paymentReference } = getRequestPaymentValues(request);
return multisigContract.submitTransaction(paymentAddress, 0, paymentReference);
};export const approveErc20WithMultisig = async (
request: ClientTypes.IRequestData,
multisigAddress: string,
signer: Signer,
): Promise<ContractTransaction> => {
const multisigContract = new Contract(multisigAddress, multisigAbi, signer);
const tokenAddress = request.currencyInfo.value;
return multisigContract.submitTransaction(tokenAddress, 0, encodeApproveErc20(request, signer));
};import { erc20FeeProxyArtifact } from '@requestnetwork/smart-contracts';
export const payErc20WithMultisig = async (
request: ClientTypes.IRequestData,
multisigAddress: string,
signer: Signer,
): Promise<ContractTransaction> => {
const multisigContract = new Contract(multisigAddress, multisigAbi, signer);
const proxyAddress = erc20FeeProxyArtifact.getAddress(request.currencyInfo.network);
return multisigContract.submitTransaction(
proxyAddress,
0,
encodePayErc20Request(request, signer),
);
};✅
Sepolia
ETH, USDC, USDT, FAU
ERC20 Payments
✅
Native Token Payments
✅
Conversion Payments
✅
rnf_invoice format 0.3.0
✅
Configure Logo and Colors
✅
Inject your own custom currency list
✅
Ethereum
ETH, USDC, USDT, DAI, REQ
Polygon
MATIC, USDC, USDT, DAI
BNB Chain
DAI, BUSD
Gnosis
USDC
Optimism
ETH, USDC, USDT, DAI
Base
ETH, USDC

Download Invoice as PDF
15 stablecoins: USDC/USDT/DAI on 5 chains (Ethereum, Polygon, Arbitrum One, Base, OP Mainnet) + 4 testnet tokens on Sepolia + USD fiat for Conversion and Crypto-to-fiat payments.
Invoice Creation: A simple form to create invoices.
Client name and email fields.
Items, amounts, and notes fields.
Invoice currency and payment currency options, supporting currency conversion via the Request Network API.
Currency Conversion: uses on-chain price feeds to calculate the exact payment currency amount based on the invoice currency at the moment of payment so you always receive the correct amount.
Dashboard: View key metrics and a table of your invoices.
Invoice Payment:
View invoice details and initiate payment using transaction calldata provided by the Request Network API.
Compatible with 80+ different crypto wallets via Reown AppKit
Real-time Updates: The app receives webhooks from the Request Network API to update the invoice status in real-time.
For Crosschain (and Samechain) Payments, EasyInvoice supports 12 stablecoins: USDC/USDT/DAI on 4 chains (Ethereum, Arbitrum One, Base, OP Mainnet)
For Crypto-to-fiat Payments, EasyInvoice supports USDC on Sepolia.
Recurring Invoice: Automatically create new invoices based on the selected start date and frequency
Payout: Send a payment without having to create a request first.
InvoiceMe Link: Prompt clients to send you an invoice prefilled with your name and email address.
Google Login: Securely log in to your account using Google OAuth.
Demo Page
✅
Playground Page
✅
ERC20 Payments
✅
Native Token Payments
✅
Fiat Price Conversion in the UI
✅
0.3.0
✅
Ethereum
USDC, USDT, DAI, AXS, AUDIO, RAI, SYLO, LDO, UST, MNT, MIR, INJ, OCEAN, ANKR, RLY, REQ, ETH
Polygon
USDC, USDT, DAI, MATIC
Sepolia
FAU, ETH, USDT, USDC
BNB Smart Chain
DAI, BUSD
Gnosis
USDC
Avalanche
USDC, USDT, AVAX
Next.js
Create Recurring Schedules: Define a payment schedule with a start date, frequency (daily, weekly, monthly, yearly), and total number of payments. The system will generate a payment permit that encapsulates all the payment details.
Payer Authorization: To authorize the payment series, the payer signs the payment permit with an EIP-712 signature. This single authorization allows the system to trigger all subsequent payments in the schedule without further interaction from the payer. For the first payment, the payer may also need to approve a token allowance for the recurring payment contract if they haven't already.
Automated Payments: Once the payer has authorized the schedule, the Request Network API backend systems automatically trigger the payments at the specified intervals. You can rely on the API to handle the entire lifecycle of the recurring payments.
Status Tracking and Webhooks: You can monitor the status of each recurring payment, including processed payments, failures, and completion status (e.g., active, paused, completed). Webhook notifications will be sent for key events like payment.confirmed and payment.failed, allowing your application to react in real time.
Flexible Management: The API provides the ability to manage the lifecycle of a recurring payment. You can cancel a recurring payment schedule to stop future payments. If a payment fails (e.g., due to insufficient funds), the schedule will be paused, and you can unpause it once the issue is resolved. Unpausing a recurring payment once issues are resolved would allow the subscription to catch up on any missed payments.
Our recurring payments feature is built on a non-custodial smart contract that enforces several security measures to protect payers' funds and ensure predictable behavior. The core principle is that all payment parameters are defined upfront and cryptographically signed by the payer, preventing any unauthorized changes.
Here are some key security features provided by the smart contract:
Signature-Protected Payments: Payments cannot be triggered without a valid EIP-712 signature from the payer. The smart contract verifies the signature for every payment attempt.
Immutable Recipient: The recipient's address is part of the signed data. Funds can only be sent to this specified address, which cannot be altered after the schedule is authorized.
Fixed Payment Amount: The amount for each payment is fixed in the signed permit. The smart contract will only transfer this exact amount, preventing any over- or under-payments.
Strict Payment Timing: Payments cannot be triggered before their scheduled time. The contract calculates the due date for each payment and will reject any attempts to trigger it prematurely.
No-Repeat Payments: The contract tracks payments, making it impossible to process the same payment more than once.
Enforced Payment Limit: The total number of payments is defined in the signed permit. The smart contract enforces this limit and will not allow any extra payments beyond the agreed-upon total.
Sequential Payments: Payments must be triggered in a strict, sequential order (e.g., payment #1, then #2, then #3). Any out-of-order attempt will fail, preventing missed or skipped payments from disrupting the schedule.
Signature Expiration: Each recurring payment schedule has a deadline. If the signature expires, no further payments can be triggered, providing a hard stop for the agreement.
The following diagram illustrates the typical flow for creating and managing recurring payments:
Recurring payments are supported on the following Blockchain networks:
Ethereum
Polygon
Arbitrum
Gnosis
Base
Binance Smart Chain
Sepolia
The recurring payments support all ERC20 currencies available in the supported networks.
See the full list here Supported Chains and Currencies
To enable recurring payments, a schedule must be created with the following endpoint:
The response includes a payment permit payload (EIP-712 typed data) for signature, and, if required, transactions for token allowance approval.
The payer must:
Approve the recurring payment contract to spend the required amount of tokens (if not already approved)
Sign the payment permit using EIP-712 compatible wallet
Example
To activate the recurring payment, the resulting signature must be submitted to the following endpoint:
A successful response confirms activation. The schedule is now active and payments will be executed automatically.
The status, processed payments, next payment date, and other details can be retrieved at any time.
Recurring payments can be cancelled or unpaused.
Cancel: stops all future payments
Unpause: resume the recurring payment after it fails three times (due to insufficient balance or allowance)
Follow the instructions below to add the Invoice Dashboard to a React or Next.js app.
Configure the invoice dashboard web component by creating a reference to it, setting its properties, and passing the reference as a prop.
Initialize the RequestNetwork object using an Ethers Signer or Viem WalletClient.
Use the config object to pass additional configuration options. Please replace the builderId with your own, arbitrarily chosen ID. This is used to track how many invoices your application creates.
Use a context provider to reinitialize the Request Network instance when the wallet changes.
A list of custom currencies to extend the default currency list.
Specify types to avoid TypeScript errors.
config
IConfig
Additional configuration parameters
config.builderId
string
Unique builder ID, arbitrarily chosen, used for metrics
config.dashboardLink
string
Path to dashboard page
config.logo
string
Path to logo file

SingleRequestProxyFactoryUniversal Compatibility: Works with any system that can make standard crypto transfers.
No Code Changes: Integrate with Request Network without modifying existing smart contracts.
Exchange Friendly: Enable payments from centralized exchanges.
Request: Create a request in the Request Network protocol
Deploy: Deploy a unique Single Request Forwarder for your request
Pay: The Payer sends funds to the Single Request Forwarder
Complete: The Single Request Forwarder forwards the payment to the Payee and emits an event to enable payment detection.
To deploy a Single Request Forwarder, call deploySingleRequestForwarder() which takes in the following arguments:
requestData: the data of the created request
signer: An Ethers v5 Signer to sign the deployment transaction
The deploySingleRequestForwarder() function automatically deploys the correct type of Single Request Forwarder based on the Request data passed into the function; either an Ethereum Single Request Forwarder or ERC20 Single Request Forwarder
It returns
Single Request Forwarder Address
To pay a request through a Single Request Forwarder using the Request Network SDK, call payRequestWithSingleRequestForwarder() which takes in the following arguments:
singleRequestForwarderAddress: the address of the SRP deployed in the previous step.
signer: A wallet signer who is making the transfer of funds.
amount: Amount of funds that need to be transferred.
Once we have the Single Request Forwarder address, we can pay by directly transferring the money to the address itself. The Single Request Forwarder will automatically process the payment. For ERC20 payments, the process of paying with a Single Request Forwarder happens in two steps:
Transferring the tokens to the Single Request Forwarder
Make a zero-value transaction to the Single Request Forwarder (i.e. Send 0 ETH to the contract)
Single Use: Each Single Request Forwarder deployment processes payments for a specific request.
Immutable Parameters: Payment details cannot be modified after deployment.
Fund Recovery: Built-in mechanisms to send stuck funds to the payment receiver.

For additional details, see the payment-network-erc777-stream-0.1.0 specification
To create a streaming request, like normal, but set the paymentNetwork parameter to the ERC777_STREAM payment network.
See Github for tests showing usage.
After creating a new Expo project, install the necessary dependencies:
1- Create a new file named index.js in the root of your project
2- Add the following content to index.js
3- Create a file named cryptoPolyfill.js in the root of your project
4- Add the following content to cryptoPolyfill.js
5- Create / Update metro.config.js to use the custom polyfills
6- Update package.json to set the main entry point to index.js
7- Ensure that app.json file includes the correct entry point
We've created a custom crypto polyfill (cryptoPolyfill.js) to provide the necessary cryptographic functions. This polyfill uses tweetnacl and node-forge libraries to implement various cryptographic operations.
Why tweetnacl and node-forge?
tweetnacl: It's a fast, secure, and easy-to-use cryptography library. It provides a pure JavaScript implementation of the NaCl cryptography library, which is particularly useful for generating random bytes, essential for cryptographic operations.
node-forge: It provides a comprehensive set of cryptographic tools and utilities. It implements various cryptographic algorithms and protocols that are not natively available in React Native. It's used in our polyfill for operations like hashing, cipher creation, and PBKDF2 key derivation.
Using these libraries allows us to implement a more complete set of cryptographic functions that closely mimic the Node.js crypto module, which is not available in React Native.
Ensure you're using compatible versions of React Native and Expo.
The crypto polyfill may not cover all use cases. Test thoroughly and adjust as needed.
Be cautious when handling private keys. Never expose them in your code or version control.
The example code uses environment variables for private keys. Ensure you set these up correctly and securely.
Constructor
The Request Network client
npm install @requestnetwork/request-client.jsparameters
Object used to create a new request
signerIdentity
Identity of the signer
string
requestId
string
The ID of the request. Hash derived from the request contents. Stored alongside the request contents so can be used to look up a request.
Wait for request to be persisted and indexed
Unwrap the request contents
Refresh the request data and balance
Cancel a request
Accept a request
Increase the expected amount
topic
string
Topic string
updatedBetween
Start time and end time
Object
Options
Promise<Request[]>
from
number (Unix timestamp)
Start time
to
number (Unix timestamp)
End time
disablePaymentDetection
boolean
Disables payment detection
disableEvents
boolean
Disabled events
signerIdentity
The value returned by getRequestFromId()
refundInformation
any
Depends on the payment network
Promise<IRequestDataWithEvents>
The payee identity and payer identity are NOT necessarily the same as the payment recipient (paymentAddress in the PaymentNetworkCreateParameters) or the payment sender (from address in the payment-subgraph). Conceptually, the payee and payer identities are used only for notifications and access control, NOT payment routing.
type
Identity type
value
string
Identity address
ETHEREUM_ADDRESS
'ethereumAddress'
Externally owned account (EOA)
ETHEREUM_SMART_CONTRACT
'ethereumSmartContract'
Smart contract account. Don't use this.
It is not possible to create or update a request using an IIdentity of this type. This type was added to support creating/updating requests using multisig wallets via EIP-1271 but this feature has not yet been implemented.
deltaAmount
number | string
The amount by which to reduce the expected amount
signerIdentity
The value returned by getRequestFromId()
paymentInformation
any
Depends on the payment network
Promise<IRequestDataWithEvents>
Manipulating private keys must be done with care. Losing them can lead to a loss of data, privacy or non-repudiation safety!
A request can be encrypted to make its details private to selected stakeholders. In this guide, we won't explain how encryption is managed under the hood. We will mention encryption or decryption of requests with payers and payees keys. Although in practice, we will use an intermediate symmetric key. See more details on .
The transaction layer manages the encryption, .
To manipulate encrypted requests you need a CipherProvider (recommended) or DecryptionProvider (deprecated). Both of them require direct access to the private key. They're best suited for backends.
EthereumPrivateKeyCipherProvider: Provides both encryption and decryption utilities.
EthereumPrivateKeyDecryptionProvider (deprecated) provides only decryption utilities.
See on .
Then you can create an encrypted request:
Note: You must give at least one encryption key you can decrypt with the decryption provider. Otherwise, an error will be triggered after the creation.
is deprecated in favor of
Let's step back for a second: the requester sent a request that he encrypted with the payer's public key, as well as with his own, to retrieve it later. This is an essential and typical example, but a request can be encrypted with many keys to give access to its status and details.
If the decryption provider knows a private key matching one of the keys used at the creation, it can decrypt it. Like a clear request you can get it from its request id.
Like a clear request, you can update it if the decryption provider is instantiated with a matching private key.
A form for creating invoices in Request Network
The Create Invoice Form allows end-users to create an invoice using the Request Network. It is built using Svelte but compiled to a Web Component, making it usable in any web environment, regardless of the framework.
To install the component, use npm:
Follow the instructions below to add the Create Invoice Form to a React or Next.js app. For a video explaining how to integrate, see the
Configure the Create Invoice Form web component by creating a reference to it, setting its properties, and passing the reference as a prop.
Initialize the RequestNetwork object using an Ethers Signer or Viem WalletClient.
Use the config object to pass additional configuration options. Please replace the builderId with your own, arbitrarily chosen ID. This is used to track how many invoices your application creates.
Use a context provider to reinitialize the Request Network instance when the wallet changes.
A list of custom currencies to extend the default currency list.
Specify types to avoid TypeScript errors.
A widget that allows builders to accept crypto payments.
The Payment Widget allows builders to accept crypto payments on their websites with minimal integration. It is built using Svelte but complied into a Web Component, making it usable in any web environment, regardless of the framework.
The Request Network SDK is the set of software packages for interacting with the Request Network. The packages can be installed via npm and allow developers to:
Create new requests
Update requests
Pay requests
Retrieve requests
Detecting payments
The Request Node is a software bundle that provides a gateway to the various layers of the Request Network Protocol:
persists the request content
Smart contracts persist the unique IPFS on-chain
indexes smart contract events
This Request Node acts as a relay server which helps reduce friction and costs for the end user. The user signs the request contents, but the node funds the fees required to persist the contents in the various protocol layers.
The Request Network Foundation operates several which builders can use to quickly test the protocol. However, for production, we advise you to run your own node which can be installed via either or .
Shown below is a diagram that depicts how the SDK and Node interact with the protocol.
Compute the payment reference, the last 8 bytes of a salted hash of the request ID.
The payment reference is the parameter that ties the request to events emitted by on-chain payments via Request Network payment smart contracts.
string
The Request Network protocol persists request contents on IPFS. When privacy is required, the SDK supports the creation and subsequent reading of encrypted requests.
This is achieved by combining public key (asymmetric) & AES (symmetric) encryption.
The "request contents" are encrypted with a random AES key. This key is then encrypted with the public key of each stakeholder of the request. The stakeholders are the users and platforms who require access to the request contents. The encrypted request contents and the encrypted AES keys are then persisted to IPFS.
The user retrieves the encrypted request content and the encrypted AES keys. They decrypt the AES key using their private key. Then they decrypt the request content using the AES key.
The privacy and encryption mechanism used by Request Network has many similarities to HTTPS. If you're not familiar with HTTPS, we recommend reading
Request is an open database for any type of payment request - from business invoices to reimbursements between friends. It aims to support products at any scale from startups to large organizations, from the private to the public sector.
The Request Protocol is the core of Request. It's the bottom layer that defines and handles the data of a request and persists it to a distributed ledger to make Request open, trustless, secure, and resilient.
This section is aimed at helping you understand how the protocol is structured, how it works and meets its requirements. It is particularly useful if you want to propose changes or implement them yourself.
The Request Protocol has one fundamental purpose: to persist, on a distributed ledger, data representing requests and to be able to retrieve these data efficiently.
To achieve this, the Request Protocol follows the layered architecture pattern. Each layer is responsible for a specific task and a specific level of abstraction. This layered architecture is highly extensible and hopefully easy to understand.
The protocol is composed of four layers:
Request logic
Transaction
Data Access
Storage
This layered architecture allows package reusability and makes the protocol more upgradeable. For example, our current implementation uses Ethereum and IPFS. Still, suppose Arweave turns out to be a better solution for storing data in a decentralized database than IPFS. In that case, we can create a new storage layer that uses Arweave and make the data-access layer using this new package instead.
The protocol follows a defined interface; each layer has to implement a specific interface. The interfaces for each layer can be found in the Types package of Request Network repository: .
The following pages present the first implementation of the protocol used for the released version of Request V2 on mainnet.
The best way to access Request Network is using the Request Network SDK with a Request Node. In this way, the Request Node operator pays the protocol fee for creating requests and reduces the number of transactions signed by the end user resulting in a better user experience.
The Request Network SDK is split into multiple packages so that Builders can pick and choose the features they need.
These are the packages that we think would be most commonly used by Builders to build applications.
This layer is responsible for the business logic of Request. This is where we define the data structure of a request.
This layer has three responsibilities:
It defines the properties of the requests and the actions performed on them.
It's responsible for the signature of the actions performed to ensure the request stakeholder identities.
import { Wallet, providers } from "ethers";
const privateKey = 'WALLET_PRIVATE_KEY'
const provider = const provider = new providers.JsonRpcProvider(
"RPC_URL",
);
const wallet = new Wallet(privateKey, provider);
const recurringPaymentPermit = ... // from API response
const signature = await wallet._signTypedData(
recurringPaymentPermit.domain,
recurringPaymentPermit.types,
recurringPaymentPermit.values
);
npm install @requestnetwork/invoice-dashboardconst request = await requestClient.createRequest(requestCreateParameters);
const requestData = request.getData()
// In case of in-memory request
const requestData = request.inMemoryInfo.requestDataimport { deploySingleRequestForwarder } from "@requestnetwork/payment-processor"
const forwarderAddress = await deploySingleRequestForwarder(
requestData,
signer
);
console.log(`Single Request Forwarder Deployed At: ${forwarderAddress}`)
// Single Request Forwarder Deployed At : 0x1234567890123456789012345678901234567890import { payRequestWithSingleRequestForwarder } from "@requestnetwork/payment-processor"
import { utils } from "ethers"
const paymentAmount = utils.parseUnits("1" , 18)
await payRequestWithSingleRequestForwarder(forwarderAddress , signer, paymentAmount) // The paymentNetwork is the method of payment and related details.
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC777_STREAM,
parameters: {
expectedFlowRate: expectedFlowRate // number, Expected amount of request currency per second
expectedStartDate: expectedStartDate // timestamp, Expected start of stream
paymentAddress: payeeIdentity,
},
}, // The paymentNetwork is the method of payment and related details.
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC777_STREAM,
parameters: {
originalRequestId: 'abcd', // first reqeust in the series
previousRequestId: 'abcd', // previous request in the series
recurrenceNumber: 1, // 1 if previous request is original, 2+ otherwise
},
},npm install @requestnetwork/request-client.js @requestnetwork/types @requestnetwork/payment-processor @requestnetwork/epk-signature buffer eventemitter3 stream-browserify http-browserify https-browserify react-native-get-random-values tweetnacl node-forge [email protected]touch index.jstouch cryptoPolyfill.js{
"expo": {
"entryPoint": "./index.js",
...
}
}npm install @requestnetwork/create-invoice-formnpm install @requestnetwork/payment-widgetlast8Bytes(hash(lowercase(requestId + salt + address)))web3Provider
ethers.Provider
An ethers v5 Provider or viem WalletClient
identity
Identity type and value
updatedBetween
Start time and end time
Object
Options
from
number (Unix timestamp)
Start time
to
number (Unix timestamp)
End time
disablePaymentDetection
boolean
Disable payment detection
disableEvents
boolean
Disable events
signerIdentity
The value returned by getRequestFromId()
refundInformation
any
Depends on the payment network
deltaAmount
number | string
The amount by which to increase the expected amount
signerIdentity
The value returned by getRequestFromId()
refundInformation
any
Depends on the payment network
Reduce the expected amount
Others...
Other features exist. Docs coming soon...


sellerInfo
SellerInfo
(Optional) Information about the seller
sellerInfo.name
string
(Optional) Seller name
productInfo
ProductInfo
(Optional) Information about the product
productInfo.name
string
(Optional) Name of the product
productInfo.description
string
(Optional) Description of the product
productInfo.image
string
(Optional) Product image
persistRequest
boolean
(Optional) Defaults to true, when set to false the request data is not persisted to the blockchain
showRNBranding
boolean
(Optional) Defaults to true, when set to false the "Powered by Request Network" banner is hidden
builderId
string
(Optional) An ID added to request to identify request created by builder
onPaymentSuccess
(request) => void
(Optional) Event that returns the Request data once the payment is successful.
onError
(error) => void
(Optional) Event that returns the error when something goes wrong.
amountInUSD
number
The total of the purchase in US Dollars
sellerAddress
string
Address that would accept the payments
supportedCurrencies
string[]
An array of currency IDS that are supported by the seller
sellerInfo.logo
string
(Optional) Seller logo
🕹️ Try it out
▶️ Demo Video
🏗️ Integration Video
📦 View on NPM
ℹ️ View Source

import { EthereumPrivateKeyCipherProvider } from '@requestnetwork/epk-cipher';
const cipherProvider = new EthereumPrivateKeyCipherProvider({
# Warning: private keys should never be stored in clear, this is a basic tutorial
key: '0x4025da5692759add08f98f4b056c41c71916a671cedc7584a80d73adc7fb43c0',
method: RequestNetwork.Types.Encryption.METHOD.ECIES,
});
const requestNetwork = new RequestNetwork({
cipherProvider,
signatureProvider,
useMockStorage: true,
});const payeeEncryptionPublicKey = {
key: 'cf4a1d0bbef8bf0e3fa479a9def565af1b22ea6266294061bfb430701b54a83699e3d47bf52e9f0224dcc29a02721810f1f624f1f70ea3cc5f1fb752cfed379d',
method: RequestNetwork.Types.Encryption.METHOD.ECIES,
};
const payerEncryptionPublicKey = {
key: '299708c07399c9b28e9870c4e643742f65c94683f35d1b3fc05d0478344ee0cc5a6a5e23f78b5ff8c93a04254232b32350c8672d2873677060d5095184dad422',
method: RequestNetwork.Types.Encryption.METHOD.ECIES,
};
const invoice = await requestNetwork._createEncryptedRequest(
{
requestParameters,
signer: requestParameters.payee,
paymentNetwork,
},
[payeeEncryptionPublicKey, payerEncryptionPublicKey],
);const invoiceFromRequestID = await requestNetwork.fromRequestId(requestId);
const requestData = invoiceFromRequestID.getData();
console.log(requestData);
/* {
requestId,
currency,
expectedAmount,
payee,
payer,
timestamp,
extensions,
version,
events,
state,
creator,
meta,
balance,
contentData,
} *///Accept
await request.accept(payerIdentity);
//Cancel
await request.cancel(payeeIdentity);
//Increase the expected amount
await request.decreaseExpectedAmountRequest(amount, payeeIdentity);
//Decrease the expected amount
await request.increaseExpectedAmountRequest(amount, payerIdentity);// Disable decryption
cipherProvider.enableDecryption(false);
// Check if decryption is enabled
const isEnabled = cipherProvider.isDecryptionEnabled();
// Re-enable decryption
cipherProvider.enableDecryption(true);// Check if encryption is available
const canEncrypt = cipherProvider.isEncryptionAvailable();
// Check if decryption is available
const canDecrypt = cipherProvider.isDecryptionAvailable();
// Check if an identity is registered
const isRegistered = await cipherProvider.isIdentityRegistered({
type: 'ethereum_address',
value: '0x123...'
});// Some codeimport PaymentWidget from "@requestnetwork/payment-widget/react";
export default function PaymentPage() {
return (
<PaymentWidget
sellerInfo={{
logo: "https://example.com/logo.png",
name: "Example Store",
}}
productInfo={{
name: "Digital Art Collection",
description: "A curated collection of digital artworks.",
image: "https://example.com/product-image.jpg",
}}
amountInUSD={1.5}
sellerAddress="0x1234567890123456789012345678901234567890"
supportedCurrencies={["REQ-mainnet","ETH-sepolia-sepolia","USDC-mainnet"]}
persistRequest={true}
onPaymentSuccess={(request) => {
console.log(request);
}}
onError={(error) => {
console.error(error);
}}
/>
);
}config.colors.main
string
Hex color code for primary buttons and labels
config.colors.secondary
string
Hex color code for for borders and accents
requestNetwork
The RequestNetwork instance
wagmiConfig
WagmiConfig
Wallet connector config
currencies
Currency[]
A list of custom currencies
config
IConfig
Additional configuration parameters
config.builderId
string
Unique builder ID, arbitrarily chosen, used for metrics
config.dashboardLink
string
Path to dashboard page
config.logo
string
Path to logo file
🕹️ Try it out
▶️ Demo Video
🏗️ Integration Video
📦 View on NPM
ℹ️ View Source

Two Types of Batch Payments:
Process previously created requests using their request IDs and receive payment calldata that can be executed on-chain to pay multiple requests simultaneously.
Submit new payment requests that are immediately processed, creating requests and returning payment calldata in a single API call for instant multi-recipient payments.
Gas Efficiency: Significantly reduce transaction costs by batching multiple payments
Simplified UX: Process up to 200 payments in a single transaction
Mixed Payment Types: Support ERC20, native tokens, and conversion payments in the same batch
Atomic Execution: All payments succeed or fail together, ensuring consistency
Single Network Limitation: All requests in a batch must be on the same blockchain network. Need multi-network batch payments? Book a call to discuss this feature.
The theoretical limit for batch payments is 100-200 payments per transaction, depending on:
Payment complexity (ERC20 vs native tokens vs conversions)
Available block gas limit on the target network
Smart contract computational requirements
For optimal performance, we recommend starting with smaller batches (10-50 payments) and scaling based on your network conditions.
The following examples demonstrate how to implement batch payment calldata execution in your application. Note: The API returns unsigned transaction calldata - your application must handle sending these transactions to the blockchain.
Batch payments support mixing different payment types in a single transaction:
ERC20 Token Payments: Standard token transfers
Native Token Payments: ETH, MATIC, etc.
Conversion Payments: Requests denominated in one currency but paid in another (e.g., USD invoices paid with USDC)
API Call: Your application calls the Request Network API to get transaction data
Blockchain Execution: Your application executes the returned transaction data on the blockchain
Error Handling: Your application handles transaction failures and retries
Validate Addresses: Always validate recipient addresses before submitting batch payments
Test on Testnets: Start with small batches on test networks before production deployment
Handle Failures Gracefully: Implement proper error handling for transaction failures
Gas Estimation: Consider gas costs when determining optimal batch sizes
User Experience: Provide clear progress indicators for multi-step approval processes
Common error scenarios and their solutions:
Network Mismatch: Ensure all requests use the same blockchain network
Insufficient Funds: Verify payer has sufficient balance for all payments plus gas
Invalid Addresses: Validate all payee addresses before batch submission
Gas Limit Exceeded: Reduce batch size if hitting network gas limits
See the batch payment feature in action in our EasyInvoice demo application:
For detailed information on all available endpoints and their parameters, please refer to the full Request Network API Reference.
For more implementation details, explore the EasyInvoice source code.
Create, update, and retrieve requests.
Sign requests using web3 wallets like Metamask
Sign requests using Ethereum private keys
Standards for data stored on Request, like invoice format
Decrypt encrypted requests using Ethereum private keys
Pay a request using a web3 wallet
Web server that allows easy access to the Request system
Tools for managing currency definitions
For a list of internal SDK packages, see Internal SDK Packages.
These packages offer pre-built components for quickly integrating certain Request Network features.
A form for creating invoices in Request Network
A dashboard for viewing and paying invoices in Request Network
A dialog box for granting third-party access to an encrypted invoice created via Request Finance
The Request Client library can be imported as ES6 or CommonJS modules.
These packages are published publicly but contain functions that are considered internal to the SDK Packages. It is less likely that a Builder would need to use these packages.
Extensions to the protocol
Indexing and batching of transactions
Storage of Request data on Ethereum and IPFS, with custom indexing
Serialize and deserialize object in the Request Network protocol
Payment detection, to compute the balance.
The Request business logic: properties and actions of requests
These smart contracts facilitate storing IPFS content addressable hashes (CIDs) on-chain.
Gnosis
100
Sepolia
11155111
Ethereum Mainnet (deprecated)
1
The REQ Token is on Ethereum Mainnet. The burn contracts facilitate locking xDAI on Gnosis, bridging xDAI to Ethereum, swapping xDAI for REQ, and burning the REQ.
Ethereum Mainnet
1
Gnosis
100
signatureParameter
Signing method and private key
method
Signing method
privateKey
string
Private key
ECDSA
'ecdsa'
"Vanilla" ECDSA
ECDSA_ETHEREUM
'ecdsa-ethereum'
Ethereum ECDSA with the prefix "\x19Ethereum Signed Message:\n"
decryptionParameters
Decryption method and private key
method
Decryption method
key
string
Private key
ECIES
'ecies'
Elliptic Curve Integrated Encryption Scheme (ECIES). An asymmetric key cipher.
AES256_CBC
'aes256-cbc'
Advanced Encryption Standard (AES). A symmetric key cipher with keys of length 256 in CBC mode.
AES256_GCM
'aes256-gcm'
Advanced Encryption Standard (AES). A symmetric key cipher with keys of length 256 in GCM mode.
Actions are the essential elements that compose a request. From this layer's point of view, a request is simply a list of different actions.
The payee creates the request requesting 1 ETH to the payer
The payer accepts the request
The payer increases the expected amount of the request by 1 ETH (the expected amount of the request can only be increased by the payer and decreased by the payee)
Given the list of these actions, we can interpret the state of the request. The example above describes a request that has been accepted by the payer where he will have to pay 2 ETH to the payee.
Note that the hash of the action determines the request Id. Therefore, this action doesn't specify the request Id since it doesn't exist yet. The update actions (accept and increaseExpectedAmount) specify the request Id in their data.
There are two kinds of action:
Create: This action is not related to an existing request, it will create a new one
Update: All other actions, it will update the state of an existing request
In addition to providing the structure to form an action composing a request, the request logic layer is also responsible for signing the action.
To abstract the signing process from the layer (and eventually be able to use it in other packages), the signing process is done through external packages named signature providers.
The protocol repository currently contains two signature provider packages:
Both packages use the Elliptic Curve Digital Signature Algorithm (ECDSA) used in Ethereum. web3-signature will connect to Metamask to ask users to sign requests. epk-signature uses private keys that are stored in the clear and managed manually.
The web3-signature provider should be used to create a fully-decentralized solution where the users manage their own private keys. The epk-signature provider is used to manage the private keys on behalf of the users. It's never a good idea to let users handle plain private keys.
Configure Logo and Colors
✅
Inject your own custom currency list
✅
Download Receipt as PDF
✅
Optimism
USDC, USDT, DAI, ETH
Moonbeam
USDC (multichain), USDC (wormhole)
Fantom
FTM
zkSync Era
ETH
Base
ETH
config.colors.main
string
Color used for primary buttons and labels
config.colors.secondary
string
Color used for borders and accents
requestNetwork
The RequestNetwork instance
wagmiConfig
WagmiConfig
Wallet connector config
currencies
Currency[]
A list of custom currencies
requestId
string
The ID of the request
salt
string
The salt of the request
address
string
Payment recipient address











A comprehensive guide to help you transition from V1 to V2 of the Request Network API
The Request Network API V2 introduces significant improvements while maintaining backward compatibility. This guide provides a comprehensive overview of the breaking changes between V1 and V2, along with a step-by-step migration guide.
V1 API Deprecation Notice
V1 of the Request Network API is deprecated and in security-fixes-only mode. This means:
Deprecated: We strongly recommend upgrading to V2
Security-fixes-only: V1 will only receive critical security patches, no new features, enhancements, or non-security bug fixes
Please migrate to V2 as soon as possible to ensure continued support and access to the latest features.
Path Parameter Changes
V1: Uses paymentReference as the path parameter
V2: Uses requestId as the path parameter
Response Schema Standardization
V1: Returns requestID (uppercase D) in create responses
V2: Returns requestId (lowercase d) in create responses
Enhanced Validation
V2: Stricter type checking and validation schemas
V1: More permissive validation
V1 Schema:
V2 Schema:
V1 Response:
V2 Response:
V1 Query Parameters:
V2 Query Parameters:
Audit Your Current Integration
List all V1 endpoints you're currently using
Identify which features you need (basic payments vs advanced features)
Choose Migration Strategy
V2 has enhanced error responses with more specific error codes:
Test Core Functionality
Create requests using V2 endpoints
Verify payment flows work correctly
Check response formats match expectations
Migration Support: with our team for migration assistance
GitHub Examples: Check the easy-invoice repository for V2 implementation examples
V1 endpoints will continue to work during the migration period. However, we recommend migrating to V2 to access improvements and future features:
Enhanced security and validation
Better error handling and debugging
Improved webhook events
Access to new features as they are released
V2 is the foundation for all future Request Network API features and improvements.
The Request Network SDK supports Hinkal Private Payments using ERC-20 tokens. Hinkal is a middleware and suite of smart contracts on EVM-compatible chains that leverage zero-knowledge proofs and private addresses to facilitate compliant and private transactions.
Each public address has exactly one Hinkal private address.
The @requestnetwork/payment-processor package provides functions to:
Pay a request from a Hinkal private address to a public address: such that the payment sender's public address never appears on-chain.
Deposit to a Hinkal private address from a public address: such that the payment recipient's public address never appears on-chain. Callers can choose to deposit to their own private address or someone else's private address.
Privacy: Obfuscates payer address when paying a request.
Compliance: Ensures transactions adhere to regulatory requirements. See for details
See for a list of chains on which Hinkal Private Payments are supported.
To use Hinkal Private Payments, install the necessary package:
To pay a request from a Hinkal private address to a public address, where only the payment sender's address is obfuscated, use the `payErc20FeeProxyRequestFromHinkalShieldedAddress()` function. Ensure the payment sender's Hinkal private address has a positive balance using
Strongly consider using to keep the request contents private, including the payer and payee identity addresses, when paying requests from a Hinkal private address. Revealing the payer and payee identity addresses increases the likelihood of un-shielding the payment sender's address via on-chain analysis.
To deposit funds to a Hinkal private address from a public address, where only the payment recipient's address is obfuscated, use the sendToHinkalShieldedAddressFromPublic() function.
Deposit to own Hinkal shielded address: omit the recipientInfo argument
Deposit to someone else's Hinkal shielded address: set recipientInfo to the shielded address of the payment recipient.
The Hinkal SDK depends on , a powerful library that enables local zero-knowledge proving in browser and Node.js environments. Snarkjs leverages to perform complex cryptographic computations efficiently.
As a result, any client-side application integrating the Hinkal SDK must adjust its Content-Security-Policy to allow the wasm-unsafe-eval directive under the script-src setting. This configuration ensures that the cryptographic processes can execute properly.
See for more details.
For more details about Hinkal Private Payments, refer to on GitHub.
This document outlines how to encrypt and decrypt requests using Lit Protocol. Encryption and decryption are performed using the end-user's wallet signatures, ensuring only they can access the data. Neither Request Network nor Lit Protocol can access the data without consent from the user.
This allows the end-user to own their data without requiring them to know about or manage their public key, as is the case when they .
Encryption with Lit Protocol supports the Add Stakeholder feature for adding view access to a 3rd party other than the payee or payer.
The LitCipherProvider is suitable for both frontend and backend use.
This implementation utilizes a two-step encryption process to secure sensitive data within requests:
import { ethers } from 'ethers';
// Get unsigned calldata to pay existing requests by their IDs
const batchPayResponse = await fetch('https://api.request.network/v2/payouts/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'your-api-key',
'x-platform-id': 'your-platform-id'
},
body: JSON.stringify({
requestIds: [
"01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb",
"02f384fdd39e5c627e04b1f2e6fd60593783b8863c3c789197f5bd381527b8ecd"
],
payer: "0x2e2E5C79F571ef1658d4C2d3684a1FE97DD30570"
})
});
const { batchPaymentTransaction, ERC20ApprovalTransactions } = await batchPayResponse.json();
// Your app must implement sending these transactions to the blockchain
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
// 1. Handle ERC20 approvals if needed
for (const approval of ERC20ApprovalTransactions) {
const tx = await signer.sendTransaction(approval);
await tx.wait();
}
// 2. Send the batch payment transaction
const batchTx = await signer.sendTransaction(batchPaymentTransaction);
await batchTx.wait();// Create new requests and process them immediately
const batchPayResponse = await fetch('https://api.request.network/v2/payouts/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'your-api-key',
'x-platform-id': 'your-platform-id'
},
body: JSON.stringify({
requests: [
{
payee: "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
amount: "10",
invoiceCurrency: "USD",
paymentCurrency: "USDC-sepolia"
},
{
payee: "0xb07D2398d2004378cad234DA0EF14f1c94A530e4",
amount: "25.50",
invoiceCurrency: "EUR",
paymentCurrency: "DAI-sepolia"
}
],
payer: "0x2e2E5C79F571ef1658d4C2d3684a1FE97DD30570"
})
});
const { batchPaymentTransaction, ERC20ApprovalTransactions } = await batchPayResponse.json();
// Your app must implement the blockchain transaction execution
// (same pattern as Batch Pay Invoices example above)import { RequestNetwork } from '@requestnetwork/request-client.js';
import { Web3SignatureProvider } from '@requestnetwork/web3-signature';
import { payRequest } from '@requestnetwork/payment-processor';const { RequestNetwork } = require('@requestnetwork/request-client.js');
const { Web3SignatureProvider } = require('@requestnetwork/web3-signature');
const { payRequest } = require("@requestnetwork/payment-processor");import { EthereumPrivateKeySignatureProvider } from "@requestnetwork/epk-signature";import { EthereumPrivateKeyDecryptionProvider } from "@requestnetwork/epk-decryption";import { PaymentReferenceCalculator } from "@requestnetwork/request-client.js";import { payRequest } from "@requestnetwork/payment-processor";Sources and artifacts of the smart contracts
Storage of Request data on Ethereum and IPFS, indexed by TheGraph
Creates transactions to be sent to Data Access, managing encryption
Typescript types shared across @requestnetwork packages
Collection of tools shared between the @requestnetwork packages

Goerli (deprecated)
goerli
5
Optimism
optimism
10
Arbitrum One
arbitrum-one
42161
Base
base
8453
zkSync Era
zksyncera
324
zkSync Goerli (deprecated)
zksynceratestnet
280
Gnosis
xdai
100
Polygon
matic
137
Mumbai
mumbai
80001
BSC
bsc
56
BSC Testnet
bsctest
97
Celo
celo
42220
Alfajores
alfajores
44787
Fantom
fantom
250
Tombchain
tombchain
6969
Core
core
1116
Avalanche
avalanche
43114
Fuse
fuse
122
Moonbeam
moonbeam
1284
Ronin
ronin
2020
Mantle
mantle
5000
Mantle Testnet
mantle-testnet
5001
NEAR
N/A
N/A
NEAR Testnet
N/A
N/A
Ethereum Mainnet
mainnet
1
Sepolia
sepolia
11155111
Goerli (deprecated)
5
Incremental: Migrate endpoints one by one (recommended)
Full Migration: Switch all endpoints at once
Parallel: Run V1 and V2 side by side
Enhanced Validation Testing
Test stricter type checking
Verify improved error responses
Performance Testing
Compare response times between V1 and V2
Test with realistic data volumes
Create Request
POST /v1/request
POST /v2/request
Get Request
GET /v1/request/{paymentReference}
GET /v2/request/{requestId}
Get Request Status
GET /v1/request/{paymentReference}/status
GET /v2/request/{requestId}/status
Get Payment Calldata
GET /v1/request/{paymentReference}/pay
GET /v2/request/{requestId}/pay
Get Payment Routes
GET /v1/request/{paymentReference}/routes
GET /v2/request/{requestId}/routes
{
"amount": "string",
"payee": "string",
"invoiceCurrency": "string",
"paymentCurrency": "string"
}{
"amount": "string",
"payee": "string",
"invoiceCurrency": "string",
"paymentCurrency": "string"
// V2 accepts additional optional fields for extended functionality
}{
"requestID": "string", // Note: uppercase D
"paymentReference": "string"
}{
"requestId": "string", // Note: lowercase d
"paymentReference": "string"
}interface PayRequestQueryV1 {
payerAddress?: string;
routeId?: string;
}interface PayRequestQueryV2 {
payerAddress?: string;
routeId?: string;
// Enhanced validation with stricter type checking
}// Get request status
const response = await fetch(`/v1/request/${paymentReference}/status`);
// Get payment calldata
const payData = await fetch(`/v1/request/${paymentReference}/pay?payerAddress=${address}`);// Get request status
const response = await fetch(`/v2/request/${requestId}/status`);
// Get payment calldata
const payData = await fetch(`/v2/request/${requestId}/pay?payerAddress=${address}`);const createResponse = await fetch('/v1/request', {
method: 'POST',
body: JSON.stringify(requestData)
});
const { requestID, paymentReference } = await createResponse.json();
// Note: requestID with uppercase Dconst createResponse = await fetch('/v2/request', {
method: 'POST',
body: JSON.stringify(requestData)
});
const { requestId, paymentReference } = await createResponse.json();
// Note: requestId with lowercase dtry {
const response = await fetch('/v2/request', {
method: 'POST',
body: JSON.stringify(requestData)
});
if (!response.ok) {
const error = await response.json();
// V2 provides more detailed error information
console.error('Error:', error.message);
console.error('Status Code:', error.statusCode);
console.error('Error Code:', error.error);
}
} catch (error) {
console.error('Request failed:', error);
}npm install @requestnetwork/payment-processorimport {
payErc20FeeProxyRequestFromHinkalShieldedAddress,
} from '@requestnetwork/payment-processor';
// Instantiation of `RequestNetwork` and `Signer` omitted for brevity
const request = await requestClient.fromRequestId('insert request id');
const requestData = request.getData();
const relayerTx = await payErc20FeeProxyRequestFromHinkalShieldedAddress(
requestData,
signer,
);import {
sendToHinkalShieldedAddressFromPublic,
} from '@requestnetwork/payment-processor';
// Instantiation of `Signer` omitted for brevity
const recipientShieldedAddress = '142590100039484718476239190022599206250779986428210948946438848754146776167,0x096d6d5d8b2292aa52e57123a58fc4d5f3d66171acd895f22ce1a5b16ac51b9e,0xc025ccc6ef46399da52763a866a3a10d2eade509af27eb8411c5d251eb8cd34d'
const tx = await sendToHinkalShieldedAddressFromPublic({
signerOrProvider: paymentSender,
tokenAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // USDC on Base
amount: '1000000', // 1 USDC
recipientInfo: recipientShieldedAddress, // omit to deposit to own Hinkal shielded address
})Settings for conversion payments
request
The request object
signerOrProvider
ethers.providers.Web3Provider | ethers.Signer = getProvider()
An ethers v5 Provider. See for explanation how to wrap a viem WalletClient to look like an ethers v5 Provider.
amount
ethers.BigNumberish
The amount to pay. Defaults to the expected amount of the request.
overrides
Omit<ethers.providers.TransactionRequest, 'to' | 'data' | 'value'>
Override transaction settings like baseFee and maxPriorityFee
paymentSettings
currency
ICurrency
The currency in which the payment is made, not the currency in which the request is denominated.
maxToSpend
ethers.BigNumberish
The maximum input currency to spend on conversion payments. Protects the user from rapidly changing exchange rates.
currencyManager
ICurrencyManager
A Currency manager handles a list of currencies and provides utility to retrieve and change format
Symmetric Encryption: The data is first encrypted using a randomly generated symmetric key (e.g., AES-256). This provides efficient encryption for larger data payloads.
Asymmetric Encryption with Lit Protocol: The symmetric key is then encrypted using Lit Protocol's decentralized key management network. Only authorized parties (payer and payee) can access the symmetric key and decrypt the data.
Ease-of-use: Encrypt using a signature instead of a public key.
Efficiency: Symmetric encryption is efficient for large data, while Lit Protocol secures the key.
Decentralized Access Control: Lit Protocol ensures that only authorized parties can decrypt the data.
The system consists of three main components:
Request Network: Handles the creation, storage, and lifecycle of payment requests on the blockchain.
Lit Protocol: Provides a decentralized key management network and encryption capabilities.
Wallet Addresses: Used as the primary identifiers for access control in Lit Protocol.
Request Creation: The payer creates a request object using the Request Network SDK.
Symmetric Key Generation: A unique symmetric key is randomly generated.
Data Encryption: The payee and payer encrypt the sensitive data within the request using the generated symmetric key.
Encrypt Symmetric Key with Lit:
Define Access Control Conditions: The payee and payer define access control conditions using Lit Actions, specifying that only the Ethereum addresses of the payer and payee can decrypt the symmetric key.
Encrypt with Lit: The payee and payer encrypt the symmetric key using Lit's encryptString function, leveraging their wallet to sign the encryption.
Store Encrypted Data: The payee and payer store the following on the Request Network:
Encrypted request data
Lit access control conditions
Encrypted symmetric key
Retrieve Request: The payer and payee retrieve the following request data from the Request Network:
Encrypted request data
Lit access control conditions
Encrypted symmetric key
Decrypt Symmetric Key with Lit: The payer and payee use Lit's decryptString function with their wallet to decrypt the encrypted symmetric key. Lit Protocol verifies the payer's and payee's addresses against access control conditions. If authorized, the symmetric key is decrypted.
Decrypt Data: The payer and payee use the decrypted symmetric key to decrypt the sensitive data.
The wallet address must be included in the original encryption parameters
Session signatures must be valid
Decryption must be enabled
The Lit Protocol client must be connected
This page is missing the RequestToken, DAIbasedREQBurner, lockForREQBurn, ChainlinkConversionPath contracts
Request Network smart contracts are available .
There are three types of contracts
Storage - These store for Requests stored in IPFS.
Payments - These process various payment types, also known as , and are deployed across many .
REQ Token and Burn Mechanism - These lock, bridge, and burn REQ tokens each time a Request is stored.
Declares data hashes and collects the fees.
After a request has been sent to IPFS, the hash is declared to the whole request network system through the RequestHashStorage.
Anyone can submit hashes.
Manages the fees for the creation of a request.
This contract is the entry point to retrieve all the hashes of the request network system.
Performs an ERC20 token transfer with a payment reference and a transfer to a second address for the payment of a fee.
Performs an ERC20 token transfer with a payment reference and a transfer to a second address.
This contract performs an Ethereum transfer with a Fee sent to a third address and stores a reference.
This contract performs an Ethereum transfer sent to a third address and stores a reference.
This contract allows users to lock funds in an escrow and perform payments in ERC20. It contains a refund and emergency feature to unlock funds if needed.
This contract makes multiple conversion payments with a payment references, in one transaction.
This contract makes multiple payments with payment references, in one transaction.
This contract makes multiple payments with references, in one transaction, without conversion.
This contract swaps ERC20 tokens before paying a request such that the payer sends currency A, but payee receives currency B.
This contract uses a chainlink price feed to pay a request denominated in one currency (usually a fiat currency like USD) but paid in an on-chain currency. This variant supports ERC20 payments.
This contract uses a chainlink price feed to pay a request denominated in one currency (usually a fiat currency like USD) but paid in an on-chain currency. This variant supports native currency payments.
This contract combines "conversion" and "swap-to-pay". It executes an ERC20 swap before paying a request denominated in one currency (usually a fiat currency like USD) but paid in an on-chain currency. This variant supports ERC20 payments.
This contract allows minting requests as NFTs thus allowing them to be transferred. The owner of the request NFT receives the payment.
A contract that allows payment through the without having to make a function call.
A contract that allows payment through without having to make a function call.
A factory smart contract responsible for deploying and contracts.
A Guide to Crypto-to-fiat Payments, Compliance, and Webhooks with the Request Network API
Crypto-to-fiat payments allow a Payer to pay a Request in cryptocurrency, while the Payee receives fiat currency directly in their bank account. This is achieved by combining the Request Network crypto payment with offramp infrastructure. This requires prerequisite compliance (KYC/Agreement) and bank account registration (payment detail) flows.
includes a reference implementation for this flow.
In a previous chapter, we used the signature providers @requestnetwork/web3-signature and @requestnetwork/epk-signature (this one is made for test purpose). But, if you are not using web3, you need to inject your own signature mechanism to the request client. This is fairly simple, you need to implement a class following this interface: (see on )
For example, your own package to sign needs an ethereum address and return the signature as a hexadecimal string:
Your signature provider would look like:
npm install @requestnetwork/lit-protocol-cipher @requestnetwork/request-client.js [email protected]import { LitProtocolCipherProvider } from '@requestnetwork/lit-protocol-cipher';
import { RequestNetwork, Types } from '@requestnetwork/request-client.js';
import { LitNodeClient } from '@lit-protocol/lit-node-client';
// Node connection configuration
const nodeConnectionConfig = {
baseURL: 'https://req-node.request.network',
connectionTimeout: 10000,
retry: {
retries: 3
}
};
// Initialize Lit Node Client
const litClient = new LitNodeClient({
litNetwork: 'datil',
debug: false
});
// Initialize the Lit Provider
const litProvider = new LitProtocolCipherProvider(
litClient,
nodeConnectionConfig,
'ethereum' // optional chain parameter
);
// Connect to Lit Network
await litProvider.initializeClient();
// Initialize wallet and get session signatures
const wallet = new Wallet('your-private-key');
const address = await wallet.getAddress();
// Get session signatures
await litProvider.getSessionSignatures(wallet, address);
// Enable decryption
litProvider.enableDecryption(true);
// Initialize Request Network
const requestNetwork = new RequestNetwork({
cipherProvider: litProvider,
signatureProvider: new Web3SignatureProvider(wallet),
nodeConnectionConfig
});const payeeIdentity = {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: 'payee-ethereum-address'
};
const payerIdentity = {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: 'payer-ethereum-address'
};
// Define encryption parameters
const encryptionParams = [
{
key: payeeIdentity.value,
method: Types.Encryption.METHOD.KMS
},
{
key: payerIdentity.value,
method: Types.Encryption.METHOD.KMS
}
];
// Create request parameters
const requestCreateParameters = {
requestInfo: {
currency: {
type: Types.RequestLogic.CURRENCY.ERC20,
value: '0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C',
network: 'sepolia',
},
expectedAmount: '1000000000000000000',
payee: payeeIdentity,
payer: payerIdentity,
timestamp: Utils.getCurrentTimestampInSecond(),
},
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
parameters: {
paymentNetworkName: 'sepolia',
paymentAddress: payeeIdentity.value,
feeAddress: '0x0000000000000000000000000000000000000000',
feeAmount: '0',
},
},
contentData: {
reason: '🍕',
dueDate: '2023.06.16',
},
signer: payeeIdentity,
};
// Create the encrypted request
const request = await requestNetwork._createEncryptedRequest({
requestParameters: requestCreateParameters,
encryptionParams
});// Fetch an existing request
const requestId = "request_id_here";
const request = await requestNetwork.fromRequestId(requestId);
// If you have the correct permissions (wallet address in encryption params),
// and decryption is enabled, the data will be automatically decrypted
const requestData = await request.getData();
// The decrypted data will include:
console.log({
requestInfo: requestData.requestInfo,
paymentNetwork: requestData.paymentNetwork,
contentData: requestData.contentData,
state: requestData.state
});// Disable decryption
litProvider.enableDecryption(false)// Proper cleanup sequence
try {
// First disconnect the wallet
await litProvider.disconnectWallet();
// Then disconnect the client
await litProvider.disconnectClient();
} catch (error) {
console.error('Cleanup error:', error);
}interface ICipherProvider {
encrypt(data: any, options: any): Promise<any>;
decrypt(encryptedData: any, options: any): Promise<any>;
isEncryptionAvailable(): boolean;
isDecryptionAvailable(): boolean;
enableDecryption(option: boolean): void;
isDecryptionEnabled(): boolean;
}
Now you can inject it into the request client:
## Example 2
For example, your own package to sign needs an internal identifier and return the signature as a Buffer:
Your signature provider would look like:
Now you can inject it into the request client:
export interface ISignatureProvider {
supportedMethods: Signature.METHOD[];
supportedIdentityTypes: Identity.TYPE[];
sign: (data: any, signer: Identity.IIdentity) => Promise<Signature.ISignedData>;
}class mySignaturePackage {
/**
* Sign data
*
* @param data the data to sign
* @param address the address to sign with
* @returns a promise resolving the signature
*/
public async sign(data: any, address: string): Promise<string>;
}import { IdentityTypes, SignatureProviderTypes, SignatureTypes } from '@requestnetwork/types';
import Utils from '@requestnetwork/utils';
// Your package
import mySignaturePackage from 'mySignaturePackage';
/**
* Implementation of the signature provider for my wallet
*/
export default class MySignatureProvider implements SignatureProviderTypes.ISignatureProvider {
/** list of supported signing methods */
public supportedMethods: SignatureTypes.METHOD[] = [SignatureTypes.METHOD.ECDSA];
/** list of supported identity types */
public supportedIdentityTypes: IdentityTypes.TYPE[] = [IdentityTypes.TYPE.ETHEREUM_ADDRESS];
/**
* Signs data
*
* @param string data the data to sign
* @returns IIdentity the identity to sign with. If not given, the default signer will be used
*
* @returns string the signature
*/
public async sign(
data: any,
signer: IdentityTypes.IIdentity,
): Promise<SignatureTypes.ISignedData> {
if (!this.supportedIdentityTypes.includes(signer.type)) {
throw Error(`Identity type not supported ${signer.type}`);
}
// Hash the normalized data (e.g. avoid case sensitivity)
const hashData = Utils.crypto.normalizeKeccak256Hash(data).value;
// use your signature package
const signatureValue = mySignaturePackage.sign(hashData, signer.value);
return {
data,
signature: {
method: SignatureTypes.METHOD.ECDSA,
value: signatureValue,
},
};
}
}import MySignatureProvider from 'mySignatureProvider';
const mySignatureProvider = new MySignatureProvider();
// We can initialize the RequestNetwork class with the signature provider
const requestNetwork = new RequestNetwork.RequestNetwork({
signatureProvider: mySignatureProvider,
});class mySignaturePackage {
/**
* Sign a Buffer
*
* @param data the data to sign
* @param walletId a way to get the right wallet to sign with
* @returns a promise resolving the signature
*/
public async sign(data: Buffer, walletId: number): Promise<Buffer>;
}import { IdentityTypes, SignatureProviderTypes, SignatureTypes } from '@requestnetwork/types';
import Utils from '@requestnetwork/utils';
// Your package
import mySignaturePackage from 'mySignaturePackage';
/** Type of the dictionary of wallet id indexed by address */
type IWalletIdDictionary = Map<string, number>;
/**
* Implementation of the signature provider for my wallet
*/
export default class MySignatureProvider implements SignatureProviderTypes.ISignatureProvider {
/** list of supported signing method */
public supportedMethods: SignatureTypes.METHOD[] = [SignatureTypes.METHOD.ECDSA];
/** list of supported identity types */
public supportedIdentityTypes: IdentityTypes.TYPE[] = [IdentityTypes.TYPE.ETHEREUM_ADDRESS];
/** Dictionary containing all the private keys indexed by address */
private walletIdDictionary: IWalletIdDictionary;
constructor(identity?: ?IdentityTypes.IIdentity, walletId?: number) {
this.walletIdDictionary = new Map<string, number>();
if (identity && walletId) {
this.addSignatureParameters(identity, walletId);
}
}
/**
* Signs data
*
* @param string data the data to sign
* @returns IIdentity the identity to sign with. If not given, the default signer will be used
*
* @returns string the signature
*/
public async sign(
data: any,
signer: IdentityTypes.IIdentity,
): Promise<SignatureTypes.ISignedData> {
if (!this.supportedIdentityTypes.includes(signer.type)) {
throw Error(`Identity type not supported ${signer.type}`);
}
// toLowerCase to avoid mismatch because of case
const walletId: number | undefined = this.walletIdDictionary.get(signer.value.toLowerCase());
if (!walletId) {
throw Error(`Identity unknown: ${signer.type}, ${signer.value}`);
}
// Hash the normalized data (e.g. avoid case sensitivity)
const hashData = Utils.crypto.normalizeKeccak256Hash(data).value;
// convert the hash from a string '0x...' to a Buffer
const hashDataBuffer = Buffer.from(hashData.slice(2), 'hex');
// use your signature package
const signatureValueBuffer = mySignaturePackage.sign(hashDataBuffer, walletId);
// convert the signature to a string '0x...'
const signatureValue = `0x${signatureValueBuffer.toString('hex')}`;
return {
data,
signature: {
method: SignatureTypes.METHOD.ECDSA,
value: signatureValue,
},
};
}
/**
* Function to add a new identity in the provider
*
* @param identity the new identity
* @param walletId the wallet id matching the identity
*/
public addIdentity(identity: IdentityTypes.IIdentity, walletId: number): void {
if (!this.supportedIdentityTypes.includes(identity.type)) {
throw Error(`Identity type not supported ${identity.type}`);
}
this.walletIdDictionary.set(identity.value.toLowerCase(), walletId);
}
}import MySignatureProvider from 'mySignatureProvider';
const mySignatureProvider = new MySignatureProvider(anIdentity, aWalletId);
// We can initialize the RequestNetwork class with the signature provider
const requestNetwork = new RequestNetwork.RequestNetwork({
signatureProvider: mySignatureProvider,
});
// later on, you can even add more supported identities
mySignatureProvider.addIdentity(anotherIdentity, anotherWalletId);All developers can immediately access the Crypto-to-fiat Sandbox to build and test their integration:
Create an account on the Request Network API Portal
Generate a Sandbox API key with crypto-to-fiat sandbox access
Start building with Sepolia testnet USDC, simulated KYC, and mock bank accounts
The sandbox provides a complete testing environment where you can:
Test the full crypto-to-fiat flow without real funds
Simulate payer KYC verification using mock documents
Work with mock bank account data and fiat payment status
Important: Other Payment Types Use Real Funds
The "Crypto-to-fiat Sandbox" setting for API keys only affects crypto-to-fiat payments. Other payment types can process real funds with any API key, even Sandbox API keys.
When you're ready to go live with real transactions:
Book a call to request production access
Discuss your use case with our team to ensure the best integration approach
Complete the approval process - we'll work with you to get everything set up
Generate Production API keys once approved
Production access includes:
Real USDC transactions on mainnet
Actual KYC verification for payers
Live bank account validation
Fiat deposits to real bank accounts
For Crypto-to-fiat Payments, the Request Network API supports USDC on Ethereum, Polygon, Arbitrum One, and Sepolia.
Many /payer endpoints in the Request Network API require a clientUserId as a path parameter. This value is an arbitrary identifier chosen by your platform to represent a user (the payer) in your own system.
You control the format: The clientUserId can be any unique string that makes sense for your application. It can be a UUID, email address, database ID, or anything unique per user on your platform.
EasyInvoice Example: In the EasyInvoice demo app, clientUserId is set to the user's email address.
Why is this useful? This approach allows you to integrate the Request Network API without having to change your existing user management logic. You simply pass your own identifier to the API, and all payer-related compliance, agreement, and payment detail records will be associated with that value.
Example usage:
In each case, replace {clientUserId} with your chosen identifier for the user.
Before a payer can use crypto-to-fiat, they must complete compliance steps:
KYC: The payer must submit a KYC application.
Agreement: The payer must sign a compliance agreement (via an iframe flow).
Bank Account: The payee's bank account must be associated with a payer for compliance reasons, even though the payee owns the account.
Submit KYC: The platform collects KYC information from the payer and submits it to the API.
KYC Review: The platform receives webhook updates as the KYC is processed (compliance.updated with kycStatus).
Agreement Signature: The platform displays an iframe for the payer to sign the compliance agreement. Once signed, the platform calls the API to update the agreement status.
Agreement Confirmation: The platform receives a webhook update when the agreement is completed (compliance.updated with agreementStatus).
POST /payer: Submit KYC application.
GET /payer/{clientUserId}: Get compliance status for a payer.
PATCH /payer/{clientUserId}: Update agreement status after signature.
Before a payer can pay in crypto and the payee can receive fiat, the platform must:
Submit the payee’s bank account details (associated with a payer for compliance).
Wait for approval of those payment details (usually less than 60 seconds, confirmed via webhook).
Create a new request with isCryptoToFiatAllowed = true.
Submit Bank Account: The platform submits the payee’s bank account details, associating them with a payer. The Request Network API forwards these details to the offramp provider (Request Tech).
Approval: The platform receives a webhook (payment_detail.updated) indicating if the payment details are approved, failed, or pending.
Create Request: Once approved, the platform creates a new request as usual, but with the isCryptoToFiatAllowed flag set to true. This signals that the request is eligible for crypto-to-fiat payment.
While it is technically possible to create a crypto-to-fiat request before the payer has completed KYC, EasyInvoice intentionally requires the payer to complete KYC first. This decision is based on several practical and UX considerations:
Bank Account Association: The payee’s bank account ("payment details") must be linked to a specific payer, which can only be done after the payer completes KYC. This ensures compliance and accurate association of payment details.
Validation Complexity: Although the payee could submit their bank account details in advance, the platform cannot validate or approve these details until the payer’s KYC is complete. This would introduce additional communication steps and potential confusion.
UI Simplicity: EasyInvoice integrates payee bank account registration directly into the Create Invoice form. If the user clicks "Create" before the bank account is approved, a loading indicator appears until approval is granted. This avoids the need for a separate bank account management page and keeps the user experience straightforward.
Protocol Fit: The crypto-to-fiat feature is integrated at the API level, not at the Request Network protocol level. Creating a request on the protocol does not require bank account details, because the protocol itself only handles crypto payments. The additional bank account and offramp logic is layered on top via the API, which transfers crypto to Request Tech, who then executes the offramp and sends fiat to the payee’s bank account. This separation adds some complexity, so the UI is intentionally kept simple.
Future Flexibility: While some clients may want to allow payees to create requests before payer KYC, this is not currently supported in EasyInvoice to avoid increased complexity. We may revisit this if there is sufficient market demand.
This approach ensures a smooth, compliant, and user-friendly experience, even if it means some technical possibilities are not exposed in the current UI.
POST /payer/{clientUserId}/payment-details: Create payment details (register bank account) for a payee.
GET /payer/{clientUserId}/payment-details: Get payment details (bank accounts) for a payee.
POST /v2/request with isCryptoToFiatAllowed = true: Create a new crypto-to-fiat request
The payer pays in crypto; Request Tech handles offramping and fiat payout.
Flow Explanation
Get Payment Calldata: The platform fetches payment calldata for the request.
User Pays: The payer signs and submits the transaction, sending crypto to Request Tech.
Offramp Processing: Request Tech receives the crypto and begins the offramp process.
Status Updates: The platform receives webhook events as the offramp progresses (payment.processing, payment.failed), with indicating the current offramp stage.
Fiat Delivered: When the offramp is complete, the platform receives a final webhook ( with ), and then a event.
Event
Description
subStatus values (if any)
compliance.updated
KYC/Agreement status updates
kycStatus: initiated, pending, approved, rejected, failed
agreementStatus: not_started, pending, completed, rejected, failed
payment_detail.updated
Payment detail (bank account) status
approved, failed, pending
payment.processing
Offramp in progress
initiated, pending_internal_assessment, ongoing_checks, sending_fiat, fiat_sent, bounced, retry_required
payment.failed
Offramp or payment failed
failed, bounced
📫 Smart Contract Addresses
ℹ️ Smart Contract Source
This page will introduce the primary operations provided by Request Network’s SDK while using the EthereumPrivateKeySignatureProvider to sign requests with a private key that is managed outside of a wallet.
This approach works well for Node.js environments without access to a Web3 wallet.
All of the following examples can be found in this repository
To create an unencrypted ERC-20 request, first construct an EthereumPrivateKeySignatureProvider with a private key.
Then, first construct a RequestNetwork, passing in the:
Request Node URL. In this example, we use the Sepolia Request Node Gateway.
EthereumPrivateKeySignatureProvider constructed in the previous step.
Prepare the Request creation parameters:
Then, call createRequest() to create the request and waitForConfirmation() to wait until the request is persisted in IPFS and the CID hash is stored on-chain.
Altogether it looks like this:
First, construct a RequestNetwork object and connect it to a Request Node. In this example, we use the Sepolia Request Node Gateway:
Then, retrieve the request and get the request data. Take note of the current request balance, to be used later for payment detection.
Then, construct an ethers v5 Provider and Wallet using a private key. These allow you to read and write to the chain, respectively.
Unfortunately, the Request Network SDK does not yet support ethers v6.
Coming soon. Probably involves publicClientToProvider() and walletClientToSigner().
Then, check that the payer has sufficient funds using hasSufficientFunds()
Then, in the case of an ERC-20 request, check that the payer has granted sufficient approval using hasErc20Approval(). If not, submit an approval transaction using approveErc20. Wait for an appropriate number of block confirmations. On Sepolia or Ethereum, 2 block confirmations should suffice. Other chains may require more.
Finally, pay the request using payRequest()
Detect that the payment was successful by polling the request and waiting until the request balance is greater than or equal to the expected amount.
Altogether it looks like this:
First, construct a RequestNetwork object and connect it to a Request Node. In this example, we use the Sepolia Request Node Gateway:
Then, call fromIdentity() to get an array of Request objects or fromRequestId() to get a single Request object. This function retrieves the Requests stored in IPFS and queries on-chain events to determine the balances paid so far. Finally, call getData() on each Request to get the request contents.
Altogether it looks like this:
The Request Network client.
GET /v2/payer/{clientUserId}
PATCH /v2/payer/{clientUserId}
POST /v2/payer/{clientUserId}/payment-details
GET /v2/payer/{clientUserId}/payment-detailsimport { ContractTransaction, providers, Signer, BigNumberish, BigNumber } from 'ethers';
import { erc20SwapConversionArtifact } from '@requestnetwork/smart-contracts';
import { ClientTypes, ExtensionTypes } from '@requestnetwork/types';
import { ITransactionOverrides } from './transaction-overrides';
import { getProvider, getSigner } from './utils';
import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20';
import { IPreparedTransaction } from './prepared-transaction';
/**
* Processes the approval transaction of a given payment ERC20 to be spent by the swap router,
* if the current approval is missing or not sufficient.
* @param request request to pay, used to know the network
* @param ownerAddress address of the payer
* @param paymentTokenAddress ERC20 currency used for the swap
* @param signerOrProvider the web3 provider. Defaults to Etherscan.
* @param minAmount ensures the approved amount is sufficient to pay this amount
* @param overrides optionally, override default transaction values, like gas.
*/
export async function approveErc20ForSwapWithConversionIfNeeded(
request: ClientTypes.IRequestData,
ownerAddress: string,
paymentTokenAddress: string,
signerOrProvider: providers.Provider | Signer = getProvider(),
minAmount: BigNumberish,
overrides?: ITransactionOverrides,
): Promise<ContractTransaction | void> {
if (
!(await hasErc20ApprovalForSwapWithConversion(
request,
ownerAddress,
paymentTokenAddress,
signerOrProvider,
minAmount,
))
) {
return approveErc20ForSwapWithConversionToPay(
request,
paymentTokenAddress,
signerOrProvider,
overrides,
);
}
}
/**
* Verify if a given payment ERC20 to be spent by the swap router
* @param request request to pay, used to know the network
* @param ownerAddress address of the payer
* @param paymentTokenAddress ERC20 currency used for the swap
* @param signerOrProvider the web3 provider. Defaults to Etherscan.
* @param minAmount ensures the approved amount is sufficient to pay this amount
* @param overrides optionally, override default transaction values, like gas.
*/
export async function hasErc20ApprovalForSwapWithConversion(
request: ClientTypes.IRequestData,
ownerAddress: string,
paymentTokenAddress: string,
signerOrProvider: providers.Provider | Signer = getProvider(),
minAmount: BigNumberish,
): Promise<boolean> {
if (!request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]) {
throw new Error(`The request must have the payment network any-to-erc20-proxy`);
}
const network =
request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY].values.network;
return await checkErc20Allowance(
ownerAddress,
erc20SwapConversionArtifact.getAddress(network),
signerOrProvider,
paymentTokenAddress,
minAmount,
);
}
/**
* Processes the approval transaction of the payment ERC20 to be spent by the swap router.
* @param request request to pay, used to know the network
* @param paymentTokenAddress picked currency for the swap to pay
* @param signerOrProvider the web3 provider. Defaults to Etherscan.
* @param overrides optionally, override default transaction values, like gas.
*/
export async function approveErc20ForSwapWithConversionToPay(
request: ClientTypes.IRequestData,
paymentTokenAddress: string,
signerOrProvider: providers.Provider | Signer = getProvider(),
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
const network =
request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY].values.network;
if (!network) {
throw new Error(`Payment network currency must have a network`);
}
const preparedTx = prepareApprovalErc20ForSwapWithConversionToPay(
request,
paymentTokenAddress,
signerOrProvider,
overrides,
);
const signer = getSigner(signerOrProvider);
const tx = await signer.sendTransaction(preparedTx);
return tx;
}
/**
* Prepare the approval transaction of the payment ERC20 to be spent by the swap router.
* @param request request to pay, used to know the network
* @param paymentTokenAddress picked currency for the swap to pay
* @param signerOrProvider the web3 provider. Defaults to Etherscan.
* @param overrides optionally, override default transaction values, like gas.
*/
export function prepareApprovalErc20ForSwapWithConversionToPay(
request: ClientTypes.IRequestData,
paymentTokenAddress: string,
signerOrProvider: providers.Provider | Signer = getProvider(),
overrides?: ITransactionOverrides,
amount?: BigNumber,
): IPreparedTransaction {
const network =
request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY].values.network;
if (!network) {
throw new Error(`Payment network currency must have a network`);
}
const encodedTx = encodeApproveAnyErc20(
paymentTokenAddress,
erc20SwapConversionArtifact.getAddress(network),
signerOrProvider,
amount,
);
return {
data: encodedTx,
to: paymentTokenAddress,
value: 0,
...overrides,
};
}
import { RequestNetwork } from "@requestnetwork/request-client.js";import { IConfig } from "./types";
export const config: IConfig = {
builderId: "request-network", // Replace with your builder ID, arbitrarily chosen, used to identify your app
dashboardLink: "/",
logo: "/assets/logo-sm.svg",
colors: {
main: "#0BB489",
secondary: "#58E1A5",
},
};
import { IConfig } from "./types";
export const config: IConfig = {
builderId: "request-network", // Replace with your builder ID, arbitrarily chosen, used to identify your app
dashboardLink: "/",
logo: "/assets/logo-sm.svg",
colors: {
main: "#0BB489",
secondary: "#58E1A5",
},
};
import { Wallet, providers, BigNumber } from 'ethers';
import {
ClientTypes,
ExtensionTypes,
IdentityTypes,
RequestLogicTypes,
} from '@requestnetwork/types';
import { deepCopy } from '@requestnetwork/utils';
import { approveErc20ForSwapWithConversionIfNeeded } from '../../src/payment/swap-conversion-erc20';
import { ERC20, ERC20__factory } from '@requestnetwork/smart-contracts/types';
import { swapToPayAnyToErc20Request } from '../../src/payment/swap-any-to-erc20';
import { IConversionSettings } from '../../src/types';
import { currencyManager } from './shared';
import { UnsupportedCurrencyError } from '@requestnetwork/currency';
/* eslint-disable no-magic-numbers */
/* eslint-disable @typescript-eslint/no-unused-expressions */
const paymentTokenAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40';
const acceptedTokenAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35';
let paymentToken: ERC20;
let acceptedToken: ERC20;
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: 'USD',
currencyInfo: {
type: RequestLogicTypes.CURRENCY.ISO4217,
value: 'USD',
},
events: [],
expectedAmount: '100',
extensions: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: '2',
paymentAddress,
salt: 'salt',
acceptedTokens: [acceptedTokenAddress],
network: 'private',
},
version: '0.1.0',
},
},
extensionsData: [],
meta: {
transactionManagerMeta: {},
},
pending: null,
requestId: 'abcd',
state: RequestLogicTypes.STATE.CREATED,
timestamp: 0,
version: '1.0',
};
const validSwapSettings = {
deadline: 2599732187000, // This test will fail in 2052
maxInputAmount: '3000000000000000000',
path: [paymentTokenAddress, acceptedTokenAddress],
};
const validConversionSettings: IConversionSettings = {
currency: {
type: 'ERC20' as any,
value: acceptedTokenAddress,
network: 'private',
},
currencyManager,
};
beforeAll(async () => {
paymentToken = await ERC20__factory.connect(paymentTokenAddress, provider);
acceptedToken = await ERC20__factory.connect(acceptedTokenAddress, provider);
});
describe('swap-any-to-erc20', () => {
describe('swapErc20FeeProxyRequest', () => {
it('should throw an error if the settings are missing', async () => {
await expect(
swapToPayAnyToErc20Request(validRequest, wallet, {
conversion: validConversionSettings,
}),
).rejects.toThrowError('Swap Settings are required');
await expect(
swapToPayAnyToErc20Request(validRequest, wallet, {
swap: validSwapSettings,
}),
).rejects.toThrowError('Conversion Settings are required');
});
it('should throw an error if the payment network is wrong', async () => {
const request = deepCopy(validRequest);
delete request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY];
await expect(
swapToPayAnyToErc20Request(request, wallet, {
conversion: validConversionSettings,
swap: validSwapSettings,
}),
).rejects.toThrowError('The request must have the payment network any-to-erc20-proxy');
});
it('should throw an error if the conversion path is impossible', async () => {
const request = deepCopy(validRequest);
(request.currencyInfo = {
type: RequestLogicTypes.CURRENCY.ISO4217,
value: 'XXX',
}),
await expect(
swapToPayAnyToErc20Request(request, wallet, {
conversion: validConversionSettings,
swap: validSwapSettings,
}),
).rejects.toThrowError(
/Impossible to find a conversion path between from XXX \(0x.*\) to ERC20_1 \(0x.*\)/,
);
});
it('should throw an error if the conversion currency is not an acceptedTokens', async () => {
const wrongCurrency: RequestLogicTypes.ICurrency = {
type: 'ERC20' as any,
value: '0x17b4158805772ced11225e77339f90beb5aae968',
network: 'private',
};
await expect(
swapToPayAnyToErc20Request(validRequest, wallet, {
conversion: {
currency: wrongCurrency,
currencyManager,
},
swap: {
deadline: 2599732187000, // This test will fail in 2052
maxInputAmount: '3000000000000000000',
path: [paymentTokenAddress, wrongCurrency.value],
},
}),
).rejects.toThrowError(new UnsupportedCurrencyError(wrongCurrency));
});
it('should swap and pay with an ERC20 request with fees', async () => {
// first approve the SwapToPay contract to spend tokens
const approvalTx = await approveErc20ForSwapWithConversionIfNeeded(
validRequest,
wallet.address,
paymentTokenAddress,
wallet.provider,
BigNumber.from(204).mul(BigNumber.from(10).pow(18)),
);
if (approvalTx) {
await approvalTx.wait(1);
}
// get the balances to compare after payment
const initialPayerBalance = await paymentToken.balanceOf(wallet.address);
const initialPayeeBalance = await acceptedToken.balanceOf(paymentAddress);
const initialBuilderBalance = await acceptedToken.balanceOf(feeAddress);
// Swap and pay
const tx = await swapToPayAnyToErc20Request(validRequest, wallet, {
swap: validSwapSettings,
conversion: validConversionSettings,
});
const confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toEqual(1);
expect(tx.hash).toBeDefined();
// Get the new balances
const finalPayerBalance = await paymentToken.balanceOf(wallet.address);
const finalPayeeBalance = await acceptedToken.balanceOf(paymentAddress);
const finalBuilderBalance = await acceptedToken.balanceOf(feeAddress);
// Check each balance
// expectedAmount: 100000000
// feeAmount: + 2000000
// = 102000000 (8 decimals)
// AggDaiUsd.sol / 101000000
// = 1009900990099009900
// Swap fees * 1.005
// = 1014950495049504949 (18 decimals)
// Swapper * 2
// = 2029900990099009898 (18 decimals) paid by payer in erc20BeforeSwap
expect(finalPayerBalance.toString()).toEqual(
initialPayerBalance.sub('2029900990099009898').toString(),
);
// expectedAmount: 100000000 (8 decimals)
// AggDaiUsd.sol / 101000000
// = 990099009900990099 (18 decimals) received by payee in erc20AfterConversion
expect(finalPayeeBalance.toString()).toEqual(
initialPayeeBalance.add('990099009900990099').toString(),
);
// feeAmount: 2000000 (8 decimals)
// AggDaiUsd.sol / 101000000
// = 19801980198019801 (18 decimals) received by fee address in erc20AfterConversion
// +
//
// Swap fees 100000000
// feeAmount + 2000000
// = 102000000 (8 decimals)
// AggDaiUsd.sol / 101000000
// = 1009900990099009900
// Swap fees * 0.005
// = 5049504950495049
// Total fees = 24851485148514850
expect(finalBuilderBalance.toString()).toEqual(
initialBuilderBalance.add('24851485148514850').toString(),
);
});
});
});
import { BigNumber, providers, Wallet } from 'ethers';
import {
ClientTypes,
CurrencyTypes,
ExtensionTypes,
IdentityTypes,
RequestLogicTypes,
} from '@requestnetwork/types';
import { deepCopy } from '@requestnetwork/utils';
import {
approveErc20ForSwapToPayIfNeeded,
getErc20Balance,
ISwapSettings,
swapErc20FeeProxyRequest,
} from '../../src';
import { ERC20__factory } from '@requestnetwork/smart-contracts/types';
import { erc20SwapToPayArtifact } from '@requestnetwork/smart-contracts';
import { revokeErc20Approval } from '../../src/payment/utils';
/* eslint-disable no-magic-numbers */
/* eslint-disable @typescript-eslint/no-unused-expressions */
const erc20ContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40';
const alphaErc20Address = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35';
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: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: '2',
paymentAddress,
salt: 'salt',
},
version: '0.1.0',
},
},
extensionsData: [],
meta: {
transactionManagerMeta: {},
},
pending: null,
requestId: 'abcd',
state: RequestLogicTypes.STATE.CREATED,
timestamp: 0,
version: '1.0',
};
const validSwapSettings: ISwapSettings = {
deadline: 2599732187000, // This test will fail in 2052
maxInputAmount: 204,
path: [alphaErc20Address, erc20ContractAddress],
};
describe('swap-erc20-fee-proxy', () => {
beforeAll(async () => {
// revoke erc20SwapToPay approval
await revokeErc20Approval(
erc20SwapToPayArtifact.getAddress(
validRequest.currencyInfo.network! as CurrencyTypes.EvmChainName,
),
alphaErc20Address,
wallet.provider,
);
});
describe('encodeSwapErc20FeeRequest', () => {
beforeAll(async () => {
// revoke erc20SwapToPay approval
await revokeErc20Approval(
erc20SwapToPayArtifact.getAddress(
validRequest.currencyInfo.network! as CurrencyTypes.EvmChainName,
),
alphaErc20Address,
wallet.provider,
);
});
it('should throw an error if the request is not erc20', async () => {
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
request.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH;
await expect(
swapErc20FeeProxyRequest(request, wallet, validSwapSettings),
).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 = deepCopy(validRequest);
request.currencyInfo.value = '';
await expect(
swapErc20FeeProxyRequest(request, wallet, validSwapSettings),
).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 = deepCopy(validRequest);
request.currencyInfo.network = '' as CurrencyTypes.EvmChainName;
await expect(
swapErc20FeeProxyRequest(request, wallet, validSwapSettings),
).rejects.toThrowError('Unsupported chain ');
});
it('should throw an error if request has no extension', async () => {
const request = deepCopy(validRequest);
request.extensions = [] as any;
await expect(
swapErc20FeeProxyRequest(request, wallet, validSwapSettings),
).rejects.toThrowError('no payment network found');
});
});
describe('swapErc20FeeProxyRequest', () => {
it('should consider override parameters', async () => {
const spy = jest.fn();
const originalSendTransaction = wallet.sendTransaction.bind(wallet);
wallet.sendTransaction = spy;
await swapErc20FeeProxyRequest(
validRequest,
wallet,
{
deadline: 2599732187000, // This test will fail in 2052
maxInputAmount: 206,
path: [alphaErc20Address, erc20ContractAddress],
},
{
overrides: { gasPrice: '20000000000' },
},
);
expect(spy).toHaveBeenCalledWith({
data: '0x5f2993bf00000000000000000000000075c35c980c0d37ef46df04d31a140b65503c0eed000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000ce000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000009af4c3db000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000',
gasPrice: '20000000000',
to: '0xA4392264a2d8c998901D10C154C91725b1BF0158',
value: 0,
});
wallet.sendTransaction = originalSendTransaction;
});
it('should swap and pay with an ERC20 request with fees', async () => {
// first approve the SwapToPay contract to spend ALPHA tokens
const approvalTx = await approveErc20ForSwapToPayIfNeeded(
validRequest,
wallet.address,
alphaErc20Address,
wallet.provider,
BigNumber.from(204).mul(BigNumber.from(10).pow(18)),
);
expect(approvalTx).toBeDefined();
if (approvalTx) {
await approvalTx.wait(1);
}
// get the balances to compare after payment
const balanceEthBefore = await wallet.getBalance();
const balanceAlphaBefore = await ERC20__factory.connect(
alphaErc20Address,
provider,
).balanceOf(wallet.address);
const issuerBalanceErc20Before = await getErc20Balance(
validRequest,
paymentAddress,
provider,
);
const feeBalanceErc20Before = await getErc20Balance(validRequest, feeAddress, provider);
// Swap and pay
const tx = await swapErc20FeeProxyRequest(validRequest, wallet, {
deadline: Date.now() + 1000000,
maxInputAmount: 206,
path: [alphaErc20Address, erc20ContractAddress],
});
const confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toEqual(1);
expect(tx.hash).toBeDefined();
// Get the new balances
const balanceEthAfter = await wallet.getBalance();
const balanceAlphaAfter = await ERC20__factory.connect(alphaErc20Address, provider).balanceOf(
wallet.address,
);
const issuerBalanceErc20After = await getErc20Balance(validRequest, paymentAddress, provider);
const feeBalanceErc20After = await getErc20Balance(validRequest, feeAddress, provider);
// Check each balance
expect(BigNumber.from(balanceEthBefore).sub(balanceEthAfter).toNumber()).toBeGreaterThan(0);
expect(BigNumber.from(balanceAlphaAfter).toString()).toEqual(
BigNumber.from(balanceAlphaBefore).sub(204).toString(),
);
expect(BigNumber.from(issuerBalanceErc20After).toString()).toEqual(
BigNumber.from(issuerBalanceErc20Before).add(100).toString(),
);
expect(BigNumber.from(feeBalanceErc20After).toString()).toEqual(
BigNumber.from(feeBalanceErc20Before).add(2).toString(),
);
});
});
});
import { constants, ContractTransaction, Signer, BigNumber, BigNumberish, providers } from 'ethers';
import { erc20FeeProxyArtifact, erc20SwapToPayArtifact } from '@requestnetwork/smart-contracts';
import { ERC20SwapToPay__factory } from '@requestnetwork/smart-contracts/types';
import { ClientTypes } from '@requestnetwork/types';
import { ITransactionOverrides } from './transaction-overrides';
import {
getAmountToPay,
getProvider,
getProxyAddress,
getRequestPaymentValues,
getSigner,
validateErc20FeeProxyRequest,
} from './utils';
import { IPreparedTransaction } from './prepared-transaction';
import { Erc20PaymentNetwork } from '@requestnetwork/payment-detection';
import { EvmChains } from '@requestnetwork/currency';
/**
* Details required for a token swap:
*
* - maxInputAmount: maximum number of ERC20 allowed for the swap before payment, considering both amount and fees
* - path: array of token addresses to be used for the "swap path".
* ['0xPaymentCurrency', '0xIntermediate1', ..., '0xRequestCurrency']
* - deadline: time in milliseconds since UNIX epoch, after which the swap should not be executed.
*/
export interface ISwapSettings {
deadline: number;
maxInputAmount: BigNumberish;
path: string[];
}
/**
* Details required for a request payment transaction
* @member overrides custom swap transaction parameters
*/
export interface ISwapTransactionOptions extends IRequestPaymentOptions {
overrides?: ITransactionOverrides;
}
/**
* Details required for a proxy payment:
* @member {BigNumberish} amount custom request amount to pay
* @member {BigNumberish} feeAmount custom fee amount to pay for the proxy
*/
export interface IRequestPaymentOptions {
amount?: BigNumberish;
feeAmount?: BigNumberish;
}
/**
* Processes a transaction to swap tokens and pay an ERC20 Request through a proxy with fees.
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
* @param swapSettings settings for the swap: swap path, max amount to swap, deadline
* @param options to override amount, feeAmount and transaction parameters
*/
export async function swapErc20FeeProxyRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
swapSettings: ISwapSettings,
options?: ISwapTransactionOptions,
): Promise<ContractTransaction> {
const preparedTx = prepareSwapToPayErc20FeeRequest(
request,
signerOrProvider,
swapSettings,
options,
);
const signer = getSigner(signerOrProvider);
const tx = await signer.sendTransaction(preparedTx);
return tx;
}
/**
* Prepare a transaction to swap tokens and pay an ERC20 Request through a proxy with fees.
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
* @param swapSettings settings for the swap: swap path, max amount to swap, deadline
* @param options to override amount, feeAmount and transaction parameters
*/
export function prepareSwapToPayErc20FeeRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
swapSettings: ISwapSettings,
options?: ISwapTransactionOptions,
): IPreparedTransaction {
const { network } = request.currencyInfo;
EvmChains.assertChainSupported(network!);
const encodedTx = encodeSwapToPayErc20FeeRequest(
request,
signerOrProvider,
swapSettings,
options,
);
const proxyAddress = erc20SwapToPayArtifact.getAddress(network);
return {
data: encodedTx,
to: proxyAddress,
value: 0,
...options?.overrides,
};
}
/**
* Encodes the call to pay a request through the ERC20 fee proxy contract, can be used with a Multisig contract.
* @param request request to pay
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum
* @param swapSettings settings for the swap
* @param options to override amount, feeAmount and transaction parameters
*/
export function encodeSwapToPayErc20FeeRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
swapSettings: ISwapSettings,
options?: IRequestPaymentOptions,
): string {
const { paymentReference, paymentAddress, feeAddress, feeAmount, network } =
getRequestPaymentValues(request);
EvmChains.assertChainSupported(network!);
validateErc20FeeProxyRequest(request, options?.amount, options?.feeAmount);
const signer = getSigner(signerOrProvider);
const tokenAddress = request.currencyInfo.value;
const amountToPay = getAmountToPay(request, options?.amount);
const feeToPay = BigNumber.from(options?.feeAmount || feeAmount || 0);
if (
swapSettings.path[swapSettings.path.length - 1].toLowerCase() !== tokenAddress.toLowerCase()
) {
throw new Error('Last item of the path should be the request currency');
}
// eslint-disable-next-line no-magic-numbers
if (Date.now() > swapSettings.deadline * 1000) {
throw new Error('A swap with a past deadline will fail, the transaction will not be pushed');
}
if (!request.currencyInfo.network) {
throw new Error('Request currency network is missing');
}
const feeProxyAddress = getProxyAddress(
request,
Erc20PaymentNetwork.ERC20FeeProxyPaymentDetector.getDeploymentInformation,
);
const swapToPayAddress = erc20FeeProxyArtifact.getAddress(network);
const swapToPayContract = ERC20SwapToPay__factory.connect(swapToPayAddress, signer);
return swapToPayContract.interface.encodeFunctionData('swapTransferWithReference', [
feeProxyAddress,
paymentAddress,
amountToPay,
swapSettings.maxInputAmount,
swapSettings.path,
`0x${paymentReference}`,
feeToPay,
feeAddress || constants.AddressZero,
// eslint-disable-next-line no-magic-numbers
Math.round(swapSettings.deadline / 1000),
]);
}
const waitForConfirmation = async (dataOrPromise) => {
const data = await dataOrPromise;
return new Promise((resolve, reject) => {
data.on("confirmed", resolve);
data.on("error", reject);
});
};
(async () => {
const {
RequestNetwork,
Types,
Utils,
} = require("@requestnetwork/request-client.js");
const {
EthereumPrivateKeySignatureProvider,
} = require("@requestnetwork/epk-signature");
const { config } = require("dotenv");
const { Wallet } = require("ethers");
// Load environment variables from .env file
config();
const payeeEpkSignatureProvider = new EthereumPrivateKeySignatureProvider({
method: Types.Signature.METHOD.ECDSA,
privateKey: process.env.PAYEE_PRIVATE_KEY, // Must include 0x prefix
});
const payerEpkSignatureProvider = new EthereumPrivateKeySignatureProvider({
method: Types.Signature.METHOD.ECDSA,
privateKey: process.env.PAYER_PRIVATE_KEY, // Must include 0x prefix
});
const payeeRequestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
signatureProvider: payeeEpkSignatureProvider,
});
const payerRequestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
signatureProvider: payerEpkSignatureProvider,
});
const payeeIdentityAddress = new Wallet(process.env.PAYEE_PRIVATE_KEY)
.address;
const payerIdentityAddress = new Wallet(process.env.PAYER_PRIVATE_KEY)
.address;
const payeeIdentity = {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentityAddress,
};
const payerIdentity = {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payerIdentityAddress,
};
// In this example, the payee is also the payment recipient.
const paymentRecipient = payeeIdentityAddress;
const feeRecipient = "0x0000000000000000000000000000000000000000";
const requestCreateParameters = {
requestInfo: {
currency: {
type: Types.RequestLogic.CURRENCY.ERC20,
value: "0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C", // FAU token address
network: "sepolia",
},
expectedAmount: "1000000000000000000", // 1.0
payee: payeeIdentity,
payer: payerIdentity,
timestamp: Utils.getCurrentTimestampInSecond(),
},
paymentNetwork: {
// We can declare payments because ERC20 fee proxy payment network inherits from declarative payment network
id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
parameters: {
paymentNetworkName: "sepolia",
paymentAddress: paymentRecipient,
feeAddress: feeRecipient,
feeAmount: "0",
},
},
contentData: {
reason: "ð",
dueDate: "2023.06.16",
builderId: "request-network",
createdWith: "quickstart",
},
signer: payeeIdentity,
};
const payeeRequest = await payeeRequestClient.createRequest(
requestCreateParameters,
);
const payeeRequestData = await payeeRequest.waitForConfirmation();
const payerRequest = await payerRequestClient.fromRequestId(
payeeRequestData.requestId,
);
const payerRequestData = payerRequest.getData();
const payerRequestDataAfterSent = await payerRequest.declareSentPayment(
payerRequestData.expectedAmount,
"payment initiated from the bank",
payerIdentity,
);
console.log(
"payerRequestDataAfterSent: " +
JSON.stringify(payerRequestDataAfterSent, null, 2),
);
const payerRequestDataAfterSentConfirmed = await waitForConfirmation(
payerRequestDataAfterSent,
);
console.log(
"payerRequestDataAfterSentConfirmed: " +
JSON.stringify(payerRequestDataAfterSentConfirmed, null, 2),
);
console.log(
"Observe extensionsData contains 3 events: paymentNetwork 'create', contentData 'create', and paymentNetwork 'declareSentPayment'",
);
const payeeRequestDataRefreshed = await payeeRequest.refresh();
const payeeRequestDataAfterReceived =
await payeeRequest.declareReceivedPayment(
payeeRequestDataRefreshed.expectedAmount,
"payment received from the bank",
payeeIdentity,
);
const payeeRequestDataAfterReceivedConfirmed = await waitForConfirmation(
payeeRequestDataAfterReceived,
);
console.log(
"payeeRequestDataAfterReceivedConfirmed: " +
JSON.stringify(payeeRequestDataAfterReceivedConfirmed, null, 2),
);
console.log(
"Observe extensionsData contains 4 events: paymentNetwork 'create', contentData 'create', paymentNetwork 'declareSentPayment', and paymentNetwork 'declareReceivedPayment'",
);
console.log(
"Request balance: " +
payeeRequestDataAfterReceivedConfirmed.balance.balance,
);
console.log(
"Request balance events: " +
JSON.stringify(
payeeRequestDataAfterReceivedConfirmed.balance.events,
null,
2,
),
);
})();
httpConfig
Options for HTTP transport (timeout, delay, retry, etc.)
paymentOptions
Payment detection options
useMockStorage
boolean
Store ephemeral requests in local memory
currencies
CurrencyInput[]
Custom currency list
currencyManager
ICurrencyManager
Custom currency manager (will override currencies). A Currency manager handles a list of currencies and provides utility to retrieve and change format
httpRequestExponentialBackoffDelay
number
Exponential backoff delay in ms when requests to the Node fail
httpRequestMaxExponentialBackoffDelay
number
Maximum exponential backoff delay in ms when requests to the Node fail
getConfirmationMaxRetry
number
Maximum number of retries to get the confirmation of a persistTransaction
getConfirmationRetryDelay
number
Delay between retry in ms to get the confirmation of a persistTransaction
getConfirmationExponentialBackoffDelay
number
Exponential backoff delay in ms to get the confirmation of a persistTransaction
getConfirmationMaxExponentialBackoffDelay
number
Maximum exponential backoff delay in ms to get the confirmation of a persistTransaction
getConfirmationDeferDelay
number
Delay to wait in ms before trying for the first time to get the confirmation of a persistTransaction
getRpcProvider
function(ChainName)
Override RPC node provider
nodeConnectionConfig
Axios configurations
signatureProvider
Required to sign and create requests
decryptionProvider
baseUrl
string
Request Node URL
Many other properties...
Sign using a private key inside of a wallet
Sign using a private key outside of a wallet
Decrypt using a private key outside of a wallet
requestClientVersionHeader
string
Name of the header containing the client version
httpRequestMaxRetry
number
Maximum number of retries to attempt when http requests to the Node fail
httpRequestRetryDelay
number
bitcoinDetectionProvider
IBitcoinDetectionProvider
Override default bitcoin payment detection
explorerApiKeys
Map<ChainName, string>
Override explorer API keys
getSubgraphClient
function(ChainName)
Create an unencrypted request
_createEncryptedRequest()
Create an encrypted request. Docs coming soon...
Compute a request ID without actually creating a request
Retrieve a request from a requestId
Retrieve an array of requests from an Identity
Retrieve an array of requests from a topic
Required to retrieve encrypted requests
Delay between retry in ms
Override subgraph payment detection
payment.confirmed
Payment fully settled (fiat delivered)
The request contents. Returned by waitForConfirmation(), getData() and many other methods.
{
"name": "rn-expo-support",
"main": "./index.js",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "expo lint"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"@expo/vector-icons": "^14.0.0",
"@react-navigation/native": "^6.0.2",
"@requestnetwork/epk-signature": "^0.6.0",
"@requestnetwork/payment-processor": "^0.44.0",
"@requestnetwork/request-client.js": "^0.45.0",
"@requestnetwork/types": "^0.42.0",
"@requestnetwork/web3-signature": "^0.5.0",
"ethers": "^5.5.1",
"eventemitter3": "^5.0.1",
"expo": "~51.0.14",
"expo-constants": "~16.0.2",
"expo-crypto": "~13.0.2",
"expo-font": "~12.0.7",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.16",
"expo-splash-screen": "~0.27.5",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.6",
"expo-web-browser": "~13.0.3",
"http-browserify": "^1.7.0",
"https-browserify": "^1.0.0",
"node-forge": "^1.3.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.2",
"react-native-crypto": "^2.2.0",
"react-native-gesture-handler": "~2.16.1",
"react-native-get-random-values": "^1.11.0",
"react-native-quick-crypto": "^0.6.1",
"react-native-randombytes": "^3.6.1",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",
"react-native-web": "~0.19.10",
"stream-browserify": "^3.0.0",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/jest": "^29.5.12",
"@types/react": "~18.2.45",
"@types/react-test-renderer": "^18.0.7",
"jest": "^29.2.1",
"jest-expo": "~51.0.1",
"react-test-renderer": "18.2.0",
"typescript": "~5.3.3"
}
}
// Buffer polyfill
import { Buffer } from "buffer";
global.Buffer = Buffer;
import "react-native-get-random-values";
// Crypto Polyfill
import cryptoPolyfill from "./cryptoPolyfill";
if (typeof global.crypto !== "object") {
global.crypto = {};
}
Object.assign(global.crypto, cryptoPolyfill);
// Event Emitter polyfill
import EventEmitter from "eventemitter3";
global.EventEmitter = EventEmitter;
// Stream Polyfill
import { Readable, Writable } from "stream-browserify";
global.Readable = Readable;
global.Writable = Writable;
// HTTP Polyfill
import http from "http-browserify";
global.http = http;
// HTTPS Polyfill
import https from "https-browserify";
global.https = https;
// Starting expo router
import "expo-router/entry";
const { getDefaultConfig } = require("expo/metro-config");
const defaultConfig = getDefaultConfig(__dirname);
defaultConfig.resolver.extraNodeModules = {
...defaultConfig.resolver.extraNodeModules,
crypto: require.resolve("./cryptoPolyfill"),
stream: require.resolve("stream-browserify"),
buffer: require.resolve("buffer"),
http: require.resolve("http-browserify"),
https: require.resolve("https-browserify"),
};
module.exports = defaultConfig;
const {
EthereumPrivateKeySignatureProvider,
} = require("@requestnetwork/epk-signature");
const { Types } = require("@requestnetwork/request-client.js");
const epkSignatureProvider = new EthereumPrivateKeySignatureProvider({
method: Types.Signature.METHOD.ECDSA,
privateKey: process.env.PAYEE_PRIVATE_KEY, // Must include 0x prefix
});const { RequestNetwork } = require("@requestnetwork/request-client.js")
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
signatureProvider: epkSignatureProvider,
});const { Types, Utils } = require("@requestnetwork/request-client.js");
const payeeIdentity = '0x7eB023BFbAeE228de6DC5B92D0BeEB1eDb1Fd567';
const payerIdentity = '0x519145B771a6e450461af89980e5C17Ff6Fd8A92';
const paymentRecipient = payeeIdentity;
const feeRecipient = '0x0000000000000000000000000000000000000000';
const requestCreateParameters = {
requestInfo: {
// The currency in which the request is denominated
currency: {
type: Types.RequestLogic.CURRENCY.ERC20,
value: '0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C',
network: 'sepolia',
},
// The expected amount as a string, in parsed units, respecting `decimals`
// Consider using `parseUnits()` from ethers or viem
expectedAmount: '1000000000000000000',
// The payee identity. Not necessarily the same as the payment recipient.
payee: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentity,
},
// The payer identity. If omitted, any identity can pay the request.
payer: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payerIdentity,
},
// The request creation timestamp.
timestamp: Utils.getCurrentTimestampInSecond(),
},
// The paymentNetwork is the method of payment and related details.
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
parameters: {
paymentNetworkName: 'sepolia',
paymentAddress: payeeIdentity,
feeAddress: feeRecipient,
feeAmount: '0',
},
},
// The contentData can contain anything.
// Consider using rnf_invoice format from @requestnetwork/data-format
contentData: {
reason: '🍕',
dueDate: '2023.06.16',
},
// The identity that signs the request, either payee or payer identity.
signer: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentity,
},
};const request = await requestClient.createRequest(requestCreateParameters);
const confirmedRequestData = await request.waitForConfirmation();const { RequestNetwork, Types } = require("@requestnetwork/request-client.js");
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
}
});const request = await requestClient.fromRequestId(
'019830e9ec0439e53ec41fc627fd1d0293ec4bc61c2a647673ec5aaaa0e6338855',
);
const requestData = request.getData();const { providers, Wallet } = require("ethers");
const provider = new providers.JsonRpcProvider(
process.env.JSON_RPC_PROVIDER_URL,
);
const payerWallet = new Wallet(
process.env.PAYER_PRIVATE_KEY, // Must include 0x prefix
provider,
);const { hasSufficientFunds } = require("@requestnetwork/payment-processor);
const _hasSufficientFunds = await hasSufficientFunds(
requestData,
payerAddress,
{
provider: provider,
},
);const {
approveErc20,
hasErc20Approval,
} = require("@requestnetwork/payment-processor);
const _hasErc20Approval = await hasErc20Approval(
requestData,
payerAddress,
provider
);
if (!_hasErc20Approval) {
const approvalTx = await approveErc20(requestData, signer);
await approvalTx.wait(2);
}const { payRequest } = require("@requestnetwork/payment-processor");
const paymentTx = await payRequest(requestData, signer);
await paymentTx.wait(2);const request = await requestClient.fromRequestId(requestData.requestId);
let requestData = request.getData();
while (requestData.balance?.balance < requestData.expectedAmount) {
requestData = await request.refresh();
await new Promise((resolve) => setTimeout(resolve, 1000));
}const { RequestNetwork, Types } = require("@requestnetwork/request-client.js");
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
});const identityAddress = "0x519145B771a6e450461af89980e5C17Ff6Fd8A92";
const requests = await requestClient.fromIdentity({
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: identityAddress,
});
const requestDatas = requests.map((request) => request.getData());const request = await requestClient.fromRequestId(
'019830e9ec0439e53ec41fc627fd1d0293ec4bc61c2a647673ec5aaaa0e6338855',
);
const requestData = request.getData();import { ContractTransaction, Signer, providers, BigNumber, constants } from 'ethers';
import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts';
import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types';
import {
ClientTypes,
CurrencyTypes,
ExtensionTypes,
PaymentTypes,
RequestLogicTypes,
} from '@requestnetwork/types';
import { ITransactionOverrides } from './transaction-overrides';
import {
comparePnTypeAndVersion,
getAmountToPay,
getPnAndNetwork,
getProvider,
getProxyAddress,
getRequestPaymentValues,
getSigner,
MAX_ALLOWANCE,
validateConversionFeeProxyRequest,
validateErc20FeeProxyRequest,
} from './utils';
import {
padAmountForChainlink,
getPaymentNetworkExtension,
} from '@requestnetwork/payment-detection';
import { IPreparedTransaction } from './prepared-transaction';
import { IConversionPaymentSettings } from './index';
import { getConversionPathForErc20Request } from './any-to-erc20-proxy';
import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20';
import { CurrencyManager } from '@requestnetwork/currency';
import {
BatchPaymentNetworks,
EnrichedRequest,
IConversionSettings,
IRequestPaymentOptions,
} from '../types';
import { validateEthFeeProxyRequest } from './eth-fee-proxy';
import { getConversionPathForEthRequest } from './any-to-eth-proxy';
const CURRENCY = RequestLogicTypes.CURRENCY;
/**
* Processes a transaction to pay a batch of requests with an ERC20 currency
* that can be different from the request currency (eg. fiat)
* The payment is made through ERC20 or ERC20Conversion proxies
* It can be used with a Multisig contract
* @param enrichedRequests List of EnrichedRequests to pay.
* @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum.
* @param options It contains 3 paramaters required to do a batch payments:
* - conversion: It must contains the currencyManager.
* - skipFeeUSDLimit: It checks the value of batchFeeAmountUSDLimit of the batch proxy deployed.
* Setting the value to true skips the USD fee limit, and reduces gas consumption.
* - version: The version of the batch conversion proxy.
* @param overrides Optionally, override default transaction values, like gas.
* @dev We only implement batchPayments using two ERC20 functions:
* batchMultiERC20ConversionPayments, and batchMultiERC20Payments.
*/
export async function payBatchConversionProxyRequest(
enrichedRequests: EnrichedRequest[],
signerOrProvider: providers.Provider | Signer = getProvider(),
options: IRequestPaymentOptions,
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
const { data, to, value } = prepareBatchConversionPaymentTransaction(enrichedRequests, options);
const signer = getSigner(signerOrProvider);
return signer.sendTransaction({ data, to, value, ...overrides });
}
/**
* Prepares a transaction to pay a batch of requests with an ERC20 currency
* that can be different from the request currency (eg. fiat).
* It can be used with a Multisig contract.
* @param enrichedRequests List of EnrichedRequests to pay.
* @param options It contains 3 paramaters required to prepare a batch payments:
* - conversion: It must contains the currencyManager.
* - skipFeeUSDLimit: It checks the value of batchFeeAmountUSDLimit of the batch proxy deployed.
* Setting the value to true skips the USD fee limit, and reduces gas consumption.
* - version: The version of the batch conversion proxy.
*/
export function prepareBatchConversionPaymentTransaction(
enrichedRequests: EnrichedRequest[],
options: IRequestPaymentOptions,
): IPreparedTransaction {
const encodedTx = encodePayBatchConversionRequest(
enrichedRequests,
options.skipFeeUSDLimit,
options.conversion,
);
const value = getBatchTxValue(enrichedRequests);
const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, options.version);
return {
data: encodedTx,
to: proxyAddress,
value,
};
}
const mapPnToDetailsBuilder: Record<
BatchPaymentNetworks,
(req: EnrichedRequest, isNative: boolean) => PaymentTypes.RequestDetail
> = {
'pn-any-to-erc20-proxy': getRequestDetailWithConversion,
'pn-any-to-eth-proxy': getRequestDetailWithConversion,
'pn-erc20-fee-proxy-contract': getRequestDetailWithoutConversion,
'pn-eth-fee-proxy-contract': getRequestDetailWithoutConversion,
};
const mapPnToAllowedCurrencies: Record<BatchPaymentNetworks, RequestLogicTypes.CURRENCY[]> = {
'pn-any-to-erc20-proxy': [CURRENCY.ERC20, CURRENCY.ISO4217, CURRENCY.ETH],
'pn-any-to-eth-proxy': [CURRENCY.ERC20, CURRENCY.ISO4217],
'pn-erc20-fee-proxy-contract': [CURRENCY.ERC20],
'pn-eth-fee-proxy-contract': [CURRENCY.ETH],
};
const mapPnToBatchId: Record<BatchPaymentNetworks, PaymentTypes.BATCH_PAYMENT_NETWORK_ID> = {
'pn-any-to-erc20-proxy':
PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS,
'pn-any-to-eth-proxy': PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS,
'pn-erc20-fee-proxy-contract': PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS,
'pn-eth-fee-proxy-contract': PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS,
};
const computeRequestDetails = ({
enrichedRequest,
extension,
}: {
enrichedRequest: EnrichedRequest;
extension: ExtensionTypes.IState<any> | undefined;
}) => {
const paymentNetworkId = enrichedRequest.paymentNetworkId;
const allowedCurrencies = mapPnToAllowedCurrencies[paymentNetworkId];
const detailsBuilder = mapPnToDetailsBuilder[paymentNetworkId];
const isNative =
paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY ||
paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT;
extension = extension ?? getPaymentNetworkExtension(enrichedRequest.request);
comparePnTypeAndVersion(extension, enrichedRequest.request);
if (!allowedCurrencies.includes(enrichedRequest.request.currencyInfo.type)) {
throw new Error(`wrong request currencyInfo type`);
}
return {
input: detailsBuilder(enrichedRequest, isNative),
extension,
};
};
/**
* Encodes a transaction to pay a batch of requests with an ERC20 currency
* that can be different from the request currency (eg. fiat).
* It can be used with a Multisig contract.
* @param enrichedRequests List of EnrichedRequests to pay.
* @param skipFeeUSDLimit It checks the value of batchFeeAmountUSDLimit of the batch proxy deployed.
* Setting the value to true skips the USD fee limit, and reduces gas consumption.
*/
function encodePayBatchConversionRequest(
enrichedRequests: EnrichedRequest[],
skipFeeUSDLimit = false,
conversion: IConversionSettings | undefined,
): string {
if (!(conversion && conversion.currencyManager)) {
throw 'the conversion object or the currencyManager is undefined';
}
const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request);
const { network } = getPnAndNetwork(enrichedRequests[0].request);
const requestDetails: Record<BatchPaymentNetworks, PaymentTypes.RequestDetail[]> = {
'pn-any-to-erc20-proxy': [],
'pn-any-to-eth-proxy': [],
'pn-erc20-fee-proxy-contract': [],
'pn-eth-fee-proxy-contract': [],
};
const requestExtensions: Record<BatchPaymentNetworks, ExtensionTypes.IState<any> | undefined> = {
'pn-any-to-erc20-proxy': undefined,
'pn-any-to-eth-proxy': undefined,
'pn-erc20-fee-proxy-contract': undefined,
'pn-eth-fee-proxy-contract': undefined,
};
for (const enrichedRequest of enrichedRequests) {
const request = enrichedRequest.request;
const { input, extension } = computeRequestDetails({
enrichedRequest,
extension: requestExtensions[enrichedRequest.paymentNetworkId],
});
requestDetails[enrichedRequest.paymentNetworkId].push(input);
requestExtensions[enrichedRequest.paymentNetworkId] = extension;
if (network !== getPnAndNetwork(request).network)
throw new Error('All the requests must have the same network');
}
/**
* The native with conversion payment inputs must be the last element.
* See BatchConversionPayment batchPayments method in @requestnetwork/smart-contracts
*/
const metaDetails = Object.entries(requestDetails)
.map(([pn, details]) => ({
paymentNetworkId: mapPnToBatchId[pn as BatchPaymentNetworks],
requestDetails: details,
}))
.filter((details) => details.requestDetails.length > 0)
.sort((a, b) => a.paymentNetworkId - b.paymentNetworkId);
const hasNativePayment =
requestDetails['pn-any-to-eth-proxy'].length > 0 ||
requestDetails['pn-eth-fee-proxy-contract'].length > 0;
const pathsToUSD = getUSDPathsForFeeLimit(
[...metaDetails.map((details) => details.requestDetails).flat()],
network,
skipFeeUSDLimit,
conversion.currencyManager,
hasNativePayment,
);
const proxyContract = BatchConversionPayments__factory.createInterface();
return proxyContract.encodeFunctionData('batchPayments', [
metaDetails,
pathsToUSD,
feeAddress || constants.AddressZero,
]);
}
/**
* Get the batch input associated to a request without conversion.
* @param enrichedRequest The enrichedRequest to pay.
*/
function getRequestDetailWithoutConversion(
enrichedRequest: EnrichedRequest,
isNative: boolean,
): PaymentTypes.RequestDetail {
const request = enrichedRequest.request;
isNative ? validateEthFeeProxyRequest(request) : validateErc20FeeProxyRequest(request);
const currencyManager =
enrichedRequest.paymentSettings?.currencyManager || CurrencyManager.getDefault();
const tokenAddress = isNative
? currencyManager.getNativeCurrency(
RequestLogicTypes.CURRENCY.ETH,
request.currencyInfo.network as string,
)?.hash
: request.currencyInfo.value;
if (!tokenAddress) {
throw new Error('Could not find the request currency');
}
const { paymentReference, paymentAddress, feeAmount } = getRequestPaymentValues(request);
return {
recipient: paymentAddress,
requestAmount: getAmountToPay(request).toString(),
path: [tokenAddress],
paymentReference: `0x${paymentReference}`,
feeAmount: feeAmount?.toString() || '0',
maxToSpend: '0',
maxRateTimespan: '0',
};
}
/**
* Get the batch input associated to a request with conversion.
* @param enrichedRequest The enrichedRequest to pay.
*/
function getRequestDetailWithConversion(
enrichedRequest: EnrichedRequest,
isNative: boolean,
): PaymentTypes.RequestDetail {
const { request, paymentSettings } = enrichedRequest;
const { path, requestCurrency } = (
isNative ? getConversionPathForEthRequest : getConversionPathForErc20Request
)(request, paymentSettings);
isNative
? validateEthFeeProxyRequest(
request,
undefined,
undefined,
ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY,
)
: validateConversionFeeProxyRequest(request, path);
const { paymentReference, paymentAddress, feeAmount, maxRateTimespan } =
getRequestPaymentValues(request);
const requestAmount = BigNumber.from(request.expectedAmount).sub(request.balance?.balance || 0);
const padRequestAmount = padAmountForChainlink(requestAmount, requestCurrency);
const padFeeAmount = padAmountForChainlink(feeAmount || 0, requestCurrency);
return {
recipient: paymentAddress,
requestAmount: padRequestAmount.toString(),
path: path,
paymentReference: `0x${paymentReference}`,
feeAmount: padFeeAmount.toString(),
maxToSpend: paymentSettings.maxToSpend.toString(),
maxRateTimespan: maxRateTimespan || '0',
};
}
const getBatchTxValue = (enrichedRequests: EnrichedRequest[]) => {
return enrichedRequests.reduce((prev, curr) => {
if (
curr.paymentNetworkId !== ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY &&
curr.paymentNetworkId !== ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT
)
return prev;
return prev.add(
curr.paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY
? curr.paymentSettings.maxToSpend
: getAmountToPay(curr.request),
);
}, BigNumber.from(0));
};
/**
* Get the list of conversion paths from tokens to the USD address through currencyManager.
* If there is no path to USD for a token, it goes to the next token.
* @param requestDetails List of ERC20 requests to pay.
* @param network The network targeted.
* @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, it skips the path calculation.
* @param currencyManager The currencyManager used to get token conversion paths to USD.
*/
function getUSDPathsForFeeLimit(
requestDetails: PaymentTypes.RequestDetail[],
network: string,
skipFeeUSDLimit: boolean,
currencyManager: CurrencyTypes.ICurrencyManager<unknown>,
hasNativePayment: boolean,
): string[][] {
if (skipFeeUSDLimit) return [];
const USDCurrency = currencyManager.fromSymbol('USD');
if (!USDCurrency) throw 'Cannot find the USD currency information';
// Native to USD conversion path
let nativeConversionPath: string[] = [];
if (hasNativePayment) {
const nativeCurrencyHash = currencyManager.getNativeCurrency(
RequestLogicTypes.CURRENCY.ETH,
network,
)?.hash;
if (!nativeCurrencyHash) throw 'Cannot find the Native currency information';
nativeConversionPath =
currencyManager.getConversionPath({ hash: nativeCurrencyHash }, USDCurrency, network) || [];
}
// get a list of unique token addresses
const tokenAddresses = requestDetails
.map((rd) => rd.path[rd.path.length - 1])
.filter((value, index, self) => self.indexOf(value) === index);
// get the token currencies and keep the one that are defined
const tokenCurrencies: Array<CurrencyTypes.CurrencyDefinition<unknown>> = tokenAddresses
.map((token) => currencyManager.fromAddress(token, network))
.filter((value): value is CurrencyTypes.CurrencyDefinition => !!value);
// get all the conversion paths to USD when it exists and return it
const path = tokenCurrencies
.map((t) => currencyManager.getConversionPath(t, USDCurrency, network))
.filter((value): value is string[] => !!value);
return hasNativePayment ? path.concat([nativeConversionPath]) : path;
}
/**
* @param network The network targeted.
* @param version The version of the batch conversion proxy, the last one by default.
* @returns
*/
function getBatchDeploymentInformation(
network: CurrencyTypes.EvmChainName,
version?: string,
): { address: string } | null {
return { address: batchConversionPaymentsArtifact.getAddress(network, version) };
}
/**
* Gets batch conversion contract Address.
* @param request The request for an ERC20 payment with/out conversion.
* @param version The version of the batch conversion proxy.
*/
export function getBatchConversionProxyAddress(
request: ClientTypes.IRequestData,
version?: string,
): string {
return getProxyAddress(request, getBatchDeploymentInformation, version);
}
/**
* ERC20 Batch conversion proxy approvals methods
*/
/**
* Processes the approval transaction of the targeted ERC20 with batch conversion proxy.
* @param request The request for an ERC20 payment with/out conversion.
* @param account The account that will be used to pay the request
* @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum.
* @param paymentSettings The payment settings are necessary for conversion payment approval.
* @param version The version of the batch conversion proxy, which can be different from request pn version.
* @param overrides Optionally, override default transaction values, like gas.
*/
export async function approveErc20BatchConversionIfNeeded(
request: ClientTypes.IRequestData,
account: string,
signerOrProvider: providers.Provider | Signer = getProvider(),
amount: BigNumber = MAX_ALLOWANCE,
paymentSettings?: IConversionPaymentSettings,
version?: string,
overrides?: ITransactionOverrides,
): Promise<ContractTransaction | void> {
if (
!(await hasErc20BatchConversionApproval(
request,
account,
signerOrProvider,
paymentSettings,
version,
))
) {
return approveErc20BatchConversion(
request,
getSigner(signerOrProvider),
amount,
paymentSettings,
version,
overrides,
);
}
}
/**
* Checks if the batch conversion proxy has the necessary allowance from a given account
* to pay a given request with ERC20 batch conversion proxy
* @param request The request for an ERC20 payment with/out conversion.
* @param account The account that will be used to pay the request
* @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum.
* @param paymentSettings The payment settings are necessary for conversion payment approval.
* @param version The version of the batch conversion proxy.
*/
export async function hasErc20BatchConversionApproval(
request: ClientTypes.IRequestData,
account: string,
signerOrProvider: providers.Provider | Signer = getProvider(),
paymentSettings?: IConversionPaymentSettings,
version?: string,
): Promise<boolean> {
return checkErc20Allowance(
account,
getBatchConversionProxyAddress(request, version),
signerOrProvider,
getTokenAddress(request, paymentSettings),
request.expectedAmount,
);
}
/**
* Processes the transaction to approve the batch conversion proxy to spend signer's tokens to pay
* the request in its payment currency. Can be used with a Multisig contract.
* @param request The request for an ERC20 payment with/out conversion.
* @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum.
* @param paymentSettings The payment settings are necessary for conversion payment approval.
* @param version The version of the batch conversion proxy, which can be different from request pn version.
* @param overrides Optionally, override default transaction values, like gas.
*/
export async function approveErc20BatchConversion(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
amount: BigNumber = MAX_ALLOWANCE,
paymentSettings?: IConversionPaymentSettings,
version?: string,
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
const preparedTx = prepareApproveErc20BatchConversion(
request,
signerOrProvider,
amount,
paymentSettings,
version,
overrides,
);
const signer = getSigner(signerOrProvider);
const tx = await signer.sendTransaction(preparedTx);
return tx;
}
/**
* Prepare the transaction to approve the proxy to spend signer's tokens to pay
* the request in its payment currency. Can be used with a Multisig contract.
* @param request The request for an ERC20 payment with/out conversion.
* @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum.
* @param paymentSettings The payment settings are necessary for conversion payment approval.
* @param version The version of the batch conversion proxy.
* @param overrides Optionally, override default transaction values, like gas.
*/
export function prepareApproveErc20BatchConversion(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
amount: BigNumber = MAX_ALLOWANCE,
paymentSettings?: IConversionPaymentSettings,
version?: string,
overrides?: ITransactionOverrides,
): IPreparedTransaction {
const encodedTx = encodeApproveErc20BatchConversion(
request,
signerOrProvider,
amount,
paymentSettings,
version,
);
return {
data: encodedTx,
to: getTokenAddress(request, paymentSettings),
value: 0,
...overrides,
};
}
/**
* Encodes the transaction to approve the batch conversion proxy to spend signer's tokens to pay
* the request in its payment currency. Can be used with a Multisig contract.
* @param request The request for an ERC20 payment with/out conversion.
* @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum.
* @param paymentSettings The payment settings are necessary for conversion payment approval.
* @param version The version of the batch conversion proxy.
*/
export function encodeApproveErc20BatchConversion(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
amount: BigNumber = MAX_ALLOWANCE,
paymentSettings?: IConversionPaymentSettings,
version?: string,
): string {
const proxyAddress = getBatchConversionProxyAddress(request, version);
return encodeApproveAnyErc20(
getTokenAddress(request, paymentSettings),
proxyAddress,
getSigner(signerOrProvider),
amount,
);
}
/**
* Get the address of the token to interact with,
* if it is a conversion payment, the info is inside paymentSettings
* @param request The request for an ERC20 payment with/out conversion.
* @param paymentSettings The payment settings are necessary for conversion payment
* */
function getTokenAddress(
request: ClientTypes.IRequestData,
paymentSettings?: IConversionPaymentSettings,
): string {
if (paymentSettings) {
if (!paymentSettings.currency) throw 'paymentSetting must have a currency';
return paymentSettings.currency.value;
}
return request.currencyInfo.value;
}
import { Wallet, BigNumber, providers, utils } from 'ethers';
import {
ClientTypes,
ExtensionTypes,
IdentityTypes,
RequestLogicTypes,
} from '@requestnetwork/types';
import { deepCopy } from '@requestnetwork/utils';
import { Erc20PaymentNetwork, PaymentReferenceCalculator } from '@requestnetwork/payment-detection';
import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types';
import { approveErc20, getErc20Balance } from '../../src/payment/erc20';
import {
getReceivableTokenIdForRequest,
mintErc20TransferableReceivable,
payErc20TransferableReceivableRequest,
} from '../../src/payment/erc20-transferable-receivable';
import { getProxyAddress } from '../../src/payment/utils';
/* 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 feeAddress = '0x75c35C980C0d37ef46DF04d31A140b65503c0eEd';
const provider = new providers.JsonRpcProvider('http://localhost:8545');
const payeeWallet = Wallet.createRandom().connect(provider);
const thirdPartyWallet = Wallet.createRandom().connect(provider);
const wallet = Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/1").connect(provider);
const paymentAddress = payeeWallet.address;
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: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: '0',
paymentAddress,
salt: '0ee84db293a752c6',
},
version: '0.2.0',
},
},
payee: {
type: IdentityTypes.TYPE.ETHEREUM_ADDRESS,
value: paymentAddress,
},
extensionsData: [],
meta: {
transactionManagerMeta: {},
},
pending: null,
requestId: '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e1',
state: RequestLogicTypes.STATE.CREATED,
timestamp: 0,
version: '1.0',
};
describe('erc20-transferable-receivable', () => {
beforeAll(async () => {
// Send funds to payeeWallet
let tx = {
to: paymentAddress,
// Convert currency unit from ether to wei
value: utils.parseEther('1'),
};
let txResponse = await wallet.sendTransaction(tx);
await txResponse.wait(1);
// Send funds to thirdPartyWallet
tx = {
to: thirdPartyWallet.address,
// Convert currency unit from ether to wei
value: utils.parseEther('1'),
};
txResponse = await wallet.sendTransaction(tx);
await txResponse.wait(1);
const mintTx = await mintErc20TransferableReceivable(validRequest, payeeWallet, {
gasLimit: BigNumber.from('20000000'),
});
const confirmedTx = await mintTx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(mintTx.hash).not.toBeUndefined();
});
beforeEach(() => {
jest.restoreAllMocks();
});
describe('mintErc20TransferableReceivable works', () => {
it('rejects paying without minting', async () => {
// Different request without a minted receivable
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
// Change the request id
request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e2';
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'The receivable for this request has not been minted yet. Please check with the payee.',
);
});
});
describe('payErc20TransferableReceivableRequest', () => {
it('should throw an error if the request is not erc20', async () => {
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
request.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH;
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'request cannot be processed, or is not an pn-erc20-transferable-receivable request',
);
});
it('should throw an error if the currencyInfo has no value', async () => {
const request = deepCopy(validRequest);
request.currencyInfo.value = '';
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'request cannot be processed, or is not an pn-erc20-transferable-receivable request',
);
});
it('should throw an error if the payee is undefined', async () => {
const request = deepCopy(validRequest);
request.payee = undefined;
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'Expected a payee for this request',
);
});
it('should throw an error if currencyInfo has no network', async () => {
const request = deepCopy(validRequest);
// @ts-expect-error Type '""' is not assignable to type 'ChainName | undefined'
request.currencyInfo.network = '';
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'Payment currency must have a network',
);
});
it('should throw an error if request has no extension', async () => {
const request = deepCopy(validRequest);
request.extensions = [] as any;
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'PaymentNetwork not found',
);
});
it('should consider override parameters', async () => {
const spy = jest.fn();
const originalSendTransaction = wallet.sendTransaction.bind(wallet);
wallet.sendTransaction = spy;
await payErc20TransferableReceivableRequest(validRequest, wallet, undefined, undefined, {
gasPrice: '20000000000',
});
const shortReference = PaymentReferenceCalculator.calculate(
validRequest.requestId,
validRequest.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]
.values.salt,
paymentAddress,
);
const tokenId = await getReceivableTokenIdForRequest(validRequest, wallet);
expect(tokenId.isZero()).toBe(false);
expect(spy).toHaveBeenCalledWith({
data: `0x314ee2d900000000000000000000000000000000${utils
.hexZeroPad(tokenId.toHexString(), 16)
.substring(
2,
)}000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000075c35c980c0d37ef46df04d31a140b65503c0eed0000000000000000000000000000000000000000000000000000000000000008${shortReference}000000000000000000000000000000000000000000000000`,
gasPrice: '20000000000',
to: '0xF426505ac145abE033fE77C666840063757Be9cd',
value: 0,
});
wallet.sendTransaction = originalSendTransaction;
});
it('should pay an ERC20 transferable receivable request with fees', async () => {
// first approve the contract
const approvalTx = await approveErc20(validRequest, wallet);
const approvalTxReceipt = await approvalTx.wait(1);
expect(approvalTxReceipt.status).toBe(1);
expect(approvalTx.hash).not.toBeUndefined();
// get the balance to compare after payment
const balanceEthBefore = await wallet.getBalance();
const balanceErc20Before = await getErc20Balance(validRequest, payeeWallet.address, provider);
const tx = await payErc20TransferableReceivableRequest(validRequest, wallet, 1, 0, {
gasLimit: BigNumber.from('20000000'),
});
const confirmedTx = await tx.wait(1);
const balanceEthAfter = await wallet.getBalance();
const balanceErc20After = await getErc20Balance(validRequest, payeeWallet.address, provider);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
expect(balanceEthAfter.lte(balanceEthBefore)).toBeTruthy(); // 'ETH balance should be lower'
// ERC20 balance should be lower
expect(
BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)),
).toBeTruthy();
});
it('other wallets can mint receivable for owner', async () => {
// Request without a receivable minted yet
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
// Change the request id
request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e3';
const mintTx = await mintErc20TransferableReceivable(request, thirdPartyWallet, {
gasLimit: BigNumber.from('20000000'),
});
let confirmedTx = await mintTx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(mintTx.hash).not.toBeUndefined();
// get the balance to compare after payment
const balanceErc20Before = await getErc20Balance(request, payeeWallet.address, provider);
const tx = await payErc20TransferableReceivableRequest(request, wallet, 1, 0, {
gasLimit: BigNumber.from('20000000'),
});
confirmedTx = await tx.wait(1);
const balanceErc20After = await getErc20Balance(request, payeeWallet.address, provider);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
// ERC20 balance should be lower
expect(
BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)),
).toBeTruthy();
});
it('rejects paying unless minted to correct owner', async () => {
// Request without a receivable minted yet
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
// Change the request id
request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e4';
let shortReference = PaymentReferenceCalculator.calculate(
request.requestId,
request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE].values
.salt,
paymentAddress,
);
let receivableContract = ERC20TransferableReceivable__factory.createInterface();
let data = receivableContract.encodeFunctionData('mint', [
thirdPartyWallet.address,
`0x${shortReference}`,
'100',
erc20ContractAddress,
]);
let tx = await thirdPartyWallet.sendTransaction({
data,
to: getProxyAddress(
request,
Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation,
),
value: 0,
});
let confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'The receivable for this request has not been minted yet. Please check with the payee.',
);
// Mint the receivable for the correct paymentAddress
shortReference = PaymentReferenceCalculator.calculate(
request.requestId,
request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE].values
.salt,
paymentAddress,
);
receivableContract = ERC20TransferableReceivable__factory.createInterface();
data = receivableContract.encodeFunctionData('mint', [
paymentAddress,
`0x${shortReference}`,
'100',
erc20ContractAddress,
]);
tx = await thirdPartyWallet.sendTransaction({
data,
to: getProxyAddress(
request,
Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation,
),
value: 0,
});
confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
// get the balance to compare after payment
const balanceErc20Before = await getErc20Balance(request, payeeWallet.address, provider);
tx = await payErc20TransferableReceivableRequest(request, wallet, 1, 0, {
gasLimit: BigNumber.from('20000000'),
});
confirmedTx = await tx.wait(1);
const balanceErc20After = await getErc20Balance(request, payeeWallet.address, provider);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
// ERC20 balance should be lower
expect(
BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)),
).toBeTruthy();
});
});
});
import React, {
createContext,
useContext,
useState,
ReactNode,
useEffect,
} from "react";
import { initializeRequestNetwork } from "./initializeRN";
import type { RequestNetwork } from "@requestnetwork/request-client.js";
import { useAccount, useWalletClient } from "wagmi";
interface ContextType {
requestNetwork: RequestNetwork | null;
}
const Context = createContext<ContextType | undefined>(undefined);
export const Provider = ({ children }: { children: ReactNode }) => {
const { data: walletClient } = useWalletClient();
const { address, isConnected, chainId } = useAccount();
const [requestNetwork, setRequestNetwork] = useState<RequestNetwork | null>(
null
);
useEffect(() => {
if (walletClient && isConnected && address && chainId) {
initializeRequestNetwork(setRequestNetwork, walletClient);
}
}, [walletClient, chainId, address, isConnected]);
return (
<Context.Provider
value={{
requestNetwork,
}}
>
{children}
</Context.Provider>
);
};
export const useAppContext = () => {
const context = useContext(Context);
if (!context) {
throw new Error("useAppContext must be used within a Context Provider");
}
return context;
};
import { IConfig } from "@/utils/types";
import { Config as WagmiConfig } from "wagmi";
import type { RequestNetwork } from "@requestnetwork/request-client.js";
declare global {
namespace JSX {
interface IntrinsicElements {
"invoice-dashboard": InvoiceDashboardElement;
"create-invoice-form": CreateInvoiceFormElement;
}
}
}
interface InvoiceDashboardElement {
ref?: React.Ref<InvoiceDashboardProps>;
}
interface CreateInvoiceFormElement {
ref?: React.Ref<CreateInvoiceFormProps>;
}
interface InvoiceDashboardProps extends HTMLElement {
config: IConfig;
wagmiConfig: WagmiConfig;
requestNetwork: RequestNetwork;
currencies: any;
}
interface CreateInvoiceFormProps extends HTMLElement {
config: IConfig;
wagmiConfig: WagmiConfig;
requestNetwork: RequestNetwork;
currencies: any;
}
import Head from "next/head";
import dynamic from "next/dynamic";
import { config } from "@/utils/config";
import { useAppContext } from "@/utils/context";
import { currencies } from "@/utils/currencies";
import { rainbowKitConfig as wagmiConfig } from "@/utils/wagmiConfig";
import { Spinner } from "@/components/ui";
const InvoiceDashboard = dynamic(
() => import("@requestnetwork/invoice-dashboard/react"),
{ ssr: false, loading: () => <Spinner /> }
);
export default function InvoiceDashboardPage() {
const { requestNetwork } = useAppContext();
return (
<>
<Head>
<title>Request Invoicing</title>
</Head>
<div className="container m-auto w-[100%]">
<InvoiceDashboard
config={config}
currencies={currencies}
requestNetwork={requestNetwork}
wagmiConfig={wagmiConfig}
/>
</div>
</>
);
}
import { Types } from "@requestnetwork/request-client.js";
export const currencies = [
{
symbol: "FAU",
address: "0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C",
network: "sepolia",
decimals: 18,
type: Types.RequestLogic.CURRENCY.ERC20,
},
{
symbol: "ETH",
address: "eth",
network: "sepolia",
decimals: 18,
type: Types.RequestLogic.CURRENCY.ETH,
},
];
import React, {
createContext,
useContext,
useState,
ReactNode,
useEffect,
} from "react";
import { initializeRequestNetwork } from "./initializeRN";
import type { RequestNetwork } from "@requestnetwork/request-client.js";
import { useAccount, useWalletClient } from "wagmi";
interface ContextType {
requestNetwork: RequestNetwork | null;
}
const Context = createContext<ContextType | undefined>(undefined);
export const Provider = ({ children }: { children: ReactNode }) => {
const { data: walletClient } = useWalletClient();
const { address, isConnected, chainId } = useAccount();
const [requestNetwork, setRequestNetwork] = useState<RequestNetwork | null>(
null
);
useEffect(() => {
if (walletClient && isConnected && address && chainId) {
initializeRequestNetwork(setRequestNetwork, walletClient);
}
}, [walletClient, chainId, address, isConnected]);
return (
<Context.Provider
value={{
requestNetwork,
}}
>
{children}
</Context.Provider>
);
};
export const useAppContext = () => {
const context = useContext(Context);
if (!context) {
throw new Error("useAppContext must be used within a Context Provider");
}
return context;
};
import { IConfig } from "@/utils/types";
import { Config as WagmiConfig } from "wagmi";
import type { RequestNetwork } from "@requestnetwork/request-client.js";
declare global {
namespace JSX {
interface IntrinsicElements {
"invoice-dashboard": InvoiceDashboardElement;
"create-invoice-form": CreateInvoiceFormElement;
}
}
}
interface InvoiceDashboardElement {
ref?: React.Ref<InvoiceDashboardProps>;
}
interface CreateInvoiceFormElement {
ref?: React.Ref<CreateInvoiceFormProps>;
}
interface InvoiceDashboardProps extends HTMLElement {
config: IConfig;
wagmiConfig: WagmiConfig;
requestNetwork: RequestNetwork;
currencies: any;
}
interface CreateInvoiceFormProps extends HTMLElement {
config: IConfig;
wagmiConfig: WagmiConfig;
requestNetwork: RequestNetwork;
currencies: any;
}
import Head from "next/head";
import dynamic from "next/dynamic";
import { config } from "@/utils/config";
import { useAppContext } from "@/utils/context";
import { currencies } from "@/utils/currencies";
import { rainbowKitConfig as wagmiConfig } from "@/utils/wagmiConfig";
import { Spinner } from "@/components/ui";
const CreateInvoiceForm = dynamic(
() => import("@requestnetwork/create-invoice-form/react"),
{ ssr: false, loading: () => <Spinner /> }
);
export default function CreateInvoice() {
const { requestNetwork } = useAppContext();
return (
<>
<Head>
<title>Request Invoicing - Create an Invoice</title>
</Head>
<div className="container m-auto w-[100%]">
<CreateInvoiceForm
config={config}
currencies={currencies}
wagmiConfig={wagmiConfig}
requestNetwork={requestNetwork}
/>
</div>
</>
);
}
import { Types } from "@requestnetwork/request-client.js";
export const currencies = [
{
symbol: "FAU",
address: "0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C",
network: "sepolia",
decimals: 18,
type: Types.RequestLogic.CURRENCY.ERC20,
},
{
symbol: "ETH",
address: "eth",
network: "sepolia",
decimals: 18,
type: Types.RequestLogic.CURRENCY.ETH,
},
];
import {
ContractTransaction,
Signer,
BigNumberish,
providers,
BigNumber,
constants,
ethers,
} from 'ethers';
import {
Erc20PaymentNetwork,
ERC20TransferableReceivablePaymentDetector,
} from '@requestnetwork/payment-detection';
import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types';
import { ClientTypes } from '@requestnetwork/types';
import { ITransactionOverrides } from './transaction-overrides';
import {
getAmountToPay,
getProxyAddress,
getProvider,
getSigner,
getRequestPaymentValues,
validateERC20TransferableReceivable,
validatePayERC20TransferableReceivable,
} from './utils';
import { IPreparedTransaction } from './prepared-transaction';
// The ERC20 receivable smart contract ABI fragment
const erc20TransferableReceivableContractAbiFragment = [
'function receivableTokenIdMapping(bytes32) public view returns (uint256)',
];
/**
* Gets the receivableTokenId from a ERC20TransferableReceivable contract given
* a paymentReference and paymentAddress of the request
* @param request
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
*/
export async function getReceivableTokenIdForRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer,
): Promise<BigNumber> {
// Setup the ERC20 proxy contract interface
const contract = new ethers.Contract(
getProxyAddress(request, ERC20TransferableReceivablePaymentDetector.getDeploymentInformation),
erc20TransferableReceivableContractAbiFragment,
signerOrProvider,
);
const { paymentReference, paymentAddress } = getRequestPaymentValues(request);
return await contract.receivableTokenIdMapping(
ethers.utils.solidityKeccak256(['address', 'bytes'], [paymentAddress, `0x${paymentReference}`]),
);
}
/**
* Helper method to determine whether a request has a receivable minted yet
*
* @param request
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
*/
export async function hasReceivableForRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer,
): Promise<boolean> {
const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider);
return !receivableTokenId.isZero();
}
/**
* Processes a transaction to mint an ERC20TransferableReceivable.
* @param request
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
* @param overrides optionally, override default transaction values, like gas.
*/
export async function mintErc20TransferableReceivable(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
const { data, to, value } = prepareMintErc20TransferableReceivableTransaction(request);
const signer = getSigner(signerOrProvider);
return signer.sendTransaction({ data, to, value, ...overrides });
}
/**
* Encodes the call to mint a request through an ERC20TransferableReceivable contract, can be used with a Multisig contract.
* @param request request to pay
*/
export function prepareMintErc20TransferableReceivableTransaction(
request: ClientTypes.IRequestData,
): IPreparedTransaction {
validateERC20TransferableReceivable(request);
return {
data: encodeMintErc20TransferableReceivableRequest(request),
to: getProxyAddress(
request,
Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation,
),
value: 0,
};
}
/**
* Encodes call to mint a request through an ERC20TransferableReceivable contract, can be used with a Multisig contract.
* @param request request to pay
*/
export function encodeMintErc20TransferableReceivableRequest(
request: ClientTypes.IRequestData,
): string {
validateERC20TransferableReceivable(request);
const tokenAddress = request.currencyInfo.value;
const { paymentReference, paymentAddress } = getRequestPaymentValues(request);
const amount = getAmountToPay(request);
const receivableContract = ERC20TransferableReceivable__factory.createInterface();
return receivableContract.encodeFunctionData('mint', [
paymentAddress,
`0x${paymentReference}`,
amount,
tokenAddress,
]);
}
/**
* Processes a transaction to pay an ERC20 receivable Request.
* @param request
* @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.
* @param feeAmount optionally, the fee amount to pay. Defaults to the fee amount of the request.
* @param overrides optionally, override default transaction values, like gas.
*/
export async function payErc20TransferableReceivableRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
amount?: BigNumberish,
feeAmount?: BigNumberish,
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
await validatePayERC20TransferableReceivable(request, signerOrProvider, amount, feeAmount);
const { data, to, value } = await prepareErc20TransferableReceivablePaymentTransaction(
request,
signerOrProvider,
amount,
feeAmount,
);
const signer = getSigner(signerOrProvider);
return signer.sendTransaction({ data, to, value, ...overrides });
}
/**
* Encodes the call to pay a request through the ERC20 receivable contract, can be used with a Multisig contract.
* @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.
* @param feeAmountOverride optionally, the fee amount to pay. Defaults to the fee amount of the request.
*/
export async function prepareErc20TransferableReceivablePaymentTransaction(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer,
amount?: BigNumberish,
feeAmountOverride?: BigNumberish,
): Promise<IPreparedTransaction> {
return {
data: await encodePayErc20TransferableReceivableRequest(
request,
signerOrProvider,
amount,
feeAmountOverride,
),
to: getProxyAddress(
request,
Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation,
),
value: 0,
};
}
/**
* Encodes the call to pay a request through the ERC20 receivable contract, can be used with a Multisig contract.
* @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.
* @param feeAmountOverride optionally, the fee amount to pay. Defaults to the fee amount of the request.
*/
export async function encodePayErc20TransferableReceivableRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer,
amount?: BigNumberish,
feeAmountOverride?: BigNumberish,
): Promise<string> {
const amountToPay = getAmountToPay(request, amount);
const { paymentReference, feeAddress, feeAmount } = getRequestPaymentValues(request);
const feeToPay = BigNumber.from(feeAmountOverride || feeAmount || 0);
const receivableContract = ERC20TransferableReceivable__factory.createInterface();
// get tokenId from request
const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider);
return receivableContract.encodeFunctionData('payOwner', [
receivableTokenId,
amountToPay,
`0x${paymentReference}`,
feeToPay,
feeAddress || constants.AddressZero,
]);
}
import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types';
import { deepCopy } from '@requestnetwork/utils';
import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency';
import * as DataConversionERC20FeeAddData from '../../utils/payment-network/erc20/any-to-erc20-proxy-add-data-generator';
import * as MetaCreate from '../../utils/payment-network/meta-pn-data-generator';
import * as TestData from '../../utils/test-data-generator';
import MetaPaymentNetwork from '../../../src/extensions/payment-network/meta';
const metaPn = new MetaPaymentNetwork(CurrencyManager.getDefault());
const baseParams = {
feeAddress: '0x0000000000000000000000000000000000000001',
feeAmount: '0',
paymentAddress: '0x0000000000000000000000000000000000000002',
refundAddress: '0x0000000000000000000000000000000000000003',
salt: 'ea3bc7caf64110ca',
network: 'rinkeby',
acceptedTokens: ['0xFab46E002BbF0b4509813474841E0716E6730136'],
maxRateTimespan: 1000000,
} as ExtensionTypes.PnAnyToErc20.ICreationParameters;
const otherBaseParams = {
...baseParams,
salt: 'ea3bc7caf64110cb',
} as ExtensionTypes.PnAnyToErc20.ICreationParameters;
/* eslint-disable @typescript-eslint/no-unused-expressions */
describe('extensions/payment-network/meta', () => {
describe('createCreationAction', () => {
it('can create a create action with all parameters', () => {
expect(
metaPn.createCreationAction({
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, otherBaseParams],
}),
).toEqual({
action: 'create',
id: ExtensionTypes.PAYMENT_NETWORK_ID.META,
parameters: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, otherBaseParams],
},
version: '0.1.0',
});
});
it('can create a create action without fee parameters', () => {
expect(
metaPn.createCreationAction({
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [
{ ...baseParams, feeAddress: undefined, feeAmount: undefined },
otherBaseParams,
],
}),
).toEqual({
action: 'create',
id: ExtensionTypes.PAYMENT_NETWORK_ID.META,
parameters: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [
{ ...baseParams, feeAddress: undefined, feeAmount: undefined },
otherBaseParams,
],
},
version: '0.1.0',
});
});
it('cannot createCreationAction with duplicated salt', () => {
expect(() => {
metaPn.createCreationAction({
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, baseParams],
});
}).toThrowError('Duplicate payment network identifier (salt)');
});
it('cannot createCreationAction with payment address not an ethereum address', () => {
expect(() => {
metaPn.createCreationAction({
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [
{ ...baseParams, paymentAddress: 'not an ethereum address' },
otherBaseParams,
],
});
}).toThrowError("paymentAddress 'not an ethereum address' is not a valid address");
});
it('cannot createCreationAction with refund address not an ethereum address', () => {
expect(() => {
metaPn.createCreationAction({
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [
{ ...baseParams, refundAddress: 'not an ethereum address' },
otherBaseParams,
],
});
}).toThrowError("refundAddress 'not an ethereum address' is not a valid address");
});
it('cannot createCreationAction with fee address not an ethereum address', () => {
expect(() => {
metaPn.createCreationAction({
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [
{ ...baseParams, feeAddress: 'not an ethereum address' },
otherBaseParams,
],
});
}).toThrowError('feeAddress is not a valid address');
});
it('cannot createCreationAction with invalid fee amount', () => {
expect(() => {
metaPn.createCreationAction({
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [
{ ...baseParams, feeAmount: '-2000' },
otherBaseParams,
],
});
}).toThrowError('feeAmount is not a valid amount');
});
});
describe('applyActionToExtension', () => {
describe('applyActionToExtension/create', () => {
it('can applyActionToExtensions of creation', () => {
// 'new extension state wrong'
expect(
metaPn.applyActionToExtension(
MetaCreate.requestStateNoExtensions.extensions,
MetaCreate.actionCreationMultipleAnyToErc20,
MetaCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
),
).toEqual(MetaCreate.extensionFullStateMultipleAnyToErc20);
});
it('cannot applyActionToExtensions of creation with a previous state', () => {
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestFullStateCreated.extensions,
MetaCreate.actionCreationMultipleAnyToErc20,
MetaCreate.requestFullStateCreated,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('This extension has already been created');
});
it('cannot applyActionToExtensions of creation on a non supported currency', () => {
const requestCreatedNoExtension: RequestLogicTypes.IRequest = deepCopy(
TestData.requestCreatedNoExtension,
);
requestCreatedNoExtension.currency = {
type: RequestLogicTypes.CURRENCY.ERC20,
value: '0x967da4048cD07aB37855c090aAF366e4ce1b9F48', // OCEAN token address
network: 'mainnet',
};
expect(() => {
metaPn.applyActionToExtension(
TestData.requestCreatedNoExtension.extensions,
MetaCreate.actionCreationMultipleAnyToErc20,
requestCreatedNoExtension,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(
'The currency (OCEAN-mainnet, 0x967da4048cD07aB37855c090aAF366e4ce1b9F48) of the request is not supported for this payment network.',
);
});
it('cannot applyActionToExtensions of creation with payment address not valid', () => {
const actionWithInvalidAddress = deepCopy(MetaCreate.actionCreationMultipleAnyToErc20);
actionWithInvalidAddress.parameters[
ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY
][0].paymentAddress = DataConversionERC20FeeAddData.invalidAddress;
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateNoExtensions.extensions,
actionWithInvalidAddress,
MetaCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(
`paymentAddress '${DataConversionERC20FeeAddData.invalidAddress}' is not a valid address`,
);
});
it('cannot applyActionToExtensions of creation with no tokens accepted', () => {
const actionWithInvalidToken = deepCopy(MetaCreate.actionCreationMultipleAnyToErc20);
actionWithInvalidToken.parameters[
ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY
][0].acceptedTokens = [];
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateNoExtensions.extensions,
actionWithInvalidToken,
MetaCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('acceptedTokens is required');
});
it('cannot applyActionToExtensions of creation with token address not valid', () => {
const actionWithInvalidToken = deepCopy(MetaCreate.actionCreationMultipleAnyToErc20);
actionWithInvalidToken.parameters[
ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY
][0].acceptedTokens = ['invalid address'];
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateNoExtensions.extensions,
actionWithInvalidToken,
MetaCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('acceptedTokens must contains only valid ethereum addresses');
});
it('cannot applyActionToExtensions of creation with refund address not valid', () => {
const testnetRefundAddress = deepCopy(MetaCreate.actionCreationMultipleAnyToErc20);
testnetRefundAddress.parameters[
ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY
][0].refundAddress = DataConversionERC20FeeAddData.invalidAddress;
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateNoExtensions.extensions,
testnetRefundAddress,
MetaCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(
`refundAddress '${DataConversionERC20FeeAddData.invalidAddress}' is not a valid address`,
);
});
it('keeps the version used at creation', () => {
const newState = metaPn.applyActionToExtension(
{},
{ ...MetaCreate.actionCreationMultipleAnyToErc20, version: 'ABCD' },
MetaCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
expect(newState[metaPn.extensionId].version).toBe('ABCD');
});
it('requires a version at creation', () => {
expect(() => {
metaPn.applyActionToExtension(
{},
{ ...MetaCreate.actionCreationMultipleAnyToErc20, version: '' },
MetaCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('version is required at creation');
});
});
describe('applyActionToExtension/applyApplyActionToExtension', () => {
it('can applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress', () => {
expect(
metaPn.applyActionToExtension(
MetaCreate.requestStateCreatedMissingAddress.extensions,
MetaCreate.actionApplyActionToPn,
MetaCreate.requestStateCreatedMissingAddress,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
),
).toEqual(MetaCreate.extensionStateWithApplyAddPaymentAddressAfterCreation);
});
it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress without a previous state', () => {
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateNoExtensions.extensions,
MetaCreate.actionApplyActionToPn,
MetaCreate.requestStateNoExtensions,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(`No payment network with identifier ${MetaCreate.salt2}`);
});
it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress without a payee', () => {
const previousState = deepCopy(MetaCreate.requestStateCreatedMissingAddress);
previousState.payee = undefined;
expect(() => {
metaPn.applyActionToExtension(
previousState.extensions,
MetaCreate.actionApplyActionToPn,
previousState,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(`The request must have a payee`);
});
it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress signed by someone else than the payee', () => {
const previousState = deepCopy(MetaCreate.requestStateCreatedMissingAddress);
expect(() => {
metaPn.applyActionToExtension(
previousState.extensions,
MetaCreate.actionApplyActionToPn,
previousState,
TestData.payerRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(`The signer must be the payee`);
});
it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress with payment address already given', () => {
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestFullStateCreated.extensions,
MetaCreate.actionApplyActionToPn,
MetaCreate.requestFullStateCreated,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(`Payment address already given`);
});
it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress with payment address not valid', () => {
const actionWithInvalidAddress = deepCopy(MetaCreate.actionApplyActionToPn);
actionWithInvalidAddress.parameters.parameters.paymentAddress =
DataConversionERC20FeeAddData.invalidAddress;
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateCreatedMissingAddress.extensions,
actionWithInvalidAddress,
MetaCreate.requestStateCreatedMissingAddress,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(
`paymentAddress '${DataConversionERC20FeeAddData.invalidAddress}' is not a valid address`,
);
});
it('cannot applyActionToExtensions applyApplyActionToExtension when the pn identifier is wrong', () => {
const actionWithInvalidPnIdentifier = deepCopy(MetaCreate.actionApplyActionToPn);
actionWithInvalidPnIdentifier.parameters.pnIdentifier = 'wrongId';
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateCreatedMissingAddress.extensions,
actionWithInvalidPnIdentifier,
MetaCreate.requestStateCreatedMissingAddress,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(`No payment network with identifier wrongId`);
});
it('cannot applyActionToExtensions applyApplyActionToExtension when the action does not exists on the sub pn', () => {
const actionWithInvalidPnAction = deepCopy(MetaCreate.actionApplyActionToPn);
actionWithInvalidPnAction.parameters.action = 'wrongAction' as ExtensionTypes.ACTION;
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateCreatedMissingAddress.extensions,
actionWithInvalidPnAction,
MetaCreate.requestStateCreatedMissingAddress,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(`Unknown action: wrongAction`);
});
});
});
describe('declarative tests', () => {
describe('applyActionToExtension/declareSentPayment', () => {
it('can applyActionToExtensions of declareSentPayment', () => {
expect(
metaPn.applyActionToExtension(
MetaCreate.requestFullStateCreated.extensions,
MetaCreate.actionDeclareSentPayment,
MetaCreate.requestFullStateCreated,
TestData.payerRaw.identity,
TestData.arbitraryTimestamp,
),
).toEqual(MetaCreate.extensionStateWithDeclaredSent);
});
it('cannot applyActionToExtensions of declareSentPayment without a previous state', () => {
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestStateNoExtensions.extensions,
MetaCreate.actionDeclareSentPayment,
MetaCreate.requestStateNoExtensions,
TestData.payerRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError(`The extension should be created before receiving any other action`);
});
});
});
});
import { keccak256Hash } from '@requestnetwork/utils';
/**
* Compute the payment reference
*
* @param requestId The requestId
* @param salt The salt for the request
* @param address Payment or refund address
*/
export function calculate(requestId: string, salt: string, address: string): string {
if (!requestId || !salt || !address) {
throw new Error('RequestId, salt and address are mandatory to calculate the payment reference');
}
// "The value is the last 8 bytes of a salted hash of the requestId: `last8Bytes(hash(requestId + salt + address))`"
/* eslint-disable no-magic-numbers */
return keccak256Hash((requestId + salt + address).toLowerCase()).slice(-16);
}
contentData
object
Arbitrary content data. Consider using rnf_invoice v0.0.3 from @requestnetwork/data-format package.
currencyInfo
Additional info about the currency in which the request is denominated: type, value, and network
pending
IPendingRequest
Shows recently submitted request contents that have not yet been persisted and indexed. Call .
state
The state of the request
expectedAmount
number | string
The requested amount
payee
Identity of the payee. Not necessarily payment recipient address.
payer
Identity of the payer. Not necessarily payment sender address.
extensions
IExtensionStates
The state of the extensions
extensionsData
any[]
Extensions raw data
events
IEvent[]
Historical list of actions that have occurred on the request (create, accept, cancel, etc.)
timestamp
number (Unix timestamp)
Timestamp when request is created. User provided, so this is an agreement between payee and payer.
nonce
number
Optional nonce to differentiate identical requests.
escrowEvents
EscrowNetworkEvent[]
Array of escrow events
ERC777
'ERC777'
Streamable fungible currency (USDCx, REQx, etc.)
on()
Event subscriber
emit()
Event emitter
currency
string
The currency in which the request is denominated.
meta
IReturnMeta
Metadata from the layer below (transaction manager), including ignored actions, if any.
balance
version
string
The Request Network protocol version
requestId
string
The ID of the request
creator
PENDING
'pending'
CREATED
'created'
ACCEPTED
'accepted'
CANCELED
'canceled'
balance
string
The sum of all payments and refunds related to this request. If this is >= expectedAmount, then the request is paid in full.
events
Array of payment events
error
IBalanceError
amount
string
The amount of the detected payment
parameters
TEventParameters
Depends on the Payment Network ID
type
Currency type
value
string
Depends on type.
ERC20 contract address '0x123'
Fiat symbol 'USD'
Native symbol 'ETH'
network
ChainName
ETH
'ETH'
Native (ETH, XDAI, etc.)
BTC
'BTC'
Bitcoin
ISO4217
'ISO4217'
Fiat (USD, EUR, etc.)
ERC20
'ERC20'
Non-native fungible currency (USDC, REQ, etc.)
The balance object
Identity of the request creator
Error occured while retrieiving payment events and calculating the balance
The chain on which the currency exists
This page will introduce the primary operations provided by Request Network’s SDK while using the Web3SignatureProvider to sign requests with a private key stored inside a wallet.
This approach works well for Browser environments with access to a web3 wallet.
Rules for how a payment should be processed and detected
A payment network is a set of rules defining how a payment should be processed and detected for a Request. It specifies:
Information required at request creation to enable payment detection
The payment method
The process for determining the balance (amount paid)
Reference-based payment networks use a payment reference to link payments to the corresponding Request. They process payments via payment proxy smart contracts deployed across a wide variety of supported chains. These contracts serve two key functions:
They forward the payment to the payment recipient's address.
They emit an event containing the payment amount and the payment reference.
The payment reference is a unique identifier derived from the Request ID and payment recipient address. This derivation happens off-chain, not in the smart contract itself.
It's important to note that the payment recipient's address can be different from the payee's identity address that typically signs to create the request.
The payment proxy smart contracts do not perform error checking. It is the responsibility of the application using the Request Network to craft the payment transaction with the correct amount and payment reference.
A payment subgraph indexes events from the payment proxy smart contracts, enabling efficient payment detection and balance calculation for each request.
Please note that Reference-based payment networks inherit from the declarative payment network. This means that all reference-based requests can also be paid using declarative payments, providing flexibility in payment methods.
This payment network is used for direct ERC20 token payments.
Example of creating a request with an ERC20 Fee Proxy Contract payment network:
For details on how to use the ERC20 Fee Proxy, see the Quickstart - Browser or Quickstart - Node.js
This payment network is used for direct ETH (or native token) payments on Ethereum and EVM-compatible chains.
Example of creating a request with an ETH Fee Proxy Contract payment network:
This payment network allows for native token payments with an optional fee mechanism. It's suitable for ETH payments on Ethereum mainnet, or native token payments on other EVM-compatible chains like Polygon's MATIC or Binance Smart Chain's BNB.
For details on how to use the ETH Fee Proxy, see Native Payment
This payment network allows for "ERC20 Conversion Payments", where the payment currency is an ERC20 token that is different from the request currency. This is most commonly used to denominate the request in fiat like USD but settle the payment in ERC20 tokens like USDC or USDT. This is useful for accounting because most people use fiat currency as their unit of account.
Key features:
Supports payments in any ERC20 token for requests created in various currencies
Uses on-chain oracles for real-time currency conversion
Includes a fee mechanism similar to the ERC20 Fee Proxy Contract
Example of creating a request with an Any-to-ERC20 Proxy Contract payment network:
In this example, the request is created in USD, but can be paid using DAI (or any other specified ERC20 token). The conversion rate is determined at the time of payment using on-chain oracles.
Conversion is different from Swap-to-Pay. For details see Difference between Conversion, Swap-to-Pay, and Swap-to-Conversion
For details on how to use the Any-to-ERC20 Conversion Proxy Contract, see Conversion Payment
This payment network allows for "Native Conversion Payments", where the payment currency is ETH (or native token) for requests denominated in other currencies, usually fiat. This is most commonly used to denominate the request in fiat like USD but settle the payment in native tokens like ETH on Ethereum or POL on Polygon PoS. This is useful for accounting because most people use fiat currency as their unit of account.
Key features:
Supports payments in ETH (or native token) for requests created in various currencies
Uses on-chain oracles for real-time currency conversion
Includes a fee mechanism similar to the ETH Fee Proxy Contract
Example of creating a request with an Any-to-ETH Proxy Contract payment network:
In this example, the request is created in USD but can be paid using ETH. The conversion rate is determined at the time of payment using on-chain oracles.
Conversion is different from Swap-to-Pay. For details see Difference between Conversion, Swap-to-Pay, and Swap-to-Conversion
For details on how to use the Any-to-ETH Proxy Contract, see Conversion Payment
ERC20 Transferable Receivable allows requests to be minted as NFTs that can be transferred or sold. When the payment is processed, the owner of the NFT receives the payment.
Example of creating an ERC20 Transferable Receivable request:
For details on how to use ERC20 Transferable Receivables, see Transferable Receivable Payment
The declarative payment network allows for manual declaration of payments and refunds. It's particularly useful for currencies or payment methods not directly supported by Request Network like traditional finance payment rails, unsupported web3 payment protocols, and unsupported chains like Solana or Tron.
Example of creating a request with a declarative payment network:
For details on how to use Declarative Payments, See Declarative Payment
The Meta Payment Network allows you to specify multiple potential payment networks for a single request. The payment can be settled by any one of the sub-payment networks.
For details on how to use Meta Payment Network, see Meta Payments
ERC777 Streaming Payment routes payments through the ERC20 Fee Proxy payment network, allowing for continuous, streaming payments.
Example of creating an ERC777 Streaming Payment request:
For details on how to use the ERC777 Stream Payment Network, see Streaming Payment
These networks require a unique payment recipient address for each request. The balance is computed from all inbound transfers to this address. This method is not recommended due to its limitations and potential for errors.
This network used the call data field of Ethereum transactions to tag and detect payments. It has been deprecated in favor of the other reference-based payment networks that use payment proxy smart contracts.
These payment networks have been superseded by the ERC20 Fee Proxy and ETH Fee Proxy networks respectively. The only difference is that the ERC20 Proxy and Ethereum Proxy don't include the service fee mechanism. For most use cases, it's recommended to use the Fee Proxy versions with the fee set to 0 if no fee is required.
Advanced payment types are built on top of payment networks and provide additional functionality or flexibility.
The Advanced Payment Types are *not* Payment Networks. You cannot create a request with one of these payment types in the paymentNetwork property.
Here are some of the advanced payment types available:
Batch payments allow you to pay multiple requests in the same transaction. This is supported for the following payment networks:
ERC20 Fee Proxy
ETH Fee Proxy
Any-to-ERC20 Proxy "ERC20 Conversion Payments"
Any-to-ETH Proxy "ETH Conversion Payments"
ERC20 Proxy networks
See Batch Payment for additional details.
Swap-to-Pay payments execute a swap immediately before routing the payment through the ERC20 Fee Proxy payment network.
See Swap-to-Pay Payment for additional details
Swap-to-Conversion is the combination of Conversion and Swap-to-Pay.
Swap-to-Conversion executes a swap in Uniswap V2 immediately before routing the payment through the Any-to-ERC20 Payment Network.
See Swap-to-Conversion Payment for additional details.
ERC20 Escrow Payment allows for escrow functionality in payments.
The Request Network Escrow lacks arbitration and is susceptible to deadlock in the case of a dispute.
See Escrow Payment for additional details.
Conversion is different from Swap-to-pay.
In a conversion payment, the request is denominated in currency A, the payer sends currency B and the payee receives currency B.
In a Swap-to-pay payment, the request is denominated in currency A, the payer sends currency B and the payee receives currency A.
They can be combined into Swap-to-Conversion.
In a swap-to-conversion payment, the request is denominated in currency A, the payer sends currency B and the payee receives currency C.
For more detailed information on specific payment networks or to contribute new implementations, please refer to our GitHub repository or join our Discord community.
import { RequestNetwork } from "@requestnetwork/request-client.js";
import { Web3SignatureProvider } from "@requestnetwork/web3-signature";
import { getTheGraphClient } from "@requestnetwork/payment-detection";
export const initializeRequestNetwork = (setter: any, walletClient: any) => {
try {
const web3SignatureProvider = new Web3SignatureProvider(walletClient);
const requestNetwork = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://gnosis.gateway.request.network/",
},
signatureProvider: web3SignatureProvider,
httpConfig: {
getConfirmationMaxRetry: 120,
},
paymentOptions: {
getSubgraphClient: (chain: string) => {
// Ternary because cannot dynamically access environment variables in the browser
const paymentsSubgraphUrl =
chain === "arbitrum-one"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_ARBITRUM_ONE || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-arbitrum-one/api"
: chain === "avalanche"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_AVALANCHE || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-avalanche/api"
: chain === "base"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_BASE || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-base/api"
: chain === "bsc"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_BSC || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-bsc/api"
: chain === "celo"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_CELO || "https://api.studio.thegraph.com/query/67444/request-payments-celo/version/latest"
: chain === "core"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_CORE || "https://thegraph.coredao.org/subgraphs/name/requestnetwork/request-payments-core"
: chain === "fantom"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_FANTOM || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-fantom/api"
: chain === "fuse"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_FUSE || "https://api.studio.thegraph.com/query/67444/request-payments-fuse/version/latest"
: chain === "mainnet"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_MAINNET || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-mainnet/api"
: chain === "matic"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_MATIC || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-matic/api"
: chain === "moonbeam"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_MOONBEAM || "https://api.studio.thegraph.com/query/67444/request-payments-moonbeam/version/latest"
: chain === "optimism"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_OPTIMISM || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-optimism/api"
: chain === "sepolia"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_SEPOLIA || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-sepolia/api"
: chain === "xdai"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_XDAI || "https://api.studio.thegraph.com/query/67444/request-payments-xdai/version/latest"
: chain === "zksyncera"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_ZKSYNCERA || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-zksyncera/api"
: undefined;
if (!paymentsSubgraphUrl) {
throw new Error(
`Cannot get subgraph client for unknown chain: ${chain}`
);
}
return getTheGraphClient(chain, paymentsSubgraphUrl);
},
},
});
setter(requestNetwork);
} catch (error) {
console.error("Failed to initialize the Request Network:", error);
setter(null);
}
};
import {
bsc,
celo,
base,
fuse,
zksync,
fantom,
coreDao,
polygon,
mainnet,
sepolia,
arbitrum,
moonbeam,
optimism,
avalanche,
gnosis,
} from "wagmi/chains";
import { http } from "wagmi";
import {
coinbaseWallet,
injectedWallet,
ledgerWallet,
metaMaskWallet,
safeWallet,
trustWallet,
walletConnectWallet,
} from "@rainbow-me/rainbowkit/wallets";
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
export const rainbowKitConfig = getDefaultConfig({
appName: "Request Invoicing",
projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID as string,
chains: [
bsc,
celo,
base,
fuse,
zksync,
gnosis,
fantom,
coreDao,
polygon,
mainnet,
sepolia,
arbitrum,
moonbeam,
optimism,
avalanche,
],
transports: {
[arbitrum.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_ARBITRUM_ONE ||
"https://arbitrum.llamarpc.com"
),
[avalanche.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_AVALANCHE || "https://avalanche.drpc.org"
),
[base.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_BASE || "https://base.llamarpc.com"
),
[bsc.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_BSC || "https://bsc.llamarpc.com"
),
[celo.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_CELO || "https://forno.celo.org"
),
[coreDao.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_CORE || "https://rpc.coredao.org"
),
[fantom.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_FANTOM ||
"https://endpoints.omniatech.io/v1/fantom/mainnet/public"
),
[fuse.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_FUSE || "https://fuse.drpc.org"
),
[mainnet.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_ETHEREUM || "https://eth.llamarpc.com"
),
[polygon.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_POLYGON || "https://1rpc.io/matic"
),
[moonbeam.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_MOONBEAM ||
"https://moonbeam-rpc.publicnode.com"
),
[optimism.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_OPTIMISM ||
"https://optimism.llamarpc.com"
),
[sepolia.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_SEPOLIA || "https://sepolia.drpc.org"
),
[gnosis.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_XDAI || "https://gnosis.drpc.org"
),
[zksync.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_ZKSYNCERA ||
"https://mainnet.era.zksync.io"
),
},
wallets: [
{
groupName: "Recommended",
wallets: [injectedWallet, metaMaskWallet, walletConnectWallet],
},
{
groupName: "Others",
wallets: [safeWallet, coinbaseWallet, ledgerWallet, trustWallet],
},
],
ssr: true,
});
import { RequestNetwork } from "@requestnetwork/request-client.js";
import { Web3SignatureProvider } from "@requestnetwork/web3-signature";
import { getTheGraphClient } from "@requestnetwork/payment-detection";
export const initializeRequestNetwork = (setter: any, walletClient: any) => {
try {
const web3SignatureProvider = new Web3SignatureProvider(walletClient);
const requestNetwork = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://gnosis.gateway.request.network/",
},
signatureProvider: web3SignatureProvider,
httpConfig: {
getConfirmationMaxRetry: 120,
},
paymentOptions: {
getSubgraphClient: (chain: string) => {
// Ternary because cannot dynamically access environment variables in the browser
const paymentsSubgraphUrl =
chain === "arbitrum-one"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_ARBITRUM_ONE || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-arbitrum-one/api"
: chain === "avalanche"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_AVALANCHE || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-avalanche/api"
: chain === "base"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_BASE || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-base/api"
: chain === "bsc"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_BSC || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-bsc/api"
: chain === "celo"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_CELO || "https://api.studio.thegraph.com/query/67444/request-payments-celo/version/latest"
: chain === "core"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_CORE || "https://thegraph.coredao.org/subgraphs/name/requestnetwork/request-payments-core"
: chain === "fantom"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_FANTOM || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-fantom/api"
: chain === "fuse"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_FUSE || "https://api.studio.thegraph.com/query/67444/request-payments-fuse/version/latest"
: chain === "mainnet"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_MAINNET || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-mainnet/api"
: chain === "matic"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_MATIC || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-matic/api"
: chain === "moonbeam"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_MOONBEAM || "https://api.studio.thegraph.com/query/67444/request-payments-moonbeam/version/latest"
: chain === "optimism"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_OPTIMISM || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-optimism/api"
: chain === "sepolia"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_SEPOLIA || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-sepolia/api"
: chain === "xdai"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_XDAI || "https://api.studio.thegraph.com/query/67444/request-payments-xdai/version/latest"
: chain === "zksyncera"
? process.env.NEXT_PUBLIC_PAYMENTS_SUBGRAPH_URL_ZKSYNCERA || "https://subgraph.satsuma-prod.com/e2e4905ab7c8/request-network--434873/request-payments-zksyncera/api"
: undefined;
if (!paymentsSubgraphUrl) {
throw new Error(
`Cannot get subgraph client for unknown chain: ${chain}`
);
}
return getTheGraphClient(chain, paymentsSubgraphUrl);
},
},
});
setter(requestNetwork);
} catch (error) {
console.error("Failed to initialize the Request Network:", error);
setter(null);
}
};
import {
bsc,
celo,
base,
fuse,
zksync,
fantom,
coreDao,
polygon,
mainnet,
sepolia,
arbitrum,
moonbeam,
optimism,
avalanche,
gnosis,
} from "wagmi/chains";
import { http } from "wagmi";
import {
coinbaseWallet,
injectedWallet,
ledgerWallet,
metaMaskWallet,
safeWallet,
trustWallet,
walletConnectWallet,
} from "@rainbow-me/rainbowkit/wallets";
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
export const rainbowKitConfig = getDefaultConfig({
appName: "Request Invoicing",
projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID as string,
chains: [
bsc,
celo,
base,
fuse,
zksync,
gnosis,
fantom,
coreDao,
polygon,
mainnet,
sepolia,
arbitrum,
moonbeam,
optimism,
avalanche,
],
transports: {
[arbitrum.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_ARBITRUM_ONE ||
"https://arbitrum.llamarpc.com"
),
[avalanche.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_AVALANCHE || "https://avalanche.drpc.org"
),
[base.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_BASE || "https://base.llamarpc.com"
),
[bsc.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_BSC || "https://bsc.llamarpc.com"
),
[celo.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_CELO || "https://forno.celo.org"
),
[coreDao.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_CORE || "https://rpc.coredao.org"
),
[fantom.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_FANTOM ||
"https://endpoints.omniatech.io/v1/fantom/mainnet/public"
),
[fuse.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_FUSE || "https://fuse.drpc.org"
),
[mainnet.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_ETHEREUM || "https://eth.llamarpc.com"
),
[polygon.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_POLYGON || "https://1rpc.io/matic"
),
[moonbeam.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_MOONBEAM ||
"https://moonbeam-rpc.publicnode.com"
),
[optimism.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_OPTIMISM ||
"https://optimism.llamarpc.com"
),
[sepolia.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_SEPOLIA || "https://sepolia.drpc.org"
),
[gnosis.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_XDAI || "https://gnosis.drpc.org"
),
[zksync.id]: http(
process.env.NEXT_PUBLIC_RPC_URL_ZKSYNCERA ||
"https://mainnet.era.zksync.io"
),
},
wallets: [
{
groupName: "Recommended",
wallets: [injectedWallet, metaMaskWallet, walletConnectWallet],
},
{
groupName: "Others",
wallets: [safeWallet, coinbaseWallet, ledgerWallet, trustWallet],
},
],
ssr: true,
});
const erc20Request = await requestNetwork.createRequest({
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
parameters: {
paymentNetworkName: 'sepolia',
paymentAddress: paymentRecipientAddress,
feeAddress: feeRecipient,
feeAmount: '0',
},
},
requestInfo: {
currency: {
type: RequestLogicTypes.CURRENCY.ERC20,
value: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT on Ethereum
network: 'mainnet'
},
expectedAmount: '1000000', // 1 USDT (6 decimals)
payee: payeeIdentity,
payer: payerIdentity,
},
signer: payeeIdentity,
});const nativeRequest = await requestNetwork.createRequest({
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT,
parameters: {
paymentNetworkName: 'mainnet',
paymentAddress: paymentRecipientAddress,
feeAddress: feeRecipient,
feeAmount: '1000000000000000', // 0.001 ETH fee
},
},
requestInfo: {
currency: {
type: RequestLogicTypes.CURRENCY.ETH,
value: 'ETH',
network: 'mainnet'
},
expectedAmount: '1000000000000000000', // 1 ETH (18 decimals)
payee: payeeIdentity,
payer: payerIdentity,
},
signer: payeeIdentity,
});const erc20ConversionRequest = await requestNetwork.createRequest({
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
parameters: {
paymentNetworkName: 'mainnet',
paymentAddress: paymentRecipientAddress,
feeAddress: feeRecipient,
feeAmount: '100', // Fee in request currency
acceptedTokens: ['0x6B175474E89094C44Da98b954EedeAC495271d0F'], // DAI address
maxRateTimespan: 1800, // 30 minutes
},
},
requestInfo: {
currency: {
type: RequestLogicTypes.CURRENCY.ISO4217,
value: 'USD'
},
expectedAmount: '1000000', // 1000.00 USD (2 decimals)
payee: payeeIdentity,
payer: payerIdentity,
},
signer: payeeIdentity,
});const nativeConversionRequest = await requestNetwork.createRequest({
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY,
parameters: {
paymentNetworkName: 'mainnet',
paymentAddress: paymentRecipientAddress,
feeAddress: feeRecipient,
feeAmount: '100', // Fee in request currency
maxRateTimespan: 1800, // 30 minutes
},
},
requestInfo: {
currency: {
type: RequestLogicTypes.CURRENCY.ISO4217,
value: 'USD'
},
expectedAmount: '1000000', // 1000.00 USD (2 decimals)
payee: payeeIdentity,
payer: payerIdentity,
},
signer: payeeIdentity,
});const transferableRequest = await requestNetwork.createRequest({
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE,
parameters: {
paymentAddress: paymentRecipientAddress,
feeAddress: feeRecipient,
feeAmount: '0',
network: 'mainnet',
},
},
requestInfo: {
currency: {
type: RequestLogicTypes.CURRENCY.ERC20,
value: erc20TokenAddress,
network: 'mainnet'
},
expectedAmount: '1000000000000000000', // 1 token with 18 decimals
payee: payeeIdentity,
payer: payerIdentity,
},
signer: payeeIdentity,
});const request = await requestNetwork.createRequest({
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.DECLARATIVE,
parameters: {
paymentInfo: {
IBAN: 'FR7630006000011234567890189',
BIC: 'BNPAFRPP'
},
},
},
requestInfo: {
currency: {
type: RequestLogicTypes.CURRENCY.ISO4217,
value: 'EUR'
},
expectedAmount: '100000', // 1000.00 EUR (2 decimals)
payee: payeeIdentity,
payer: payerIdentity,
},
signer: payeeIdentity,
});const streamingRequest = await requestNetwork.createRequest({
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC777_STREAM,
parameters: {
paymentAddress: paymentRecipientAddress,
feeAddress: feeRecipient,
feeAmount: '0',
network: 'mainnet',
tokenAddress: erc777TokenAddress,
},
},
requestInfo: {
currency: {
type: RequestLogicTypes.CURRENCY.ERC777,
value: erc777TokenAddress,
network: 'mainnet'
},
expectedAmount: '1000000000000000000', // 1 token with 18 decimals
payee: payeeIdentity,
payer: payerIdentity,
},
signer: payeeIdentity,
});How to update a request (coming soon...)
How to pay a request
How to detect a payment
How to retrieve a user’s requests
To create an unencrypted ERC-20 request, first connect to an ethers v5 Provider and Signer or wagmi / viem WalletClient.
Unfortunately, the Request Network SDK does not yet support ethers v6.
import { providers } from "ethers";
let provider;
if (process.env.WEB3_PROVIDER_URL === undefined) {
// Connect to Metamask and other injected wallets
provider = new providers.Web3Provider(
import { useWalletClient } from "wagmi";
const { data: walletClient } = useWalletClient();Very similar to wagmi, but without using hooks. Construct your own WalletClient object.
Then, construct a Web3SignatureProvider, passing in the ethers Provider or viem WalletClient.
Then, construct a RequestNetwork, passing in the:
Request Node URL. In this example, we use the Sepolia Request Node Gateway.
Web3SignatureProvider constructed in the previous step.
Then, prepare the Request creation parameters:
Then, call createRequest() to prepare a Request object.
Finally, call request.waitForConfirmation() to wait until:
The request contents are persisted in IPFS
The Content-addressable ID (CID) is stored on-chain
The resulting on-chain event is indexed by the storage subgraph.
First, construct a RequestNetwork object and connect it to a Request Node. In this example, we use the Sepolia Request Node Gateway:
Then, retrieve the request and get the request data. Take note of the current request balance, to be used later for payment detection.
Then, construct an ethers v5 Provider and Signer. These allow you to read and write to the chain, respectively.
Unfortunately, the Request Network SDK does not yet support ethers v6.
Ethers.js Adapters copied from https://wagmi.sh/react/ethers-adapters
Very similar to wagmi, but without using hooks. Instead, call publicClientToProvider() or walletClientToSigner()
Then, check that the payer has sufficient funds using hasSufficientFunds()
Then, in the case of an ERC-20 request, check that the payer has granted sufficient approval using hasErc20Approval(). If not, submit an approval transaction using approveErc20. Wait for an appropriate number of block confirmations. On Sepolia or Ethereum, 2 block confirmations should suffice. Other chains may require more.
Finally, pay the request using payRequest()
You can detect that the payment was successful by polling the request and waiting until the request balance is greater than or equal to the expected amount.
First, construct a RequestNetwork object and connect it to a Request Node. In this example, we use the Sepolia Request Node Gateway:
Then, call fromIdentity() to get an array of Request objects or fromRequestId() to get a single Request object. This function retrieves the Requests stored in IPFS and queries on-chain events to determine the balances paid so far. Finally, call getData() on each Request to get the request contents.
import { providers } from "ethers";
let provider;
if (process.env.WEB3_PROVIDER_URL === undefined) {
// Connect to Metamask and other injected wallets
provider = new providers.Web3Provider(window.ethereum);
} else {
// Connect to your own Ethereum node or 3rd party node provider
provider = new providers.JsonRpcProvider(process.env.WEB3_PROVIDER_URL);
}
// getDefaultProvider() won't work because it doesn't include a Signer.
const signer = await provider.getSigner();import { useEthersV5Provider } from './use-ethers-v5-provider';
import { useEthersV5Signer } from './use-ethers-v5-signer';
return Page() {
const provider = useEthersV5Provider();
const signer = useEthersV5Signer();
...
}import { useMemo } from "react";
import { providers } from "ethers";
import { type HttpTransport } from "viem";
import { type PublicClient, usePublicClient } from "wagmi";
export function publicClientToProvider(publicClient: PublicClient) {
const { chain, transport } = publicClient;
const network = {
chainId: chain.id,
name: chain.name,
ensAddress: chain.contracts?.ensRegistry?.address,
};
if (transport.type === "fallback")
return new providers.FallbackProvider(
(transport.transports as ReturnType<HttpTransport>[]).map(
({ value }) => new providers.JsonRpcProvider(value?.url, network)
)
);
return new providers.JsonRpcProvider(transport.url as string, network);
}
/** Hook to convert a viem Public Client to an ethers.js Provider. */
export function useEthersV5Provider({ chainId }: { chainId?: number } = {}) {
const publicClient = usePublicClient({ chainId });
return useMemo(() => publicClientToProvider(publicClient), [publicClient]);
}import { useMemo } from "react";
import { providers } from "ethers";
import { type WalletClient, useWalletClient } from "wagmi";
export function walletClientToSigner(walletClient: WalletClient) {
const { account, chain, transport } = walletClient;
const network = {
chainId: chain.id,
name: chain.name,
ensAddress: chain.contracts?.ensRegistry?.address,
};
const provider = new providers.Web3Provider(transport, network);
const signer = provider.getSigner(account.address);
return signer;
}
/** Hook to convert a viem Wallet Client to an ethers.js Signer. */
export function useEthersV5Signer({ chainId }: { chainId?: number } = {}) {
const { data: walletClient } = useWalletClient({ chainId });
return useMemo(
() => (walletClient ? walletClientToSigner(walletClient) : undefined),
[walletClient]
);
}
const identityAddress = "0x519145B771a6e450461af89980e5C17Ff6Fd8A92";
const requests = await requestClient.fromIdentity({
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: identityAddress,
});
const requestDatas = requests.map((request) => request.getData());const request = await requestClient.fromRequestId(
'019830e9ec0439e53ec41fc627fd1d0293ec4bc61c2a647673ec5aaaa0e6338855',
);
const requestData = request.getData();import { Web3SignatureProvider } from "@requestnetwork/web3-signature";
const web3SignatureProvider = new Web3SignatureProvider(provider);import { RequestNetwork } from "@requestnetwork/request-client.js"
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
signatureProvider: web3SignatureProvider,
});import { Types, Utils } from "@requestnetwork/request-client.js";
const payeeIdentity = '0x7eB023BFbAeE228de6DC5B92D0BeEB1eDb1Fd567';
const payerIdentity = '0x519145B771a6e450461af89980e5C17Ff6Fd8A92';
const paymentRecipient = payeeIdentity;
const feeRecipient = '0x0000000000000000000000000000000000000000';
const requestCreateParameters = {
requestInfo: {
// The currency in which the request is denominated
currency: {
type: Types.RequestLogic.CURRENCY.ERC20,
value: '0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C',
network: 'sepolia',
},
// The expected amount as a string, in parsed units, respecting `decimals`
// Consider using `parseUnits()` from ethers or viem
expectedAmount: '1000000000000000000',
// The payee identity. Not necessarily the same as the payment recipient.
payee: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentity,
},
// The payer identity. If omitted, any identity can pay the request.
payer: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payerIdentity,
},
// The request creation timestamp.
timestamp: Utils.getCurrentTimestampInSecond(),
},
// The paymentNetwork is the method of payment and related details.
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
parameters: {
paymentNetworkName: 'sepolia',
paymentAddress: payeeIdentity,
feeAddress: feeRecipient,
feeAmount: '0',
},
},
// The contentData can contain anything.
// Consider using rnf_invoice format from @requestnetwork/data-format
contentData: {
reason: '🍕',
dueDate: '2023.06.16',
},
// The identity that signs the request, either payee or payer identity.
signer: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentity,
},
};const request = await requestClient.createRequest(requestCreateParameters);const confirmedRequestData = await request.waitForConfirmation();import { RequestNetwork, Types } from "@requestnetwork/request-client.js";
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
}
});const request = await requestClient.fromRequestId(
'019830e9ec0439e53ec41fc627fd1d0293ec4bc61c2a647673ec5aaaa0e6338855',
);
const requestData = request.getData();import { hasSufficientFunds } from "@requestnetwork/payment-processor";
const _hasSufficientFunds = await hasSufficientFunds(
requestData,
payerAddress,
{
provider: provider,
},
);import { approveErc20, hasErc20Approval } from "@requestnetwork/payment-processor";
const _hasErc20Approval = await hasErc20Approval(
requestData,
payerAddress,
provider
);
if (!_hasErc20Approval) {
const approvalTx = await approveErc20(requestData, signer);
await approvalTx.wait(2);
}import { payRequest } from "@requestnetwork/payment-processor";
const paymentTx = await payRequest(requestData, signer);
await paymentTx.wait(2);const request = await requestClient.fromRequestId(requestData.requestId);
let requestData = request.getData();
while (requestData.balance?.balance < requestData.expectedAmount) {
requestData = await request.refresh();
await new Promise((resolve) => setTimeout(resolve, 1000));
}import { RequestNetwork, Types } from "@requestnetwork/request-client.js";
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
});import nacl from "tweetnacl";
import forge from "node-forge";
import { Buffer } from "buffer";
const randomBytes = (size, callback) => {
if (typeof size !== "number") {
throw new TypeError("Expected number");
}
const bytes = Buffer.from(nacl.randomBytes(size));
if (callback) {
callback(null, bytes);
return;
}
return bytes;
};
const createHash = (algorithm) => {
const md = forge.md[algorithm.toLowerCase()].create();
return {
update: function (data) {
md.update(
typeof data === "string" ? data : forge.util.createBuffer(data)
);
return this;
},
digest: function (encoding) {
const digest = md.digest().getBytes();
return encoding === "hex"
? forge.util.bytesToHex(digest)
: Buffer.from(digest, "binary");
},
};
};
const createCipheriv = (algorithm, key, iv) => {
const cipher = forge.cipher.createCipher(
algorithm,
forge.util.createBuffer(key)
);
cipher.start({ iv: forge.util.createBuffer(iv) });
let output = forge.util.createBuffer();
return {
update: (data) => {
cipher.update(forge.util.createBuffer(data));
output.putBuffer(cipher.output);
return Buffer.from(output.getBytes(), "binary");
},
final: () => {
cipher.finish();
output.putBuffer(cipher.output);
const result = Buffer.from(output.getBytes(), "binary");
output.clear();
return result;
},
getAuthTag: () => {
if (algorithm.includes("gcm")) {
return Buffer.from(cipher.mode.tag.getBytes(), "binary");
}
throw new Error("getAuthTag is only supported for GCM mode");
},
};
};
const createDecipheriv = (algorithm, key, iv) => {
const decipher = forge.cipher.createDecipher(
algorithm,
forge.util.createBuffer(key)
);
decipher.start({ iv: forge.util.createBuffer(iv) });
let output = forge.util.createBuffer();
let authTag;
return {
update: (data) => {
decipher.update(forge.util.createBuffer(data));
output.putBuffer(decipher.output);
return Buffer.from(output.getBytes(), "binary");
},
final: () => {
decipher.finish();
output.putBuffer(decipher.output);
const result = Buffer.from(output.getBytes(), "binary");
output.clear();
return result;
},
setAuthTag: (tag) => {
if (algorithm.includes("gcm")) {
authTag = tag;
decipher.mode.tag = forge.util.createBuffer(tag);
} else {
throw new Error("setAuthTag is only supported for GCM mode");
}
},
};
};
const pbkdf2 = (password, salt, iterations, keylen, digest, callback) => {
try {
const derivedKey = forge.pkcs5.pbkdf2(
password,
salt,
iterations,
keylen,
digest
);
const result = Buffer.from(derivedKey, "binary");
if (callback) {
callback(null, result);
} else {
return result;
}
} catch (error) {
if (callback) {
callback(error);
} else {
throw error;
}
}
};
const randomFillSync = (buffer, offset, size) => {
const randomBytes = nacl.randomBytes(size);
buffer.set(randomBytes, offset);
return buffer;
};
const timingSafeEqual = (a, b) => {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
};
const cryptoPolyfill = {
randomBytes,
createHash,
createCipheriv,
createDecipheriv,
pbkdf2,
randomFillSync,
timingSafeEqual,
};
cryptoPolyfill.default = cryptoPolyfill;
module.exports = {
randomBytes,
createHash,
createCipheriv,
createDecipheriv,
pbkdf2,
randomFillSync,
timingSafeEqual,
};
export default cryptoPolyfill;
import { BigNumber, providers, Wallet } from 'ethers';
import {
ClientTypes,
ExtensionTypes,
IdentityTypes,
RequestLogicTypes,
} from '@requestnetwork/types';
import {
approveErc20BatchConversionIfNeeded,
getBatchConversionProxyAddress,
getErc20Balance,
IConversionPaymentSettings,
payBatchConversionProxyRequest,
prepareBatchConversionPaymentTransaction,
} from '../../src';
import { deepCopy } from '@requestnetwork/utils';
import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils';
import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts';
import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency';
import { CurrencyTypes } from '@requestnetwork/types/src';
import { EnrichedRequest, IRequestPaymentOptions } from 'payment-processor/src/types';
/* eslint-disable no-magic-numbers */
/* eslint-disable @typescript-eslint/no-unused-expressions */
/** Used to to calculate batch fees */
const BATCH_DENOMINATOR = 10000;
const BATCH_FEE = 30;
const BATCH_CONV_FEE = 30;
const DAITokenAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35';
const FAUTokenAddress = '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 currencyManager = new CurrencyManager([
...CurrencyManager.getDefaultList(),
{
address: DAITokenAddress,
decimals: 18,
network: 'private',
symbol: 'DAI',
type: RequestLogicTypes.CURRENCY.ERC20,
},
]);
// Cf. ERC20Alpha in TestERC20.sol
const currency: RequestLogicTypes.ICurrency = {
type: RequestLogicTypes.CURRENCY.ERC20,
value: DAITokenAddress,
network: 'private',
};
const nativeCurrency: RequestLogicTypes.ICurrency = {
type: RequestLogicTypes.CURRENCY.ETH,
value: 'ETH-private',
network: 'private',
};
const conversionPaymentSettings: IConversionPaymentSettings = {
currency: currency,
maxToSpend: '10000000000000000000000000000',
currencyManager: currencyManager,
};
const ethConversionPaymentSettings: IConversionPaymentSettings = {
currency: nativeCurrency,
maxToSpend: '200000000000000000000',
currencyManager: currencyManager,
};
const options: IRequestPaymentOptions = {
conversion: {
currency: currency,
currencyManager: currencyManager,
},
skipFeeUSDLimit: true,
};
// requests setting
const EURExpectedAmount = 55000_00; // 55 000 â¬
const EURFeeAmount = 2_00; // 2 â¬
// amounts used for DAI and FAU requests
const expectedAmount = 100000;
const feeAmount = 100;
const EURToERC20ValidRequest: ClientTypes.IRequestData = {
balance: {
balance: '0',
events: [],
},
contentData: {},
creator: {
type: IdentityTypes.TYPE.ETHEREUM_ADDRESS,
value: wallet.address,
},
currency: 'EUR',
currencyInfo: {
type: RequestLogicTypes.CURRENCY.ISO4217,
value: 'EUR',
},
events: [],
expectedAmount: EURExpectedAmount,
extensions: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: EURFeeAmount,
paymentAddress,
salt: 'salt',
network: 'private',
acceptedTokens: [DAITokenAddress],
},
version: '0.1.0',
},
},
extensionsData: [],
meta: {
transactionManagerMeta: {},
},
pending: null,
requestId: 'abcd',
state: RequestLogicTypes.STATE.CREATED,
timestamp: 0,
version: '1.0',
};
const DAIValidRequest: ClientTypes.IRequestData = {
balance: {
balance: '0',
events: [],
},
contentData: {},
creator: {
type: IdentityTypes.TYPE.ETHEREUM_ADDRESS,
value: wallet.address,
},
currency: 'DAI',
currencyInfo: currency,
events: [],
expectedAmount: expectedAmount,
extensions: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: feeAmount,
paymentAddress: paymentAddress,
salt: 'salt',
},
version: '0.1.0',
},
},
extensionsData: [],
meta: {
transactionManagerMeta: {},
},
pending: null,
requestId: 'abcd',
state: RequestLogicTypes.STATE.CREATED,
timestamp: 0,
version: '1.0',
};
const EURToETHValidRequest: ClientTypes.IRequestData = {
...EURToERC20ValidRequest,
extensions: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: EURFeeAmount,
paymentAddress,
salt: 'salt',
network: 'private',
},
version: '0.1.0',
},
},
};
const ETHValidRequest: ClientTypes.IRequestData = {
...DAIValidRequest,
currency: 'ETH-private',
currencyInfo: nativeCurrency,
extensions: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: feeAmount,
paymentAddress,
salt: 'salt',
network: 'private',
},
version: '0.1.0',
},
},
};
const FAUValidRequest = deepCopy(DAIValidRequest) as ClientTypes.IRequestData;
FAUValidRequest.currencyInfo = {
network: 'private',
type: RequestLogicTypes.CURRENCY.ERC20 as any,
value: FAUTokenAddress,
};
let enrichedRequests: EnrichedRequest[] = [];
// EUR and FAU requests modified within tests to throw errors
let EURRequest: ClientTypes.IRequestData;
let FAURequest: ClientTypes.IRequestData;
/**
* Calcul the expected amount to pay for X euro into Y tokens
* @param amount in fiat: EUR
*/
const expectedConversionAmount = (amount: number, isNative?: boolean): BigNumber => {
// token decimals 10**18
// amount amount / 100
// AggEurUsd.sol x 1.20
// AggDaiUsd.sol / 1.01 OR AggEthUsd.sol / 500
return BigNumber.from(10)
.pow(18)
.mul(amount)
.div(100)
.mul(120)
.div(100)
.mul(100)
.div(isNative ? 50000 : 101);
};
describe('batch-proxy', () => {
beforeAll(async () => {
// Revoke DAI and FAU approvals
await revokeErc20Approval(
getBatchConversionProxyAddress(DAIValidRequest),
DAITokenAddress,
wallet,
);
await revokeErc20Approval(
getBatchConversionProxyAddress(FAUValidRequest),
FAUTokenAddress,
wallet,
);
// Approve the contract to spent DAI with a conversion request
const approvalTx = await approveErc20BatchConversionIfNeeded(
EURToERC20ValidRequest,
wallet.address,
wallet.provider,
undefined,
conversionPaymentSettings,
);
expect(approvalTx).toBeDefined();
if (approvalTx) {
await approvalTx.wait(1);
}
});
describe(`Conversion:`, () => {
beforeEach(() => {
jest.restoreAllMocks();
EURRequest = deepCopy(EURToERC20ValidRequest);
enrichedRequests = [
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURToERC20ValidRequest,
paymentSettings: conversionPaymentSettings,
},
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURRequest,
paymentSettings: conversionPaymentSettings,
},
];
});
describe('Throw an error', () => {
it('should throw an error if the token is not accepted', async () => {
await expect(
payBatchConversionProxyRequest(
[
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURToERC20ValidRequest,
paymentSettings: {
...conversionPaymentSettings,
currency: {
...conversionPaymentSettings.currency,
value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b',
},
} as IConversionPaymentSettings,
},
],
wallet,
options,
),
).rejects.toThrowError(
new UnsupportedCurrencyError({
value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b',
network: 'private',
}),
);
});
it('should throw an error if request has no currency within paymentSettings', async () => {
const wrongPaymentSettings = deepCopy(conversionPaymentSettings);
wrongPaymentSettings.currency = undefined;
await expect(
payBatchConversionProxyRequest(
[
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURRequest,
paymentSettings: wrongPaymentSettings,
},
],
wallet,
options,
),
).rejects.toThrowError('currency must be provided in the paymentSettings');
});
it('should throw an error if the request has a wrong network', async () => {
EURRequest.extensions = {
// ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: feeAmount,
paymentAddress: paymentAddress,
salt: 'salt',
network: 'fakePrivate',
},
version: '0.1.0',
},
};
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError('All the requests must have the same network');
});
it('should throw an error if the request has a wrong payment network id', async () => {
EURRequest.extensions = {
// ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: feeAmount,
paymentAddress: paymentAddress,
network: 'private',
salt: 'salt',
},
version: '0.1.0',
},
};
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError(
'request cannot be processed, or is not an pn-any-to-erc20-proxy request',
);
});
it("should throw an error if one request's currencyInfo has no value", async () => {
EURRequest.currencyInfo.value = '';
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError("The currency '' is unknown or not supported");
});
it('should throw an error if a request has no extension', async () => {
EURRequest.extensions = [] as any;
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError('no payment network found');
});
it('should throw an error if there is a wrong version mapping', async () => {
EURRequest.extensions = {
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: {
...EURRequest.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY],
version: '0.3.0',
},
};
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError('Every payment network type and version must be identical');
});
});
describe('payment', () => {
it('should consider override parameters', async () => {
const spy = jest.fn();
const originalSendTransaction = wallet.sendTransaction.bind(wallet);
wallet.sendTransaction = spy;
await payBatchConversionProxyRequest(
[
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURToERC20ValidRequest,
paymentSettings: conversionPaymentSettings,
},
],
wallet,
options,
{ gasPrice: '20000000000' },
);
expect(spy).toHaveBeenCalledWith({
data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000500918bd80000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000bebc2000000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
gasPrice: '20000000000',
to: getBatchConversionProxyAddress(EURToERC20ValidRequest, '0.1.0'),
value: BigNumber.from(0),
});
wallet.sendTransaction = originalSendTransaction;
});
for (const skipFeeUSDLimit of ['true', 'false']) {
it(`should convert and pay a request in EUR with ERC20, ${
skipFeeUSDLimit === 'true' ? 'skipFeeUSDLimit' : 'no skipFeeUSDLimit'
} `, async () => {
// Get the balances to compare after payment
const initialETHFromBalance = await wallet.getBalance();
const initialDAIFromBalance = await getErc20Balance(
DAIValidRequest,
wallet.address,
provider,
);
const tx = await payBatchConversionProxyRequest(
[
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURToERC20ValidRequest,
paymentSettings: conversionPaymentSettings,
},
],
wallet,
{
...options,
skipFeeUSDLimit: skipFeeUSDLimit === 'true',
},
);
const confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toEqual(1);
expect(tx.hash).toBeDefined();
// Get the new balances
const ETHFromBalance = await wallet.getBalance();
const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider);
// Check each balance
const amountToPay = expectedConversionAmount(EURExpectedAmount);
const feeToPay = expectedConversionAmount(EURFeeAmount);
const totalFeeToPay =
skipFeeUSDLimit === 'true'
? amountToPay.add(feeToPay).mul(BATCH_CONV_FEE).div(BATCH_DENOMINATOR).add(feeToPay)
: BigNumber.from('150891089116411368418'); // eq to $150 batch fee (USD limit) + 2â¬
const expectedAmountToPay = amountToPay.add(totalFeeToPay);
expect(
BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(),
).toBeGreaterThan(0);
expect(
BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance)),
// Calculation of expectedAmountToPay when there there is no fee USD limit
// expectedAmount: 55 000
// feeAmount: + 2
// AggEurUsd.sol x 1.20
// AggDaiUsd.sol / 1.01
// BATCH_CONV_FEE x 1.003
).toEqual(expectedAmountToPay);
});
it(`should convert and pay a request in EUR with ETH, ${
skipFeeUSDLimit === 'true' ? 'skipFeeUSDLimit' : 'no skipFeeUSDLimit'
} `, async () => {
const fromOldBalance = await provider.getBalance(wallet.address);
const toOldBalance = await provider.getBalance(paymentAddress);
const feeOldBalance = await provider.getBalance(feeAddress);
const tx = await payBatchConversionProxyRequest(
[
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY,
request: EURToETHValidRequest,
paymentSettings: ethConversionPaymentSettings,
},
],
wallet,
{
...options,
skipFeeUSDLimit: skipFeeUSDLimit === 'true',
},
);
const confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toEqual(1);
expect(tx.hash).toBeDefined();
// Get the new balances
const fromNewBalance = await provider.getBalance(wallet.address);
const toNewBalance = await provider.getBalance(paymentAddress);
const feeNewBalance = await provider.getBalance(feeAddress);
const gasPrice = confirmedTx.effectiveGasPrice;
const amountToPay = expectedConversionAmount(EURExpectedAmount, true);
const feeToPay = expectedConversionAmount(EURFeeAmount, true);
const totalFeeToPay =
skipFeeUSDLimit === 'true'
? amountToPay.add(feeToPay).mul(BATCH_CONV_FEE).div(BATCH_DENOMINATOR).add(feeToPay)
: // Capped fee total:
// 2 (Fees in â¬)
// x 1.20
// + 150 (Batch Fees capped to 150$)
// / 500
BigNumber.from('304800000000000000');
const expectedAmountToPay = amountToPay.add(totalFeeToPay);
expect(
fromOldBalance
.sub(fromNewBalance)
.sub(confirmedTx.cumulativeGasUsed.mul(gasPrice))
.toString(),
).toEqual(expectedAmountToPay.toString());
expect(feeNewBalance.sub(feeOldBalance).toString()).toEqual(totalFeeToPay.toString());
expect(toNewBalance.sub(toOldBalance).toString()).toEqual(amountToPay.toString());
});
}
it('should convert and pay two requests in EUR with ERC20', async () => {
// Get initial balances
const initialETHFromBalance = await wallet.getBalance();
const initialDAIFromBalance = await getErc20Balance(
DAIValidRequest,
wallet.address,
provider,
);
// Convert and pay
const tx = await payBatchConversionProxyRequest(
Array(2).fill({
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURToERC20ValidRequest,
paymentSettings: conversionPaymentSettings,
}),
wallet,
options,
);
const confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toEqual(1);
expect(tx.hash).toBeDefined();
// Get balances
const ETHFromBalance = await wallet.getBalance();
const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider);
// Checks ETH balances
expect(
BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(),
).toBeGreaterThan(0);
// Checks DAI balances
const amountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of requests: 2
const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of requests: 2
const expectedAmoutToPay = amountToPay
.add(feeToPay)
.mul(BATCH_DENOMINATOR + BATCH_CONV_FEE)
.div(BATCH_DENOMINATOR);
expect(BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance))).toEqual(
expectedAmoutToPay,
);
});
it('should convert and pay two requests in EUR with ETH', async () => {
const fromOldBalance = await provider.getBalance(wallet.address);
const toOldBalance = await provider.getBalance(paymentAddress);
const feeOldBalance = await provider.getBalance(feeAddress);
// Convert and pay
const tx = await payBatchConversionProxyRequest(
Array(2).fill({
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY,
request: EURToETHValidRequest,
paymentSettings: ethConversionPaymentSettings,
}),
wallet,
options,
);
const confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toEqual(1);
expect(tx.hash).toBeDefined();
// Get the new balances
const fromNewBalance = await provider.getBalance(wallet.address);
const toNewBalance = await provider.getBalance(paymentAddress);
const feeNewBalance = await provider.getBalance(feeAddress);
const gasPrice = confirmedTx.effectiveGasPrice;
const amountToPay = expectedConversionAmount(EURExpectedAmount, true).mul(2);
const feeToPay = expectedConversionAmount(EURFeeAmount, true).mul(2);
const totalFeeToPay = amountToPay
.add(feeToPay)
.mul(BATCH_CONV_FEE)
.div(BATCH_DENOMINATOR)
.add(feeToPay);
const expectedAmountToPay = amountToPay.add(totalFeeToPay);
expect(
fromOldBalance
.sub(fromNewBalance)
.sub(confirmedTx.cumulativeGasUsed.mul(gasPrice))
.toString(),
).toEqual(expectedAmountToPay.toString());
expect(feeNewBalance.sub(feeOldBalance).toString()).toEqual(totalFeeToPay.toString());
expect(toNewBalance.sub(toOldBalance).toString()).toEqual(amountToPay.toString());
});
it('should pay heterogeneous payments (ETH/ERC20 with/without conversion)', async () => {
const fromOldBalanceETH = await provider.getBalance(wallet.address);
const toOldBalanceETH = await provider.getBalance(paymentAddress);
const feeOldBalanceETH = await provider.getBalance(feeAddress);
const fromOldBalanceDAI = await getErc20Balance(DAIValidRequest, wallet.address, provider);
const toOldBalanceDAI = await getErc20Balance(DAIValidRequest, paymentAddress, provider);
const feeOldBalanceDAI = await getErc20Balance(DAIValidRequest, feeAddress, provider);
// Convert the two first requests and pay the three requests
const tx = await payBatchConversionProxyRequest(
[
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURToERC20ValidRequest,
paymentSettings: conversionPaymentSettings,
},
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
request: EURToERC20ValidRequest,
paymentSettings: conversionPaymentSettings,
},
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
request: DAIValidRequest,
paymentSettings: { maxToSpend: '0' },
},
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT,
request: ETHValidRequest,
paymentSettings: {
...ethConversionPaymentSettings,
maxToSpend: '0',
},
},
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY,
request: EURToETHValidRequest,
paymentSettings: ethConversionPaymentSettings,
},
],
wallet,
options,
);
const confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toEqual(1);
expect(tx.hash).toBeDefined();
const gasPrice = confirmedTx.effectiveGasPrice;
const fromNewBalanceETH = await provider.getBalance(wallet.address);
const toNewBalanceETH = await provider.getBalance(paymentAddress);
const feeNewBalanceETH = await provider.getBalance(feeAddress);
const fromNewBalanceDAI = await getErc20Balance(DAIValidRequest, wallet.address, provider);
const toNewBalanceDAI = await getErc20Balance(DAIValidRequest, paymentAddress, provider);
const feeNewBalanceDAI = await getErc20Balance(DAIValidRequest, feeAddress, provider);
// Computes amount related to DAI with conversion payments
const DAIConvAmount = expectedConversionAmount(EURExpectedAmount).mul(2);
const DAIConvFeeAmount = expectedConversionAmount(EURFeeAmount).mul(2);
const DAIConvTotalFees = DAIConvAmount.add(DAIConvFeeAmount)
.mul(BATCH_CONV_FEE)
.div(BATCH_DENOMINATOR)
.add(DAIConvFeeAmount);
const DAIConvTotal = DAIConvAmount.add(DAIConvTotalFees);
// Computes amount related to payments without conversion (same for ETH or ERC20)
const NoConvAmount = BigNumber.from(expectedAmount);
const NoConvFeeAmount = BigNumber.from(feeAmount);
const NoConvTotalFees = NoConvAmount.add(NoConvFeeAmount)
.mul(BATCH_CONV_FEE)
.div(BATCH_DENOMINATOR)
.add(NoConvFeeAmount);
const NoConvTotal = NoConvAmount.add(NoConvTotalFees);
// Computes amount related to ETH with conversion payments
const ETHConvAmount = expectedConversionAmount(EURExpectedAmount, true);
const ETHConvFeeAmount = expectedConversionAmount(EURFeeAmount, true);
const ETHConvTotalFees = ETHConvAmount.add(ETHConvFeeAmount)
.mul(BATCH_CONV_FEE)
.div(BATCH_DENOMINATOR)
.add(ETHConvFeeAmount);
const ETHConvTotal = ETHConvAmount.add(ETHConvTotalFees);
// Totals
const DAIAmount = DAIConvAmount.add(NoConvAmount);
const DAIFeesTotal = DAIConvTotalFees.add(NoConvTotalFees);
const DAITotal = DAIConvTotal.add(NoConvTotal);
const ETHAmount = ETHConvAmount.add(NoConvAmount);
const ETHFeesTotal = ETHConvTotalFees.add(NoConvTotalFees);
const ETHTotal = ETHConvTotal.add(NoConvTotal);
// DAI Checks
expect(BigNumber.from(fromOldBalanceDAI).sub(fromNewBalanceDAI)).toEqual(DAITotal);
expect(BigNumber.from(toNewBalanceDAI).sub(toOldBalanceDAI)).toEqual(DAIAmount);
expect(BigNumber.from(feeNewBalanceDAI).sub(feeOldBalanceDAI)).toEqual(DAIFeesTotal);
// ETH Checks
expect(
fromOldBalanceETH
.sub(fromNewBalanceETH)
.sub(confirmedTx.cumulativeGasUsed.mul(gasPrice))
.toString(),
).toEqual(ETHTotal.toString());
expect(toNewBalanceETH.sub(toOldBalanceETH)).toEqual(ETHAmount);
expect(feeNewBalanceETH.sub(feeOldBalanceETH).toString()).toEqual(ETHFeesTotal.toString());
}, 20000);
});
});
describe('No conversion:', () => {
beforeEach(() => {
FAURequest = deepCopy(FAUValidRequest);
enrichedRequests = [
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
request: DAIValidRequest,
paymentSettings: { maxToSpend: '0' },
},
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
request: FAURequest,
paymentSettings: { maxToSpend: '0' },
},
];
});
describe('Throw an error', () => {
it('should throw an error if the request is not erc20', async () => {
FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH;
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError('wrong request currencyInfo type');
});
it("should throw an error if one request's currencyInfo has no value", async () => {
FAURequest.currencyInfo.value = '';
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError(
'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request',
);
});
it("should throw an error if one request's currencyInfo has no network", async () => {
FAURequest.currencyInfo.network = '' as CurrencyTypes.ChainName;
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).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 () => {
FAURequest.extensions = [] as any;
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError('no payment network found');
});
it('should throw an error if there is a wrong version mapping', async () => {
FAURequest.extensions = {
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: {
...DAIValidRequest.extensions[
ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT
],
version: '0.3.0',
},
};
await expect(
payBatchConversionProxyRequest(enrichedRequests, wallet, options),
).rejects.toThrowError('Every payment network type and version must be identical');
});
});
describe('payBatchConversionProxyRequest', () => {
it('should consider override parameters', async () => {
const spy = jest.fn();
const originalSendTransaction = wallet.sendTransaction.bind(wallet);
wallet.sendTransaction = spy;
await payBatchConversionProxyRequest(enrichedRequests, wallet, options, {
gasPrice: '20000000000',
});
expect(spy).toHaveBeenCalledWith({
data: '0x92cddb9100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
gasPrice: '20000000000',
to: getBatchConversionProxyAddress(DAIValidRequest, '0.1.0'),
value: BigNumber.from(0),
});
wallet.sendTransaction = originalSendTransaction;
});
it(`should pay 2 different ERC20 requests with fees`, async () => {
// Approve the contract for DAI and FAU tokens
const FAUApprovalTx = await approveErc20BatchConversionIfNeeded(
FAUValidRequest,
wallet.address,
wallet,
);
if (FAUApprovalTx) await FAUApprovalTx.wait(1);
const DAIApprovalTx = await approveErc20BatchConversionIfNeeded(
DAIValidRequest,
wallet.address,
wallet,
);
if (DAIApprovalTx) await DAIApprovalTx.wait(1);
// Get initial balances
const initialETHFromBalance = await wallet.getBalance();
const initialDAIFromBalance = await getErc20Balance(
DAIValidRequest,
wallet.address,
provider,
);
const initialDAIFeeBalance = await getErc20Balance(DAIValidRequest, feeAddress, provider);
const initialFAUFromBalance = await getErc20Balance(
FAUValidRequest,
wallet.address,
provider,
);
const initialFAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider);
// Batch payment
const tx = await payBatchConversionProxyRequest(enrichedRequests, wallet, options);
const confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
// Get balances
const ETHFromBalance = await wallet.getBalance();
const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider);
const DAIFeeBalance = await getErc20Balance(DAIValidRequest, feeAddress, provider);
const FAUFromBalance = await getErc20Balance(FAUValidRequest, wallet.address, provider);
const FAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider);
// Checks ETH balances
expect(ETHFromBalance.lte(initialETHFromBalance)).toBeTruthy(); // 'ETH balance should be lower'
// Check FAU balances
const expectedFAUFeeAmountToPay =
feeAmount + ((FAUValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR;
expect(BigNumber.from(FAUFromBalance)).toEqual(
BigNumber.from(initialFAUFromBalance).sub(
(FAUValidRequest.expectedAmount as number) + expectedFAUFeeAmountToPay,
),
);
expect(BigNumber.from(FAUFeeBalance)).toEqual(
BigNumber.from(initialFAUFeeBalance).add(expectedFAUFeeAmountToPay),
);
// Check DAI balances
const expectedDAIFeeAmountToPay =
feeAmount + ((DAIValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR;
expect(BigNumber.from(DAIFromBalance)).toEqual(
BigNumber.from(initialDAIFromBalance)
.sub(DAIValidRequest.expectedAmount as number)
.sub(expectedDAIFeeAmountToPay),
);
expect(BigNumber.from(DAIFeeBalance)).toEqual(
BigNumber.from(initialDAIFeeBalance).add(expectedDAIFeeAmountToPay),
);
});
});
describe('prepareBatchPaymentTransaction', () => {
it('should consider the version mapping', () => {
expect(
prepareBatchConversionPaymentTransaction(
[
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
request: {
...DAIValidRequest,
extensions: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: {
...DAIValidRequest.extensions[
ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT
],
version: '0.1.0',
},
},
} as any,
} as EnrichedRequest,
{
paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
request: {
...FAUValidRequest,
extensions: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: {
...FAUValidRequest.extensions[
ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT
],
version: '0.1.0',
},
},
} as any,
} as unknown as EnrichedRequest,
],
options,
).to,
).toBe(batchConversionPaymentsArtifact.getAddress('private', '0.1.0'));
});
});
});
});
Create an unencrypted request
Promise<>
Identical to
Equal to OR
topics
string[]
List of hashes, usually one for each of the request stakeholders
contentData
Object
Additional arbitrary request contents
disablePaymentDetection
boolean
Disable payment detection
disableEvents
boolean
Diable event callbacks
payer
Identity of the payer. Required if payee not set
extensionsData
any[]
Raw extensionsData. Not recommended. Prefer contentData and paymentNetwork instead.
timestamp
number (Unix timestamp)
Timestamp when request is created. User provided, so this is an agreement between payee and payer.
nonce
number
Optional nonce to differentiate identical requests created at the same timestamp.
ERC777
'ERC777'
Streamable fungible currency (USDCx, REQx, etc.)
BITCOIN_ADDRESS_BASED
'pn-bitcoin-address-based'
Payee generates a new Bitcoin address. Use block explorer to detect all payments to that address.
ERC20_ADDRESS_BASED
'pn-erc20-address-based'
Payee generates a new Ethereum address. Use block explorer to detect all payments to that address.
ERC20_FEE_PROXY_CONTRACT
'pn-erc20-fee-proxy-contract'
Send ERC20 via smart contract with an optional fee.
ERC20_PROXY_CONTRACT
'pn-erc20-proxy-contract'
Send ERC20 via smart contract
ERC20_TRANSFERABLE_RECEIVABLE
'pn-erc20-transferable-receivable'
Mint a Request as an NFT. The holder receives the payment.
ERC777_STREAM
'pn-erc777-stream'
Superfluid stream
ETH_FEE_PROXY_CONTRACT
'pn-eth-fee-proxy-contract'
Send native token via smart contract with an optional fee.
ETH_INPUT_DATA
'pn-eth-input-data'
Send native token with paymentReference in the call data.
NATIVE_TOKEN
'pn-native-token'
Send native token via smart contract with an optional fee on NEAR.
TESTNET_BITCOIN_ADDRESS_BASED
'pn-testnet-bitcoin-address-based'
Payee generates a new Bitcoin testnet address. Use block explorer to detect all payments to that address.
payerDelegate
Identity that can update the request on behalf of the payer
salt
string
A random number with at least 8 bytes of randomness. It must be unique to each request
parameters
Parameters to create a request
options
Options to create a request
requestInfo
Core request contents
signer
Identity of the creator and signer of the request. Must be either the payee or payer.
paymentNetwork
skipRefresh
boolean
Disable the request refresh after creation. Warning: the balance will be null.
currency
string | ICurrency
The currency in which the request is denominated. Not necessarily the currency in which the payment will occur.
expectedAmount
number | string
The requested amount in machine-readable integer units.
payee
id
Payment network ID
parameters
Payment network parameters. Contents depend on id
type
Currency type
value
string
Depends on type.
ERC20 contract address '0x123'
Fiat symbol 'USD'
Native symbol 'ETH'
network
ChainName
ETH
'ETH'
Native (ETH, XDAI, etc.)
BTC
'BTC'
Bitcoin
ISO4217
'ISO4217'
Fiat (USD, EUR, etc.)
ERC20
'ERC20'
Non-native fungible currency (USDC, REQ, etc.)
ANY_DECLARATIVE
'pn-any-declarative'
Payer declares payment sent. Payee declares payment received.
ANY_TO_ERC20_PROXY
'pn-any-to-erc20-proxy'
Swap to ERC20 before sending to payee
ANY_TO_ETH_PROXY
'pn-any-to-eth-proxy'
Swap to native token before sending to payee. Only works on EVM-compatible chains.
ANY_TO_NATIVE_TOKEN
'pn-any-to-native-token'
paymentInfo
any
refundInfo
any
payeeDelegate
paymentAddress
string
The payment recipient address
refundAddress
string
The refund recipient address
salt
string
paymentNetworkName
ChainName
The chain name on which the payment will occur
feeAddress
string
The address to which fees will be sent
feeAmount
string
The fee amount in machine-readable integer units
maxRateTimespan
number
The maximum acceptable time span between the payment and the conversion rate timestamp
network
ChainName
The network of the tokens accepted for payments
acceptedTokens
string[]
A list of token addresses accepted for payments and refunds
network
EvmChainName
the network of the tokens accepted for payments
expectedFlowRate
string
expectedStartDate
string
previousRequestId
string
originalRequestId
string
recurrenceNumber
string
Payment method
Identity of the payee. Required if payer not set
The chain on which the currency exists
Swap to native token before sending to payee. Only works on NEAR.
Identity that can update the request on behalf of the payee
(async () => {
const {
RequestNetwork,
Types,
Utils,
} = require("@requestnetwork/request-client.js");
const {
EthereumPrivateKeySignatureProvider,
} = require("@requestnetwork/epk-signature");
const { config } = require("dotenv");
const { Wallet } = require("ethers");
// Load environment variables from .env file
config();
const epkSignatureProvider = new EthereumPrivateKeySignatureProvider({
method: Types.Signature.METHOD.ECDSA,
privateKey: process.env.PAYEE_PRIVATE_KEY, // Must include 0x prefix
});
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
signatureProvider: epkSignatureProvider,
});
// In this example, the payee is also the payer and payment recipient.
const payeeIdentity = new Wallet(process.env.PAYEE_PRIVATE_KEY).address;
const payerIdentity = payeeIdentity;
const paymentRecipient = payeeIdentity;
const feeRecipient = "0x0000000000000000000000000000000000000000";
const requestCreateParameters = {
requestInfo: {
currency: {
type: Types.RequestLogic.CURRENCY.ERC20,
value: "0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C",
network: "sepolia",
},
expectedAmount: "1000000000000000000",
payee: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentity,
},
payer: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payerIdentity,
},
timestamp: Utils.getCurrentTimestampInSecond(),
},
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
parameters: {
paymentNetworkName: "sepolia",
paymentAddress: paymentRecipient,
feeAddress: feeRecipient,
feeAmount: "0",
},
},
contentData: {
reason: "ð",
dueDate: "2023.06.16",
},
signer: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentity,
},
};
const request = await requestClient.createRequest(requestCreateParameters);
const requestData = await request.waitForConfirmation();
console.log(JSON.stringify(requestData));
})();
(async () => {
const {
RequestNetwork,
Types,
Utils,
} = require("@requestnetwork/request-client.js");
const {
EthereumPrivateKeySignatureProvider,
} = require("@requestnetwork/epk-signature");
const {
approveErc20,
hasSufficientFunds,
hasErc20Approval,
payRequest,
} = require("@requestnetwork/payment-processor");
const { providers, Wallet } = require("ethers");
const { config } = require("dotenv");
// Load environment variables from .env file
config();
const epkSignatureProvider = new EthereumPrivateKeySignatureProvider({
method: Types.Signature.METHOD.ECDSA,
privateKey: process.env.PAYEE_PRIVATE_KEY, // Must include 0x prefix
});
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
signatureProvider: epkSignatureProvider,
});
const payeeIdentity = new Wallet(process.env.PAYEE_PRIVATE_KEY).address;
const payerIdentity = payeeIdentity;
const paymentRecipient = payeeIdentity;
const feeRecipient = "0x0000000000000000000000000000000000000000";
const requestCreateParameters = {
requestInfo: {
currency: {
type: Types.RequestLogic.CURRENCY.ERC20,
value: "0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C",
network: "sepolia",
},
expectedAmount: "1000000000000000000",
payee: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentity,
},
payer: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payerIdentity,
},
timestamp: Utils.getCurrentTimestampInSecond(),
},
paymentNetwork: {
id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
parameters: {
paymentNetworkName: "sepolia",
paymentAddress: paymentRecipient,
feeAddress: feeRecipient,
feeAmount: "0",
},
},
contentData: {
reason: "ð",
dueDate: "2023.06.16",
},
signer: {
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: payeeIdentity,
},
};
const request = await requestClient.createRequest(requestCreateParameters);
let requestData = await request.waitForConfirmation();
console.log(`Created Request: ${JSON.stringify(requestData)}`);
const provider = new providers.JsonRpcProvider(
process.env.JSON_RPC_PROVIDER_URL,
);
const payerWallet = new Wallet(
process.env.PAYER_PRIVATE_KEY, // Must have 0x prefix
provider,
);
console.log(
`Checking if payer ${payerWallet.address} has sufficient funds...`,
);
const _hasSufficientFunds = await hasSufficientFunds({
request: requestData,
address: payerWallet.address,
providerOptions: { provider: provider },
});
console.log(`_hasSufficientFunds = ${_hasSufficientFunds}`);
if (!_hasSufficientFunds) {
throw new Error(`Insufficient Funds: ${payerWallet.address}`);
}
console.log(
`Checking if payer ${payerWallet.address} has sufficient approval...`,
);
const _hasErc20Approval = await hasErc20Approval(
requestData,
payerWallet.address,
provider,
);
console.log(`_hasErc20Approval = ${_hasErc20Approval}`);
if (!_hasErc20Approval) {
console.log(`Requesting approval...`);
const approvalTx = await approveErc20(requestData, payerWallet);
await approvalTx.wait(2);
console.log(`Approval granted. ${approvalTx.hash}`);
}
const paymentTx = await payRequest(requestData, payerWallet);
await paymentTx.wait(2);
console.log(`Payment complete. ${paymentTx.hash}`);
let startTime = Date.now();
while (requestData.balance?.balance < requestData.expectedAmount) {
requestData = await request.refresh();
console.log(`current balance = ${requestData.balance?.balance}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
// Check if 5 seconds have passed, and if so, break out of the loop
if (Date.now() - startTime >= 5000) {
console.log("Timeout: Exiting loop after 5 seconds.");
break;
}
}
})();
(async () => {
const {
RequestNetwork,
Types,
} = require("@requestnetwork/request-client.js");
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://sepolia.gateway.request.network/",
},
});
const identity = "0x519145B771a6e450461af89980e5C17Ff6Fd8A92";
const requests = await requestClient.fromIdentity({
type: Types.Identity.TYPE.ETHEREUM_ADDRESS,
value: identity,
});
const requestDatas = requests.map((request) => request.getData());
console.log(JSON.stringify(requestDatas));
})();
In this page you will learn how to integrate Request Network API into your application
We will be creating a simple node server integrating the Request Network API to create payments and track their status. We are going to use fastify as our server and use drizzle with SQLite to store our payment data. Additionally, we'll be creating a simple React web application to interact with the API and execute payments.
View the entire codebase on Code Sandbox.
In this section we'll create your API that integrates Request Network's API to create and track payments. After we are done with it, we'll jump over and create a web app connecting to your API and put everything together!
As mentioned, we are using fastify and drizzle for this demo, you can of course choose whatever suits you best.
Create a new project. In it create a folder called rn-test-backend inside and copy over this package.json file to rn-test-backend.
The folder structure for the demo is going to be simple:
Then run npm install and when that's done, run npm run db:push.
Before starting your integration, you need to sign up on our , which you can access via this .
On our API portal dashboard, you can easily create an API key.
Now copy over its value to your .env file
Let's create two new endpoints, one for creating a payment on our API and the other to fetch all of the payments users have made on our API.
Note: the amount our API receives is human readable, so just send over the amount in invoiceCurrency you wish, no BigNumbers needed!
Call our /payments endpoint with the right data to create a payout and let's see what we get back.
The response should look something like the following object ():
Now you can check your database with npm run db:studio and assert that the payment is there.
In order for your app to make use of our payment tracking easily and in real-time, we provide webhook support. You just provide the endpoint and the Request Network API does the rest.
Let's create a new route for handling webhook calls.
We'll go into more detail on how to get the RN_WEBHOOK_SECRET in the next subsection.
As you may know, it's impossible for our webhooks to call your locally running server. In order to test them, use a tool like . Install it and run ngrok http 3000 in your terminal. In a few moments, you should see something similar to the screenshot below and copy the URL.
Next up, go back to the and add a new webhook. In the case above it's the URL from ngrok with the /webhooks appendix ().
Next thing, copy over the signing secret and add it to your .env file, then restart the app.
If you want to test it out, click the Send test event button and observe your server's logs. Your output should look something like the following:
Once your application is deployed, you will need to add a new webhook via the just like we did above, but use your deployment URL's webhook route.
Then copy over the secret to your deployment's variables and you can test your handlers just as we did above!
To make use of payment tracking, we need to map different event types to handlers. For demo purposes, let's create a new handler that will update the status of a payment in your API to confirmed when it's been confirmed by Request Network.
This is it for the API, now to properly test this, we're going to build a simple frontend app that will interact with the newly created API!
We cannot test out the entire flow without a user actually paying a request. For testing purposes, I will use a wallet. In order for you to properly test this, I advise using a wallet and giving yourself some test Sepolia ETH from a faucet like .
If you really want to check out what happens to your funds, create two accounts in your wallet. We'll be using Request Network to move funds from one to another.
Well be using to create a simple React app. Move to the root directory in the created project and run npm create vite@latest rn-test-frontend -- --template react-ts in the terminal. Then move to the created directory rn-test-frontend, run npm install .
NOTE: We are not going to be using any advanced patterns or libraries here, we'll try to keep it as simple as possible and let you build in your own way.
Next up, let's scaffold our app. Create a folder called components, and then create two files CreatePayment.tsx and ViewPayments.tsx.
Next up, let's modify our App.tsx file to display two tabs.
The final result should look something like this:
We'll be using to enable wallet connection. To do that we need to do a few things:
Install wagmi and its dependencies npm install wagmi viem @tanstack/react-query --save
Create a wagmi config at src/config/wagmi.ts
Update main.tsx to include the new providers
Create a new component at src/components/wallet-connect/index.tsx
Render this component from our App component
The final result should look something like this with the wallet connection working.
Since we have created a few payments via cURL before, we can implement viewing of payments first. Let's create a .env file and add the following to it:
Next up, let's modify the ViewPayments component.
It should look something like this:
Let's update our CreatePayment component. It's going to do the following:
The user inputs payment information - the payee address, amount, invoice currency and payment currency
After submitting the form, we create a payment on the API, receive the response and use the transactions property to execute the payment with our connected wallet.
Immediately after that succeeds, we update the payment status on the backend to in-progress
The end result is a form that looks like the following:
We recommend using two different Metamask accounts you own. That way you will be able to confirm that the funds were moved on your very own.
NOTE: For this demo, we recommend inputting your second account for the Payee address value and use the same invoice and payment currencies.
Let's create a payment from the client, moving 0.02 Sepolia ETH to our second account
Create the payment and sign the transaction
Navigate to the View payments tab , verify that the last payment is In progress and let's wait for the transaction to go through. You can patiently watch your server's logs to check when the webhook is called.
In a few moments the payment's status should be set to Confirmed .
This is it, you have succesfully built a basic application integrating our API to move actual test funds between two wallets.
Happy building 🎉
{
"name": "request-api-demo",
"version": "1.0.0",
"description": "Request API demo",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"dev:watch": "ts-node-dev --respawn --transpile-only src/index.ts",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
},
"keywords": [
"fastify",
"typescript",
"node"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.0.0",
"ts-node": "^10.9.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"@fastify/cors": "^11.1.0",
"@types/better-sqlite3": "^7.6.13",
"better-sqlite3": "^12.2.0",
"dotenv": "^17.2.1",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.5",
"fastify": "^5.5.0"
}
}
// src/db/index.ts
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema';
const sqlite = new Database('database.sqlite');
export const db = drizzle(sqlite, { schema });// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const payments = sqliteTable('payments', {
id: integer('id').primaryKey({ autoIncrement: true }),
requestId: text('request_id').notNull(),
status: text('status').notNull(),
});
export type Payment = typeof payments.$inferSelect;// src/index.ts
import 'dotenv/config';
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
const fastify = Fastify({
logger: true
});
fastify.get('/', async (request: FastifyRequest, reply: FastifyReply) => {
return { message: 'Hello World!' };
});
const start = async () => {
try {
const port = 3000;
const host = 'localhost';
await fastify.register(require('@fastify/cors'), {
origin: true, // change to your frontend URL in production
methods: ['GET', 'POST', 'PATCH'],
});
await fastify.listen({ port, host });
console.log(`Server listening on http://${host}:${port}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
// drizzle-config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
dbCredentials: {
url: './database.sqlite',
},
});// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
// .env
RN_API_KEY=<insert-you-api-key>
RN_API_URL=https://api.request.network/v2 // src/index.ts
import 'dotenv/config';
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import { db } from './db';
import { payments } from './db/schema';
const fastify = Fastify({
logger: true
});
fastify.get('/', async (request: FastifyRequest, reply: FastifyReply) => {
return { message: 'Hello World!' };
});
interface PaymentBody {
payee: string;
amount: string;
invoiceCurrency: string;
paymentCurrency: string;
}
fastify.post('/payments', async (request: FastifyRequest<{ Body: PaymentBody }>, reply: FastifyReply) => {
try {
const { payee, amount, invoiceCurrency, paymentCurrency } = request.body;
if (!payee || !amount || !invoiceCurrency || !paymentCurrency) {
return reply.status(400).send({
error: 'Missing required fields: payee, amount, invoiceCurrency, paymentCurrency'
});
}
const response = await fetch(`${process.env.RN_API_URL}/payouts`, {
method: 'POST',
headers: {
'X-Api-Key': process.env.RN_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
payee,
amount,
invoiceCurrency,
paymentCurrency
})
});
if (!response.ok) {
const errorText = await response.text();
fastify.log.error(`Request Network API error: ${response.status} - ${errorText}`);
return reply.status(response.status).send({
error: 'Failed to create payment with Request Network API',
details: errorText
});
}
const rnApiResponse: any = await response.json();
console.log('Request Network API response:', JSON.stringify(rnApiResponse, null, 2));
const [savedPayment] = await db.insert(payments).values({
requestId: rnApiResponse.requestId,
status: 'pending'
}).returning();
console.log('Payment saved to database:', savedPayment);
return {
payment: savedPayment,
calldata: {
transactions: rnApiResponse.transactions,
metadata: rnApiResponse.metadata
}
};
} catch (error) {
console.error('Error creating payment:', error);
return reply.status(500).send({
error: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
interface UpdatePaymentStatusBody {
status: string;
}
// we will use this endpoint later on, don't think too much about it right now!
fastify.patch('/payments/:id', async (request: FastifyRequest<{
Params: { id: string };
Body: UpdatePaymentStatusBody
}>, reply: FastifyReply) => {
try {
const { id } = request.params;
const { status } = request.body;
if (!status) {
return reply.status(400).send({
error: 'Status is required'
});
}
const updatedPayment = await db.update(payments)
.set({ status })
.where(eq(payments.id, parseInt(id)))
.returning();
if (!updatedPayment.length) {
return reply.status(404).send({
error: 'Payment not found'
});
}
console.log('Payment status updated:', updatedPayment[0]);
return {
payment: updatedPayment[0]
};
} catch (error) {
console.error('Error updating payment status:', error);
return reply.status(500).send({
error: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
fastify.get('/payments', async (request: FastifyRequest, reply: FastifyReply) => {
try {
const allPayments = await db.select().from(payments);
return { payments: allPayments };
} catch (error) {
console.error('Error fetching payments:', error);
return reply.status(500).send({
error: 'Failed to fetch payments',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
const start = async () => {
try {
const port = 3000;
const host = 'localhost';
await fastify.register(require('@fastify/cors'), {
origin: true, // change to your frontend URL in production
methods: ['GET', 'POST', 'PATCH'],
});
await fastify.listen({ port, host });
console.log(`Server listening on http://${host}:${port}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
curl -X POST http://localhost:3000/payments \
-H "Content-Type: application/json" \
-d '{
"payee": "<RECIPIENT_ADDRESS>",
"amount": "0.2",
"invoiceCurrency": "ETH-sepolia-sepolia",
"paymentCurrency": "ETH-sepolia-sepolia"
}'{
"requestId": "011d9f76e07a678b8321ccfaa300efd4d80832652b8bbc07ea4069ca71006210b5",
"paymentReference": "0xe23a6b02059c2b30",
"transactions": [
{
"data": "0xb868980b00000000000000000000000029eab540117632a112ea29ba8be686a1b66467a700000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dead0000000000000000000000000000000000000000000000000000000000000008e23a6b02059c2b30000000000000000000000000000000000000000000000000",
"to": "0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687",
"value": {
"type": "BigNumber",
"hex": "0x02c68af0bb140000"
}
}
],
"metadata": {
"stepsRequired": 1,
"needsApproval": false,
"paymentTransactionIndex": 0
}
}// Add this to src/index.ts
import crypto from "node:crypto";
fastify.post('/webhooks', async (request: FastifyRequest, reply: FastifyReply) => {
let webhookData: Record<string, unknown> = {};
try {
const body = request.body as Record<string, unknown>;
webhookData = body;
const signature = request.headers['x-request-network-signature'] as string;
const webhookSecret = process.env.RN_WEBHOOK_SECRET;
if (!webhookSecret) {
fastify.log.error('RN_WEBHOOK_SECRET is not set');
return reply.status(500).send({ error: 'Webhook secret not configured' });
}
const expectedSignature = crypto.createHmac('sha256', webhookSecret)
.update(JSON.stringify(body))
.digest('hex');
if (signature !== expectedSignature) {
fastify.log.error('Invalid webhook signature');
return reply.status(401).send({ error: 'Invalid signature' });
}
const { requestId, event } = body;
console.log(`Webhook received: ${event} for request ${requestId}`, {
webhookData: body
});
// Log the event
console.log(`Webhook event: ${event}`);
console.log('Full webhook data:', JSON.stringify(body, null, 2));
return reply.send({ code: 200, message: 'Webhook received' });
} catch (error) {
console.error('Webhook error:', {
error,
requestId: webhookData?.requestId,
event: webhookData?.event,
});
return reply.status(500).send({ error: 'Internal server error' });
}
});// .env
RN_API_KEY=<YOUR_API_KEY>
RN_API_URL=https://api.request.network/v2
RN_WEBHOOK_SECRET=<THE_SECRET_WE_JUST_CREATED>Webhook received: payment.confirmed for request req_test123456789abcdef {
webhookData: {
event: 'payment.confirmed',
requestId: 'req_test123456789abcdef',
requestID: 'req_test123456789abcdef',
paymentReference: '0x1234567890abcdef1234567890abcdef12345678',
explorer: 'https://scan.request.network/request/req_test123456789abcdef',
amount: '100.0',
totalAmountPaid: '100.0',
expectedAmount: '100.0',
timestamp: '2025-08-28T12:25:45.995Z',
txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
network: 'ethereum',
currency: 'USDC',
paymentCurrency: 'USDC',
isCryptoToFiat: false,
subStatus: '',
paymentProcessor: 'request-network',
fees: [ [Object] ]
}
}
Webhook event: payment.confirmed
Full webhook data: {
"event": "payment.confirmed",
"requestId": "req_test123456789abcdef",
"requestID": "req_test123456789abcdef",
"paymentReference": "0x1234567890abcdef1234567890abcdef12345678",
"explorer": "https://scan.request.network/request/req_test123456789abcdef",
"amount": "100.0",
"totalAmountPaid": "100.0",
"expectedAmount": "100.0",
"timestamp": "2025-08-28T12:25:45.995Z",
"txHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"network": "ethereum",
"currency": "USDC",
"paymentCurrency": "USDC",
"isCryptoToFiat": false,
"subStatus": "",
"paymentProcessor": "request-network",
"fees": [
{
"type": "network",
"amount": "0.02",
"currency": "ETH"
}
]
}// Update our handler in src/index.ts
fastify.post('/webhooks', async (request: FastifyRequest, reply: FastifyReply) => {
let webhookData: Record<string, unknown> = {};
try {
const body = request.body as Record<string, unknown>;
webhookData = body;
const signature = request.headers['x-request-network-signature'] as string;
const webhookSecret = process.env.RN_WEBHOOK_SECRET;
if (!webhookSecret) {
fastify.log.error('RN_WEBHOOK_SECRET is not set');
return reply.status(500).send({ error: 'Webhook secret not configured' });
}
const expectedSignature = crypto.createHmac('sha256', webhookSecret)
.update(JSON.stringify(body))
.digest('hex');
if (signature !== expectedSignature) {
fastify.log.error('Invalid webhook signature');
return reply.status(401).send({ error: 'Invalid signature' });
}
const { requestId, event } = body;
console.log(`Webhook received: ${event} for request ${requestId}`, {
webhookData: body
});
// Log the event
console.log(`Webhook event: ${event}`);
console.log('Full webhook data:', JSON.stringify(body, null, 2));
switch (event) {
// handling the payment.confirmed event
case "payment.confirmed":
await db.update(payments)
.set({ status: 'confirmed' })
.where(eq(payments.requestId, requestId as string));
break;
}
return reply.send({ code: 200, message: 'Webhook received' });
} catch (error) {
console.error('Webhook error:', {
error,
requestId: webhookData?.requestId,
event: webhookData?.event,
});
return reply.status(500).send({ error: 'Internal server error' });
}
});// src/components/create-payment/index.tsx
import React from 'react';
const CreatePayment: React.FC = () => {
return (
<div>
<h2>Create Payment</h2>
<p>This will be a form to create new payments</p>
</div>
);
};
export default CreatePayment;// src/components/view-payments/index.tsx
import React from 'react';
const ViewPayments: React.FC = () => {
return (
<div>
<h2>View Payments</h2>
<p>This will show all payments from the database</p>
</div>
);
};
export default ViewPayments;// src/App.tsx
import { useState } from 'react'
import './App.css'
import ViewPayments from './components/ViewPayments'
import CreatePayment from './components/CreatePayment'
type TabType = 'view' | 'create';
function App() {
const [activeTab, setActiveTab] = useState<TabType>('view');
return (
<div className="app">
<h1>Request Network Demo</h1>
<div className="tabs">
<button
className={activeTab === 'view' ? 'tab-button active' : 'tab-button'}
onClick={() => setActiveTab('view')}
>
View Payments
</button>
<button
className={activeTab === 'create' ? 'tab-button active' : 'tab-button'}
onClick={() => setActiveTab('create')}
>
Create Payment
</button>
</div>
<div className="tab-content">
{activeTab === 'view' && <ViewPayments />}
{activeTab === 'create' && <CreatePayment />}
</div>
</div>
)
}
export default App
// src/App.css
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
.tab-button {
border-radius: 0px;
padding: 12px 24px;
border: none;
background: none;
cursor: pointer;
font-size: 16px;
border-bottom: 3px solid transparent;
transition: all 0.2s;
}
.tab-button:hover {
background-color: #f5f5f5;
}
.tab-button.active {
border-bottom-color: #11c9a0;
color: #11c9a0;
font-weight: 600;
}
.tab-content {
min-height: 400px;
}
h1 {
text-align: center;
margin-bottom: 40px;
}
// src/config/wagmi.ts
import { createConfig, http } from 'wagmi'
import { sepolia } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'
export const config = createConfig({
chains: [sepolia],
connectors: [
injected(),
],
transports: {
[sepolia.id]: http(),
},
})// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from './config/wagmi'
import App from './App.tsx'
import './index.css'
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</WagmiProvider>
</React.StrictMode>,
)
// src/components/wallet-connect/index.tsx
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import './styles.css';
const WalletConnect: React.FC = () => {
const { address, isConnected } = useAccount()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
if (isConnected) {
return (
<div className="wallet-status">
<div className="wallet-info">
<span>Connected: {address?.slice(0, 6)}...{address?.slice(-4)}</span>
<button onClick={() => disconnect()} className="disconnect-btn">
Disconnect
</button>
</div>
</div>
)
}
return (
<div className="wallet-connect">
<button
onClick={() => connect({ connector: connectors[0] })}
className="connect-btn"
>
Connect Wallet
</button>
</div>
)
}
export default WalletConnect// src/components/wallet-connect/styles.css
.connect-btn {
background: #646cff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.connect-btn:hover {
background: #5145d4;
}
.disconnect-btn {
background: #dc2626;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.disconnect-btn:hover {
background: #b91c1c;
}// src/App.tsx
import { useState } from 'react'
import './App.css'
import ViewPayments from './components/view-payments'
import CreatePayment from './components/create-payment'
import WalletConnect from './components/wallet-connect';
type TabType = 'view' | 'create';
function App() {
const [activeTab, setActiveTab] = useState<TabType>('view');
return (
<div className="app">
<div className='header'>
<h1>Request Network Demo</h1>
<WalletConnect />
</div>
<div className="tabs">
<button
className={activeTab === 'view' ? 'tab-button active' : 'tab-button'}
onClick={() => setActiveTab('view')}
>
View Payments
</button>
<button
className={activeTab === 'create' ? 'tab-button active' : 'tab-button'}
onClick={() => setActiveTab('create')}
>
Create Payment
</button>
</div>
<div className="tab-content">
{activeTab === 'view' && <ViewPayments />}
{activeTab === 'create' && <CreatePayment />}
</div>
</div>
)
}
export default App
// src/App.css, add this class in
.header {
display: flex;
justify-content: space-between;
gap: 32px;
align-items: center;
margin-bottom: 40px;
}// .env
VITE_API_URL=http://localhost:3000// src/components/view-payments/index.tsx
import React, { useState, useEffect } from 'react';
import './styles.css';
interface Payment {
id: number;
requestId: string;
status: string;
}
const ViewPayments: React.FC = () => {
const [payments, setPayments] = useState<Payment[]>([]);
const [isLoading, setIsLoading] = useState(true);
const fetchPayments = async () => {
try {
const response = await fetch(`${import.meta.env.VITE_API_URL}/payments`);
if (response.ok) {
const data = await response.json();
setPayments(data.payments || []);
} else {
console.error('Failed to fetch payments');
}
} catch (error) {
console.error('Error fetching payments:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchPayments();
const interval = setInterval(fetchPayments, 3000);
return () => clearInterval(interval);
}, []);
const getStatusClass = (status: string) => {
switch (status.toLowerCase()) {
case 'pending':
return 'status-pending';
case 'in-progress':
return 'status-in-progress';
case 'confirmed':
return 'status-confirmed';
case 'failed':
return 'status-failed';
default:
return 'status-pending';
}
};
if (isLoading && payments.length === 0) {
return (
<div className="view-payments">
<h2>View Payments</h2>
<div className="loading">Loading payments...</div>
</div>
);
}
return (
<div className="view-payments">
<h2>View Payments</h2>
<div className="payments-container">
{payments.length === 0 ? (
<div className="no-payments">No payments found</div>
) : (
payments.map((payment) => (
<div key={payment.id} className="payment-item">
<span className="payment-id">Payment ID: {payment.id}</span>
<span className={`status-pill ${getStatusClass(payment.status)}`}>
{payment.status}
</span>
</div>
))
)}
</div>
</div>
);
};
export default ViewPayments;// src/components/view-payments/styles.css
.view-payments {
max-width: 600px;
margin: 0 auto;
}
.payments-container {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
}
.payment-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border: 1px solid #4a4a4a;
border-radius: 8px;
background-color: #2a2a2a;
transition: background-color 0.2s;
}
.payment-item:hover {
background-color: #333333;
}
.payment-id {
font-weight: 500;
color: #e5e7eb;
}
.status-pill {
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid transparent;
}
.status-pending {
background-color: #451a03;
color: #fcd34d;
border-color: #92400e;
}
.status-in-progress {
background-color: #1e3a8a;
color: #93c5fd;
border-color: #3b82f6;
}
.status-confirmed {
background-color: #064e3b;
color: #6ee7b7;
border-color: #10b981;
}
.status-failed {
background-color: #7f1d1d;
color: #fca5a5;
border-color: #ef4444;
}
.no-payments {
text-align: center;
color: #9ca3af;
font-style: italic;
padding: 40px 20px;
background-color: #1f1f1f;
border-radius: 8px;
border: 1px solid #4a4a4a;
}
.loading {
text-align: center;
color: #9ca3af;
padding: 20px;
background-color: #1f1f1f;
border-radius: 8px;
border: 1px solid #4a4a4a;
}// src/components/create-payment/index.tsx
import React, { useState } from 'react';
import { useSendTransaction, useAccount } from 'wagmi';
import './styles.css';
interface PaymentForm {
payee: string;
amount: string;
invoiceCurrency: string;
paymentCurrency: string;
}
const CreatePayment: React.FC = () => {
const [formData, setFormData] = useState<PaymentForm>({
payee: '',
amount: '',
invoiceCurrency: 'ETH-sepolia-sepolia',
paymentCurrency: 'ETH-sepolia-sepolia'
});
const [isExecuting, setIsExecuting] = useState(false);
const { sendTransactionAsync } = useSendTransaction();
const { isConnected } = useAccount();
const currencyOptions = [
{ value: 'ETH-sepolia-sepolia', label: 'ETH (Sepolia)' },
{ value: 'FAU-sepolia', label: 'FAU (Sepolia)' },
{ value: 'fUSDC-sepolia', label: 'fUSDC (Sepolia)' }
];
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const updatePaymentStatus = async (paymentId: number, status: string) => {
try {
const response = await fetch(`${import.meta.env.VITE_API_URL}/payments/${paymentId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status }),
});
if (!response.ok) {
console.error('Failed to update payment status');
} else {
console.log(`Payment ${paymentId} status updated to: ${status}`);
}
} catch (error) {
console.error('Error updating payment status:', error);
}
};
const executeTransactions = async (transactions: Array<{ to: string; data: string; value: { hex: string } }>, paymentId: number) => {
if (!isConnected) {
alert('Please connect your wallet first');
return;
}
try {
for (let i = 0; i < transactions.length; i++) {
const tx = transactions[i];
console.log(`Executing transaction ${i + 1}/${transactions.length}:`, tx);
const txHash = await sendTransactionAsync({
to: tx.to as `0x${string}`,
data: tx.data as `0x${string}`,
value: BigInt(tx.value.hex)
});
// As soon as we start sending transactions, update status to 'in-progress'
await updatePaymentStatus(paymentId, 'in-progress');
console.log(`Transaction ${i + 1} sent with hash:`, txHash);
}
alert('All transactions executed successfully!');
} catch (error) {
console.error('Transaction execution failed:', error);
alert(`Transaction failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
await updatePaymentStatus(paymentId, 'failed');
throw error;
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsExecuting(true);
try {
const response = await fetch(`${import.meta.env.VITE_API_URL}/payments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to create payment');
}
const data = await response.json();
console.log('Backend response:', data);
if (data.calldata && data.calldata.transactions) {
await executeTransactions(data.calldata.transactions, data.payment.id);
} else {
throw new Error('No transaction data received from backend');
}
} catch (error) {
console.error('Error in payment flow:', error);
alert(`Payment failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsExecuting(false);
}
};
return (
<div className="create-payment">
<h2>Create Payment</h2>
<form onSubmit={handleSubmit} className="payment-form">
<div className="form-group">
<label htmlFor="payee">Payee Address</label>
<input
type="text"
id="payee"
name="payee"
value={formData.payee}
onChange={handleInputChange}
placeholder="0x..."
required
/>
</div>
<div className="form-group">
<label htmlFor="amount">Payment Amount</label>
<input
type="number"
id="amount"
name="amount"
value={formData.amount}
onChange={handleInputChange}
placeholder="0.00"
step="0.000001"
min="0"
required
/>
</div>
<div className="form-group">
<label htmlFor="invoiceCurrency">Invoice Currency</label>
<select
id="invoiceCurrency"
name="invoiceCurrency"
value={formData.invoiceCurrency}
onChange={handleInputChange}
required
>
{currencyOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="paymentCurrency">Payment Currency</label>
<select
id="paymentCurrency"
name="paymentCurrency"
value={formData.paymentCurrency}
onChange={handleInputChange}
required
>
{currencyOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<button
type="submit"
className="submit-btn"
disabled={isExecuting || !isConnected}
>
{isExecuting ? 'Processing...' : 'Create & Execute Payment'}
</button>
{!isConnected && (
<p style={{ color: '#dc2626', fontSize: '14px', marginTop: '10px' }}>
Please connect your wallet to create payments
</p>
)}
</form>
</div>
);
};
export default CreatePayment;// src/components/create-payment/index.tsx
.create-payment {
max-width: 500px;
margin: 0 auto;
}
.payment-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-weight: 600;
color: #374151;
font-size: 14px;
}
.form-group input,
.form-group select {
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.2s;
background-color: inherit;
color: inherit;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #11c9a0;
box-shadow: 0 0 0 3px rgba(100, 108, 255, 0.1);
}
.form-group select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 12px center;
background-repeat: no-repeat;
background-size: 16px;
padding-right: 40px;
}
.form-group select option {
background-color: #1a1a1a;
color: #ffffff;
padding: 8px 12px;
}
.form-group select option:hover {
background-color: #11c9a0;
}
.submit-btn {
background: #11c9a0;
color: white;
border: none;
padding: 14px 24px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 10px;
}
.submit-btn:hover {
background: #5145d4;
}
.submit-btn:active {
background: #4338ca;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
appearance: textfield;
-moz-appearance: textfield;
}


















Create a new payment request
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
The wallet address of the payer
The wallet address of the payee. Required for all requests except crypto-to-fiat
The payable amount of the invoice, in human readable format
Invoice Currency ID, from the Request Network Token List e.g: USD
Payment currency ID, from the Request Network Token List e.g: ETH-sepolia-sepolia
Whether crypto-to-fiat payment is available for this request
Merchant reference for receipt tracking and identification
ID of the original request for recurring payments
Payment reference of the original request for recurring payments
Get the status of a payment request
The requestId for the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdbAPI key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
Request status retrieved successfully
Request not found
Too Many Requests
Update a recurring request
The requestId for the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdbAPI key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
Recurrence updated successfully
No content
Request not found
Too Many Requests
No content
Get the calldata needed to pay a request. For same-chain payments, returns transaction calldata that can be directly executed. For crosschain payments (when chain and token parameters are provided and differ from the request's native chain), returns a payment intent that needs to be signed and processed through the crosschain bridge. For off-ramp payments, use the query parameters clientUserId and paymentDetailsId. Note: Crosschain requests with an expectedAmount less than 1 are rejected.
The requestId of the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdbThe wallet address of the payer.
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7The source chain of the crosschain payment
The source token of the crosschain payment
The amount to pay, in human readable format
Optional client user ID for off-ramp payments
user-123Optional payment details ID for off-ramp payments
fa898aec-519c-46be-9b4c-e76ef4ff99d9Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)
0.02Address to receive the fee
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7Initiate a payment without having to create a request first. Supports both one-time and recurring payments. For recurring payments, specify the recurrence object with start date, frequency, total executions, and payer address. The system will create a recurring payment schedule and return the necessary transactions for allowance approval and signature submission. Optionally includes customer information (firstName, lastName, email, address) and a merchant reference field for checkout widget implementations and receipt tracking.
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
The wallet address of the payee
The payable amount of the invoice, in human readable format
Invoice Currency ID, from the Request Network Token List e.g: USD
Payment currency ID, from the Request Network Token List e.g: ETH-sepolia-sepolia
Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)
Address to receive the fee
The wallet address of the payer, use to check if payer approval exists
Merchant reference for receipt tracking and identification
Get a list of all available tokens, or filter by network, symbol, or id.
The network of the token(s)
mainnetThe symbol of the token
USDCWhether to return only the first token. can only be used when both network and symbol are provided.
trueThe Request Network id of the token
USDC-mainnetAPI key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
List of tokens retrieved successfully
Validation failed
Token not found
Too Many Requests
Get a list of currency objects (with all details) that can be converted to from the specified currency. Optionally filter by network using the 'network' query parameter.
The network of the token to filter by
A comma-separated list of networks to filter by (e.g., sepolia,mainnet,polygon)
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
Conversion routes retrieved successfully
Currency not found
Too Many Requests
Get available payment routes for a request. This endpoint analyzes the payer's wallet balance across supported chains and returns possible payment methods. Routes include direct same-chain payments and crosschain bridging options when the payer has sufficient balance on different chains than the request's native chain.
The requestId of the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdbThe wallet address of the payer
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7The amount to pay, in human readable format
Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)
Address to receive the fee
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
Get the calldata needed to pay a request. For same-chain payments, returns transaction calldata that can be directly executed. For crosschain payments (when chain and token parameters are provided and differ from the request's native chain), returns a payment intent that needs to be signed and processed through the crosschain bridge. For off-ramp payments, use the query parameters clientUserId and paymentDetailsId. Note: Crosschain requests with an expectedAmount less than 1 are rejected.
The requestId of the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdbThe wallet address of the payer.
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7The source chain of the crosschain payment
The source token of the crosschain payment
The amount to pay, in human readable format
Optional client user ID for off-ramp payments
user-123Optional payment details ID for off-ramp payments
fa898aec-519c-46be-9b4c-e76ef4ff99d9Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)
0.02Address to receive the fee
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7Send a payment intent
The payment intent ID
01JNZYZPK7B4YBPD44TM72NDNJAPI key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
Initiate a payment without having to create a request first. Supports both one-time and recurring payments. For recurring payments, specify the recurrence object with start date, frequency, total executions, and payer address. The system will create a recurring payment schedule and return the necessary transactions for allowance approval and signature submission. Optionally includes customer information (firstName, lastName, email, address) and a merchant reference field for checkout widget implementations and receipt tracking.
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
The wallet address of the payee
The payable amount of the invoice, in human readable format
Invoice Currency ID, from the Request Network Token List e.g: USD
Payment currency ID, from the Request Network Token List e.g: ETH-sepolia-sepolia
Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)
Address to receive the fee
The wallet address of the payer, use to check if payer approval exists
Merchant reference for receipt tracking and identification
Submit a signature for a recurring payment permit to activate the recurring payment schedule. This endpoint is called after creating a recurring payment and obtaining the permit data. The signature authorizes the recurring payment contract to execute payments on behalf of the payer according to the schedule. Once activated, payments will be executed automatically at the specified intervals.
The ID of the recurring payment
01JXYJKCAHGFTDR15F2D072ESGAPI key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
The signature of the recurring payment permit.
Retrieve the current status and execution details of a recurring payment. Returns information about executed payments, remaining executions, next payment date, and overall status. This endpoint is useful for monitoring recurring payment progress and checking if payments are being executed as expected. Note: Customer information (PII) is not included in the response for security reasons.
The ID of the recurring payment
01JXYJKCAHGFTDR15F2D072ESGAPI key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
Recurring payment status retrieved successfully
Recurring payment not found
Too Many Requests
Update a recurring payment by cancelling it or unpausing it. When cancelling, optionally returns a transaction to decrease allowance. When unpausing, resumes execution of a paused recurring payment.
The ID of the recurring payment
01JXYJKCAHGFTDR15F2D072ESGAPI key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
The action to perform on the recurring payment
Recurring payment updated successfully
Bad request
Recurring payment not found
Too Many Requests
Pays multiple payment requests in one transaction by either creating new requests or using existing request IDs. All requests must be on the same network. Supports mixed ERC20, Native, and conversion requests.
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
The request IDs of the existing requests to be paid. Requests must be on the same network. Either requests or requestIds must be provided, but not both.
The wallet address of the payer, user to check if approval is needed or not.
Checks compliance status and returns necessary URLs for completing compliance.
API key for authentication
Client User ID
First Name
Last Name
Company Name
Date of birth in YYYY-MM-DD format
^\d{4}-\d{2}-\d{2}$Address Line 1
Address Line 2
City
State
Postcode
Country
Nationality
Phone in E.164 format
^\+?[1-9]\d{1,14}$Social Security Number
Source of Funds
Business Activity
Retrieves the comprehensive compliance status for a specific user, including KYC and agreement status.
The client user ID to check compliance status for
user-123API key for authentication
Compliance status retrieved successfully
Unauthorized
User not found
Too Many Requests
Update the agreement completion status for a user.
The client user ID to update
user-123API key for authentication
Compliance status updated successfully
Invalid request data
Unauthorized
User not found
Too Many Requests
Create payment details for a user
The client user ID
user-123API key for authentication
Name of the bank
Name of the account holder
Bank account number
Bank routing number (US)
Type of beneficiary
Three-letter currency code (ISO 4217)
Primary address line
Secondary address line
City name
State or province code
Two-letter country code (ISO 3166-1 alpha-2)
Date of birth in YYYY-MM-DD format
^\d{4}-\d{2}-\d{2}$Postal or ZIP code
Payment rail type
localPossible values: UK bank sort code
International Bank Account Number
SWIFT/BIC code
Government-issued ID number
Type of government-issued ID (e.g., passport, driver's license)
Type of bank account
French RIB number
Australian BSB number
New Zealand NCC number
Bank branch code
Bank code
Indian Financial System Code
Retrieves the registered bank account details for a user. Optionally filter by payment details ID.
The client user ID to get payment details for
user-123Optional ID of specific payment details to retrieve
fa898aec-519c-46be-9b4c-e76ef4ff99d9API key for authentication
Payment details retrieved successfully
Unauthorized
User or payment details not found
Too Many Requests
Create a new payment request
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
The wallet address of the payer
The wallet address of the payee. Required for all requests except crypto-to-fiat
The payable amount of the invoice, in human readable format
Invoice Currency ID, from the Request Network Token List e.g: USD
Payment currency ID, from the Request Network Token List e.g: ETH-sepolia-sepolia
Whether crypto-to-fiat payment is available for this request
Merchant reference for receipt tracking and identification
ID of the original request for recurring payments
Payment reference of the original request for recurring payments
Request created successfully
Validation failed
Wallet not found
Too Many Requests
Batch payment calldata retrieved successfully
Requests must be on the same network
Too Many Requests
Available payment routes
Invalid or missing wallet address
Request not found
Too Many Requests
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
Payment calldata retrieved successfully
Validation failed
Request not found
Too Many Requests
Payment intent sent successfully
No content
Payment intent data not found
Too Many Requests
No content
Request created and payment initiated successfully
Wallet not found
Too Many Requests
Recurring payment signature submitted successfully
Bad request
Recurring payment not found
Too Many Requests
Compliance data retrieved successfully
Invalid request data
Unauthorized
Request not found
Too Many Requests
Payment details created successfully
Invalid request data
Unauthorized
User not found
Too Many Requests
POST /v2/payouts/batch HTTP/1.1
Host: api.request.network
Content-Type: application/json
Accept: */*
Content-Length: 747
{
"requests": [
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "2",
"invoiceCurrency": "FAU-sepolia",
"paymentCurrency": "FAU-sepolia"
},
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "2",
"invoiceCurrency": "fUSDC-sepolia",
"paymentCurrency": "fUSDC-sepolia"
},
{
"payee": "0xb07D2398d2004378cad234DA0EF14f1c94A530e4",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "FAU-sepolia"
},
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "0.00001",
"invoiceCurrency": "ETH-sepolia-sepolia",
"paymentCurrency": "ETH-sepolia-sepolia"
},
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia"
}
],
"payer": "0x2e2E5C79F571ef1658d4C2d3684a1FE97DD30570"
}{
"ERC20ApprovalTransactions": [
{
"data": "0x095ea7b3...",
"to": "0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C",
"value": 0
}
],
"batchPaymentTransaction": {
"data": "0x92cddb91...",
"to": "0x67818703c92580c0e106e401F253E8A410A66f8B",
"value": {
"type": "BigNumber",
"hex": "0x0d83b3d1afc58b"
}
}
}GET /v2/request/{requestId}/routes?wallet=0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7 HTTP/1.1
Host: api.request.network
Accept: */*
{
"routes": [
{
"id": "REQUEST_NETWORK_PAYMENT",
"fee": 0,
"speed": "FAST",
"price_impact": 0,
"chain": "MAINNET",
"token": "REQ"
}
]
}POST /v2/request/payment-intents/{paymentIntentId} HTTP/1.1
Host: api.request.network
Content-Type: application/json
Accept: */*
Content-Length: 154
{
"signedPaymentIntent": {
"signature": "text",
"nonce": "text",
"deadline": "text"
},
"signedApprovalPermit": {
"signature": "text",
"nonce": "text",
"deadline": "text"
}
}GET /v2/request/{requestId}/pay HTTP/1.1
Host: api.request.network
Accept: */*
{
"transactions": [
{
"data": "0xb868980b...00",
"to": "0x11BF2fDA23bF0A98365e1A4e04A87C9339e8687",
"value": {
"type": "BigNumber",
"hex": "0x038d7ea4c68000"
}
}
],
"metadata": {
"stepsRequired": 1,
"needsApproval": false,
"approvalTransactionIndex": null,
"hasEnoughBalance": true,
"hasEnoughGas": true
}
}{
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb",
"paymentReference": "0xb3581f0b0f74cc61",
"transactions": [
{
"data": "0xb868980b...00",
"to": "0x11BF2fDA23bF0A98365e1A4e04A87C9339e8687",
"value": {
"type": "BigNumber",
"hex": "0x038d7ea4c68000"
}
}
],
"metadata": {
"stepsRequired": 1,
"needsApproval": false,
"approvalTransactionIndex": null,
"hasEnoughBalance": true,
"hasEnoughGas": true
}
}POST /v2/payouts/recurring/{id} HTTP/1.1
Host: api.request.network
Content-Type: application/json
Accept: */*
Content-Length: 122
{
"permitSignature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1b"
}{
"message": "Recurring payment activated successfully",
"id": "01JXYJKCAHGFTDR15F2D072ESG",
"status": "active"
}{
"processedPayments": 3,
"totalPayments": 30,
"lastPaymentDate": "2025-01-04T10:00:00.000Z",
"nextPaymentDate": "2025-01-05T10:00:00.000Z",
"status": "active",
"requests": [
{
"paymentReference": "0xb3581f0b0f74cc61",
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb"
}
],
"payments": [
{
"id": "01JXYJKCAHGFTDR15F2D072ESG",
"amount": "10",
"timestamp": "2025-01-04T10:00:00.000Z",
"txHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
]
}PATCH /v2/payouts/recurring/{id} HTTP/1.1
Host: api.request.network
Content-Type: application/json
Accept: */*
Content-Length: 19
{
"action": "cancel"
}{
"id": "01JXYJKCAHGFTDR15F2D072ESG",
"status": "cancelled",
"transactions": [
{
"to": "0xA0b86a33E6441b8c4C8C8C8C8C8C8C8C8C8C8C8",
"data": "0x095ea7b30000000000000000000000000363dD3ccD4f187d7033c57354CA81f998451D590000000000000000000000000000000000000000000000000000000000000000",
"value": "0x0"
}
],
"metadata": {
"remainingPayments": 5,
"remainingAmount": "5000000000000000000",
"processedPayments": 3,
"totalPayments": 8
}
}POST /v2/payouts HTTP/1.1
Host: api.request.network
Content-Type: application/json
Accept: */*
Content-Length: 213
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia",
"feePercentage": "0.02",
"feeAddress": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7"
}GET /v2/payouts/recurring/{id} HTTP/1.1
Host: api.request.network
Accept: */*
POST /v2/payer HTTP/1.1
Host: api.request.network
x-api-key: text
Content-Type: application/json
Accept: */*
Content-Length: 312
{
"clientUserId": "user-123",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"beneficiaryType": "individual",
"dateOfBirth": "1985-12-12",
"addressLine1": "123 Main Street",
"city": "New York",
"state": "NY",
"postcode": "10001",
"country": "US",
"nationality": "US",
"phone": "+12125551234",
"ssn": "123-45-6789"
}{
"agreementUrl": "https://core-api.pay.so/v1/public/agreements?email=john.doe%40example.com",
"kycUrl": "https://sumsub.com/idensic/l/#/sbx_VvK9E9P2A23xQPoA",
"status": {
"agreementStatus": "not_started",
"kycStatus": "not_started"
}
}{
"kycStatus": "completed",
"agreementStatus": "completed",
"isCompliant": true,
"userId": "a25a4274-8f50-4579-b476-8f35b297d4ad"
}{
"success": true
}POST /v2/payer/{clientUserId}/payment-details HTTP/1.1
Host: api.request.network
x-api-key: text
Content-Type: application/json
Accept: */*
Content-Length: 316
{
"bankName": "Chase",
"accountName": "Gordon's Chase Business Account",
"accountNumber": "253009233489",
"routingNumber": "026013356",
"beneficiaryType": "business",
"currency": "usd",
"addressLine1": "24 Theatre St.",
"city": "Paramount",
"state": "CA",
"postalCode": "90723",
"country": "US",
"dateOfBirth": "1985-12-12",
"rails": "local"
}{
"payment_detail": {
"id": "pd_123456",
"clientUserId": "user-123",
"bankName": "Chase",
"accountName": "Gordon's Chase Business Account",
"currency": "usd",
"beneficiaryType": "business"
}
}{
"paymentDetails": [
{
"id": "fa898aec-519c-46be-9b4c-e76ef4ff99d9",
"userId": "a25a4274-8f50-4579-b476-8f35b297d4ad",
"bankName": "Chase",
"accountName": "Gordon's Chase Business Account",
"beneficiaryType": "business",
"accountNumber": "253009233489",
"routingNumber": "026013356",
"currency": "usd",
"status": "approved",
"rails": "local"
}
]
}GET /v2/payer/{clientUserId} HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
PATCH /v2/payer/{clientUserId} HTTP/1.1
Host: api.request.network
x-api-key: text
Content-Type: application/json
Accept: */*
Content-Length: 27
{
"agreementCompleted": true
}GET /v2/payer/{clientUserId}/payment-details HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
POST /v2/request HTTP/1.1
Host: api.request.network
Content-Type: application/json
Accept: */*
Content-Length: 224
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia",
"isCryptoToFiatAvailable": false,
"recurrence": {
"startDate": "2030-01-01",
"frequency": "DAILY"
}
}{
"paymentReference": "0xb3581f0b0f74cc61",
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb"
}[
{
"id": "USDC-mainnet",
"name": "USD Coin",
"symbol": "USDC",
"decimals": 6,
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"network": "mainnet",
"type": "ERC20",
"hash": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"chainId": 1
},
{
"id": "USDT-mainnet",
"name": "Tether USD",
"symbol": "USDT",
"decimals": 6,
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"network": "mainnet",
"type": "ERC20",
"hash": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"chainId": 1
}
]{
"currencyId": "USD",
"network": "mainnet",
"conversionRoutes": [
{
"id": "USDT-mainnet",
"name": "Tether USD",
"symbol": "USDT",
"decimals": 6,
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"network": "mainnet",
"type": "ERC20",
"hash": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"chainId": 1
},
{
"id": "ETH-mainnet",
"name": "Ether",
"symbol": "ETH",
"decimals": 18,
"address": "0xf5af88e117747e87fc5929f2ff87221b1447652e",
"network": "mainnet",
"type": "ETH",
"hash": "0xf5af88e117747e87fc5929f2ff87221b1447652e",
"chainId": 1
}
]
}GET /v2/currencies HTTP/1.1
Host: api.request.network
Accept: */*
GET /v1/currencies/{currencyId}/conversion-routes HTTP/1.1
Host: api.request.network
Accept: */*
Request created successfully
Validation failed
Wallet not found
Too Many Requests
API key for authentication (optional if using Client ID)
Client ID for frontend authentication (optional if using API key)
Origin header (required for Client ID auth, automatically set by browser)
Payment calldata retrieved successfully
Validation failed
Request not found
Too Many Requests
Request created and payment initiated successfully
Wallet not found
Too Many Requests
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xe72Ecea44b6d8B2b3cf5171214D9730E86213cA2
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x3b4837C9F4A606b71e61FD56Db6241781194df92
0xcE80D17d38cfee8E5E6c682F7712bfb5A04Ae912
0xD5933C74414ce80D9d7082cc89FBAdcfF4751fAF
0xC5519f3fcECC8EC85caaF8836563dEe9a00080f9
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xaD61121DAfAAe495095Cd466022b519Cb7503a4E
0xc861aE0Cd70b73b0C8F1D62Fa669E6D1d7
D7e0aB
0x05e94CDdd14E0b18317AE21BAFAEC24156BdB7C9
0xB5E53C3d145Cbaa61C7028736A1fF0bC6817A4c5
0xf8cACE7EE4c03Eb4f225434B0709527938D365b4
0x7c285b9F2dA5E2c10feA25C00Ce1aCB107F85475
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0x1550A8C4F4E5afC67Ea07e8ac590fdcAdB4bBfb1
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x80D1EE67ffAf7047d3E6EbF7317cF0eAd63FFc78
N/A
0xf8cACE7EE4c03Eb4f225434B0709527938D365b4
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xA5186dec7dC1ec85B42A3cd2Dc8289e248530B07
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x78384dB9674109A3Edf9F2814eFF4B6fc25D816A
0x8296D56321cf207925a7804E5A8E3F579838e6Ad
0xEdfD8386d5DE52072B4Ad8dC69BBD0bB89f9A1fb
0xFbBd0854048a8A75a8823c230e673F8331140483
N/A
0xAdc0001eA67Ab36D5321612c6b500572704fFF20
0x3dF89c727eaDF67eeD7b4d09EC4F2b41f8Dec2ca
N/A
N/A
N/A
N/A
N/A
0x9Fd503e723e5EfcCde3183632b443fFF49E68715
N/A
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
0x3E3B04e1bF170522a5c5DDE628C4d365c0342239
0x3b4837C9F4A606b71e61FD56Db6241781194df92
N/A
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x05D782aD6D6556179A6387Ff1D2fA104FD5c515a
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x3b4837C9F4A606b71e61FD56Db6241781194df92
0xd6C04C5d0e561D94B15622e770045776D4ce3739
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xbbd9c5D112343A4Aa2bc194245760CaeeaF118Be
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
N/A
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
N/A
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
N/A
0xEEc4790306C43DC00cebbE4D0c36Fadf8634B533
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
0x8d996a0591a0F9eB65301592C88303e07Ec481db
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
N/A
N/A
N/A
N/A
N/A
N/A
N/A
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
N/A
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xA5186dec7dC1ec85B42A3cd2Dc8289e248530B07
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
N/A
N/A
N/A
N/A
N/A
N/A
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0x1550A8C4F4E5afC67Ea07e8ac590fdcAdB4bBfb1
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x80D1EE67ffAf7047d3E6EbF7317cF0eAd63FFc78
N/A
N/A
N/A
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
0xf8cACE7EE4c03Eb4f225434B0709527938D365b4
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C
0x5f821c20947ff9be22e823edc5b3c709b33121b3
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
0x7DfD5955a1Ed6Bf74ccF8e24FF53E0a9A7e9F477
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a
0x67818703c92580c0e106e401F253E8A410A66f8B
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
N/A
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814
0xc31323ea7513799e1e112Dc15a05d5b600Cc357e
0xd9C3889eB8DA6ce449bfFE3cd194d08A436e96f2
0x090D3583e3f5953e2CC758b146f4Ae11f8224ad7
N/A
0x1aF3f22685AcdDc788B3730ed415912d8f654420
0x6e28Cc56C2E64c9250f39Cb134686C87dB196532
N/A
0xE9A708db0D30409e39810C44cA240fd15cdA9b1a
N/A
N/A
0x0C41700ee1B363DB2ebC1a985f65cAf6eC4b1023
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
0x937Db37ffb67083242fbC6AdD472146bF10E01ec
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
N/A
N/A
N/A
N/A
0x2171a0dc12a9E5b1659feF2BB20E54c84Fa7dB0C
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x612cF8a29A9c8965a5fE512b7463165861c07EAa
N/A
N/A
N/A
N/A
N/A
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
N/A
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
N/A
N/A
N/A
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
N/A
0x02561967c48e87cfB079763F3BEf6424A5A166A7
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0xee07ef5B414955188d2A9fF50bdCE784A49031Fc
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
0x4BA012eae4d64da79Bd6bcdBa366803fCe701A4C
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
N/A
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0xAe23992483FeDA6E718a808Ce824f6864F13B64B
N/A
0xe9cbD1Aa5496628F4302426693Ad63006C56959F
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
N/A
N/A
pay.reqnetwork.near
N/A
N/A
N/A
N/A
N/A
pay.reqnetwork.testnet
N/A
N/A
N/A
N/A
N/A
0x268C146Afb4790902Ee26A6D2d3aff968623Ec80
0x2256938E8225a998C498bf86B43c1768EE14b90B
0x899ddc13d5DBc252916ba2D70928518f3C836ba1
0xd6c085A4D14e9e171f4aF58F7F48bd81173f167E
0xa9cEaA10c12dcB33BAbC2D779e37732311504652
0x24a66afda3666fb0202f439708ece45c8121a9bb
DaiBasedREQBurner
0x2CFa65DcB34311293c6a52F1D7BEB8f4E31E5117
RequestToken
0x8f8221afbb33998d8584a2b05749ba73c37a938a
Burner (Deprecated)
0x7b3C4D90e8AF6030d66c07F8F815f9505E379d6F
lockForREQBurn
0x2171a0dc12a9E5b1659feF2BB20E54c84Fa7dB0C
POST /v2/request HTTP/1.1
Host: api.request.network
Content-Type: application/json
Accept: */*
Content-Length: 224
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia",
"isCryptoToFiatAvailable": false,
"recurrence": {
"startDate": "2030-01-01",
"frequency": "DAILY"
}
}{
"paymentReference": "0xb3581f0b0f74cc61",
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb"
}{
"hasBeenPaid": true,
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb",
"isListening": false,
"txHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}GET /v2/request/{requestId}/pay HTTP/1.1
Host: api.request.network
Accept: */*
{
"transactions": [
{
"data": "0xb868980b...00",
"to": "0x11BF2fDA23bF0A98365e1A4e04A87C9339e8687",
"value": {
"type": "BigNumber",
"hex": "0x038d7ea4c68000"
}
}
],
"metadata": {
"stepsRequired": 1,
"needsApproval": false,
"approvalTransactionIndex": null,
"hasEnoughBalance": true,
"hasEnoughGas": true
}
}POST /v2/payouts HTTP/1.1
Host: api.request.network
Content-Type: application/json
Accept: */*
Content-Length: 213
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia",
"feePercentage": "0.02",
"feeAddress": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7"
}GET /v2/request/{requestId} HTTP/1.1
Host: api.request.network
Accept: */*
PATCH /v2/request/{requestId} HTTP/1.1
Host: api.request.network
Accept: */*
{
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb",
"paymentReference": "0xb3581f0b0f74cc61",
"transactions": [
{
"data": "0xb868980b...00",
"to": "0x11BF2fDA23bF0A98365e1A4e04A87C9339e8687",
"value": {
"type": "BigNumber",
"hex": "0x038d7ea4c68000"
}
}
],
"metadata": {
"stepsRequired": 1,
"needsApproval": false,
"approvalTransactionIndex": null,
"hasEnoughBalance": true,
"hasEnoughGas": true
}
}