Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
01a0afe
advanced-logic and payment-detection
yomarion Mar 19, 2023
0dfe034
fix import
yomarion Mar 19, 2023
c671f5d
chore: replaced rinkeby with matic for TheGraph codegen
yomarion Mar 19, 2023
8af51c7
added payment-detection test for balance
yomarion Mar 19, 2023
5288331
fix: thegraph codegen issues with superfluid
yomarion Mar 19, 2023
f8a60fd
Merge remote-tracking branch 'origin/master' into feat/erc20-fee-prox…
yomarion Mar 19, 2023
c22485c
fix: chain utils
yomarion Mar 19, 2023
b77d304
fix deleted file
yomarion Mar 19, 2023
7fe103b
fix artifact and tenderly related types
yomarion Mar 20, 2023
957e3c6
fix: Utils.isSameNetwork and test
yomarion Mar 20, 2023
cd5baa1
fix: payment-detection near versionMap
yomarion Mar 20, 2023
7092478
dirty-wip before inheriting the abstract class
yomarion Mar 21, 2023
411dd1e
working tests with ERC20NearFeeProxy detector
yomarion Mar 21, 2023
37c0e47
tests running with standard retriever design
yomarion Mar 21, 2023
9d78781
standard info retriever for native token detections
yomarion Mar 22, 2023
dc3fcca
minor type fix
yomarion Mar 22, 2023
e3ebcde
got rid of ERC20NearFeeProxy
yomarion Mar 22, 2023
aeb51d3
fix prettier
yomarion Mar 22, 2023
5454cd4
fix prettier
yomarion Mar 22, 2023
1a30b10
prettier nycrc files
yomarion Mar 22, 2023
4a54df9
fix: mock types
yomarion Mar 22, 2023
6e2b501
fix: wrong mocked advanced-logic types
yomarion Mar 22, 2023
83531e0
minor test fix
yomarion Mar 22, 2023
d1fd986
chore: reduce irrelevant strong typing in tests
yomarion Mar 23, 2023
e89e7de
chore: improve type details in payment detector
yomarion Mar 23, 2023
5a6b0ba
Merge branch 'master' into feat/erc20-fee-proxy-on-near
yomarion Mar 24, 2023
b6b39e9
Merge remote-tracking branch 'origin/master' into feat/erc20-fee-prox…
yomarion Mar 24, 2023
2e45994
fix: merge details
yomarion Mar 26, 2023
ebfe888
Strongly typing NearChainName 1/n
yomarion Mar 26, 2023
d4add72
Strongly typing NearChainName 2/n
yomarion Mar 26, 2023
db7f5a8
fix: test unit test not unit-testable
yomarion Mar 27, 2023
66fd5ae
Enforce TGetSubGraphClient usage
yomarion Mar 27, 2023
3dd4d96
tiny type fix
yomarion Mar 27, 2023
b2c542e
Merge remote-tracking branch 'origin/master' into feat/erc20-fee-prox…
yomarion Mar 29, 2023
c71835e
fixes Alex' feedback
yomarion Mar 30, 2023
87a1207
reset yarn.lock
yomarion Mar 30, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

## Description

This extension allows payments and refunds to be made in ERC20 tokens on Ethereum and EVM-compatible blockchains.
This extension allows payments and refunds to be made in fungible tokens, including:

- ERC20 tokens on Ethereum and EVM-compatible blockchains.
- Fungible tokens on Near and Near testnet (as defined by NEP-141 and NEP-148)

This Payment Network is similar to the [ERC20 Proxy Contract](./payment-network-erc20-proxy-contract-0.1.0.md) extension, with the added feature of allowing a fee to be taken from the payment.

The payment is mainly expected through a proxy payment contract, but the request issuer can also declare payments manually. Fees shall not be paid for declarative payments.

The proxy contract does the ERC20 token transfer on behalf of the user. The contract ensures a link between an ERC20 transfer and a request through a `paymentReference`. This `paymentReference` consists of the last 8 bytes of a salted hash of the requestId: `last8Bytes(hash(lowercase(requestId + salt + address)))`:
The proxy contract does the fungible token transfer on behalf of the user. The contract ensures a link between a token transfer and a request through a `paymentReference`. This `paymentReference` consists of the last 8 bytes of a salted hash of the requestId: `last8Bytes(hash(lowercase(requestId + salt + address)))`:

The contract also ensures that the `feeAmount` amount of the ERC20 transfer will be forwarded to the `feeAddress`.
The contract also ensures that the `feeAmount` amount of the token transfer will be forwarded to the `feeAddress`.

