Request Network Docs
WebsiteGithubStatusDiscord
  • Request Network Docs
  • Request Network API
    • Create and Pay Requests
    • Crosschain Payments
    • EasyInvoice: API Demo App
    • API Portal: Manage API Keys and Webhooks
      • Manage API Keys and Webhooks programmatically
    • Full API Reference
  • General
    • Lifecycle of a Request
    • Request Scan
    • Supported Chains
      • Smart Contract Addresses
    • Request Network Token List
  • Advanced
    • Request Network SDK
      • Get Started
        • Quickstart - Browser
        • Quickstart - Node.js
        • Installation
        • SDK Injector
        • Request Node Gateways
      • SDK Demo Apps
        • Request Invoicing
          • Pay from Safe Multisig
        • Request Checkout
        • Components
          • Create Invoice Form
          • Invoice Dashboard
          • Payment Widget
          • Add Stakeholder
      • SDK Guides
        • Request Client
          • Configure the Request Client
          • Updating a Request
          • Payment Reference
          • Compute a Request ID without creating the request
          • Use your own signature mechanism
          • Support a new currency
          • In-Memory Requests
        • Encryption and Decryption
          • Encrypt with a wallet signature using Lit Protocol
          • Encrypt with an Ethereum private key
          • Share an encrypted request
        • Payment
          • Detect a payment
          • Native Payment
          • Conversion Payment
          • Declarative Payment
          • Configuring Payment Fees
          • Single Request Forwarder
          • Batch Payment
          • Swap-to-Pay Payment
          • Swap-to-Conversion Payment
          • Transferable Receivable Payment
          • Meta Payments
          • Escrow Payment
          • Streaming Payment
          • Pay through a proxy-contract with a multisig
          • Hinkal Private Payments
        • Mobile using Expo
      • SDK Reference
        • request-client.js
          • RequestNetwork
            • createRequest()
            • computeRequestId()
            • fromRequestId()
            • fromIdentity()
            • fromTopic()
          • Request
            • waitForConfirmation()
            • getData()
            • refresh()
            • cancel()
            • accept()
            • increaseExpectedAmountRequest()
            • reduceExpectedAmountRequest()
          • IIdentity
          • IRequestDataWithEvents
          • PaymentReferenceCalculator
        • payment-processor
          • payRequest()
        • web3-signature
          • Web3SignatureProvider
        • epk-signature
          • EthereumPrivateKeySignatureProvider
        • epk-decryption
          • EthereumPrivateKeyDecryptionProvider
    • Protocol Overview
      • SDK and Request Node Overview
      • Payment Networks
      • Private Requests using Encryption
      • Smart Contracts Overview
    • Internal SDK Architecture
      • Request Logic
      • Advanced Logic
      • Transaction
      • Data-access
      • Storage
      • Data flow
      • Request IPFS network
  • FAQ
  • Glossary
  • Contributing
Powered by GitBook
On this page
  • Functions:
  • Tests:

Was this helpful?

Edit on GitHub
Export as PDF
  1. Advanced
  2. Request Network SDK
  3. SDK Guides
  4. Payment

Swap-to-Pay Payment

PreviousBatch PaymentNextSwap-to-Conversion Payment

Last updated 2 months ago

Was this helpful?

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

Functions:

Tests:

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