Skip to content

Commit 6ac8c28

Browse files
authored
feat: ERC20FeeProxy on Near testnet (#1086)
1 parent 0629e45 commit 6ac8c28

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+998
-321
lines changed

packages/advanced-logic/specs/payment-network-erc20-fee-proxy-contract-0.1.0.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
## Description
44

5-
This extension allows payments and refunds to be made in ERC20 tokens on Ethereum and EVM-compatible blockchains.
5+
This extension allows payments and refunds to be made in fungible tokens, including:
6+
7+
- ERC20 tokens on Ethereum and EVM-compatible blockchains.
8+
- Fungible tokens on Near and Near testnet (as defined by NEP-141 and NEP-148)
9+
610
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.
711

812
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.
913

10-
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)))`:
14+
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)))`:
1115

12-
The contract also ensures that the `feeAmount` amount of the ERC20 transfer will be forwarded to the `feeAddress`.
16+
The contract also ensures that the `feeAmount` amount of the token transfer will be forwarded to the `feeAddress`.
1317

1418
- `requestId` is the id of the request
1519
- `salt` is a random number with at least 8 bytes of randomness. It must be unique to each request
@@ -24,7 +28,7 @@ As a payment network, this extension allows to deduce a payment `balance` for th
2428

2529
## Payment Proxy Contract
2630

27-
The contract contains one function called `transferFromWithReferenceAndFee` which takes 6 arguments:
31+
On EVMs, the contract contains one function called `transferFromWithReferenceAndFee` which takes 6 arguments:
2832

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

