Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 104 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

Request Network Docs

Loading...

Request Network API

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

General

Loading...

Loading...

Loading...

Loading...

Loading...

Advanced

Request Network SDK

Get Started

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Components

Web Components for integrating Request Network. Usable in any framework.

Loading...

Loading...

Loading...

Loading...

SDK Guides

Request Client

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Encryption and Decryption

Loading...

Loading...

Loading...

Payment

Loading...

Native Payment

Loading...

Loading...

Configuring Payment Fees

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

SDK Reference

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...

Protocol Overview

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Request Network Docs

Request Network is a protocol for creating payment requests and reconciling payments.

Talk to an expert

Learn how Request Network can streamline Web3 invoicing and payments for your app - book a call with us.

What you can do with Request Network

  • Easily add invoicing features to your app

  • Enable crypto payments in just a few clicks

  • Reconcile invoices and payments with 100% accuracy

Start Building with Request Network

EasyInvoice: API Demo App

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.

Talk to an expert

Discover how your app can have its own EasyInvoice features - book a call with us.

Key Features

Invoice Creation

  • 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

  • Dashboard: View key metrics and a table of your invoices.

Invoice Payment

  • 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.

Invoice Crosschain Payment

Crypto-to-fiat Payment

Batch Pay Invoices

Recurring Invoices

  • Recurring Invoice: Automatically create new invoices based on the selected start date and frequency

Payout

  • Payout: Send a payment without having to create a request first.

Batch Payout

InvoiceMe Link

  • InvoiceMe Link: Prompt clients to send you an invoice prefilled with your name and email address.

Login

  • Google Login: Securely log in to your account using Google OAuth.

Create and Pay Requests

The Request Network API provides an interface for creating and paying requests within your application.

Talk to an expert

Discover how Request Network API can enhance your app's features - book a call with us.

Core Functionality

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

Create and Pay Request Workflow

The following diagram illustrates the typical flow for creating and paying requests using the Request Network API:

Crosschain Payments

Pay requests using stablecoins from any supported network, without manual bridging or token swaps.

Talk to an expert Discover how Request Network API can enhance your app's features - book a call with us.

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.

Benefits

  • 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.

Supported Networks

Crosschain payments are supported on the following blockchain networks:

  • Base

  • Optimism

  • Arbitrum

  • Ethereum

Supported Stablecoins

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

How it works

1. Request creation

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.

2. Payment route fetching

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.

Route Ranking

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.

Fee breakdown

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:

  1. 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.

  2. Service Fees:

    The total fees also include any service fees charged by the crosschain infrastructure for facilitating transfers or swaps between different blockchains.

Samechain routes

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

3. Getting payment calldata

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.

4. Signing the payment intent

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,
};

5. Sending the signed data

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.

Custom fee configuration

It will be possible in the future to add a custom fee to the payment, this is currently under development.

Batch Payments

Process multiple payment requests or payouts efficiently in a single blockchain transaction

Talk to an expert

Discover how Request Network API can enhance your app's features - book a call with us.

Overview

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:

Batch Pay Invoices

Process previously created requests using their request IDs and receive payment calldata that can be executed on-chain to pay multiple requests simultaneously.

Batch Payouts

Submit new payment requests that are immediately processed, creating requests and returning payment calldata in a single API call for instant multi-recipient payments.

Key Benefits

  • 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.

Batch Processing Limits

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.

Batch Payment Workflow

Endpoints

Implementation Examples

The following examples demonstrate how to implement batch payment calldata execution in your application. Note: The API returns unsigned transaction calldata - your application must handle sending these transactions to the blockchain.

Batch Pay Invoices Example

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();

Batch Payouts Example

// 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)

Supported Payment Types

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)

Key Implementation Notes

Your Responsibility

  • 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

Best Practices

  1. Validate Addresses: Always validate recipient addresses before submitting batch payments

  2. Test on Testnets: Start with small batches on test networks before production deployment

  3. Handle Failures Gracefully: Implement proper error handling for transaction failures

  4. Gas Estimation: Consider gas costs when determining optimal batch sizes

  5. User Experience: Provide clear progress indicators for multi-step approval processes

Error Handling

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

Demo Application

See the batch payment feature in action in our EasyInvoice demo application:

EasyInvoice Batch Pay Invoices

EasyInvoice: Batch Payouts

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.

Crypto-to-fiat Payments

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.

Crypto-to-fiat payments are only available in version 2 of the Request Network API.

Getting Started with Crypto-to-fiat Payments

Sandbox Access - Get Started Today

All developers can immediately access the Crypto-to-fiat Sandbox to build and test their integration:

  1. Create an account on the Request Network API Portal

  2. Generate a Sandbox API key with crypto-to-fiat sandbox access

  3. 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.

Production Access - Launch When Ready

When you're ready to go live with real transactions:

  1. Book a call to request production access

  2. Discuss your use case with our team to ensure the best integration approach

  3. Complete the approval process - we'll work with you to get everything set up

  4. 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

Understanding 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.

Compliance & Payer Onboarding

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.

Compliance Flow Diagram

Flow Explanation

  1. Submit KYC: The platform collects KYC information from the payer and submits it to the API.

  2. KYC Review: The platform receives webhook updates as the KYC is processed (compliance.updated with kycStatus).

  3. 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.

  4. Agreement Confirmation: The platform receives a webhook update when the agreement is completed (compliance.updated with agreementStatus).

Relevant Endpoints

  • POST /payer: Submit KYC application.

  • GET /payer/{clientUserId}: Get compliance status for a payer.

  • PATCH /payer/{clientUserId}: Update agreement status after signature.

Setting Up a Crypto-to-Fiat Request (Payee Flow)

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.

Payment Details Flow Diagram

Flow Explanation

  1. 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).

  2. Approval: The platform receives a webhook (payment_detail.updated) indicating if the payment details are approved, failed, or pending.

  3. 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.

Design Rationale & UX Constraints

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.

Relevant Endpoints

  • 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

Paying a Crypto-to-Fiat Request

The payer pays in crypto; Request Tech handles offramping and fiat payout.

Payment Flow Diagram

Flow Explanation

  1. Get Payment Calldata: The platform fetches payment calldata for the request.

  2. User Pays: The payer signs and submits the transaction, sending crypto to Request Tech.

  3. Offramp Processing: Request Tech receives the crypto and begins the offramp process.

  4. Status Updates: The platform receives webhook events as the offramp progresses (payment.processing, payment.failed), with subStatus indicating the current offramp stage.

  5. 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.

Crypto-to-fiat Webhook Event Reference

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)

API Portal: Manage API Keys and Webhooks

An app for managing Request Network API keys and webhooks.

Overview

The Request Network API Portal provides app developers with a platform to securely manage their API keys and webhook endpoints.

Key Features

API Key Management

  • 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.

Webhook Management

  • 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 });
});

Usage

Creating API Keys

  • Navigate to the "API Keys" section.

  • Click on "Create new key."

  • Store the key securely and never share it publicly.

Configuring Webhooks

  • 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.

Security Considerations

  • 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.

Lifecycle of a Request

The typical lifecycle of a request is as follows:

Create a request

  • 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

Requests are created by storing their CIDs on Gnosis, but this doesn't mean payment must occur on Gnosis. Payment can occur on any of the supported chains including 20+ EVM-compatible chains or NEAR.

Update a request

  • 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.

Pay a request

  • 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.

Most requests are "reference-based" meaning that a paymentReference derived from the request contents is logged on-chain via a smart contract that emits an event. Nothing gets written back to IPFS when paying a "reference-based" request.

The exception is when paying a "declarative" request, in which case, data is written back to IPFS. This includes when the payer declares that the payment was sent and the payee declares that the payment was received.

Retrieve a request / Detect a payment

  • 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.

The request balance is calculated by adding up all the on-chain payment events with the same paymentReference. Partial payments are possible.

Request Scan

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.

Intended Audience

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.

Usage

User Interface

  • 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.

Searching for Data

  • Request: Enter a request ID in the search bar to view its details.

  • Address: Enter an address to see its requests and payment history.

Demo Video

Migrate to V2

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.

Breaking Changes from V1 to V2

Core Architectural Changes

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

API Interface Differences

Endpoint Structure Changes

Request Endpoints

Payment Endpoints

Request Schema Differences

Create Request Schema

V1 Schema:

V2 Schema:

Create Response Schema

V1 Response:

V2 Response:

Payment Query Parameter Differences

Pay Request Query Parameters

V1 Query Parameters:

V2 Query Parameters:

Step-by-Step Migration Guide

Phase 1: Assessment and Planning

  1. Audit Your Current Integration

    • List all V1 endpoints you're currently using

    • Identify which features you need (basic payments vs advanced features)

  2. 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

Phase 2: Update Path Parameters

Before (V1):

After (V2):

Phase 3: Update Response Handling

Before (V1):

After (V2):

Phase 4: Update Error Handling

V2 has enhanced error responses with more specific error codes:

