Transferable Receivable Payment
Last updated
Last updated
import { Wallet, BigNumber, providers, utils } from 'ethers';
import {
ClientTypes,
ExtensionTypes,
IdentityTypes,
RequestLogicTypes,
} from '@requestnetwork/types';
import { deepCopy } from '@requestnetwork/utils';
import { Erc20PaymentNetwork, PaymentReferenceCalculator } from '@requestnetwork/payment-detection';
import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types';
import { approveErc20, getErc20Balance } from '../../src/payment/erc20';
import {
getReceivableTokenIdForRequest,
mintErc20TransferableReceivable,
payErc20TransferableReceivableRequest,
} from '../../src/payment/erc20-transferable-receivable';
import { getProxyAddress } from '../../src/payment/utils';
/* eslint-disable no-magic-numbers */
/* eslint-disable @typescript-eslint/no-unused-expressions */
const erc20ContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40';
const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat';
const feeAddress = '0x75c35C980C0d37ef46DF04d31A140b65503c0eEd';
const provider = new providers.JsonRpcProvider('http://localhost:8545');
const payeeWallet = Wallet.createRandom().connect(provider);
const thirdPartyWallet = Wallet.createRandom().connect(provider);
const wallet = Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/1").connect(provider);
const paymentAddress = payeeWallet.address;
const validRequest: ClientTypes.IRequestData = {
balance: {
balance: '0',
events: [],
},
contentData: {},
creator: {
type: IdentityTypes.TYPE.ETHEREUM_ADDRESS,
value: wallet.address,
},
currency: 'DAI',
currencyInfo: {
network: 'private',
type: RequestLogicTypes.CURRENCY.ERC20,
value: erc20ContractAddress,
},
events: [],
expectedAmount: '100',
extensions: {
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]: {
events: [],
id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
feeAddress,
feeAmount: '0',
paymentAddress,
salt: '0ee84db293a752c6',
},
version: '0.2.0',
},
},
payee: {
type: IdentityTypes.TYPE.ETHEREUM_ADDRESS,
value: paymentAddress,
},
extensionsData: [],
meta: {
transactionManagerMeta: {},
},
pending: null,
requestId: '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e1',
state: RequestLogicTypes.STATE.CREATED,
timestamp: 0,
version: '1.0',
};
describe('erc20-transferable-receivable', () => {
beforeAll(async () => {
// Send funds to payeeWallet
let tx = {
to: paymentAddress,
// Convert currency unit from ether to wei
value: utils.parseEther('1'),
};
let txResponse = await wallet.sendTransaction(tx);
await txResponse.wait(1);
// Send funds to thirdPartyWallet
tx = {
to: thirdPartyWallet.address,
// Convert currency unit from ether to wei
value: utils.parseEther('1'),
};
txResponse = await wallet.sendTransaction(tx);
await txResponse.wait(1);
const mintTx = await mintErc20TransferableReceivable(validRequest, payeeWallet, {
gasLimit: BigNumber.from('20000000'),
});
const confirmedTx = await mintTx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(mintTx.hash).not.toBeUndefined();
});
beforeEach(() => {
jest.restoreAllMocks();
});
describe('mintErc20TransferableReceivable works', () => {
it('rejects paying without minting', async () => {
// Different request without a minted receivable
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
// Change the request id
request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e2';
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'The receivable for this request has not been minted yet. Please check with the payee.',
);
});
});
describe('payErc20TransferableReceivableRequest', () => {
it('should throw an error if the request is not erc20', async () => {
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
request.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH;
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'request cannot be processed, or is not an pn-erc20-transferable-receivable request',
);
});
it('should throw an error if the currencyInfo has no value', async () => {
const request = deepCopy(validRequest);
request.currencyInfo.value = '';
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'request cannot be processed, or is not an pn-erc20-transferable-receivable request',
);
});
it('should throw an error if the payee is undefined', async () => {
const request = deepCopy(validRequest);
request.payee = undefined;
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'Expected a payee for this request',
);
});
it('should throw an error if currencyInfo has no network', async () => {
const request = deepCopy(validRequest);
// @ts-expect-error Type '""' is not assignable to type 'ChainName | undefined'
request.currencyInfo.network = '';
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'Payment currency must have a network',
);
});
it('should throw an error if request has no extension', async () => {
const request = deepCopy(validRequest);
request.extensions = [] as any;
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'PaymentNetwork not found',
);
});
it('should consider override parameters', async () => {
const spy = jest.fn();
const originalSendTransaction = wallet.sendTransaction.bind(wallet);
wallet.sendTransaction = spy;
await payErc20TransferableReceivableRequest(validRequest, wallet, undefined, undefined, {
gasPrice: '20000000000',
});
const shortReference = PaymentReferenceCalculator.calculate(
validRequest.requestId,
validRequest.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]
.values.salt,
paymentAddress,
);
const tokenId = await getReceivableTokenIdForRequest(validRequest, wallet);
expect(tokenId.isZero()).toBe(false);
expect(spy).toHaveBeenCalledWith({
data: `0x314ee2d900000000000000000000000000000000${utils
.hexZeroPad(tokenId.toHexString(), 16)
.substring(
2,
)}000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000075c35c980c0d37ef46df04d31a140b65503c0eed0000000000000000000000000000000000000000000000000000000000000008${shortReference}000000000000000000000000000000000000000000000000`,
gasPrice: '20000000000',
to: '0xF426505ac145abE033fE77C666840063757Be9cd',
value: 0,
});
wallet.sendTransaction = originalSendTransaction;
});
it('should pay an ERC20 transferable receivable request with fees', async () => {
// first approve the contract
const approvalTx = await approveErc20(validRequest, wallet);
const approvalTxReceipt = await approvalTx.wait(1);
expect(approvalTxReceipt.status).toBe(1);
expect(approvalTx.hash).not.toBeUndefined();
// get the balance to compare after payment
const balanceEthBefore = await wallet.getBalance();
const balanceErc20Before = await getErc20Balance(validRequest, payeeWallet.address, provider);
const tx = await payErc20TransferableReceivableRequest(validRequest, wallet, 1, 0, {
gasLimit: BigNumber.from('20000000'),
});
const confirmedTx = await tx.wait(1);
const balanceEthAfter = await wallet.getBalance();
const balanceErc20After = await getErc20Balance(validRequest, payeeWallet.address, provider);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
expect(balanceEthAfter.lte(balanceEthBefore)).toBeTruthy(); // 'ETH balance should be lower'
// ERC20 balance should be lower
expect(
BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)),
).toBeTruthy();
});
it('other wallets can mint receivable for owner', async () => {
// Request without a receivable minted yet
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
// Change the request id
request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e3';
const mintTx = await mintErc20TransferableReceivable(request, thirdPartyWallet, {
gasLimit: BigNumber.from('20000000'),
});
let confirmedTx = await mintTx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(mintTx.hash).not.toBeUndefined();
// get the balance to compare after payment
const balanceErc20Before = await getErc20Balance(request, payeeWallet.address, provider);
const tx = await payErc20TransferableReceivableRequest(request, wallet, 1, 0, {
gasLimit: BigNumber.from('20000000'),
});
confirmedTx = await tx.wait(1);
const balanceErc20After = await getErc20Balance(request, payeeWallet.address, provider);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
// ERC20 balance should be lower
expect(
BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)),
).toBeTruthy();
});
it('rejects paying unless minted to correct owner', async () => {
// Request without a receivable minted yet
const request = deepCopy(validRequest) as ClientTypes.IRequestData;
// Change the request id
request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e4';
let shortReference = PaymentReferenceCalculator.calculate(
request.requestId,
request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE].values
.salt,
paymentAddress,
);
let receivableContract = ERC20TransferableReceivable__factory.createInterface();
let data = receivableContract.encodeFunctionData('mint', [
thirdPartyWallet.address,
`0x${shortReference}`,
'100',
erc20ContractAddress,
]);
let tx = await thirdPartyWallet.sendTransaction({
data,
to: getProxyAddress(
request,
Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation,
),
value: 0,
});
let confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError(
'The receivable for this request has not been minted yet. Please check with the payee.',
);
// Mint the receivable for the correct paymentAddress
shortReference = PaymentReferenceCalculator.calculate(
request.requestId,
request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE].values
.salt,
paymentAddress,
);
receivableContract = ERC20TransferableReceivable__factory.createInterface();
data = receivableContract.encodeFunctionData('mint', [
paymentAddress,
`0x${shortReference}`,
'100',
erc20ContractAddress,
]);
tx = await thirdPartyWallet.sendTransaction({
data,
to: getProxyAddress(
request,
Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation,
),
value: 0,
});
confirmedTx = await tx.wait(1);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
// get the balance to compare after payment
const balanceErc20Before = await getErc20Balance(request, payeeWallet.address, provider);
tx = await payErc20TransferableReceivableRequest(request, wallet, 1, 0, {
gasLimit: BigNumber.from('20000000'),
});
confirmedTx = await tx.wait(1);
const balanceErc20After = await getErc20Balance(request, payeeWallet.address, provider);
expect(confirmedTx.status).toBe(1);
expect(tx.hash).not.toBeUndefined();
// ERC20 balance should be lower
expect(
BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)),
).toBeTruthy();
});
});
});
import {
ContractTransaction,
Signer,
BigNumberish,
providers,
BigNumber,
constants,
ethers,
} from 'ethers';
import {
Erc20PaymentNetwork,
ERC20TransferableReceivablePaymentDetector,
} from '@requestnetwork/payment-detection';
import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types';
import { ClientTypes } from '@requestnetwork/types';
import { ITransactionOverrides } from './transaction-overrides';
import {
getAmountToPay,
getProxyAddress,
getProvider,
getSigner,
getRequestPaymentValues,
validateERC20TransferableReceivable,
validatePayERC20TransferableReceivable,
} from './utils';
import { IPreparedTransaction } from './prepared-transaction';
// The ERC20 receivable smart contract ABI fragment
const erc20TransferableReceivableContractAbiFragment = [
'function receivableTokenIdMapping(bytes32) public view returns (uint256)',
];
/**
* Gets the receivableTokenId from a ERC20TransferableReceivable contract given
* a paymentReference and paymentAddress of the request
* @param request
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
*/
export async function getReceivableTokenIdForRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer,
): Promise<BigNumber> {
// Setup the ERC20 proxy contract interface
const contract = new ethers.Contract(
getProxyAddress(request, ERC20TransferableReceivablePaymentDetector.getDeploymentInformation),
erc20TransferableReceivableContractAbiFragment,
signerOrProvider,
);
const { paymentReference, paymentAddress } = getRequestPaymentValues(request);
return await contract.receivableTokenIdMapping(
ethers.utils.solidityKeccak256(['address', 'bytes'], [paymentAddress, `0x${paymentReference}`]),
);
}
/**
* Helper method to determine whether a request has a receivable minted yet
*
* @param request
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
*/
export async function hasReceivableForRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer,
): Promise<boolean> {
const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider);
return !receivableTokenId.isZero();
}
/**
* Processes a transaction to mint an ERC20TransferableReceivable.
* @param request
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
* @param overrides optionally, override default transaction values, like gas.
*/
export async function mintErc20TransferableReceivable(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
const { data, to, value } = prepareMintErc20TransferableReceivableTransaction(request);
const signer = getSigner(signerOrProvider);
return signer.sendTransaction({ data, to, value, ...overrides });
}
/**
* Encodes the call to mint a request through an ERC20TransferableReceivable contract, can be used with a Multisig contract.
* @param request request to pay
*/
export function prepareMintErc20TransferableReceivableTransaction(
request: ClientTypes.IRequestData,
): IPreparedTransaction {
validateERC20TransferableReceivable(request);
return {
data: encodeMintErc20TransferableReceivableRequest(request),
to: getProxyAddress(
request,
Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation,
),
value: 0,
};
}
/**
* Encodes call to mint a request through an ERC20TransferableReceivable contract, can be used with a Multisig contract.
* @param request request to pay
*/
export function encodeMintErc20TransferableReceivableRequest(
request: ClientTypes.IRequestData,
): string {
validateERC20TransferableReceivable(request);
const tokenAddress = request.currencyInfo.value;
const { paymentReference, paymentAddress } = getRequestPaymentValues(request);
const amount = getAmountToPay(request);
const receivableContract = ERC20TransferableReceivable__factory.createInterface();
return receivableContract.encodeFunctionData('mint', [
paymentAddress,
`0x${paymentReference}`,
amount,
tokenAddress,
]);
}
/**
* Processes a transaction to pay an ERC20 receivable Request.
* @param request
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
* @param amount optionally, the amount to pay. Defaults to remaining amount of the request.
* @param feeAmount optionally, the fee amount to pay. Defaults to the fee amount of the request.
* @param overrides optionally, override default transaction values, like gas.
*/
export async function payErc20TransferableReceivableRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer = getProvider(),
amount?: BigNumberish,
feeAmount?: BigNumberish,
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
await validatePayERC20TransferableReceivable(request, signerOrProvider, amount, feeAmount);
const { data, to, value } = await prepareErc20TransferableReceivablePaymentTransaction(
request,
signerOrProvider,
amount,
feeAmount,
);
const signer = getSigner(signerOrProvider);
return signer.sendTransaction({ data, to, value, ...overrides });
}
/**
* Encodes the call to pay a request through the ERC20 receivable contract, can be used with a Multisig contract.
* @param request request to pay
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
* @param amount optionally, the amount to pay. Defaults to remaining amount of the request.
* @param feeAmountOverride optionally, the fee amount to pay. Defaults to the fee amount of the request.
*/
export async function prepareErc20TransferableReceivablePaymentTransaction(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer,
amount?: BigNumberish,
feeAmountOverride?: BigNumberish,
): Promise<IPreparedTransaction> {
return {
data: await encodePayErc20TransferableReceivableRequest(
request,
signerOrProvider,
amount,
feeAmountOverride,
),
to: getProxyAddress(
request,
Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation,
),
value: 0,
};
}
/**
* Encodes the call to pay a request through the ERC20 receivable contract, can be used with a Multisig contract.
* @param request request to pay
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
* @param amount optionally, the amount to pay. Defaults to remaining amount of the request.
* @param feeAmountOverride optionally, the fee amount to pay. Defaults to the fee amount of the request.
*/
export async function encodePayErc20TransferableReceivableRequest(
request: ClientTypes.IRequestData,
signerOrProvider: providers.Provider | Signer,
amount?: BigNumberish,
feeAmountOverride?: BigNumberish,
): Promise<string> {
const amountToPay = getAmountToPay(request, amount);
const { paymentReference, feeAddress, feeAmount } = getRequestPaymentValues(request);
const feeToPay = BigNumber.from(feeAmountOverride || feeAmount || 0);
const receivableContract = ERC20TransferableReceivable__factory.createInterface();
// get tokenId from request
const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider);
return receivableContract.encodeFunctionData('payOwner', [
receivableTokenId,
amountToPay,
`0x${paymentReference}`,
feeToPay,
feeAddress || constants.AddressZero,
]);
}