Skip to content

Commit af836c2

Browse files
authored
feat: near conversion payment processor (#921)
1 parent c2e44c5 commit af836c2

File tree

6 files changed

+344
-31
lines changed

6 files changed

+344
-31
lines changed

packages/payment-detection/test/near/near-native-conversion.test.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = {
2929
extensions: { anyToNativeToken: [mockNearPaymentNetwork] },
3030
};
3131
const salt = 'a6475e4c3d45feb6';
32-
const paymentAddress = 'gus.near';
32+
const paymentAddress = 'issuer.near';
3333
const feeAddress = 'fee.near';
3434
const network = 'aurora';
3535
const feeAmount = '5';
@@ -57,32 +57,23 @@ const request: any = {
5757
},
5858
};
5959
const graphPaymentEvent = {
60-
amount: '5000000000',
61-
amountInCrypto: '10000000000',
60+
// 500 USD
61+
amount: '50000',
62+
amountInCrypto: null,
6263
block: 10088347,
6364
currency: 'USD',
6465
feeAddress,
65-
feeAmount: '5000000',
66-
feeAmountInCrypto: '10000000',
66+
// .05 USD
67+
feeAmount: '5',
68+
feeAmountInCrypto: null,
6769
from: 'payer.near',
70+
to: paymentAddress,
6871
maxRateTimespan: 0,
6972
timestamp: 1643647285,
7073
receiptId,
7174
gasUsed: '144262',
7275
gasPrice: '2425000017',
7376
};
74-
const expectedRetrieverEvent = {
75-
amount: graphPaymentEvent.amount,
76-
name: 'payment',
77-
parameters: {
78-
...graphPaymentEvent,
79-
amount: undefined,
80-
timestamp: undefined,
81-
to: paymentAddress,
82-
maxRateTimespan: graphPaymentEvent.maxRateTimespan.toString(),
83-
},
84-
timestamp: graphPaymentEvent.timestamp,
85-
};
8677