Phase 5: Testing and Validation

  1. Test Core Functionality

    • Create requests using V2 endpoints

    • Verify payment flows work correctly

    • Check response formats match expectations

  2. Enhanced Validation Testing

    • Test stricter type checking

    • Verify improved error responses

  3. Performance Testing

    • Compare response times between V1 and V2

    • Test with realistic data volumes

Support and Resources

  • GitHub Examples: Check the easy-invoice repository for V2 implementation examples

Backward Compatibility

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.

Smart Contract Addresses

Payment

Ethereum

Sepolia

Optimism

Arbitrum One

Base

zkSync Era

Gnosis

Polygon

BNB Smart Chain (BSC)

BNB Smart Chain (BSC) Testnet

Celo

Alfajores

Fantom

Tombchain

Core

Avalanche

Fuse

Moonbeam

Ronin

Mantle

NEAR

NEAR Testnet

Storage

Gnosis

Sepolia

Ethereum (Deprecated)

REQ Token and Burn Mechanism

Ethereum

Gnosis

Installation

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.

SDK Packages

These are the packages that we think would be most commonly used by Builders to build applications.

Component Packages

These packages offer pre-built components for quickly integrating certain Request Network features.

Import the packages

The Request Client library can be imported as ES6 or CommonJS modules.

Internal SDK Packages

Quickstart - Browser

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.

You will learn:

  • How to create a request

  • How to update a request (coming soon...)

  • How to pay a request

  • How to detect a payment

  • How to retrieve a user’s requests

Create a request

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.

CodeSandBox: Create a request

Pay a request

First, construct a RequestNetwork object and connect it to a Request Node. In this example, we use the Sepolia Request Node Gateway:

Note that paying a request doesn't require a SignatureProvider be passed into the RequestNetwork object.

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.

CodeSandBox: Create and pay a request and detect a payment

Video: Create and pay a request and detect a payment

Retrieve a user's requests

First, construct a RequestNetwork object and connect it to a Request Node. In this example, we use the Sepolia Request Node Gateway:

Then, call fromIdentity() to get an array of Request objects or fromRequestId() to get a single Request object. This function retrieves the Requests stored in IPFS and queries on-chain events to determine the balances paid so far. Finally, call getData() on each Request to get the request contents.

CodeSandBox: Retrieve a user's requests

Video: Retrieve a user's requests

Quickstart - Node.js

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.

You will learn:

  • How to create a request

  • How to update a request (coming soon...)

  • How to pay a request

  • How to detect a payment

  • How to retrieve a user’s requests

Repository

Create a request

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:

Pay a request / Detect a payment

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:

Retrieve a user's requests

First, construct a RequestNetwork object and connect it to a Request Node. In this example, we use the Sepolia Request Node Gateway:

Then, call fromIdentity() to get an array of Request objects or fromRequestId() to get a single Request object. This function retrieves the Requests stored in IPFS and queries on-chain events to determine the balances paid so far. Finally, call getData() on each Request to get the request contents.

Altogether it looks like this:

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.

Feature
V1 Endpoint
V2 Endpoint
Feature
V1 Endpoint
V2 Endpoint

Migration Support: with our team for migration assistance

For an explanation about what each smart contract does, see

Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Contract Name
Address
Package
Description

For a list of internal SDK packages, see .

Package
Description

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.

Package
Description

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);
}

ERC20FeeProxy

0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C

ERC20Proxy

0x5f821c20947ff9be22e823edc5b3c709b33121b3

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

0x7DfD5955a1Ed6Bf74ccF8e24FF53E0a9A7e9F477

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xe72Ecea44b6d8B2b3cf5171214D9730E86213cA2

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x3b4837C9F4A606b71e61FD56Db6241781194df92

ERC20TransferableReceivable

0xcE80D17d38cfee8E5E6c682F7712bfb5A04Ae912

SingleRequestProxyFactory

0xD5933C74414ce80D9d7082cc89FBAdcfF4751fAF

ChainlinkConversionPath

0xC5519f3fcECC8EC85caaF8836563dEe9a00080f9

ERC20FeeProxy

0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE

ERC20Proxy

0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40

EthereumFeeProxy

0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687

EthereumProxy

0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e

ERC20EscrowToPay

0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a

BatchConversionPayments

0x67818703c92580c0e106e401F253E8A410A66f8B

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xaD61121DAfAAe495095Cd466022b519Cb7503a4E

ETHConversionProxy

0xc861aE0Cd70b73b0C8F1D62Fa669E6D1d7

D7e0aB

ERC20SwapToConversion

0x05e94CDdd14E0b18317AE21BAFAEC24156BdB7C9

ERC20TransferableReceivable

0xB5E53C3d145Cbaa61C7028736A1fF0bC6817A4c5

SingleRequestProxyFactory

0xf8cACE7EE4c03Eb4f225434B0709527938D365b4

ChainlinkConversionPath

0x7c285b9F2dA5E2c10feA25C00Ce1aCB107F85475

ERC20FeeProxy

0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE

ERC20Proxy

N/A

EthereumFeeProxy

0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687

EthereumProxy

0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e

ERC20EscrowToPay

0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0x1550A8C4F4E5afC67Ea07e8ac590fdcAdB4bBfb1

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x80D1EE67ffAf7047d3E6EbF7317cF0eAd63FFc78

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0xf8cACE7EE4c03Eb4f225434B0709527938D365b4

ChainlinkConversionPath

0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA

ERC20FeeProxy

0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9

ERC20Proxy

N/A

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

N/A

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xA5186dec7dC1ec85B42A3cd2Dc8289e248530B07

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0x4D417AA04DBb207201a794E5B7381B3cde815281

ChainlinkConversionPath

0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA

ERC20FeeProxy

0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814

ERC20Proxy

0xc31323ea7513799e1e112Dc15a05d5b600Cc357e

EthereumFeeProxy

0xd9C3889eB8DA6ce449bfFE3cd194d08A436e96f2

EthereumProxy

0x090D3583e3f5953e2CC758b146f4Ae11f8224ad7

ERC20EscrowToPay

N/A

BatchConversionPayments

0x1aF3f22685AcdDc788B3730ed415912d8f654420

ERC20SwapToPay

0x78384dB9674109A3Edf9F2814eFF4B6fc25D816A

ERC20ConversionProxy

0x8296D56321cf207925a7804E5A8E3F579838e6Ad

ETHConversionProxy

0xEdfD8386d5DE52072B4Ad8dC69BBD0bB89f9A1fb

ERC20SwapToConversion

0xFbBd0854048a8A75a8823c230e673F8331140483

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0xAdc0001eA67Ab36D5321612c6b500572704fFF20

ChainlinkConversionPath

0x3dF89c727eaDF67eeD7b4d09EC4F2b41f8Dec2ca

ERC20FeeProxy

0x6e28Cc56C2E64c9250f39Cb134686C87dB196532

ERC20Proxy

N/A

EthereumFeeProxy

0xE9A708db0D30409e39810C44cA240fd15cdA9b1a

EthereumProxy

N/A

ERC20EscrowToPay

N/A

BatchConversionPayments

0x0C41700ee1B363DB2ebC1a985f65cAf6eC4b1023

ERC20SwapToPay

N/A

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0x9Fd503e723e5EfcCde3183632b443fFF49E68715

ChainlinkConversionPath

N/A

ERC20FeeProxy

0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9

ERC20Proxy

N/A

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

N/A

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E

ETHConversionProxy

0x3E3B04e1bF170522a5c5DDE628C4d365c0342239

ERC20SwapToConversion

0x3b4837C9F4A606b71e61FD56Db6241781194df92

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0x4D417AA04DBb207201a794E5B7381B3cde815281

ChainlinkConversionPath

0x05D782aD6D6556179A6387Ff1D2fA104FD5c515a

ERC20FeeProxy

0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9

ERC20Proxy

N/A

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

0x937Db37ffb67083242fbC6AdD472146bF10E01ec

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x3b4837C9F4A606b71e61FD56Db6241781194df92

ERC20TransferableReceivable

0xd6C04C5d0e561D94B15622e770045776D4ce3739

SingleRequestProxyFactory

0x4D417AA04DBb207201a794E5B7381B3cde815281

ChainlinkConversionPath

0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA

ERC20FeeProxy

0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9

ERC20Proxy

N/A

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

N/A

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xbbd9c5D112343A4Aa2bc194245760CaeeaF118Be

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0x4D417AA04DBb207201a794E5B7381B3cde815281

ChainlinkConversionPath

0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA

ERC20FeeProxy

0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9

ERC20Proxy

N/A

EthereumFeeProxy

N/A

EthereumProxy

N/A

ERC20EscrowToPay

N/A

BatchConversionPayments

N/A

ERC20SwapToPay

N/A

ERC20ConversionProxy

0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E

ETHConversionProxy

N/A

ERC20SwapToConversion

0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

0xEEc4790306C43DC00cebbE4D0c36Fadf8634B533

ERC20FeeProxy

0x2171a0dc12a9E5b1659feF2BB20E54c84Fa7dB0C

ERC20Proxy

N/A

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

N/A

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0x8d996a0591a0F9eB65301592C88303e07Ec481db