- `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
Expand All @@ -24,7 +28,7 @@ As a payment network, this extension allows to deduce a payment `balance` for th

## Payment Proxy Contract

The contract contains one function called `transferFromWithReferenceAndFee` which takes 6 arguments:
On EVMs, the contract contains one function called `transferFromWithReferenceAndFee` which takes 6 arguments:

- `tokenAddress` is the address of the ERC20 contract
- `to` is the destination address for the tokens
Expand All @@ -33,9 +37,14 @@ The contract contains one function called `transferFromWithReferenceAndFee` whic
- `feeAmount` is the amount of tokens to transfer to the fee destination address
- `feeAddress` is the destination address for the fee

The `TransferWithReferenceAndFee` event is emitted when the tokens are transfered. This event contains the same 6 arguments as the `transferFromWithReferenceAndFee` function.
On Near, users send fungible tokens to the contract with the `ft_transfer_call` method, if the `msg` value given is a valid JSON object with 4 of the 6 arguments listed above: `to`, `paymentReference`, `feeAmount` and `feeAddress`. The `tokenAdress` is taken from the calling fungible token contract. The `amount` is equal to the transfer (total) `amount` less `feeAmount`.

On EVM-compatible chains, the `TransferWithReferenceAndFee` event is emitted when the tokens are transfered. This event contains the same 6 arguments as the `transferFromWithReferenceAndFee` function.

[See smart contract source](https://github.com/RequestNetwork/requestNetwork/blob/master/packages/smart-contracts/src/contracts/ERC20FeeProxy.sol)
On Near and Near testnet, a JSON message is logged by the method `on_transfer_with_reference`, containing the same 6 arguments.

[See EVM smart contract source](https://github.com/RequestNetwork/requestNetwork/blob/master/packages/smart-contracts/src/contracts/ERC20FeeProxy.sol)
[See Near smart contract source](https://github.com/RequestNetwork/near-contracts)

| Network | Contract Address |
| -------------------------- | ------------------------------------------ |
Expand All @@ -45,6 +54,9 @@ The `TransferWithReferenceAndFee` event is emitted when the tokens are transfere
| Ethereum Testnet - Rinkeby | 0xda46309973bffddd5a10ce12c44d2ee266f45a44 |
| Matic Testnet - Mumbai | 0x131eb294E3803F23dc2882AB795631A12D1d8929 |
| Private | 0x75c35C980C0d37ef46DF04d31A140b65503c0eEd |
| Near Testnet | pay.reqnetwork.testnet |

The updated list of deployment address can be found [in the smart-contracts package](../../smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts);

## Manual payment declaration

Expand Down
67 changes: 30 additions & 37 deletions packages/advanced-logic/src/advanced-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
IdentityTypes,
RequestLogicTypes,
} from '@requestnetwork/types';
import { CurrencyManager, ICurrencyManager } from '@requestnetwork/currency';
import { CurrencyManager, ICurrencyManager, NearChains } from '@requestnetwork/currency';

import ContentData from './extensions/content-data';
import AddressBasedBtc from './extensions/payment-network/bitcoin/mainnet-address-based';
Expand Down Expand Up @@ -106,8 +106,9 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
protected getExtensionForActionAndState(
extensionAction: ExtensionTypes.IAction,
requestState: RequestLogicTypes.IRequest,
): ExtensionTypes.IExtension<any> {
): ExtensionTypes.IExtension {
const id: ExtensionTypes.ID = extensionAction.id;
const network = this.getNetwork(extensionAction, requestState) || requestState.currency.network;
const extension: ExtensionTypes.IExtension | undefined = {
[ExtensionTypes.ID.CONTENT_DATA]: this.extensions.contentData,
[ExtensionTypes.PAYMENT_NETWORK_ID.BITCOIN_ADDRESS_BASED]: this.extensions.addressBasedBtc,
Expand All @@ -117,17 +118,17 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_ADDRESS_BASED]: this.extensions.addressBasedErc20,
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_PROXY_CONTRACT]: this.extensions.proxyContractErc20,
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]:
this.extensions.feeProxyContractErc20,
this.getFeeProxyContractErc20ForNetwork(network),
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC777_STREAM]: this.extensions.erc777Stream,
[ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA]: this.extensions.ethereumInputData,
[ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN]:
this.getNativeTokenExtensionForActionAndState(extensionAction, requestState),
this.getNativeTokenExtensionForNetwork(network),
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: this.extensions.anyToErc20Proxy,
[ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT]:
this.extensions.feeProxyContractEth,
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: this.extensions.anyToEthProxy,
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE_TOKEN]:
this.getAnyToNativeTokenExtensionForActionAndState(extensionAction, requestState),
this.getAnyToNativeTokenExtensionForNetwork(network),
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]:
this.extensions.erc20TransferableReceivable,
}[id];
Expand All @@ -137,8 +138,6 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
id === ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN ||
id === ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE_TOKEN
) {
const network =
this.getNetwork(extensionAction, requestState) || requestState.currency.network;
throw Error(`extension with id: ${id} not found for network: ${network}`);
}

Expand All @@ -148,17 +147,35 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
}

public getNativeTokenExtensionForNetwork(
network: CurrencyTypes.ChainName,
network?: CurrencyTypes.ChainName,
): ExtensionTypes.IExtension<ExtensionTypes.PnReferenceBased.ICreationParameters> | undefined {
return this.extensions.nativeToken.find((nativeTokenExtension) =>
nativeTokenExtension.supportedNetworks.includes(network),
);
return network
? this.extensions.nativeToken.find((nativeTokenExtension) =>
nativeTokenExtension.supportedNetworks.includes(network),
)
: undefined;
}

public getAnyToNativeTokenExtensionForNetwork(
network?: CurrencyTypes.ChainName,
): AnyToNative | undefined {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Not related) Return type simplification for maintainability

return network
? this.extensions.anyToNativeToken.find((anyToNativeTokenExtension) =>
anyToNativeTokenExtension.supportedNetworks.includes(network),
)
: undefined;
}

public getFeeProxyContractErc20ForNetwork(network?: string): FeeProxyContractErc20 {
return NearChains.isChainSupported(network)
? new FeeProxyContractErc20(undefined, undefined, network)
: this.extensions.feeProxyContractErc20;
}

protected getNativeTokenExtensionForActionAndState(
protected getNetwork(
extensionAction: ExtensionTypes.IAction,
requestState: RequestLogicTypes.IRequest,
): ExtensionTypes.IExtension<ExtensionTypes.PnReferenceBased.ICreationParameters> | undefined {
): CurrencyTypes.ChainName | undefined {
if (
requestState.currency.network &&
extensionAction.parameters.paymentNetworkName &&
Expand All @@ -168,30 +185,6 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
`Cannot apply action for network ${extensionAction.parameters.paymentNetworkName} on state with payment network: ${requestState.currency.network}`,
);
}
const network = requestState.currency.network ?? extensionAction.parameters.paymentNetworkName;
return network ? this.getNativeTokenExtensionForNetwork(network) : undefined;
}

public getAnyToNativeTokenExtensionForNetwork(
network: CurrencyTypes.ChainName,
): ExtensionTypes.IExtension<ExtensionTypes.PnAnyToEth.ICreationParameters> | undefined {
return this.extensions.anyToNativeToken.find((anyToNativeTokenExtension) =>
anyToNativeTokenExtension.supportedNetworks.includes(network),
);
}

protected getAnyToNativeTokenExtensionForActionAndState(
extensionAction: ExtensionTypes.IAction,
requestState: RequestLogicTypes.IRequest,
): ExtensionTypes.IExtension<ExtensionTypes.PnAnyToEth.ICreationParameters> | undefined {
const network = this.getNetwork(extensionAction, requestState);
return network ? this.getAnyToNativeTokenExtensionForNetwork(network) : undefined;
}

protected getNetwork(
extensionAction: ExtensionTypes.IAction,
requestState: RequestLogicTypes.IRequest,
): CurrencyTypes.ChainName | undefined {
const network =
extensionAction.action === 'create'
? extensionAction.parameters.network
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,63 @@
import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types';
import { NearChains, Utils } from '@requestnetwork/currency';
import { UnsupportedNetworkError } from '../address-based';
import { FeeReferenceBasedPaymentNetwork } from '../fee-reference-based';

const CURRENT_VERSION = '0.2.0';
const EVM_CURRENT_VERSION = '0.2.0';
const NEAR_CURRENT_VERSION = 'NEAR-0.1.0';

/**
* Implementation of the payment network to pay in ERC20, including third-party fees payment, based on a reference provided to a proxy contract.
*/
export default class Erc20FeeProxyPaymentNetwork<
TCreationParameters extends ExtensionTypes.PnFeeReferenceBased.ICreationParameters = ExtensionTypes.PnFeeReferenceBased.ICreationParameters,
> extends FeeReferenceBasedPaymentNetwork<TCreationParameters> {
/**
* @param network is only relevant for non-EVM chains (Near and Near testnet)
*/
public constructor(
extensionId: ExtensionTypes.PAYMENT_NETWORK_ID = ExtensionTypes.PAYMENT_NETWORK_ID
.ERC20_FEE_PROXY_CONTRACT,
currentVersion: string = CURRENT_VERSION,
currentVersion?: string | undefined,
protected network?: string | undefined,
) {
super(extensionId, currentVersion, RequestLogicTypes.CURRENCY.ERC20);
super(
extensionId,
currentVersion ?? Erc20FeeProxyPaymentNetwork.getDefaultCurrencyVersion(network),
RequestLogicTypes.CURRENCY.ERC20,
);
}

protected static getDefaultCurrencyVersion(network?: string): string {
return NearChains.isChainSupported(network) ? NEAR_CURRENT_VERSION : EVM_CURRENT_VERSION;
}

// Override `validate` to account for network-specific instanciation (non-EVM only)
protected validate(
request: RequestLogicTypes.IRequest,
extensionAction: ExtensionTypes.IAction,
): void {
if (
this.network &&
request.currency.network &&
!Utils.isSameNetwork(this.network, request.currency.network)
) {
throw new UnsupportedNetworkError(request.currency.network, [this.network]);
}
super.validate(request, extensionAction);
}

// Override `isValidAddress` to account for network-specific instanciation (non-EVM only)
protected isValidAddress(address: string): boolean {
try {
NearChains.assertChainSupported(this.network);
if (NearChains.isTestnet(this.network)) {
return this.isValidAddressForSymbolAndNetwork(address, 'NEAR-testnet', 'near-testnet');
} else {
return this.isValidAddressForSymbolAndNetwork(address, 'NEAR', 'near');
}
} catch {
return super.isValidAddress(address);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types';

import Erc20FeeProxyContract from '../../../../src/extensions/payment-network/erc20/fee-proxy-contract';

import * as DataERC20FeeAddData from '../../../utils/payment-network/erc20/fee-proxy-contract-add-data-generator';
import * as DataERC20FeeCreate from '../../../utils/payment-network/erc20/fee-proxy-contract-create-data-generator';
import * as DataNearERC20FeeCreate from '../../../utils/payment-network/erc20/near-fee-proxy-contract';
import * as TestData from '../../../utils/test-data-generator';
import { deepCopy } from '@requestnetwork/utils';
import { AdvancedLogic } from '../../../../src';

const erc20FeeProxyContract = new Erc20FeeProxyContract();
const advancedLogic = new AdvancedLogic();
const erc20FeeProxyContract = advancedLogic.getFeeProxyContractErc20ForNetwork();

/* eslint-disable @typescript-eslint/no-unused-expressions */
describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
Expand Down Expand Up @@ -69,7 +70,7 @@ describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
});
});

it('cannot createCreationAction with payment address not an ethereum address', () => {
it('cannot createCreationAction with an invalid payment address', () => {
// 'must throw'
expect(() => {
erc20FeeProxyContract.createCreationAction({
Expand Down Expand Up @@ -112,6 +113,44 @@ describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
});
}).toThrowError('feeAmount is not a valid amount');
});

describe('on Near testnet', () => {
const extension = advancedLogic.getFeeProxyContractErc20ForNetwork('near-testnet');
it('can create a create action with all parameters', () => {
expect(
extension.createCreationAction({
feeAddress: 'buidler.reqnetwork.testnet',
feeAmount: '0',
paymentAddress: 'issuer.reqnetwork.testnet',
refundAddress: 'payer.reqnetwork.testnet',
salt: 'ea3bc7caf64110ca',
}),
).toEqual({
action: 'create',
id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
parameters: {
feeAddress: 'buidler.reqnetwork.testnet',
feeAmount: '0',
paymentAddress: 'issuer.reqnetwork.testnet',
refundAddress: 'payer.reqnetwork.testnet',
salt: 'ea3bc7caf64110ca',
},
version: 'NEAR-0.1.0',
});
});

it('cannot createCreationAction with an invalid payment address', () => {
expect(() => {
extension.createCreationAction({
paymentAddress: '0x0000000000000000000000000000000000000002',
refundAddress: 'payer.reqnetwork.testnet',
salt: 'ea3bc7caf64110ca',
});
}).toThrowError(
"paymentAddress '0x0000000000000000000000000000000000000002' is not a valid address",
);
});
});
});

describe('createAddPaymentAddressAction', () => {
Expand Down Expand Up @@ -338,6 +377,36 @@ describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
);
}).toThrowError('version is required at creation');
});

describe('on Near testnet', () => {
const extension = advancedLogic.getFeeProxyContractErc20ForNetwork('near-testnet');
it('can applyActionToExtensions of creation', () => {
expect(
extension.applyActionToExtension(
DataNearERC20FeeCreate.requestStateNoExtensions.extensions,
DataNearERC20FeeCreate.actionCreationFull,
DataNearERC20FeeCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
),
).toEqual(DataNearERC20FeeCreate.extensionFullState);
});
it('cannot applyActionToExtensions of creation', () => {
// 'new extension state wrong'
expect(() =>
extension.applyActionToExtension(
// State with currency on the wrong network
DataERC20FeeCreate.requestStateNoExtensions.extensions,
DataNearERC20FeeCreate.actionCreationFull,
DataERC20FeeCreate.requestStateNoExtensions,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
),
).toThrowError(
"Payment network 'mainnet' is not supported by this extension (only near-testnet)",
);
});
});
});

describe('applyActionToExtension/addPaymentAddress', () => {
Expand Down
Loading