8778
describe('Near payments detection', () => {
8879
beforeAll(() => {
@@ -108,7 +99,25 @@ describe('Near payments detection', () => {
10899
);
109100
const events = await infoRetriever.getTransferEvents();
110101
expect(events).toHaveLength(1);
111-
expect(events[0]).toEqual(expectedRetrieverEvent);
102+
expect(events[0]).toEqual({
103+
amount: graphPaymentEvent.amount,
104+
name: 'payment',
105+
parameters: {
106+
amountInCrypto: null,
107+
block: 10088347,
108+
currency: 'USD',
109+
feeAddress,
110+
feeAmount: '5',
111+
feeAmountInCrypto: undefined,
112+
from: 'payer.near',
113+
to: paymentAddress,
114+
maxRateTimespan: '0',
115+
receiptId,
116+
gasUsed: '144262',
117+
gasPrice: '2425000017',
118+
},
119+
timestamp: graphPaymentEvent.timestamp,
120+
});
112121
});
113122

114123
it('PaymentNetworkFactory can get the detector (testnet)', async () => {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { BigNumberish } from 'ethers';
2+
import { WalletConnection } from 'near-api-js';
3+
4+
import { ClientTypes, PaymentTypes, RequestLogicTypes } from '@requestnetwork/types';
5+
6+
import {
7+
getRequestPaymentValues,
8+
validateRequest,
9+
getAmountToPay,
10+
getPaymentExtensionVersion,
11+
} from './utils';
12+
import { isNearNetwork, processNearPaymentWithConversion } from './utils-near';
13+
import { IConversionPaymentSettings } from '.';
14+
import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency';
15+
16+
/**
17+
* Processes the transaction to pay a request in NEAR with on-chain conversion.
18+
* @param request the request to pay
19+
* @param walletConnection the Web3 provider, or signer. Defaults to window.ethereum.
20+
* @param amount optionally, the amount to pay. Defaults to remaining amount of the request.
21+
*/
22+
export async function payNearConversionRequest(
23+
request: ClientTypes.IRequestData,
24+
walletConnection: WalletConnection,
25+
paymentSettings: IConversionPaymentSettings,
26+
amount?: BigNumberish,
27+
): Promise<void> {
28+
validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE);
29+
30+
const currencyManager = paymentSettings.currencyManager || CurrencyManager.getDefault();
31+
const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan, network } =
32+
getRequestPaymentValues(request);
33+
34+
const requestCurrency = currencyManager.fromStorageCurrency(request.currencyInfo);
35+
if (!requestCurrency) {
36+
throw new UnsupportedCurrencyError(request.currencyInfo);
37+
}
38+
39+
if (!paymentReference) {
40+
throw new Error('Cannot pay without a paymentReference');
41+
}
42+
43+
if (!network || !isNearNetwork(network)) {
44+
throw new Error('Should be a near network');
45+
}
46+
47+
const amountToPay = getAmountToPay(request, amount).toString();
48+
const version = getPaymentExtensionVersion(request);
49+
50+
return processNearPaymentWithConversion(
51+
walletConnection,
52+
network,
53+
amountToPay,
54+
paymentAddress,
55+
paymentReference,
56+
getTicker(request.currencyInfo),
57+
feeAddress || '0x',
58+
feeAmount || 0,
59+
maxRateTimespan || '0',
60+
version,
61+
);
62+
}
63+
64+
const getTicker = (currency: RequestLogicTypes.ICurrency): string => {
65+
switch (currency.type) {
66+
case RequestLogicTypes.CURRENCY.ISO4217:
67+
return currency.value;
68+
default:
69+
// FIXME: Flux oracles are compatible with ERC20 identified by tickers. Ex: USDT, DAI.
70+
// Warning: although Flux oracles are compatible with ETH and BTC, the request contract
71+
// for native payments and conversions only handles 2 decimals, not suited for cryptos.
72+
throw new Error('Near payment with conversion only implemented for fiat denominations.');
73+
}
74+
};

packages/payment-processor/src/payment/utils-near.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { BigNumber, BigNumberish, ethers } from 'ethers';
22
import { Contract } from 'near-api-js';
33
import { Near, WalletConnection } from 'near-api-js';
4-
import { NearNativeTokenPaymentDetector } from '@requestnetwork/payment-detection';
4+
import {
5+
NearNativeTokenPaymentDetector,
6+
NearConversionNativeTokenPaymentDetector,
7+
} from '@requestnetwork/payment-detection';
58

69
export const isValidNearAddress = async (nearNetwork: Near, address: string): Promise<boolean> => {
710
try {
@@ -32,15 +35,12 @@ export const isNearAccountSolvent = (
3235
const GAS_LIMIT_IN_TGAS = 50;
3336
const GAS_LIMIT = ethers.utils.parseUnits(GAS_LIMIT_IN_TGAS.toString(), 12);
3437

35-
/**
36-
* Export used for mocking only.
37-
*/
3838
export const processNearPayment = async (
3939
walletConnection: WalletConnection,
4040
network: string,
4141
amount: BigNumberish,
4242
to: string,
43-
payment_reference: string,
43+
paymentReference: string,
4444
version = '0.2.0',
4545
): Promise<void> => {
4646
if (version !== '0.2.0') {
@@ -67,7 +67,65 @@ export const processNearPayment = async (
6767
await contract.transfer_with_reference(
6868
{
6969
to,
70-
payment_reference,
70+
payment_reference: paymentReference,
71+
},
72+
GAS_LIMIT.toString(),
73+
amount.toString(),
74+
);
75+
return;
76+
} catch (e) {
77+
throw new Error(`Could not pay Near request. Got ${e.message}`);
78+
}
79+
};
80+
81+
/**
82+
* Processes a payment in Near native token, with conversion.
83+
*
84+
* @param amount is defined with 2 decimals, denominated in `currency`
85+
* @param currency is a currency ticker (e.g. "ETH" or "USD")
86+
* @param maxRateTimespan accepts any kind rate's age if '0'
87+
*/
88+
export const processNearPaymentWithConversion = async (
89+
walletConnection: WalletConnection,
90+
network: string,
91+
amount: BigNumberish,
92+
to: string,
93+
paymentReference: string,
94+
currency: string,
95+
feeAddress: string,
96+
feeAmount: BigNumberish,
97+
maxRateTimespan = '0',
98+
version = '0.1.0',
99+
): Promise<void> => {
100+
if (version !== '0.1.0') {
101+
throw new Error('Native Token with conversion payments on Near only support v0.1.0 extensions');
102+
}
103+
104+
if (!(await isValidNearAddress(walletConnection._near, to))) {
105+
throw new Error(`Invalid NEAR payment address: ${to}`);
106+
}
107+
108+
if (!(await isValidNearAddress(walletConnection._near, feeAddress))) {
109+
throw new Error(`Invalid NEAR fee address: ${feeAddress}`);
110+
}
111+
try {
112+
const contract = new Contract(
113+
walletConnection.account(),
114+
NearConversionNativeTokenPaymentDetector.getContractName(network, version),
115+
{
116+
changeMethods: ['transfer_with_reference'],
117+
viewMethods: [],
118+
},
119+
) as any;
120+
await contract.transfer_with_reference(
121+
{
122+
payment_reference: paymentReference,
123+
to,
124+
amount,
125+
currency,
126+
fee_address: feeAddress,
127+
fee_amount: feeAmount,
128+
max_rate_timespan: maxRateTimespan,
71129
},
72130
GAS_LIMIT.toString(),
73131
amount.toString(),

packages/payment-processor/src/payment/utils.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,16 @@ export function validateRequest(
206206

207207
// Compatibility of the request currency type with the payment network
208208
const expectedCurrencyType = currenciesMap[paymentNetworkId];
209-
const validCurrencyType =
210-
paymentNetworkId === PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY
211-
? // Any currency type is valid with Any to ERC20 conversion
212-
true
213-
: expectedCurrencyType &&
214-
request.currencyInfo.type === expectedCurrencyType &&
215-
request.currencyInfo.network;
209+
const validCurrencyType = [
210+
PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY,
211+
PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE,
212+
PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY,
213+
].includes(paymentNetworkId)
214+
? // Any currency type is valid with Any to ERC20 / ETH / Native conversion
215+
true
216+
: expectedCurrencyType &&
217+
request.currencyInfo.type === expectedCurrencyType &&
218+
request.currencyInfo.network;
216219

217220
// ERC20 based payment networks are only valid if the request currency has a value
218221
const validCurrencyValue =

0 commit comments

Comments
 (0)