ChainlinkConversionPath

0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA

ERC20FeeProxy

0x612cF8a29A9c8965a5fE512b7463165861c07EAa

ERC20Proxy

N/A

EthereumFeeProxy

N/A

EthereumProxy

N/A

ERC20EscrowToPay

N/A

BatchConversionPayments

N/A

ERC20SwapToPay

N/A

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

N/A

ERC20FeeProxy

0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9

ERC20Proxy

N/A

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

N/A

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xf0f49873C50765239F6f9534Ba13c4fe16eD5f2E

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA

ERC20FeeProxy

0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE

ERC20Proxy

N/A

EthereumFeeProxy

0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687

EthereumProxy

N/A

ERC20EscrowToPay

N/A

BatchConversionPayments

N/A

ERC20SwapToPay

N/A

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

N/A

ERC20FeeProxy

0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE

ERC20Proxy

0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40

EthereumFeeProxy

0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687

EthereumProxy

0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e

ERC20EscrowToPay

N/A

BatchConversionPayments

0x02561967c48e87cfB079763F3BEf6424A5A166A7

ERC20SwapToPay

N/A

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

N/A

ERC20FeeProxy

0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9

ERC20Proxy

N/A

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

N/A

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0xA5186dec7dC1ec85B42A3cd2Dc8289e248530B07

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x1d6B06C6f7adFd9314BD4C58a6D306261113a1D4

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0x4D417AA04DBb207201a794E5B7381B3cde815281

ChainlinkConversionPath

0x0818Ad7016138f0A40DFAe30F64a923c2A8F61bA

ERC20FeeProxy

0xee07ef5B414955188d2A9fF50bdCE784A49031Fc

ERC20Proxy

N/A

EthereumFeeProxy

0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8

EthereumProxy

0x322F0037d272E980984F89E94Aae43BD0FC065E6

ERC20EscrowToPay

0x4BA012eae4d64da79Bd6bcdBa366803fCe701A4C

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

N/A

ERC20FeeProxy

0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE

ERC20Proxy

N/A

EthereumFeeProxy

0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687

EthereumProxy

0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e

ERC20EscrowToPay

0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

0x1550A8C4F4E5afC67Ea07e8ac590fdcAdB4bBfb1

ETHConversionProxy

0x7Ebf48a26253810629C191b56C3212Fd0D211c26

ERC20SwapToConversion

0x80D1EE67ffAf7047d3E6EbF7317cF0eAd63FFc78

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

N/A

ERC20FeeProxy

0xAe23992483FeDA6E718a808Ce824f6864F13B64B

ERC20Proxy

N/A

EthereumFeeProxy

0xe9cbD1Aa5496628F4302426693Ad63006C56959F

EthereumProxy

0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e

ERC20EscrowToPay

0x26d4912fA5aC84F185843E19eeEdcc47f4Cc9F1a

BatchConversionPayments

0x3cF63891928B8CeebB81C95426600a18cd59C03f

ERC20SwapToPay

0x626e6E3dac82205EA5FfB526092F4DCe525E46a9

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

N/A

ERC20FeeProxy

0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE

ERC20Proxy

0x88Ecc15fDC2985A7926171B938BB2Cd808A5ba40

EthereumFeeProxy

0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687

EthereumProxy

0x171Ee0881407d4c0C11eA1a2FB7D5b4cdED71e6e

ERC20EscrowToPay

N/A

BatchConversionPayments

N/A

ERC20SwapToPay

N/A

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

0xf8cACE7EE4c03Eb4f225434B0709527938D365b4

ChainlinkConversionPath

N/A

ERC20FeeProxy

pay.reqnetwork.near

ERC20Proxy

N/A

EthereumFeeProxy

N/A

EthereumProxy

N/A

ERC20EscrowToPay

N/A

BatchConversionPayments

N/A

ERC20SwapToPay

N/A

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

N/A

ERC20FeeProxy

pay.reqnetwork.testnet

ERC20Proxy

N/A

EthereumFeeProxy

N/A

EthereumProxy

N/A

ERC20EscrowToPay

N/A

BatchConversionPayments

N/A

ERC20SwapToPay

N/A

ERC20ConversionProxy

N/A

ETHConversionProxy

N/A

ERC20SwapToConversion

N/A

ERC20TransferableReceivable

N/A

SingleRequestProxyFactory

N/A

ChainlinkConversionPath

N/A

RequestOpenHashSubmitter

0x268C146Afb4790902Ee26A6D2d3aff968623Ec80

RequestHashStorage

0x2256938E8225a998C498bf86B43c1768EE14b90B

RequestOpenHashSubmitter

0x899ddc13d5DBc252916ba2D70928518f3C836ba1

RequestHashStorage

0xd6c085A4D14e9e171f4aF58F7F48bd81173f167E

RequestOpenHashSubmitter

0xa9cEaA10c12dcB33BAbC2D779e37732311504652

RequestHashStorage

0x24a66afda3666fb0202f439708ece45c8121a9bb

DaiBasedREQBurner

0x2CFa65DcB34311293c6a52F1D7BEB8f4E31E5117

RequestToken

0x8f8221afbb33998d8584a2b05749ba73c37a938a

Burner (Deprecated)

0x7b3C4D90e8AF6030d66c07F8F815f9505E379d6F

lockForREQBurn

0x2171a0dc12a9E5b1659feF2BB20E54c84Fa7dB0C

@requestnetwork/request-client.js

Create, update, and retrieve requests.

@requestnetwork/web3-signature

Sign requests using web3 wallets like Metamask

@requestnetwork/epk-signature

Sign requests using Ethereum private keys

@requestnetwork/data-format

Standards for data stored on Request, like invoice format

@requestnetwork/epk-decryption

Decrypt encrypted requests using Ethereum private keys

@requestnetwork/payment-processor

Pay a request using a web3 wallet

@requestnetwork/request-node

Web server that allows easy access to the Request system

@requestnetwork/currency

Tools for managing currency definitions

@requestnetwork/create-invoice-form

A form for creating invoices in Request Network

@requestnetwork/invoice-dashboard

A dashboard for viewing and paying invoices in Request Network

@requestnetwork/add-stakeholder

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");

@requestnetwork/advanced-logic

Extensions to the protocol

@requestnetwork/data-access

Indexing and batching of transactions

@requestnetwork/ethereum-storage

Storage of Request data on Ethereum and IPFS, with custom indexing

@requestnetwork/multi-format

Serialize and deserialize object in the Request Network protocol

@requestnetwork/payment-detection

Payment detection, to compute the balance.

@requestnetwork/request-logic

The Request business logic: properties and actions of requests

@requestnetwork/smart-contracts

Sources and artifacts of the smart contracts

@requestnetwork/thegraph-data-access

Storage of Request data on Ethereum and IPFS, indexed by TheGraph

@requestnetwork/transaction-manager

Creates transactions to be sent to Data Access, managing encryption

@requestnetwork/types

Typescript types shared across @requestnetwork packages

@requestnetwork/utils

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();
page.tsx
import { useEthersV5Provider } from './use-ethers-v5-provider';
import { useEthersV5Signer } from './use-ethers-v5-signer';

return Page() {
  const provider = useEthersV5Provider();
  const signer = useEthersV5Signer();
  ...
}
use-ethers-v5-provider.ts
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]);
}
use-ethers-v5-signer.ts
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();
Quickstart
Full API Reference
Book a call
Smart Contracts Overview
Internal SDK Packages
SDK Packages
https://wagmi.sh/react/ethers-adapters
https://github.com/RequestNetwork/quickstart-node.js
Request Scan Demo Video

Request Node Gateways

The Request Network Foundation operates several Request Node "Gateways" that are free for anyone to use. These gateways offer endpoints for creating and retrieving requests. They also pay the protocol fee for creating requests, about USD $0.10 per request, paid in XDAI.

Request Checkout

The playground for adding crypto payments to any website, build using Payment Widget.

Framework
Deployment
Source

Next.js

Payment Widget Demo Video

Features

Feature
Status

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

Chains and Currencies

Chain
Currencies

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

Payment Widget Integration Video

Request Invoicing

An Invoicing Template for creating, viewing, and paying requests in Request Network. Built using the Invoice Dashboard and Create Invoice Form Web Components.

Framework
Deployment
Source

Next.js

Request Invoicing Demo Video

Features

Feature
Status

ERC20 Payments

Native Token Payments

Conversion Payments

Configure Logo and Colors

Inject your own custom currency list

Download Invoice as PDF

Chains and Currencies

Payment Currencies

Chain
Currencies

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

Invoice Currencies

Invoices can be denominated in the following fiat currencies, in addition to the Payment Currencieslisted above.

  • USD

  • EUR

  • CNY

  • GBP

  • JPY

Request Invoicing Integration Video

SDK Demo Apps

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 Components are built using Svelte but compiled to Web Components making them usable in any web environment, regardless of the framework.

Use a Template

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.

SDK Injector

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.

Features

  • 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)

