Request Network Docs
WebsiteGithubStatusDiscord
  • Request Network Docs
  • Request Network API
    • Create and Pay Requests
    • Crosschain Payments
    • Crypto-to-fiat Payments
    • EasyInvoice: API Demo App
    • API Portal: Manage API Keys and Webhooks
    • 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

Transferable Receivable Payment

PreviousSwap-to-Conversion PaymentNextMeta Payments

Last updated 7 days ago

Was this helpful?

Functions:

Tests:

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