Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Web Components for integrating Request Network. Usable in any framework.
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...
Request Network is a protocol for creating payment requests and reconciling payments.
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.
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 50+ different crypto wallets.
Real-time Updates: The app receives webhooks from the Request Network API to update the invoice status in real-time.
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.
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.
Fee Collection: When paying a request, you can specify a fee percentage (between 0 and 100) and a fee address, which will add the fee on top of the payment amount - meaning the payer will pay the original amount plus the fee percentage, with the fee portion being sent to the specified fee address.
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.
For detailed information on all available endpoints and their parameters, please refer to the full Request Network API Reference
The following diagram illustrates the typical flow for creating and paying requests using the Request Network API:
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.
Crosschain payments are supported on the following blockchain networks:
Base
Optimism
Arbitrum
Ethereum
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 Supported Stablecoins and Supported Networks.
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 POST /v2/request endpoint.
To display a list of possible routes for a given request and payer address, use the GET /v2/request/{requestId}/routes 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 not included in the total fee shown for the route.
Service Fees:
The total fees also include any service fees charged by the crosschain infrastructure for facilitating transfers or swaps between different blockchains.
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 GET /v2/request/{requestId}/pay endpoint returns an unsigned payment intent. It will also return an unsigned approval permit or unsigned approval calldata, depending on whether the paymentCurrency
supports EIP-2612 Permit. 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 GET /v2/request/{requestId}/pay 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 GET /v2/request/{requestId}/pay 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 EIP-2612 Permit, indicated by the metadata
response parameter.
"metadata": {
"supportsEIP2612": true
}
If the token does not support EIP-2612 Permit, the payer must sign and submit a standard ERC20 approval transaction.
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,
};
Finally, the signed payment intent (and possibly the signed approval permit) are sent back to execute the crosschain payment via the POST /v2/request/payment-intents/{paymentIntentId} 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.
Process multiple payment requests or payouts efficiently in a single blockchain transaction
Batch payments enable you to process multiple payment requests efficiently in a single blockchain transaction, reducing gas costs and simplifying multi-recipient workflows.
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.
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)
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.
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 Request Tech offramp infrastructure. This requires prerequisite compliance (KYC/Agreement) and bank account registration (payment detail) flows.
EasyInvoice includes a reference implementation for this flow.
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
clientUserId
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:
GET /v2/payer/{clientUserId}
PATCH /v2/payer/{clientUserId}
POST /v2/payer/{clientUserId}/payment-details
GET /v2/payer/{clientUserId}/payment-details
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 subStatus indicating the current offramp stage.
Fiat Delivered: When the offramp is complete, the platform receives a final webhook (payment.processing with subStatus: fiat_sent), and then a payment.confirmed 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
payment.confirmed
Payment fully settled (fiat delivered)
An app for managing Request Network API keys and webhooks.
The Request Network API Portal provides app developers with a platform to securely manage their API keys and webhook endpoints.
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:
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 });
});
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.
The typical lifecycle of a request is as follows:
The payer or payee signs the request which contains the payee, payer, currency, amount, payment details, and arbitrary content data.
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.
An explorer app for viewing requests, payments, and addresses in Request Network.
Request Scan is an explorer for viewing requests and payments in Request Network. It enables users to explore and scrutinize requests, payments, and addresses within the Request Network ecosystem.
Request Scan caters to a broad audience:
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.
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
Incremental: Migrate endpoints one by one (recommended)
Full Migration: Switch all endpoints at once
Parallel: Run V1 and V2 side by side
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
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
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 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.
These packages offer pre-built components for quickly integrating certain Request Network features.
The Request Client library can be imported as ES6 or CommonJS modules.
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.
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.
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.
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 Request
s 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.
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.
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 Request
s 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:
Try it out
View Source
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 to learn more.
Important: V2 is designed to coexist with V1. You can migrate incrementally and don't need to migrate all endpoints at once. See the for documentation of the V1 endpoints.
Migration Support: with our team for migration assistance
For an explanation about what each smart contract does, see
For a list of internal SDK packages, see .
These packages are published publicly but contain functions that are considered internal to the . It is less likely that a Builder would need to use these packages.
Very similar to wagmi, but without using hooks. Construct your own WalletClient
object.
Ethers.js Adapters copied from
All of the following examples can be found in this repository
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 D
const createResponse = await fetch('/v2/request', {
method: 'POST',
body: JSON.stringify(requestData)
});
const { requestId, paymentReference } = await createResponse.json();
// Note: requestId with lowercase d
try {
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);
}
0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C
0x5f821c20947ff9be22e823edc5b3c709b33121b3
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
0x7DfD5955a1Ed6Bf74ccF8e24FF53E0a9A7e9F477
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xe72Ecea44b6d8B2b3cf5171214D9730E86213cA2
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x3b4837C9F4A606b71e61FD56Db6241781194df92
0xcE80D17d38cfee8E5E6c682F7712bfb5A04Ae912
0xD5933C74414ce80D9d7082cc89FBAdcfF4751fAF
0xC5519f3fcECC8EC85caaF8836563dEe9a00080f9
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a
0x67818703c92580c0e106e401F253E8A410A66f8B
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xaD61121DAfAAe495095Cd466022b519Cb7503a4E
0xc861aE0Cd70b73b0C8F1D62Fa669E6D1d7
D7e0aB
0x05e94CDdd14E0b18317AE21BAFAEC24156BdB7C9
0xB5E53C3d145Cbaa61C7028736A1fF0bC6817A4c5
0xf8cACE7EE4c03Eb4f225434B0709527938D365b4
0x7c285b9F2dA5E2c10feA25C00Ce1aCB107F85475
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
N/A
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0x1550A8C4F4E5afC67Ea07e8ac590fdcAdB4bBfb1
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x80D1EE67ffAf7047d3E6EbF7317cF0eAd63FFc78
N/A
0xf8cACE7EE4c03Eb4f225434B0709527938D365b4
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xA5186dec7dC1ec85B42A3cd2Dc8289e248530B07
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814
0xc31323ea7513799e1e112Dc15a05d5b600Cc357e
0xd9C3889eB8DA6ce449bfFE3cd194d08A436e96f2
0x090D3583e3f5953e2CC758b146f4Ae11f8224ad7
N/A
0x1aF3f22685AcdDc788B3730ed415912d8f654420
0x78384dB9674109A3Edf9F2814eFF4B6fc25D816A
0x8296D56321cf207925a7804E5A8E3F579838e6Ad
0xEdfD8386d5DE52072B4Ad8dC69BBD0bB89f9A1fb
0xFbBd0854048a8A75a8823c230e673F8331140483
N/A
0xAdc0001eA67Ab36D5321612c6b500572704fFF20
0x3dF89c727eaDF67eeD7b4d09EC4F2b41f8Dec2ca
0x6e28Cc56C2E64c9250f39Cb134686C87dB196532
N/A
0xE9A708db0D30409e39810C44cA240fd15cdA9b1a
N/A
N/A
0x0C41700ee1B363DB2ebC1a985f65cAf6eC4b1023
N/A
N/A
N/A
N/A
N/A
0x9Fd503e723e5EfcCde3183632b443fFF49E68715
N/A
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
0x3E3B04e1bF170522a5c5DDE628C4d365c0342239
0x3b4837C9F4A606b71e61FD56Db6241781194df92
N/A
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x05D782aD6D6556179A6387Ff1D2fA104FD5c515a
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
0x937Db37ffb67083242fbC6AdD472146bF10E01ec
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x3b4837C9F4A606b71e61FD56Db6241781194df92
0xd6C04C5d0e561D94B15622e770045776D4ce3739
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xbbd9c5D112343A4Aa2bc194245760CaeeaF118Be
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
N/A
N/A
N/A
N/A
N/A
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
N/A
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
N/A
0xEEc4790306C43DC00cebbE4D0c36Fadf8634B533
0x2171a0dc12a9E5b1659feF2BB20E54c84Fa7dB0C
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
0x8d996a0591a0F9eB65301592C88303e07Ec481db
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x612cF8a29A9c8965a5fE512b7463165861c07EAa
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
N/A
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
N/A
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
N/A
0x02561967c48e87cfB079763F3BEf6424A5A166A7
N/A
N/A
N/A
N/A
N/A
N/A
N/A
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
N/A
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0xA5186dec7dC1ec85B42A3cd2Dc8289e248530B07
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4
N/A
0x4D417AA04DBb207201a794E5B7381B3cde815281
0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA
0xee07ef5B414955188d2A9fF50bdCE784A49031Fc
N/A
0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8
0x322F0037d272E980984F89E94Aae43BD0FC065E6
0x4BA012eae4d64da79Bd6bcdBa366803fCe701A4C
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
N/A
N/A
N/A
N/A
N/A
N/A
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
N/A
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
0x1550A8C4F4E5afC67Ea07e8ac590fdcAdB4bBfb1
0x7Ebf48a26253810629C191b56C3212Fd0D211c26
0x80D1EE67ffAf7047d3E6EbF7317cF0eAd63FFc78
N/A
N/A
N/A
0xAe23992483FeDA6E718a808Ce824f6864F13B64B
N/A
0xe9cbD1Aa5496628F4302426693Ad63006C56959F
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a
0x3cF63891928B8CeebB81C95426600a18cd59C03f
0x626e6E3dac82205EA5FfB526092F4DCe525E46a9
N/A
N/A
N/A
N/A
N/A
N/A
0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE
0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40
0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687
0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e
N/A
N/A
N/A
N/A
N/A
N/A
N/A
0xf8cACE7EE4c03Eb4f225434B0709527938D365b4
N/A
pay.reqnetwork.near
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
pay.reqnetwork.testnet
N/A
N/A
N/A
N/A
N/A
N/A
N/A
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
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
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
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");
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
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
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.
import { useWalletClient } from "wagmi";
const { data: walletClient } = useWalletClient();
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 { 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]
);
}
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/",
},
});
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();
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();
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.
The playground for adding crypto payments to any website, build using Payment Widget.
Next.js
Demo Page
Playground Page
ERC20 Payments
Native Token Payments
Fiat Price Conversion in the UI
Configure Logo and Colors
Inject your own custom currency list
Download Receipt as PDF
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
Optimism
USDC, USDT, DAI, ETH
Moonbeam
USDC (multichain), USDC (wormhole)
Fantom
FTM
zkSync Era
ETH
Base
ETH
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
ERC20 Payments
Native Token Payments
Conversion Payments
Configure Logo and Colors
Inject your own custom currency list
Download Invoice as PDF
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
Sepolia
ETH, USDC, USDT, FAU
Invoices can be denominated in the following fiat currencies, in addition to the Payment Currencieslisted above.
USD
EUR
CNY
GBP
JPY
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 Create a new repository from a template repository.
A CLI tool to help inject Request Network functionality into projects
The Request Network Injector CLI simplifies the integration process by automatically injecting pre-built, customizable functions into your project. This tool allows new and experienced builders to quickly integrate the Request Network Protocol into their applications with minimal setup.
Automatic injection of essential Request Network functions
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
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.
Click the WalletConnect button on the Safe top-navigation bar and paste the Pairing Code into the resulting dialog.
A dialog box for granting third-party access to an encrypted invoice created via Request Finance
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.
A form for creating invoices in Request Network
To install the component, use npm:
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.
After a request is created, it can be updated:
Feature exists. More docs coming soon...
The following command creates a new Request Client instance and configures it to :
Connect to the Gnosis Request Node Gateway maintained by the Request Network Foundation.
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 RequestNetwork
object.
Try it out
Try it out
Demo Video
View Source
Gnosis Gateway
real
Gnosis Gateway (deprecated alias)
real
Sepolia Gateway
test
0.3.0
0.3.0
The add-stakeholder
component allows Builders to quickly integrate the widget into their applications.
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.
The Create Invoice Form allows end-users to create an invoice using the Request Network. It is built using but compiled to a , making it usable in any web environment, regardless of the framework.
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
npm install @requestnetwork/add-stakeholder
import '@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="..."/>
npm install @requestnetwork/create-invoice-form
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
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
accept
accept a request, indicating that it will be paid
payer
cancel
cancel a request
payee, payer
reduceExpectedAmount
reduce the expected amount
payee
increaseExpectedAmount
increase the expected amount
payer
addStakeholders
grant 1 or more third parties access to view an encrypted request
payee, payer, third party
const web3SignatureProvider = new Web3SignatureProvider(provider);
const requestClient = new RequestNetwork({
nodeConnectionConfig: {
baseURL: 'https://xdai.gateway.request.network/'
},
signatureProvider: web3SignatureProvider,
});
const requestClient = new RequestNetwork({
useMockStorage: true,
});
/** 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>
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 github)
export interface ISignatureProvider {
supportedMethods: Signature.METHOD[];
supportedIdentityTypes: Identity.TYPE[];
sign: (data: any, signer: Identity.IIdentity) => Promise<Signature.ISignedData>;
}
For example, your own package to sign needs an ethereum address and return the signature as a hexadecimal string:
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>;
}
Your signature provider would look like:
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,
},
};
}
}
Now you can inject it into the request client:
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,
});
## Example 2
For example, your own package to sign needs an internal identifier and return the signature as a Buffer:
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>;
}
Your signature provider would look like:
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);
}
}
Now you can inject it into the request client:
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);
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 github.
The transaction layer manages the encryption, see more details on the Request Protocol section.
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 Github.
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,
});
Then you can create an encrypted request:
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],
);
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.
EthereumPrivateKeyDecryptionProvider is deprecated in favor of EthereumPrivateKeyCipherProvider
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.
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,
} */
Like a clear request, you can update it if the decryption provider is instantiated with a matching private key.
//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 code
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...
A dashboard for viewing and paying invoices in Request Network
The Invoice Dashboard component allows end-users to view and pay an invoice in 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:
npm install @requestnetwork/invoice-dashboard
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
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
The payment proxy smart contracts enable the various payment types.
The most widely deployed payment proxy is the ERC20FeeProxy. The most frequently used payment proxy is the ERC20ConversionProxy.
Ethereum Mainnet
mainnet
1
Sepolia
sepolia
11155111
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
These smart contracts facilitate storing IPFS content addressable hashes (CIDs) on-chain.
Gnosis
100
Sepolia
11155111
Ethereum Mainnet (deprecated)
1
Goerli (deprecated)
5
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
Request payments can be detected easily, thanks to the integration of The Graph.
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.
View on NPM
Demo Video
View Source
Try it out
Demo Video
Integration Video
View on NPM
View Source
Make sure to scroll horizontally to see all the payment proxy types!
Our indexes Request's proxy smart contracts and allow you to query payment data easily.
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
property.
Avoid 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.
// Request parameters
const requestParameters = {...}
const web3SignatureProvider = new Web3SignatureProvider(
ethersProvider!.provider
);
const inMemoryRequestNetwork = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://gnosis.gateway.request.network",
},
signatureProvider: web3SignatureProvider,
});
let inMemoryRequest =
await inMemoryRequestNetwork.createRequest(requestParameters);
To pay an in-memory request, pass the inMemoryInfo.requestData
property to the payment function.
import {
payRequest
} from "@requestnetwork/payment-processor";
const paymentTx = await payRequest(
inMemoryRequest.inMemoryInfo.requestData,
signer
);
await paymentTx.wait(confirmationBlocks);
In-memory requests need to be persisted using a new RequestNetwork
client that does not use the skipPersistence
property.
const persistingRequestNetwork = new RequestNetwork({
nodeConnectionConfig: {
baseURL: "https://gnosis.gateway.request.network",
},
});
await persistingRequestNetwork.persistRequest(inMemoryRequest);
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 PaymentReferenceCalculator to calculate the payment reference.
Request Network allows you to support any currency. Head out to the currency 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:
const list: CurrencyInput[] = [
{ type: RequestLogicTypes.CURRENCY.ETH, decimals: 18, network: 'anything', symbol: 'ANY' },
];
const currencyManager = new CurrencyManager(list);
To implement new types of currencies (aside fiat, BTC, ETH, ERC20), head towards payment networks.
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 Encrypt with an Ethereum private key.
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:
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.
npm install @requestnetwork/lit-protocol-cipher @requestnetwork/request-client.js ethers@5.7.2
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)
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
// 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;
}
The Single Request Forwarder is a smart contract solution that enables integration with Request Network's payment system without modifying existing smart contracts.
Universal 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.
const request = await requestClient.createRequest(requestCreateParameters);
const requestData = request.getData()
// In case of in-memory request
const requestData = request.inMemoryInfo.requestData
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
import { deploySingleRequestForwarder } from "@requestnetwork/payment-processor"
const forwarderAddress = await deploySingleRequestForwarder(
requestData,
signer
);
console.log(`Single Request Forwarder Deployed At: ${forwarderAddress}`)
// Single Request Forwarder Deployed At : 0x1234567890123456789012345678901234567890
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.
import { payRequestWithSingleRequestForwarder } from "@requestnetwork/payment-processor"
import { utils } from "ethers"
const paymentAmount = utils.parseUnits("1" , 18)
await payRequestWithSingleRequestForwarder(forwarderAddress , signer, paymentAmount)
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.
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.
npm install @requestnetwork/payment-widget
import 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);
}}
/>
);
}
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
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.
Try it out
Demo Video
Integration Video
View on NPM
View Source
Try it out
Demo Video
Integration Video
View on NPM
View Source
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 Hinkal Compliance for details
See Hinkal Supported Chains for a list of chains on which Hinkal Private Payments are supported.
To use Hinkal Private Payments, install the necessary package:
npm install @requestnetwork/payment-processor
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 Deposit to a Hinkal private address
Strongly consider using Encryption and Decryption 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.
import {
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,
);
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.
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
})
The Hinkal SDK depends on snarkjs, a powerful library that enables local zero-knowledge proving in browser and Node.js environments. Snarkjs leverages WebAssembly 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 Hinkal SDK Integration for more details.
For more details about Hinkal Private Payments, refer to Pull Request #1482 on GitHub.
In this example, we will use the . Here is its partial abi:
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)',
];
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),
);
};
The Request Network Escrow isn't a separate payment network. Rather, it builds on top of the ERC20_FEE_PROXY_CONTRACT
payment network.
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:
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.
The Request Network client.
import { RequestNetwork } from "@requestnetwork/request-client.js";
nodeConnectionConfig
Axios configurations
signatureProvider
Required to sign and create requests
decryptionProvider
Required to retrieve encrypted requests
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
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
Delay between retry in ms
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
bitcoinDetectionProvider
IBitcoinDetectionProvider
Override default bitcoin payment detection
explorerApiKeys
Map<ChainName, string>
Override explorer API keys
getSubgraphClient
function(ChainName)
Override subgraph payment detection
getRpcProvider
function(ChainName)
Override RPC node provider
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
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.
Pay a series of requests with a stream of ERC777 Super Tokens from Superfluid.
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.
See Github for tests showing usage.
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 .
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.
For additional details, see the
To create a streaming request, like normal, but set the paymentNetwork
parameter to the ERC777_STREAM
payment network.
Promise<[]>
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 ethers@5.5.1
touch index.js
touch cryptoPolyfill.js
{
"expo": {
"entryPoint": "./index.js",
...
}
}
npm install @requestnetwork/request-client.js
Constructor
The Request Network client
// 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
},
},
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
Retrieve a request from a request ID
requestId
string
The ID of the request
Object
Options
Promise<Request>
disablePaymentDetection
boolean
Disable payment detection
disableEvents
boolean
Disable events
Create an unencrypted request
parameters
Parameters to create a request
options
Options to create a request
Promise<Request>
requestInfo
Core request contents
signer
Identity of the creator and signer of the request. Must be either the payee or payer.
paymentNetwork
Payment method
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
skipRefresh
boolean
Disable the request refresh after creation. Warning: the balance
will be null.
currency
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
Identity of the payee. Required if payer not set
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.
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
The chain on which the currency exists
ETH
'ETH'
Native (ETH, XDAI, etc.)
BTC
'BTC'
Bitcoin
ISO4217
'ISO4217'
Fiat (USD, EUR, etc.)
ERC20
'ERC20'
Non-native fungible currency (USDC, REQ, etc.)
ERC777
'ERC777'
Streamable fungible currency (USDCx, REQx, 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'
Swap to native token before sending to payee. Only works on NEAR.
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.
paymentInfo
any
refundInfo
any
payeeDelegate
Identity that can update the request on behalf of the payee
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
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
Identical to PnAnyToAnyConversion.ICreationParameters
Equal to IOriginalRequestCreationParameters OR ISubsequentRequestCreationParameters
expectedFlowRate
string
expectedStartDate
string
previousRequestId
string
originalRequestId
string
recurrenceNumber
string
string |
Promise<>
signerIdentity
The value returned by getRequestFromId()
refundInformation
any
Depends on the payment network
Refresh the request data and balance
requestAndMeta
IReturnGetRequestFromId
The value returned by getRequestFromId()
Promise<IRequestDataWithEvents>
Wait for a request to be persisted and indexed
None
Promise<IRequestDataWithEvents>
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:
const tokenList = await fetch(
"https://requestnetwork.github.io/request-token-list/latest.json"
).then((res) => res.json());
Each token in the list contains the following information:
{
"id": "TKN-mainnet"
"name": "Token Name",
"address": "0x...",
"symbol": "TKN",
"decimals": 18,
"chainId": 1,
"logoURI": "https://..."
}
We welcome community contributions! To add a new token to the list:
Fork the request-token-list repository on Github
Add your token information to tokens/token-list.json
Make sure your token meets our requirements (see CONTRIBUTING.md)
Run tests locally: npm test
Create a Pull Request
An object used to interact with requests. Returned by createRequest() and _createEncryptedRequest()
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
Reduce the expected amount
Others...
Other features exist. Docs coming soon...
Promise<>
Promise<[]>
parameters
Object used to create a new request
signerIdentity
Identity of the signer
signerIdentity
The value returned by getRequestFromId()
refundInformation
any
Depends on the payment network
topic
string
Topic string
updatedBetween
Start time and end time
Object
Options
from
number (Unix timestamp)
Start time
to
number (Unix timestamp)
End time
disablePaymentDetection
boolean
Disables payment detection
disableEvents
boolean
Disabled events
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
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
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.
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.
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.
type
Identity type
value
string
Identity address
import { payRequest } from "@requestnetwork/payment-processor";
request
The request object
signerOrProvider
ethers.providers.Web3Provider | ethers.Signer = getProvider()
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
Settings for conversion payments
Promise<ethers.ContractTransaction>
This is what ethers returns after submitting a transaction.
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
An ethers v5 Provider. See for explanation how to wrap a viem WalletClient to look like an ethers v5 Provider.
Compute the payment reference, the last 8 bytes of a salted hash of the request ID.
last8Bytes(hash(lowercase(requestId + salt + address)))
The payment reference is the parameter that ties the request to events emitted by on-chain payments via Request Network payment smart contracts.
import { PaymentReferenceCalculator } from "@requestnetwork/request-client.js";
requestId
string
The ID of the request
salt
string
The salt of the request
address
string
Payment recipient address
string
Function
Pay a request
Constructor
Sign using a private key inside of a wallet
The request contents. Returned by waitForConfirmation(), getData() and many other methods.
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
The balance object
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
version
string
The Request Network protocol version
requestId
string
The ID of the request
creator
Identity of the request creator
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.
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
Error occured while retrieiving payment events and calculating the balance
escrowEvents
EscrowNetworkEvent[]
Array of escrow events
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
The chain on which the currency exists
ETH
'ETH'
Native (ETH, XDAI, etc.)
BTC
'BTC'
Bitcoin
ISO4217
'ISO4217'
Fiat (USD, EUR, etc.)
ERC20
'ERC20'
Non-native fungible currency (USDC, REQ, etc.)
ERC777
'ERC777'
Streamable fungible currency (USDCx, REQx, etc.)
Shows recently submitted request contents that have not yet been persisted and indexed. Call .
[]
Constructor
Decrypt using a private key outside of a wallet
import { EthereumPrivateKeyDecryptionProvider } from "@requestnetwork/epk-decryption";
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.
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:
IPFS persists the request content
Smart contracts persist the unique IPFS CID on-chain
The Graph 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 Request Nodes 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 npm or docker.
Shown below is a diagram that depicts how the SDK and Node interact with the protocol.
Sign using a private key outside of a wallet
import { EthereumPrivateKeySignatureProvider } from "@requestnetwork/epk-signature";
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"
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:
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,
});
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:
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,
});
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:
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,
});
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:
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,
});
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:
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,
});
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:
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,
});
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:
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,
});
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.
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
Data-Access is the layer that organizes the data in the correct format before having them stored in the storage layer. This layer is similar as the persistence layer in the classical layered architecture pattern.
https://github.com/RequestNetwork/requestNetwork/tree/master/packages/data-access
Heavy communication with the Storage layer can be costly. For example, for a solution using Ethereum, every Ethereum transactions cost some gas.
Data-Access layer will gather transactions and batch them into blocks. This solution allows for less communication with the Storage layer. In this case, it will allow consuming less gas for Ethereum transactions.
Data-Access is also responsible for other side tasks:
Indexing transactions to allow retrieval
Accessing transactions through a local cache
Synchronizing with the storage
The storage phase is only complete when indexing has completed. Because this indexing is an Ethereum transaction, you cannot know how long it will take.
It is because when a block is created or read from the storage, the transactions inside it will be indexed and kept in a local cache. When a user wants to get information about a request, Data-Access will directly fetch them from this local cache.
Data-Access stays synchronized with the storage layer. For example, it pulls for new blocks, added by other users, in the storage every 10 seconds.
This page is missing the RequestToken, DAIbasedREQBurner, lockForREQBurn, ChainlinkConversionPath contracts
Request Network smart contracts are available here.
There are three types of contracts
Storage - These store Content Identifiers (CIDs) for Requests stored in IPFS.
Payments - These process various payment types, also known as Payment Networks, and are deployed across many Supported Chains.
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 ERC20FeeProxy without having to make a function call.
A contract that allows payment through EthereumFeeProxy without having to make a function call.
A factory smart contract responsible for deploying ERC20SingleRequestProxy and EthereumSingleRequestProxy contracts.
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: https://github.com/RequestNetwork/requestNetwork/tree/master/packages/types.
The following pages present the first implementation of the protocol used for the released version of Request V2 on mainnet.
The transaction layer is responsible for converting actions into a channel of transactions and vice versa. It also optionally encrypts and decrypts those transactions such that only stakeholders of the request can read them.
https://github.com/RequestNetwork/requestNetwork/tree/master/packages/transaction-manager
Transactions can be stored in the clear, unencrypted, meaning that anyone can read the request. Transactions can also be encrypted, such that only stakeholders of the request can read them. Any number of stakeholders can be included in the creation of an encrypted request. Request uses an encryption scheme nearly identical to HTTPS. It uses a symmetric key (usually referred to as the "channel key") to encrypt and decrypt the transaction channel content, and asymmetric keys to encrypt and decrypt copies of the symmetric key.
A unique channel key that is shared with all the stakeholders
A set of public and private key pairs, each pair controlled by a single stakeholder
The channel key uses Advanced Encryption Standard (AES), a symmetric encryption technology; this means the same key is used to encrypt and decrypt.
The public and private key pairs use Elliptic Curve Integrated Encryption Scheme (ECIES), an asymmetric encryption technology where the public key encrypts and the private key decrypts.
Every transaction of the same request is encrypted with the same channel key. The encrypted transactions form the channel (hence the name channel key).
The channel key is encrypted with each stakeholder's public key. This way, every stakeholder can decrypt the channel key and in turn, decrypt the transactions in the channel.
This design using both symmetric and asymmetric encryption allows the transaction data once and only the channel key needs to be duplicated, once for each stakeholder.
See the details of encrypted request creation in Encrypt with an Ethereum private key
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.
It manages extensions that can be created to extend the features of the Request Protocol through the Advanced Logic package.
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.
Simplicity is one of the most important characteristics we want to achieve in the Protocol. This is why the actions available in Request Logic are the minimal set of actions needed for any kind of request for payment. In the same way, the basic request state is universally common to any request, every request has a payee (a recipient), a currency (what requested), an expected amount (how much requested) and a basic state (accepted, canceled). To enable more advanced features for the users, we conceived Advanced Logic.
Advanced Logic is a package that allows the user to define extensions that can be added to the request. An extension is an isolated context inside the request that contains his actions and his state. For example, the extension content-data
allows the user to add metadata to a request (e.g. the additional data needed for an invoice). The Advanced Logic layer is also where the payment networks allowing payment detection are implemented.
Similar to Request Logic, a specific extension can define different actions related to it. There is the Create action of the extension and, eventually different update actions. The extension is initialized at the same time as the request, and any action of the Request Logic can add extension data. There is a specific action, AddExtensionData
, in Request Logic, only intended to add extension data to the request with no other side-effect.
The specification for each extension can be found at this link:
Storage defines where the data are stored. How to store these data and how to retrieve them.
The currently used package, named ethereum-storage
, uses IPFS to store the data immutably and uses the Ethereum network to persist the IPFS hash of the data and make them permanently available to everyone.
The storage of data implementation is:
Open: Anyone should be able to access the data (though it can be encrypted)
Decentralized: The database is trustless; we don’t have to refer to a third party to trust the data
Resilient: The database should always be available, nobody should be able to shutdown it alone
One of the advantages of IPFS as a storage solution is that it is content addressable. When a file is deleted, if someone reuploads the file, anybody will be able to access it with the same path. For a specific block of data, we will get a specific hash; the hash is persisted on Ethereum to ensure requests immutability.
We use Ethereum to store IPFS hashes. The hashes are stored as event logs of a specific smart contract to stay at a minimal cost.
The Ethereum smart contracts are also used to enforce the fee cost of storing a block to Request. The user will store the file's size in addition to the hash. A fee related to this hash will be paid in Ether when storing the hash.
For our solution, we use additional smart contracts for fee verification. Using external smart contracts allows us to implement different fee rules in the future. More information can be found in the ethereum-storage repository.
Retrieves the comprehensive compliance status for a specific user, including KYC and agreement status.
The client user ID to check compliance status for
user-123
API key for authentication
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-123
Optional ID of specific payment details to retrieve
fa898aec-519c-46be-9b4c-e76ef4ff99d9
API key for authentication
Smart Contract Addresses
Smart Contract Source
The interplanetary file system (IPFS) is a decentralized network to store and share files:
The RequestHashStorage smart contract address can be found on
(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));
})();
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
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": "john.doe@example.com",
"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"
}
}
GET /v2/payer/{clientUserId} HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
{
"kycStatus": "completed",
"agreementStatus": "completed",
"isCompliant": true,
"userId": "a25a4274-8f50-4579-b476-8f35b297d4ad"
}
Update the agreement completion status for a user.
The client user ID to update
user-123
API key for authentication
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
}
{
"success": true
}
Create payment details for a user
The client user ID
user-123
API 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
local
Possible 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
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"
}
}
GET /v2/payer/{clientUserId}/payment-details HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
{
"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"
}
]
}
Create a new payment request
API key for authentication
The wallet address of the payer
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
Whether crypto-to-fiat payment is available for this request
POST /v2/request HTTP/1.1
Host: api.request.network
x-api-key: text
Content-Type: application/json
Accept: */*
Content-Length: 238
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia",
"isCryptoToFiatAvailable": false,
"recurrence": {
"startDate": "2025-01-01T00:00:00.000Z",
"frequency": "DAILY"
}
}
{
"paymentReference": "0xb3581f0b0f74cc61",
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb"
}
"xdai": {
"address": "0x2256938E8225a998C498bf86B43c1768EE14b90B"
},
"sepolia": {
"address": "0xd6c085A4D14e9e171f4aF58F7F48bd81173f167E"
}
Get the status of a payment request
The requestId for the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
API key for authentication
GET /v2/request/{requestId} HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
{
"hasBeenPaid": true,
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb",
"isListening": false,
"txHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
Update a recurring request
The requestId for the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
API key for authentication
PATCH /v2/request/{requestId} HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
No content
Get the calldata needed to pay a request. For cross-chain payments, returns a payment intent that needs to be signed. For same-chain payments, returns transaction calldata. For off-ramp payments, use the query parameters clientUserId and paymentDetailsId.
The requestId of the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
The wallet address of the payer.
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
The source chain of the cross chain payment
The source token of the cross chain payment
The amount to pay, in human readable format
Optional client user ID for off-ramp payments
user-123
Optional payment details ID for off-ramp payments
fa898aec-519c-46be-9b4c-e76ef4ff99d9
Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)
0.02
Address to receive the fee
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
API key for authentication
GET /v2/request/{requestId}/pay HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
{
"transactions": [
{
"data": "0xb868980b...00",
"to": "0x11BF2fDA23bF0A98365e1A4e04A87C9339e8687",
"value": {
"type": "BigNumber",
"hex": "0x038d7ea4c68000"
}
}
],
"metadata": {
"stepsRequired": 1,
"needsApproval": false,
"approvalTransactionIndex": null,
"hasEnoughBalance": true,
"hasEnoughGas": true
}
}
Get the payment routes for a request
The requestId of the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
The wallet address of the payer
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
The 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
GET /v2/request/{requestId}/routes HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
{
"routes": [
{
"id": "REQUEST_NETWORK_PAYMENT",
"fee": 0,
"speed": "FAST",
"price_impact": 0,
"chain": "MAINNET",
"token": "REQ"
}
]
}
Get the calldata needed to pay a request. For cross-chain payments, returns a payment intent that needs to be signed. For same-chain payments, returns transaction calldata. For off-ramp payments, use the query parameters clientUserId and paymentDetailsId.
The requestId of the request
01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
The wallet address of the payer.
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
The source chain of the cross chain payment
The source token of the cross chain payment
The amount to pay, in human readable format
Optional client user ID for off-ramp payments
user-123
Optional payment details ID for off-ramp payments
fa898aec-519c-46be-9b4c-e76ef4ff99d9
Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)
0.02
Address to receive the fee
0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
API key for authentication
GET /v2/request/{requestId}/pay HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*
{
"transactions": [
{
"data": "0xb868980b...00",
"to": "0x11BF2fDA23bF0A98365e1A4e04A87C9339e8687",
"value": {
"type": "BigNumber",
"hex": "0x038d7ea4c68000"
}
}
],
"metadata": {
"stepsRequired": 1,
"needsApproval": false,
"approvalTransactionIndex": null,
"hasEnoughBalance": true,
"hasEnoughGas": true
}
}
Create a new payment request
API key for authentication
The wallet address of the payer
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
Whether crypto-to-fiat payment is available for this request
POST /v2/request HTTP/1.1
Host: api.request.network
x-api-key: text
Content-Type: application/json
Accept: */*
Content-Length: 238
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia",
"isCryptoToFiatAvailable": false,
"recurrence": {
"startDate": "2025-01-01T00:00:00.000Z",
"frequency": "DAILY"
}
}
{
"paymentReference": "0xb3581f0b0f74cc61",
"requestId": "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb"
}
Initiate a payment without having to create a request first
API key for authentication
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
POST /v2/payouts HTTP/1.1
Host: api.request.network
x-api-key: text
Content-Type: application/json
Accept: */*
Content-Length: 213
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia",
"feePercentage": "0.02",
"feeAddress": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7"
}
{
"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
}
}
Send a payment intent
The payment intent ID
01JNZYZPK7B4YBPD44TM72NDNJ
API key for authentication
POST /v2/request/payment-intents/{paymentIntentId} HTTP/1.1
Host: api.request.network
x-api-key: text
Content-Type: application/json
Accept: */*
Content-Length: 154
{
"signedPaymentIntent": {
"signature": "text",
"nonce": "text",
"deadline": "text"
},
"signedApprovalPermit": {
"signature": "text",
"nonce": "text",
"deadline": "text"
}
}
No content
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
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
POST /v2/payouts/batch HTTP/1.1
Host: api.request.network
x-api-key: text
Content-Type: application/json
Accept: */*
Content-Length: 1152
{
"requests": [
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "2",
"invoiceCurrency": "FAU-sepolia",
"paymentCurrency": "FAU-sepolia",
"feePercentage": "0.02",
"feeAddress": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7"
},
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "2",
"invoiceCurrency": "fUSDC-sepolia",
"paymentCurrency": "fUSDC-sepolia",
"feePercentage": "0.02",
"feeAddress": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7"
},
{
"payee": "0xb07D2398d2004378cad234DA0EF14f1c94A530e4",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "FAU-sepolia",
"feePercentage": "0.02",
"feeAddress": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7"
},
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "0.00001",
"invoiceCurrency": "ETH-sepolia-sepolia",
"paymentCurrency": "ETH-sepolia-sepolia",
"feePercentage": "0.02",
"feeAddress": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7"
},
{
"payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7",
"amount": "10",
"invoiceCurrency": "USD",
"paymentCurrency": "ETH-sepolia-sepolia",
"feePercentage": "0.02",
"feeAddress": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7"
}
],
"payer": "0x2e2E5C79F571ef1658d4C2d3684a1FE97DD30570"
}
{
"ERC20ApprovalTransactions": [
{
"data": "0x095ea7b3...",
"to": "0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C",
"value": 0
}
],
"batchPaymentTransaction": {
"data": "0x92cddb91...",
"to": "0x67818703c92580c0e106e401F253E8A410A66f8B",
"value": {
"type": "BigNumber",
"hex": "0x0d83b3d1afc58b"
}
}
}
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 {
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 { 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 { 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 { 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 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 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 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 { 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 { 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, 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 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 { 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 { 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'));
});
});
});
});
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 { 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 { 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 { 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 {
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 { 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,
),
);
})();
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(),
);
});
});
});
// 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";
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;
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;
{
"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"
}
}
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);
}