How it works

  1. The CLI analyzes your project structure

  2. You select the functions you want to inject

  3. You choose your preferred language (TypeScript or JavaScript)

  4. The tool injects the selected functions into your project

  5. Necessary dependencies are automatically installed

Demo Video

Pay from Safe Multisig

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.

Add Stakeholder

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.

Access is granted on a per-request basis. Once we prove market demand for this feature, we hope that Request Finance will improve its widget to allow granting access to multiple requests at a time.

Installation

Usage

Web Component in React, Next.js, or Vue

Native Svelte component

Web Component in Svelte

Web Component in Browser

Create Invoice Form

A form for creating invoices in Request Network

Installation

To install the component, use npm:

Usage

Usage in React and Next.js

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.

Props

Next Steps

Updating a Request

After a request is created, it can be updated:

Feature exists. More docs coming soon...

Configure the Request Client

Configure the client

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.

Mock Storage

To create mock storage requests, where the request is stored in memory on the local machine and cleared as soon as the script is finished running, set the useMockStorage argument to true when instantiating the RequestNetworkobject.

Try it out

Try it out

Demo Video

View Source

Gateway
real/test
URL

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

The web component supports any wallet connector module built on top of . This provides the flexibility to use any wagmi-compatible wallet connector, such as .

Prop
Type
Description
Name
Description
Role Authorized
🕹️
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

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,
});
https://gnosis.gateway.request.network
https://xdai.gateway.request.network
https://sepolia.gateway.request.network
https://checkout.request.network
https://github.com/RequestNetwork/rn-checkout
rnf_invoice format
https://invoicing.request.network
https://github.com/RequestNetwork/invoicing-template
rnf_invoice format
https://codesandbox.io/p/sandbox/create-a-request-shffng?file=/app/page.tsx:43,1
https://codesandbox.io/p/sandbox/pay-a-request-dn7kcf?file=/app/page.tsx:71,1
https://codesandbox.io/p/sandbox/retrieve-a-users-requests-mqrjqy?file=/app/page.tsx:10,1
Request Finance Add Stakeholder
Web Component
Svelte
@requestnetwork/add-stakeholder
Svelte
Web Component
create-invoice.tsx
wagmiConfig.ts
wagmi
RainbowKit
initializeRN.ts
config.ts
context.tsx
currencies.ts
types.d.ts
SDK Demo Apps
A video showing the Request Checkout Payment Widget .
A video showing the Request Checkout Payment Widget integration.
A video showing the Request Invoicing template where you can create, pay, and list all your requests.
A video showing how to integrate the Request Invoicing web components into a Next.js application.
Request Injector Demo Video

Compute a Request ID without creating the request

/** Create request parameters */
export interface ICreateRequestParameters {
  requestInfo: RequestLogic.ICreateParameters | IRequestInfo;
  signer: Identity.IIdentity;
  paymentNetwork?: Payment.PaymentNetworkCreateParameters;
  topics?: any[];
  contentData?: any;
  disablePaymentDetection?: boolean;
  disableEvents?: boolean;
}
  
  /**
   * Gets the ID of a request without creating it.
   *
   * @param requestParameters Parameters to create a request
   * @returns The requestId
   */
  public async computeRequestId(
    parameters: Types.ICreateRequestParameters,
  ): Promise<RequestLogicTypes.RequestId>

Use your own signature mechanism

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>;
}

Example 1

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);

Encrypt with an Ethereum private key

Manipulating private keys must be done with care. Losing them can lead to a loss of data, privacy or non-repudiation safety!

For an introduction to Encryption and Decryption in Request Network, see Private Requests using Encryption

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.

Create an encrypted request

EthereumPrivateKeyCipherProvider

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

EthereumPrivateKeyDecryptionProvider is deprecated in favor of EthereumPrivateKeyCipherProvider

Get invoice information from its request ID

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,
} */

Accepting/canceling an invoice information

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);

Enabling/Disabling Decryption

// Disable decryption
cipherProvider.enableDecryption(false);
// Check if decryption is enabled
const isEnabled = cipherProvider.isDecryptionEnabled();
// Re-enable decryption
cipherProvider.enableDecryption(true);

Checking Capabilities

// 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
🕹️
▶️
ℹ️
✅
✅
✅
✅
✅
✅
✅
✅
✅
✅
✅
✅
✅
✅
✅
✅
Quickstart - Browser

Share an encrypted request

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...

Invoice Dashboard

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.

Installation

To install the component, use npm:

npm install @requestnetwork/invoice-dashboard

Usage

Usage in React and Next.js

Follow the instructions below to add the Invoice Dashboard to a React or Next.js app.

invoice-dashboard.tsx

Configure the invoice dashboard web component by creating a reference to it, setting its properties, and passing the reference as a prop.

wagmiConfig.ts

The web component supports any wallet connector module built on top of wagmi. This provides the flexibility to use any wagmi-compatible wallet connector, such as RainbowKit.

initializeRN.ts

Initialize the RequestNetwork object using an Ethers Signer or Viem WalletClient.

config.ts

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.

context.tsx

Use a context provider to reinitialize the Request Network instance when the wallet changes.

currencies.ts

A list of custom currencies to extend the default currency list.

types.d.ts

Specify types to avoid TypeScript errors.

Props

Prop
Type
Description

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

Supported Chains

Payments

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.

For an explanation of what each smart contract does, see Smart Contracts Overview

Chain
Chain Name
Chain ID
ERC20FeeProxy
EthereumFeeProxy
Erc20ConversionProxy
EthConversionProxy
BatchConversionPayments
ERC20SwapToPay
Erc20SwapToConversion
ERC20TransferableReceivable
ERC20EscrowToPay
ERC20Proxy
EthereumProxy
BatchPayments (deprecated)

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

Storage

These smart contracts facilitate storing IPFS content addressable hashes (CIDs) on-chain.

Chain
Chain ID
RequestHashStorage
RequestOpenHashSubmitter

Gnosis

100

Sepolia

11155111

Ethereum Mainnet (deprecated)

1

Goerli (deprecated)

5

REQ Token and Burn Mechanism

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.

Chain
Chain ID
lockForREQBurn
DaiBasedREQBurner
RequestToken

Ethereum Mainnet

1

Gnosis

100

Detect a payment

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.

📦
▶️
ℹ️
😉
RequestNetwork
payment-subgraphs

In-Memory Requests

Overview

In-memory requests allow for creating and managing requests without immediately persisting them to storage. This enables faster payment workflows and deferred persistence.

Key benefits:

  • 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.

How it works:

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.

1

Install necessary dependencies

To create in-memory requests, it is necessary to install the following package:

npm install @requestnetwork/request-client.js

Along with the following package for payments:

npm install @requestnetwork/payment-processor
2

Create an in-memory request

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);
3

Pay an in-memory request

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);
4

Persist in-memory request

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);

Payment Reference

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.

Support a new currency

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.

🕹️
▶️
🏗️
📦
ℹ️

Encrypt with a wallet signature using Lit Protocol

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.

Introduction

This implementation utilizes a two-step encryption process to secure sensitive data within requests:

  1. 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.

  2. 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.

For a deeper introduction to Encryption and Decryption in Request Network, see Private Requests using Encryption

Benefits

  • 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.

Architecture

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.

Workflow

Encryption Process

  1. Request Creation: The payer creates a request object using the Request Network SDK.

  2. Symmetric Key Generation: A unique symmetric key is randomly generated.

  3. Data Encryption: The payee and payer encrypt the sensitive data within the request using the generated symmetric key.

  4. 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.

  5. Store Encrypted Data: The payee and payer store the following on the Request Network:

    • Encrypted request data

    • Lit access control conditions

    • Encrypted symmetric key

Decryption Process

  1. Retrieve Request: The payer and payee retrieve the following request data from the Request Network:

    1. Encrypted request data

    2. Lit access control conditions

    3. Encrypted symmetric key

  2. 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.

  3. Decrypt Data: The payer and payee use the decrypted symmetric key to decrypt the sensitive data.

Installation

npm install @requestnetwork/lit-protocol-cipher @requestnetwork/request-client.js ethers@5.7.2

Usage

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
});

Creating Encrypted Requests

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
});

Decrypting Requests

// 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

// Disable decryption
litProvider.enableDecryption(false)

Decryption Requirements

  1. The wallet address must be included in the original encryption parameters

  2. Session signatures must be valid

  3. Decryption must be enabled

  4. The Lit Protocol client must be connected

Cleanup

// 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);
}

ICipherProvider Interface

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;
}

Single Request Forwarder

Overview

The Single Request Forwarder is a smart contract solution that enables integration with Request Network's payment system without modifying existing smart contracts.

The Single Request Forwarder Factory contact addresses can be found : Smart Contract Addresses

The contract name is SingleRequestProxyFactory

Key Benefits:

  • 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.

How it works:

  1. Request: Create a request in the Request Network protocol

  2. Deploy: Deploy a unique Single Request Forwarder for your request

  3. Pay: The Payer sends funds to the Single Request Forwarder

  4. Complete: The Single Request Forwarder forwards the payment to the Payee and emits an event to enable payment detection.

Integration Guide