36-
The `TransferWithReferenceAndFee` event is emitted when the tokens are transfered. This event contains the same 6 arguments as the `transferFromWithReferenceAndFee` function.
40+
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`.
41+
42+
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.
3743

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

4049
| Network | Contract Address |
4150
| -------------------------- | ------------------------------------------ |
@@ -45,6 +54,9 @@ The `TransferWithReferenceAndFee` event is emitted when the tokens are transfere
4554
| Ethereum Testnet - Rinkeby | 0xda46309973bffddd5a10ce12c44d2ee266f45a44 |
4655
| Matic Testnet - Mumbai | 0x131eb294E3803F23dc2882AB795631A12D1d8929 |
4756
| Private | 0x75c35C980C0d37ef46DF04d31A140b65503c0eEd |
57+
| Near Testnet | pay.reqnetwork.testnet |
58+
59+
The updated list of deployment address can be found [in the smart-contracts package](../../smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts);
4860

4961
## Manual payment declaration
5062

packages/advanced-logic/src/advanced-logic.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
IdentityTypes,
66
RequestLogicTypes,
77
} from '@requestnetwork/types';
8-
import { CurrencyManager, ICurrencyManager } from '@requestnetwork/currency';
8+
import {
9+
CurrencyManager,
10+
ICurrencyManager,
11+
NearChains,
12+
isSameChain,
13+
} from '@requestnetwork/currency';
914

1015
import ContentData from './extensions/content-data';
1116
import AddressBasedBtc from './extensions/payment-network/bitcoin/mainnet-address-based';
@@ -106,8 +111,9 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
106111
protected getExtensionForActionAndState(
107112
extensionAction: ExtensionTypes.IAction,
108113
requestState: RequestLogicTypes.IRequest,
109-
): ExtensionTypes.IExtension<any> {
114+
): ExtensionTypes.IExtension {
110115
const id: ExtensionTypes.ID = extensionAction.id;
116+
const network = this.getNetwork(extensionAction, requestState) || requestState.currency.network;
111117
const extension: ExtensionTypes.IExtension | undefined = {
112118
[ExtensionTypes.ID.CONTENT_DATA]: this.extensions.contentData,
113119
[ExtensionTypes.PAYMENT_NETWORK_ID.BITCOIN_ADDRESS_BASED]: this.extensions.addressBasedBtc,
@@ -117,17 +123,17 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
117123
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_ADDRESS_BASED]: this.extensions.addressBasedErc20,
118124
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_PROXY_CONTRACT]: this.extensions.proxyContractErc20,
119125
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]:
120-
this.extensions.feeProxyContractErc20,
126+
this.getFeeProxyContractErc20ForNetwork(network),
121127
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC777_STREAM]: this.extensions.erc777Stream,
122128
[ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA]: this.extensions.ethereumInputData,
123129
[ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN]:
124-
this.getNativeTokenExtensionForActionAndState(extensionAction, requestState),
130+
this.getNativeTokenExtensionForNetwork(network),
125131
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: this.extensions.anyToErc20Proxy,
126132
[ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT]:
127133
this.extensions.feeProxyContractEth,
128134
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: this.extensions.anyToEthProxy,
129135
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE_TOKEN]:
130-
this.getAnyToNativeTokenExtensionForActionAndState(extensionAction, requestState),
136+
this.getAnyToNativeTokenExtensionForNetwork(network),
131137
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]:
132138
this.extensions.erc20TransferableReceivable,
133139
}[id];
@@ -137,8 +143,6 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
137143
id === ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN ||
138144
id === ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE_TOKEN
139145
) {
140-
const network =
141-
this.getNetwork(extensionAction, requestState) || requestState.currency.network;
142146
throw Error(`extension with id: ${id} not found for network: ${network}`);
143147
}
144148

@@ -148,50 +152,44 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
148152
}
149153

150154
public getNativeTokenExtensionForNetwork(
151-
network: CurrencyTypes.ChainName,
155+
network?: CurrencyTypes.ChainName,
152156
): ExtensionTypes.IExtension<ExtensionTypes.PnReferenceBased.ICreationParameters> | undefined {
153-
return this.extensions.nativeToken.find((nativeTokenExtension) =>
154-
nativeTokenExtension.supportedNetworks.includes(network),
155-
);
157+
return network
158+
? this.extensions.nativeToken.find((nativeTokenExtension) =>
159+
nativeTokenExtension.supportedNetworks.includes(network),
160+
)
161+
: undefined;
162+
}
163+
164+
public getAnyToNativeTokenExtensionForNetwork(
165+
network?: CurrencyTypes.ChainName,
166+
): AnyToNative | undefined {
167+
return network
168+
? this.extensions.anyToNativeToken.find((anyToNativeTokenExtension) =>
169+
anyToNativeTokenExtension.supportedNetworks.includes(network),
170+
)
171+
: undefined;
172+
}
173+
174+
public getFeeProxyContractErc20ForNetwork(network?: string): FeeProxyContractErc20 {
175+
return NearChains.isChainSupported(network)
176+
? new FeeProxyContractErc20(undefined, undefined, network)
177+
: this.extensions.feeProxyContractErc20;
156178
}
157179

158-
protected getNativeTokenExtensionForActionAndState(
180+
protected getNetwork(
159181
extensionAction: ExtensionTypes.IAction,
160182
requestState: RequestLogicTypes.IRequest,
161-
): ExtensionTypes.IExtension<ExtensionTypes.PnReferenceBased.ICreationParameters> | undefined {
183+
): CurrencyTypes.ChainName | undefined {
162184
if (
163185
requestState.currency.network &&
164186
extensionAction.parameters.paymentNetworkName &&
165-
requestState.currency.network !== extensionAction.parameters.paymentNetworkName
187+
!isSameChain(requestState.currency.network, extensionAction.parameters.paymentNetworkName)
166188
) {
167189
throw new Error(
168190
`Cannot apply action for network ${extensionAction.parameters.paymentNetworkName} on state with payment network: ${requestState.currency.network}`,
169191
);
170192
}
171-
const network = requestState.currency.network ?? extensionAction.parameters.paymentNetworkName;
172-
return network ? this.getNativeTokenExtensionForNetwork(network) : undefined;
173-
}
174-
175-
public getAnyToNativeTokenExtensionForNetwork(
176-
network: CurrencyTypes.ChainName,
177-
): ExtensionTypes.IExtension<ExtensionTypes.PnAnyToEth.ICreationParameters> | undefined {
178-
return this.extensions.anyToNativeToken.find((anyToNativeTokenExtension) =>
179-
anyToNativeTokenExtension.supportedNetworks.includes(network),
180-
);
181-
}
182-
183-
protected getAnyToNativeTokenExtensionForActionAndState(
184-
extensionAction: ExtensionTypes.IAction,
185-
requestState: RequestLogicTypes.IRequest,
186-
): ExtensionTypes.IExtension<ExtensionTypes.PnAnyToEth.ICreationParameters> | undefined {
187-
const network = this.getNetwork(extensionAction, requestState);
188-
return network ? this.getAnyToNativeTokenExtensionForNetwork(network) : undefined;
189-
}
190-
191-
protected getNetwork(
192-
extensionAction: ExtensionTypes.IAction,
193-
requestState: RequestLogicTypes.IRequest,
194-
): CurrencyTypes.ChainName | undefined {
195193
const network =
196194
extensionAction.action === 'create'
197195
? extensionAction.parameters.network
Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,62 @@
1-
import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types';
1+
import { CurrencyTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types';
2+
import { NearChains, isSameChain } from '@requestnetwork/currency';
3+
import { UnsupportedNetworkError } from '../address-based';
24
import { FeeReferenceBasedPaymentNetwork } from '../fee-reference-based';
35

4-
const CURRENT_VERSION = '0.2.0';
6+
const EVM_CURRENT_VERSION = '0.2.0';
7+
const NEAR_CURRENT_VERSION = 'NEAR-0.1.0';
58

69
/**
710
* Implementation of the payment network to pay in ERC20, including third-party fees payment, based on a reference provided to a proxy contract.
811
*/
912
export default class Erc20FeeProxyPaymentNetwork<
1013
TCreationParameters extends ExtensionTypes.PnFeeReferenceBased.ICreationParameters = ExtensionTypes.PnFeeReferenceBased.ICreationParameters,
1114
> extends FeeReferenceBasedPaymentNetwork<TCreationParameters> {
15+
/**
16+
* @param network is only relevant for non-EVM chains (Near and Near testnet)
17+
*/
1218
public constructor(
1319
extensionId: ExtensionTypes.PAYMENT_NETWORK_ID = ExtensionTypes.PAYMENT_NETWORK_ID
1420
.ERC20_FEE_PROXY_CONTRACT,
15-
currentVersion: string = CURRENT_VERSION,
21+
currentVersion?: string | undefined,
22+
protected network?: string | undefined,
1623
) {
17-
super(extensionId, currentVersion, RequestLogicTypes.CURRENCY.ERC20);
24+
super(
25+
extensionId,
26+
currentVersion ?? Erc20FeeProxyPaymentNetwork.getDefaultCurrencyVersion(network),
27+
RequestLogicTypes.CURRENCY.ERC20,
28+
);
29+
}
30+
31+
protected static getDefaultCurrencyVersion(network?: string): string {
32+
return NearChains.isChainSupported(network) ? NEAR_CURRENT_VERSION : EVM_CURRENT_VERSION;
33+
}
34+
35+
// Override `validate` to account for network-specific instanciation (non-EVM only)
36+
protected validate(
37+
request: RequestLogicTypes.IRequest,
38+
extensionAction: ExtensionTypes.IAction,
39+
): void {
40+
if (
41+
this.network &&
42+
request.currency.network &&
43+
!isSameChain(this.network, request.currency.network)
44+
) {
45+
throw new UnsupportedNetworkError(request.currency.network, [this.network]);
46+
}
47+
super.validate(request, extensionAction);
48+
}
49+
50+
// Override `isValidAddress` to account for network-specific instanciation (non-EVM only)
51+
protected isValidAddress(address: string): boolean {
52+
if (NearChains.isChainSupported(this.network)) {
53+
if (NearChains.isTestnet(this.network as CurrencyTypes.NearChainName)) {
54+
return this.isValidAddressForSymbolAndNetwork(address, 'NEAR-testnet', 'near-testnet');
55+
} else {
56+
return this.isValidAddressForSymbolAndNetwork(address, 'NEAR', 'near');
57+
}
58+
} else {
59+
return super.isValidAddress(address);
60+
}
1861
}
1962
}

packages/advanced-logic/test/extensions/payment-network/erc20/fee-proxy-contract.test.ts

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types';
22

3-
import Erc20FeeProxyContract from '../../../../src/extensions/payment-network/erc20/fee-proxy-contract';
4-
53
import * as DataERC20FeeAddData from '../../../utils/payment-network/erc20/fee-proxy-contract-add-data-generator';
64
import * as DataERC20FeeCreate from '../../../utils/payment-network/erc20/fee-proxy-contract-create-data-generator';
5+
import * as DataNearERC20FeeCreate from '../../../utils/payment-network/erc20/near-fee-proxy-contract';
76
import * as TestData from '../../../utils/test-data-generator';
87
import { deepCopy } from '@requestnetwork/utils';
8+
import { AdvancedLogic } from '../../../../src';
99

10-
const erc20FeeProxyContract = new Erc20FeeProxyContract();
10+
const advancedLogic = new AdvancedLogic();
11+
const erc20FeeProxyContract = advancedLogic.getFeeProxyContractErc20ForNetwork();
1112

1213
/* eslint-disable @typescript-eslint/no-unused-expressions */
1314
describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
@@ -69,7 +70,7 @@ describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
6970
});
7071
});
7172

72-
it('cannot createCreationAction with payment address not an ethereum address', () => {
73+
it('cannot createCreationAction with an invalid payment address', () => {
7374
// 'must throw'
7475
expect(() => {
7576
erc20FeeProxyContract.createCreationAction({
@@ -112,6 +113,44 @@ describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
112113
});
113114
}).toThrowError('feeAmount is not a valid amount');
114115
});
116+
117+
describe('on Near testnet', () => {
118+
const extension = advancedLogic.getFeeProxyContractErc20ForNetwork('near-testnet');
119+
it('can create a create action with all parameters', () => {
120+
expect(
121+
extension.createCreationAction({
122+
feeAddress: 'buidler.reqnetwork.testnet',
123+
feeAmount: '0',
124+
paymentAddress: 'issuer.reqnetwork.testnet',
125+
refundAddress: 'payer.reqnetwork.testnet',
126+
salt: 'ea3bc7caf64110ca',
127+
}),
128+
).toEqual({
129+
action: 'create',
130+
id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT,
131+
parameters: {
132+
feeAddress: 'buidler.reqnetwork.testnet',
133+
feeAmount: '0',
134+
paymentAddress: 'issuer.reqnetwork.testnet',
135+
refundAddress: 'payer.reqnetwork.testnet',
136+
salt: 'ea3bc7caf64110ca',
137+
},
138+
version: 'NEAR-0.1.0',
139+
});
140+
});
141+
142+
it('cannot createCreationAction with an invalid payment address', () => {
143+
expect(() => {
144+
extension.createCreationAction({
145+
paymentAddress: '0x0000000000000000000000000000000000000002',
146+
refundAddress: 'payer.reqnetwork.testnet',
147+
salt: 'ea3bc7caf64110ca',
148+
});
149+
}).toThrowError(
150+
"paymentAddress '0x0000000000000000000000000000000000000002' is not a valid address",
151+
);
152+
});
153+
});
115154
});
116155

117156
describe('createAddPaymentAddressAction', () => {
@@ -338,6 +377,36 @@ describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
338377
);
339378
}).toThrowError('version is required at creation');
340379
});
380+
381+
describe('on Near testnet', () => {
382+
const extension = advancedLogic.getFeeProxyContractErc20ForNetwork('near-testnet');
383+
it('can applyActionToExtensions of creation', () => {
384+
expect(
385+
extension.applyActionToExtension(
386+
DataNearERC20FeeCreate.requestStateNoExtensions.extensions,
387+
DataNearERC20FeeCreate.actionCreationFull,
388+
DataNearERC20FeeCreate.requestStateNoExtensions,
389+
TestData.otherIdRaw.identity,
390+
TestData.arbitraryTimestamp,
391+
),
392+
).toEqual(DataNearERC20FeeCreate.extensionFullState);
393+
});
394+
it('cannot applyActionToExtensions of creation', () => {
395+
// 'new extension state wrong'
396+
expect(() =>
397+
extension.applyActionToExtension(
398+
// State with currency on the wrong network
399+
DataERC20FeeCreate.requestStateNoExtensions.extensions,
400+
DataNearERC20FeeCreate.actionCreationFull,
401+
DataERC20FeeCreate.requestStateNoExtensions,
402+
TestData.otherIdRaw.identity,
403+
TestData.arbitraryTimestamp,
404+
),
405+
).toThrowError(
406+
"Payment network 'mainnet' is not supported by this extension (only near-testnet)",
407+
);
408+
});
409+
});
341410
});
342411

343412
describe('applyActionToExtension/addPaymentAddress', () => {

0 commit comments

Comments
 (0)