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
  • Payment Processor Functions:
  • Payment Processor Test:

Was this helpful?

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

Swap-to-Conversion Payment

PreviousSwap-to-Pay PaymentNextTransferable Receivable Payment

Last updated 2 months ago

Was this helpful?

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:

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