Create a request

For a complete guide on request creation, see Create a request

const request = await requestClient.createRequest(requestCreateParameters);

const requestData = request.getData() 

// In case of in-memory request
 const requestData = request.inMemoryInfo.requestData

Deploy Single Request Forwarder

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

Pay through a Single Request Forwarder

Pay through a Single Request Forwarder using the RN SDK

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)

Pay through a Single Request Forwarder using a Direct Transfer

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)

Design Features:

  • 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.

Batch Payment

Functions:

Tests:

Swap-to-Conversion Payment

A "swap-to-conversion" payment is where the request is denominated in currency A, the payer sends currency B and the payee receives currency C.

Payment Processor Functions:

Payment Processor Test:

Payment Widget

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.

Installation

npm install @requestnetwork/payment-widget

Usage

Usage in React and Next.js

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);
      }}
    />
  );
}

Props

Prop
Type
Description

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

🕹️
▶️
🏗️
📦
ℹ️

Transferable Receivable Payment

Functions:

Tests:

Meta Payments

Tests:

Try it out

Demo Video

Integration Video

View on NPM

View Source

🕹️
▶️
🏗️
📦
ℹ️

Declarative Payment

Example:

Hinkal Private Payments

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.

Paying a request where the payment recipient address is a Hinkal private address is not supported because the Request Network payment proxy smart contracts can only send funds to public addresses. Consider using Declarative Payment instead.

Benefits

  • Privacy: Obfuscates payer address when paying a request.

  • Compliance: Ensures transactions adhere to regulatory requirements. See Hinkal Compliance for details

Supported Chains

See Hinkal Supported Chains for a list of chains on which Hinkal Private Payments are supported.

Installation

To use Hinkal Private Payments, install the necessary package:

npm install @requestnetwork/payment-processor

Usage

Pay a request from a Hinkal private address

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,
);

See Quickstart - Browser for how to instantiate a RequestNetwork and Signer

Deposit to a Hinkal private address

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.

Hinkal private addresses must be shared out-of-band. This SDK doesn't offer functions for sharing Hinkal private addresses.

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
})

See Quickstart - Browser for how to instantiate a Signer

Content Security Policy

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.

Details

For more details about Hinkal Private Payments, refer to Pull Request #1482 on GitHub.

Pay through a proxy-contract with a multisig

The imports you will need:

Pay ETH request

Pay ERC20 request

Approve ERC20 spending

Pay ERC20 request

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),
  );
};
Gnosis multisig

Escrow Payment

Escrow

The Request Network Escrow isn't a separate payment network. Rather, it builds on top of the ERC20_FEE_PROXY_CONTRACT payment network.

Typical Workflow

  1. Using the request-client.js package, the payer creates a request with the ERC20_FEE_PROXY_CONTRACT payment network.

  2. Using the payment-processor package, payer:

    1. Approves the escrow contract using approveErc20ForEscrow()

    2. Pays the escrow contract using payEscrow()

    3. Waits until the work is complete

    4. Pays the payee from the Escrow contract using payRequestFromEscrow()

These steps are shown by our unit tests:

https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/test/payment/erc20-escrow-payment.test.ts#L200-L339

Swap-to-Pay Payment

A "swap-to-pay" payment is where the payment sender sends one currency but the payment recipient receives a different currency.

Functions:

Tests:

Cover

Request Network API

  • Fast

  • Easy to Integrate

Cover

API Demo App: EasyInvoice

  • Try out Request Network

  • Create and Pay Invoices

EasyInvoice Create Invoice Page
EasyInvoice Dashboard
EasyInvoice Invoice Payment Page
EasyInvoice supports 50+ wallets via Reown AppKit
Create New Invoice page - Recurring Invoice Enabled
Invoice Dashboard - Recurring Invoice
EasyInvoice Direct Payment page
Create InvoiceMe Link page
Create Invoice via InvoiceMe Link
EasyInvioce Login Page
Typical Lifecycle of a Request
Request Scan Landing Page
Currency Selectors from the Request Invoicing app
Request Injector in Terminal
Screenshot of @requestnetwork/create-invoice-form 0.2.0
Screenshot of @requestnetwork/invoice-dashboard 0.3.0
Single Request Forwarder Payment Flow
Screenshot of @requestnetwork/payment-widget@0.1.0

Conversion Payment

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.

RequestNetwork

Description

The Request Network client.

Usage

import { RequestNetwork } from "@requestnetwork/request-client.js";

Constructor Paramters

Name
Type
Required?
Description

nodeConnectionConfig

recommended

Axios configurations

signatureProvider

recommended

Required to sign and create requests

decryptionProvider

optional

Required to retrieve encrypted requests

httpConfig

optional

Options for HTTP transport (timeout, delay, retry, etc.)

paymentOptions

optional

Payment detection options

useMockStorage

boolean

optional

Store ephemeral requests in local memory

currencies

CurrencyInput[]

optional

Custom currency list

currencyManager

ICurrencyManager

optional

Custom currency manager (will override currencies). A Currency manager handles a list of currencies and provides utility to retrieve and change format

Types and Interfaces

AxiosRequestConfig Properties

Name
Type
Required?
Description

baseUrl

string

recommended

Request Node URL

Many other properties...

optional

ISignatureProvider Implementations

Type
Description

Sign using a private key inside of a wallet

Sign using a private key outside of a wallet

IDecryptionProvider Implementations

Type
Description

Decrypt using a private key outside of a wallet

IHttpDataAccessConfig Properties

Name
Type
Required?
Description

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

PaymentNetworkOptions Properties

Name
Type
Required?
Description

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

Instance Methods

Name
Description

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

Mobile using Expo

Introduction

Installation

After creating a new Expo project, install the necessary dependencies:

Setup

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

Polyfills and Configurations

Crypto Polyfills

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?

  1. 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.

  2. 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.

Important Notes

  1. Ensure you're using compatible versions of React Native and Expo.

  2. The crypto polyfill may not cover all use cases. Test thoroughly and adjust as needed.

  3. Be cautious when handling private keys. Never expose them in your code or version control.

  4. The example code uses environment variables for private keys. Ensure you set these up correctly and securely.

request-client.js

Install

Exports

Streaming Payment

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.

Create a Streaming Request

Create the first request in a series

Create subsequent requests in a series

Tests

See Github for tests showing usage.

fromIdentity()

Description

Retrieve an array of requests from an Identity

Parameters

Returns

Types and Interfaces

Types.ITimestampBoundaries

options

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 .

Name
Type
Description

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.

Name
Type
Required?
Description

Promise<[]>

Name
Type
Required?
Description
Name
Type
Required?
Description
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

RequestNetwork

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

IIdentity

Identity type and value

updatedBetween

ITimestampBoundaries

Start time and end time

options

Object

Options

from

number (Unix timestamp)

Start time

to

number (Unix timestamp)

End time

disablePaymentDetection

boolean

Disable payment detection

disableEvents

boolean

Disable events

AxiosRequestConfig
ISignatureProvider
IDecryptionProvider
IHttpDataAccessConfig
PaymentNetworkOptions
Web3SignatureProvider
EthereumPrivateKeySignatureProvider
EthereumPrivateKeyDecryptionProvider
createRequest()
computeRequestId()
fromRequestId()
fromIdentity()
fromTopic()
here
payment-network-erc20-fee-proxy
payment-network-erc777-stream-0.1.0 specification
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/advanced-logic/test/extensions/payment-network/erc777/stream.test.ts
https://github.com/RequestNetwork/requestNetwork/tree/master/packages/payment-detection/test/erc777
Request
Create a request

fromRequestId()

Description

Retrieve a request from a request ID

Parameters

Name
Type
Required?
Description

requestId

string

The ID of the request

Object

Options

Returns

Promise<Request>

Types and Interfaces

options

Name
Type
Required?
Description

disablePaymentDetection

boolean

Disable payment detection

disableEvents

boolean

Disable events

createRequest()

Description

Create an unencrypted request

Parameters

Name
Type
Required?
Description

parameters

Parameters to create a request

options

Options to create a request

Returns

Promise<Request>

Types and Interfaces

ICreateRequestParameters

Name
Type
Required?
Description

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

ICreateRequestOptions

Name
Type
Requried
Description

skipRefresh

boolean

Disable the request refresh after creation. Warning: the balance will be null.

IRequestInfo

Name
Type
Required?
Description

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.

PaymentNetworkCreateParameters

Name
Type
Required
Description

id

Payment network ID

parameters

Payment network parameters. Contents depend on id

ICurrency

Name
Type
Required?
Description

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

Types.RequestLogic.CURRENCY

Name
Value

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.)

Types.Extension.PAYMENT_NETWORK_ID

Name
Value
Description

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.

Escrow payments use the ERC20_FEE_PROXY_CONTRACT payment network.

ICreationParameters

PnAnyDeclarative.ICreationParameters

Name
Type
Required
Description

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

PnAddressBased.ICreationParameters

Name
Type
Required
Description

paymentAddress

string

The payment recipient address

refundAddress

string

The refund recipient address

PnReferenceBased.ICreationParameters

Name
Type
Required
Description

salt

string

paymentNetworkName

ChainName

The chain name on which the payment will occur

PnFeeReferenceBased.ICreationParameters

Name
Type
Required
Description

feeAddress

string

The address to which fees will be sent

feeAmount

string

The fee amount in machine-readable integer units

PnAnyToAnyConversion.ICreationParameters

Name
Type
Required
Description

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

PnAnyToErc20.ICreationParameters

Name
Type
Required
Description

acceptedTokens

string[]

A list of token addresses accepted for payments and refunds

network

EvmChainName

the network of the tokens accepted for payments

PnAnyToEth.ICreationParameters

Identical to PnAnyToAnyConversion.ICreationParameters

PnStreamReferenceBased.ICreationParameters

Equal to IOriginalRequestCreationParameters OR ISubsequentRequestCreationParameters

IOriginalRequestCreationParameters

Name
Type
Required
Description

expectedFlowRate

string

expectedStartDate

string

ISubsequentRequestCreationParameters

Name
Type
Required
Description

previousRequestId

string

originalRequestId

string

recurrenceNumber

string

cancel()

Description

Cancel a request

Parameters

Returns

string |

Name
Type
Required?
Description

Promise<>

signerIdentity

IIdentity

required

The value returned by getRequestFromId()

refundInformation

any

optional

Depends on the payment network

options
ICreateRequestParameters
ICreateRequestOptions
IRequestInfo
IIdentity
PaymentNetworkCreateParameters
ICurrency
IIdentity
IIdentity
Types.Extension.PAYMENT_NETWORK_ID
ICreationParameters
Types.RequestLogic.CURRENCY
IIdentity
IIdentity
IRequestDataWithEvents

refresh()

Description

Refresh the request data and balance

Parameters

Name
Type
Required?
Description

requestAndMeta

IReturnGetRequestFromId

optional

The value returned by getRequestFromId()

Returns

Promise<IRequestDataWithEvents>

waitForConfirmation()

Description

Wait for a request to be persisted and indexed

Parameters

None

Returns

Promise<IRequestDataWithEvents>

getData()

Description

Unwrap the request contents

Parameters

None

Returns

Promise<IRequestDataWithEvents>

Request Network Token List

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.

Usage

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());

Token List Structure

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://..."
}

Adding a New Token

We welcome community contributions! To add a new token to the list:

  1. Fork the request-token-list repository on Github

  2. Add your token information to tokens/token-list.json

  3. Make sure your token meets our requirements (see CONTRIBUTING.md)

  4. Run tests locally: npm test

  5. Create a Pull Request

Request

Description

An object used to interact with requests. Returned by createRequest() and _createEncryptedRequest()

Properties

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.

Instance Methods

computeRequestId()

Description

Compute a request ID without actually creating a request

Parameters

Returns

string

accept()

Description

Accept a request

Parameters

Returns

fromTopic()

Description

Retrieve an array of requests from a topic

Parameters

Returns

Types and Interfaces

Types.ITimestampBoundaries

options

Name
Description

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...

Name
Type
Required?
Description
Name
Type
Required?
Description

Promise<>

Name
Type
Required?
Description

Promise<[]>

Name
Type
Required?
Description
Name
Type
Required?
Description

parameters

ICreateRequestParameters

Object used to create a new request

signerIdentity

IIdentity

Identity of the signer

signerIdentity

IIdentity

required

The value returned by getRequestFromId()

refundInformation

any

optional

Depends on the payment network

topic

string

Topic string

updatedBetween

iTimestampBoundaries

Start time and end time

options

Object

Options

from

number (Unix timestamp)

Start time

to

number (Unix timestamp)

End time

disablePaymentDetection

boolean

Disables payment detection

disableEvents

boolean

Disabled events

waitForConfirmation()
getData()
refresh()
cancel()
accept()
increaseExpectedAmountRequest()
reduceExpectedAmountRequest()
IRequestDataWithEvents
Request

increaseExpectedAmountRequest()

Description

Increase the expected amount

Parameters

Returns

Promise<IRequestDataWithEvents>

Name
Type
Required?
Description

deltaAmount

number | string

required

The amount by which to increase the expected amount

signerIdentity

required

The value returned by getRequestFromId()

refundInformation

any

optional

Depends on the payment network

IIdentity

reduceExpectedAmountRequest()

Description

Reduce the expected amount

Parameters

Returns

Promise<IRequestDataWithEvents>

Name
Type
Required?
Description

deltaAmount

number | string

required

The amount by which to reduce the expected amount

signerIdentity

required

The value returned by getRequestFromId()

paymentInformation

any

optional

Depends on the payment network

IIdentity

IIdentity

Description

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.

Types.Identity.TYPE

Name
Value
Description

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.

Name
Type
Required
Description

type

Identity type

value

string

Identity address

Types.Identity.TYPE

payRequest()

Usage

import { payRequest } from "@requestnetwork/payment-processor";

Parameters

Name
Type
Required?
Description

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

Returns

Promise<ethers.ContractTransaction>

This is what ethers returns after submitting a transaction.

Types and Interfaces

IConversionPaymentSettings

Name
Type
Required?
Description

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.

IRequestData
ConversionPaymentSettings
Pay a request

PaymentReferenceCalculator

Description

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.

Usage

import { PaymentReferenceCalculator } from "@requestnetwork/request-client.js";

Static method: calculate()

Parameters

Name
Type
Required
Description

requestId

string

The ID of the request

salt

string

The salt of the request

address

string

Payment recipient address

Returns

string

Implementation

payment-processor

Install

npm install @requestnetwork/payment-processor

Exports

Name
Type
Description

Function

Pay a request

payRequest()

web3-signature

Install

npm install @requestnetwork/web3-signature

Exports

Name
Type
Description

Constructor

Sign using a private key inside of a wallet

Web3SignatureProvider

IRequestDataWithEvents

Description

The request contents. Returned by waitForConfirmation(), getData() and many other methods.

Instance Methods

Name
Description

on()

Event subscriber

emit()

Event emitter

Inherited Properties

IRequestData

Name
Type
Required?
Description

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

IRequest

Name
Type
Required?
Description

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.

Types and Interfaces

Types.RequestLogic.STATE

Name
Value

PENDING

'pending'

CREATED

'created'

ACCEPTED

'accepted'

CANCELED

'canceled'

IBalanceWithEvents

Name
Type
Required?
Description

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

IPaymentNetworkEvent

Name
Type
Required?
Description

amount

string

The amount of the detected payment

parameters

TEventParameters

Depends on the Payment Network ID

ICurrency

Name
Type
Required?
Description

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

Types.RequestLogic.CURRENCY

Name
Value

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 .

[]

IBalanceWithEvents
ICurrency
waitForConfirmation()
IIdentity
Types.RequestLogic.STATE
IIdentity
IIdentity
IPaymentNetworkEvent
Types.RequestLogic.CURRENCY

Web3SignatureProvider

Description

Sign using a private key inside of a wallet

Usage

import { Web3SignatureProvider } from "@requestnetwork/web3-signature";

Constructor Paramters

Name
Type
Required?
Description

web3Provider

ethers.Provider

An ethers v5 Provider or viem WalletClient

epk-decryption

Install

npm install @requestnetwork/epk-decryption

epk-signature

Install

npm install @requestnetwork/epk-signature

Exports

Name
Type
Description

Constructor

EthereumPrivateKeySignatureProvider

EthereumPrivateKeyDecryptionProvider

Description

Decrypt using a private key outside of a wallet

Usage

import { EthereumPrivateKeyDecryptionProvider } from "@requestnetwork/epk-decryption";

Constructor Parameters

Name
Type
Required?
Description

decryptionParameters

Decryption method and private key

Types and Interfaces

IDecryptionParameters

Name
Type
Required?
Description

method

Decryption method

key

string

Private key

Types.Decryption.METHOD

Name
Value

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.

IDecryptionParameters
Types.Decryption.METHOD

SDK and Request Node Overview

Request Network SDK

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

Request Node

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.

Interaction between SDK and Request Node

Shown below is a diagram that depicts how the SDK and Node interact with the protocol.

Interacting with Request Network via a Request Node

EthereumPrivateKeySignatureProvider

Description

Sign using a private key outside of a wallet

Usage

import { EthereumPrivateKeySignatureProvider } from "@requestnetwork/epk-signature";

Constructor Paramters

Name
Type
Required?
Description

signatureParameter

Signing method and private key

Types and Interfaces

ISignatureParameters

Name
Type
Required?
Description

method

Signing method

privateKey

string

Private key

Types.Signature.METHOD

Name
Value

ECDSA

'ecdsa'

"Vanilla" ECDSA

ECDSA_ETHEREUM

'ecdsa-ethereum'

Ethereum ECDSA with the prefix "\x19Ethereum Signed Message:\n"

ISignatureParameters
Types.Signature.METHOD

Payment Networks

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:

  1. Information required at request creation to enable payment detection

  2. The payment method

  3. The process for determining the balance (amount paid)

Reference-based Payment Networks (Recommended)

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:

  1. They forward the payment to the payment recipient's address.

  2. 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.

ERC20 Fee Proxy Contract

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

ETH Fee Proxy Contract ("Native Payment")

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

Any-to-ERC20 Proxy Contract "ERC20 Conversion Payments"

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

Any-to-ETH Proxy Contract "Native Conversion Payments"

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

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

Declarative Payment Network

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

Meta Payment Network

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 Stream Payment Network "Streaming Payment"

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

Other Payment Networks

Address-based Payment Networks (Not Recommended)

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.

ETH Input Data Payment Network (Deprecated)

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.

ERC20 Proxy and Ethereum Proxy (Superseded)

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

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

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.

ERC20 Swap-to-Pay Payments

Swap-to-Pay payments execute a swap immediately before routing the payment through the ERC20 Fee Proxy payment network.

Swap-to-Pay is different from Conversion. For details see Difference between Conversion, Swap-to-Pay, and Swap-to-Conversion

See Swap-to-Pay Payment for additional details

ERC20 Swap-to-Conversion

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.

Swap-to-Pay is different from Conversion. For details see Difference between Conversion, Swap-to-Pay, and Swap-to-Conversion

ERC20 Escrow Payment

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.

Difference between Conversion, Swap-to-Pay, and Swap-to-Conversion

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.

Private Requests using Encryption

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.

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.

Decryption

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.

Similarity to HTTPS

The privacy and encryption mechanism used by Request Network has many similarities to HTTPS. If you're not familiar with HTTPS, we recommend reading

HTTPS explained with carrier pigeons
Request encryption overview
Request decryption overview

Data-access

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

Blocks

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.

Local cache for accessing transaction

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.

Smart Contracts Overview

This page is missing the RequestToken, DAIbasedREQBurner, lockForREQBurn, ChainlinkConversionPath contracts

Contracts Overview

Request Network smart contracts are available here.

Contracts type

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.

Storage

RequestOpenHashSubmitter

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.

StorageFeeCollector

Manages the fees for the creation of a request.

RequestHashStorage

This contract is the entry point to retrieve all the hashes of the request network system.

Payments

ERC20FeeProxy

Performs an ERC20 token transfer with a payment reference and a transfer to a second address for the payment of a fee.

ERC20Proxy

Performs an ERC20 token transfer with a payment reference and a transfer to a second address.

EthereumFeeProxy

This contract performs an Ethereum transfer with a Fee sent to a third address and stores a reference.

EthereumProxy

This contract performs an Ethereum transfer sent to a third address and stores a reference.

ERC20EscrowToPay

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.

BatchConversionPayments

This contract makes multiple conversion payments with a payment references, in one transaction.

BatchNoConversionPayments

This contract makes multiple payments with payment references, in one transaction.

This contract makes multiple payments with references, in one transaction, without conversion.

ERC20SwapToPay

This contract swaps ERC20 tokens before paying a request such that the payer sends currency A, but payee receives currency B.

Swap-to-Pay is different from Conversion. For details see Difference between Conversion, Swap-to-Pay, and Swap-to-Conversion

ERC20ConversionProxy

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.

ETHConversionProxy

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.

ERC20SwapToConversion

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.

ERC20TransferableReceivable

This contract allows minting requests as NFTs thus allowing them to be transferred. The owner of the request NFT receives the payment.

ERC20SingleRequestProxy

A contract that allows payment through the ERC20FeeProxy without having to make a function call.

EthereumSingleRequestProxy

A contract that allows payment through EthereumFeeProxy without having to make a function call.

SingleRequestProxyFactory

A factory smart contract responsible for deploying ERC20SingleRequestProxy and EthereumSingleRequestProxy contracts.

Internal SDK Architecture

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.

Overview

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.

Interface vs implementation

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.

Transaction

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

Encryption

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

Request Logic

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

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

Signature

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.

Advanced Logic

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.

Layers of the Request Protocol, each layer is described in the next section.
The different steps to encrypt the transaction

The specification for each extension can be found at this link:

https://github.com/RequestNetwork/requestNetwork/tree/master/packages/request-logic
epk-signature
web3-signature
https://github.com/RequestNetwork/requestNetwork/tree/master/packages/advanced-logic/specs
Example of a request in Request Logic represented by a list of actions
Example of a request with extension data: the payee creates a request with content data and declarative payment information, the payer accepts the request and declares a sent payment in the same time, and finally, the payee declares the received payment

Storage

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

IPFS

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.

Ethereum

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.

https://github.com/RequestNetwork/quickstart-node-js/blob/main/src/createRequest.js
https://github.com/RequestNetwork/quickstart-node-js/blob/main/src/payRequest.js
https://github.com/RequestNetwork/quickstart-node-js/blob/main/src/retrieveRequest.js

Get compliance status for a user

get

Retrieves the comprehensive compliance status for a specific user, including KYC and agreement status.

Path parameters
clientUserIdstringRequired

The client user ID to check compliance status for

Example: user-123
Header parameters
x-api-keystringRequired

API key for authentication

Responses
200
Compliance status retrieved successfully
application/json
401
Unauthorized
404
User not found
application/json
429
Too Many Requests
get

Get payment details for a user

get

Retrieves the registered bank account details for a user. Optionally filter by payment details ID.

Path parameters
clientUserIdstringRequired

The client user ID to get payment details for

Example: user-123
Query parameters
paymentDetailsIdstringOptional

Optional ID of specific payment details to retrieve

Example: fa898aec-519c-46be-9b4c-e76ef4ff99d9
Header parameters
x-api-keystringRequired

API key for authentication

Responses
200
Payment details retrieved successfully
application/json
401
Unauthorized
404
User or payment details not found
application/json
429
Too Many Requests
get

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));
})();

Create compliance data for a user

post

Checks compliance status and returns necessary URLs for completing compliance.

Header parameters
x-api-keystringRequired

API key for authentication

Body
clientUserIdstring · min: 1Required

Client User ID

emailstring · emailRequired

Email

firstNamestring · min: 1Required

First Name

lastNamestring · min: 1Required

Last Name

beneficiaryTypestring · enumRequiredPossible values:
companyNamestringOptional

Company Name

dateOfBirthstringRequired

Date of birth in YYYY-MM-DD format

Pattern: ^\d{4}-\d{2}-\d{2}$
addressLine1string · min: 1Required

Address Line 1

addressLine2stringOptional

Address Line 2

citystring · min: 1Required

City

statestring · min: 1Required

State

postcodestring · min: 1Required

Postcode

countrystring · min: 2 · max: 2Required

Country

nationalitystring · min: 2 · max: 2Required

Nationality

phonestringRequired

Phone in E.164 format

Pattern: ^\+?[1-9]\d{1,14}$
ssnstring · min: 1Required

Social Security Number

sourceOfFundsstringOptional

Source of Funds

businessActivitystringOptional

Business Activity

Responses
200
Compliance data retrieved successfully
application/json
Responseany
400
Invalid request data
application/json
401
Unauthorized
404
Request not found
application/json
429
Too Many Requests
post
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 agreement status

patch

Update the agreement completion status for a user.

Path parameters
clientUserIdstringRequired

The client user ID to update

Example: user-123
Header parameters
x-api-keystringRequired

API key for authentication

Body
agreementCompletedbooleanRequired
Responses
200
Compliance status updated successfully
application/json
400
Invalid request data
application/json
401
Unauthorized
404
User not found
application/json
429
Too Many Requests
patch
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

post

Create payment details for a user

Path parameters
clientUserIdstringRequired

The client user ID

Example: user-123
Header parameters
x-api-keystringRequired

API key for authentication

Body
bankNamestring · min: 1Required

Name of the bank

accountNamestring · min: 1Required

Name of the account holder

accountNumberstringOptional

Bank account number

routingNumberstringOptional

Bank routing number (US)

beneficiaryTypestring · enumRequired

Type of beneficiary

Possible values:
currencystring · min: 3 · max: 3Required

Three-letter currency code (ISO 4217)

addressLine1string · min: 1Required

Primary address line

addressLine2stringOptional

Secondary address line

citystring · min: 1Required

City name

statestringOptional

State or province code

countrystring · min: 2 · max: 2Required

Two-letter country code (ISO 3166-1 alpha-2)

dateOfBirthstringRequired

Date of birth in YYYY-MM-DD format

Pattern: ^\d{4}-\d{2}-\d{2}$
postalCodestring · min: 1Required

Postal or ZIP code

railsstring · enumOptional

Payment rail type

Default: localPossible values:
sortCodestringOptional

UK bank sort code

ibanstringOptional

International Bank Account Number

swiftBicstringOptional

SWIFT/BIC code

documentNumberstringOptional

Government-issued ID number

documentTypestringOptional

Type of government-issued ID (e.g., passport, driver's license)

accountTypestring · enumOptional

Type of bank account

Possible values:
ribNumberstringOptional

French RIB number

bsbNumberstringOptional

Australian BSB number

nccstringOptional

New Zealand NCC number

branchCodestringOptional

Bank branch code

bankCodestringOptional

Bank code

ifscstringOptional

Indian Financial System Code

Responses
201
Payment details created successfully
application/json
400
Invalid request data
application/json
401
Unauthorized
404
User not found
application/json
429
Too Many Requests
post
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 request

post

Create a new payment request

Header parameters
x-api-keystringRequired

API key for authentication

Body
payerstringOptional

The wallet address of the payer

payeestringRequired

The wallet address of the payee

amountstringRequired

The payable amount of the invoice, in human readable format

invoiceCurrencystringRequired

Invoice Currency ID, from the Request Network Token List e.g: USD

paymentCurrencystringRequired

Payment currency ID, from the Request Network Token List e.g: ETH-sepolia-sepolia

isCryptoToFiatAvailablebooleanOptional

Whether crypto-to-fiat payment is available for this request

Responses
201
Request created successfully
application/json
401
Unauthorized
404
Wallet not found
429
Too Many Requests
post
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"
}
https://github.com/RequestNetwork/requestNetwork/tree/master/packages/ethereum-storage
https://ipfs.io
GitHub

Get request status

get

Get the status of a payment request

Path parameters
requestIdstringRequired

The requestId for the request

Example: 01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
Header parameters
x-api-keystringRequired

API key for authentication

Responses
200
Request status retrieved successfully
application/json
Responseany
401
Unauthorized
404
Request not found
429
Too Many Requests
get
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

patch

Update a recurring request

Path parameters
requestIdstringRequired

The requestId for the request

Example: 01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
Header parameters
x-api-keystringRequired

API key for authentication

Responses
200
Recurrence updated successfully
401
Unauthorized
404
Request not found
429
Too Many Requests
patch
PATCH /v2/request/{requestId} HTTP/1.1
Host: api.request.network
x-api-key: text
Accept: */*

No content

Get payment calldata

get

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.

Path parameters
requestIdstringRequired

The requestId of the request

Example: 01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
Query parameters
walletstringOptional

The wallet address of the payer.

Example: 0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
chainstring · enumOptional

The source chain of the cross chain payment

Possible values:
tokenstring · enumOptional

The source token of the cross chain payment

Possible values:
amountstringOptional

The amount to pay, in human readable format

clientUserIdstringOptional

Optional client user ID for off-ramp payments

Example: user-123
paymentDetailsIdstringOptional

Optional payment details ID for off-ramp payments

Example: fa898aec-519c-46be-9b4c-e76ef4ff99d9
feePercentagestringOptional

Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)

Example: 0.02
feeAddressstringOptional

Address to receive the fee

Example: 0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
Header parameters
x-api-keystringRequired

API key for authentication

Responses
200
Payment calldata retrieved successfully
application/json
400
Validation failed
application/json
401
Unauthorized
404
Request not found
429
Too Many Requests
get
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 payment routes

get

Get the payment routes for a request

Path parameters
requestIdstringRequired

The requestId of the request

Example: 01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
Query parameters
walletstringRequired

The wallet address of the payer

Example: 0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
amountstringOptional

The amount to pay, in human readable format

feePercentagestringOptional

Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)

feeAddressstringOptional

Address to receive the fee

Header parameters
x-api-keystringRequired

API key for authentication

Responses
200
Available payment routes
application/json
400
Invalid or missing wallet address
401
Unauthorized
404
Request not found
429
Too Many Requests
get
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 payment calldata

get

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.

Path parameters
requestIdstringRequired

The requestId of the request

Example: 01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb
Query parameters
walletstringOptional

The wallet address of the payer.

Example: 0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
chainstring · enumOptional

The source chain of the cross chain payment

Possible values:
tokenstring · enumOptional

The source token of the cross chain payment

Possible values:
amountstringOptional

The amount to pay, in human readable format

clientUserIdstringOptional

Optional client user ID for off-ramp payments

Example: user-123
paymentDetailsIdstringOptional

Optional payment details ID for off-ramp payments

Example: fa898aec-519c-46be-9b4c-e76ef4ff99d9
feePercentagestringOptional

Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)

Example: 0.02
feeAddressstringOptional

Address to receive the fee

Example: 0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7
Header parameters
x-api-keystringRequired

API key for authentication

Responses
200
Payment calldata retrieved successfully
application/json
400
Validation failed
application/json
401
Unauthorized
404
Request not found
429
Too Many Requests
get
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 request

post

Create a new payment request

Header parameters
x-api-keystringRequired

API key for authentication

Body
payerstringOptional

The wallet address of the payer

payeestringRequired

The wallet address of the payee

amountstringRequired

The payable amount of the invoice, in human readable format

invoiceCurrencystringRequired

Invoice Currency ID, from the Request Network Token List e.g: USD

paymentCurrencystringRequired

Payment currency ID, from the Request Network Token List e.g: ETH-sepolia-sepolia

isCryptoToFiatAvailablebooleanOptional

Whether crypto-to-fiat payment is available for this request

Responses
201
Request created successfully
application/json
401
Unauthorized
404
Wallet not found
429
Too Many Requests
post
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

post

Initiate a payment without having to create a request first

Header parameters
x-api-keystringRequired

API key for authentication

Body
payeestringRequired

The wallet address of the payee

amountstringRequired

The payable amount of the invoice, in human readable format

invoiceCurrencystringRequired

Invoice Currency ID, from the Request Network Token List e.g: USD

paymentCurrencystringRequired

Payment currency ID, from the Request Network Token List e.g: ETH-sepolia-sepolia

feePercentagestringOptional

Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)

feeAddressstringOptional

Address to receive the fee

Responses
201
Request created and payment initiated successfully
application/json
Responseany
401
Unauthorized
404
Wallet not found
429
Too Many Requests
post
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

post

Send a payment intent

Path parameters
paymentIntentIdstringRequired

The payment intent ID

Example: 01JNZYZPK7B4YBPD44TM72NDNJ
Header parameters
x-api-keystringRequired

API key for authentication

Body
Responses
401
Unauthorized
404
Payment intent data not found
429
Too Many Requests
post
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

Pay multiple requests in one transaction

post

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.

Header parameters
x-api-keystringRequired

API key for authentication

Body
payeestringRequired

The wallet address of the payee

amountstringRequired

The payable amount of the invoice, in human readable format

invoiceCurrencystringRequired

Invoice Currency ID, from the Request Network Token List e.g: USD

paymentCurrencystringRequired

Payment currency ID, from the Request Network Token List e.g: ETH-sepolia-sepolia

feePercentagestringOptional

Fee percentage to apply at payment time (e.g., '2.5' for 2.5%)

feeAddressstringOptional

Address to receive the fee

Responses
201
Batch payment calldata retrieved successfully
application/json
400
Requests must be on the same network
application/json
401
Unauthorized
429
Too Many Requests
post
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"
    }
  }
}
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/wagmiConfig.ts
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,
});
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/wagmiConfig.ts
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,
});
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/config.ts
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",
  },
};
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/config.ts
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",
  },
};
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/initializeRN.ts
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);
  }
};
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/initializeRN.ts
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);
  }
};
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/pages/create-invoice.tsx
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>
    </>
  );
}
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/context.tsx
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;
};
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/context.tsx
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;
};
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/currencies.ts
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,
  },
];
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/utils/currencies.ts
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,
  },
];
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/src/payment/batch-conversion-proxy.ts
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;
}
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/pages/index.tsx
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>
    </>
  );
}
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/src/payment/swap-conversion-erc20.ts
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,
  };
}
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/test/payment/batch-proxy.test.ts
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'));
      });
    });
  });
});
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/types.d.ts
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;
}
https://github.com/RequestNetwork/invoicing-template/blob/01e44755b274d17c0718ca03f077d68a9fe8baec/types.d.ts
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;
}
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/test/payment/swap-any-to-erc20.test.ts
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(),
      );
    });
  });
});
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts
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();
    });
  });
});
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/src/payment/erc20-transferable-receivable.ts
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,
  ]);
}
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/advanced-logic/test/extensions/payment-network/meta.test.ts
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`);
      });
    });
  });
});
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/src/payment/swap-erc20-fee-proxy.ts
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),
  ]);
}
https://github.com/RequestNetwork/quickstart-node-js/blob/main/src/declarePaymentSentAndReceived.js
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,
      ),
  );
})();
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts
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(),
      );
    });
  });
});
https://github.com/RequestNetwork/rn-expo-support/blob/5a771c31051ce89c4feee533692028d45e1415ab/index.js
// 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";
https://github.com/RequestNetwork/rn-expo-support/blob/5a771c31051ce89c4feee533692028d45e1415ab/cryptoPolyfill.js
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;
https://github.com/RequestNetwork/rn-expo-support/blob/5a771c31051ce89c4feee533692028d45e1415ab/metro.config.js
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;
https://github.com/RequestNetwork/rn-expo-support/blob/5a771c31051ce89c4feee533692028d45e1415ab/package.json
{
  "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"
  }
}
https://github.com/RequestNetwork/requestNetwork/blob/master/packages/payment-detection/src/payment-reference-calculator.ts
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);
}