Skip to content

Commit

Permalink
balance check tested and processor implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
yomarion committed Apr 7, 2023
1 parent 6ac8c28 commit 982e867
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 24 deletions.
5 changes: 2 additions & 3 deletions packages/payment-processor/src/payment/erc20-fee-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ export function _getErc20FeeProxyPaymentUrl(
feeAmountOverride?: BigNumberish,
): string {
validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT);
const { paymentReference, paymentAddress, feeAddress, feeAmount, version } =
const { paymentReference, paymentAddress, feeAddress, feeAmount, version, network } =
getRequestPaymentValues(request);
const { network } = request.currencyInfo;
EvmChains.assertChainSupported(network!);
const contractAddress = erc20FeeProxyArtifact.getAddress(network, version);
const amountToPay = getAmountToPay(request, amount);
Expand All @@ -92,7 +91,7 @@ export function _getErc20FeeProxyPaymentUrl(
}

/**
* Prepate the transaction to pay a request through the ERC20 fee proxy contract, can be used with a Multisig contract.
* Prepare the transaction to pay a request through the ERC20 fee proxy 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.
Expand Down
4 changes: 2 additions & 2 deletions packages/payment-processor/src/payment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ export async function isSolvent(
): Promise<boolean> {
// Near case
if (NearChains.isChainSupported(currency.network) && providerOptions?.nearWalletConnection) {
return isNearAccountSolvent(amount, providerOptions.nearWalletConnection);
return isNearAccountSolvent(amount, providerOptions.nearWalletConnection, currency);
}
// Main case (web3)
// Main case (EVM)
if (!providerOptions?.provider) {
throw new Error('provider missing');
}
Expand Down
64 changes: 64 additions & 0 deletions packages/payment-processor/src/payment/near-fungible.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { BigNumberish } from 'ethers';
import { WalletConnection } from 'near-api-js';

import { ClientTypes, ExtensionTypes } from '@requestnetwork/types';

import {
getRequestPaymentValues,
validateRequest,
getAmountToPay,
getPaymentExtensionVersion,
} from './utils';
import {
INearTransactionCallback,
isReceiverReady,
processNearFungiblePayment,
} from './utils-near';
import { NearChains } from '@requestnetwork/currency';

/**
* Processes the transaction to pay a request in fungible token on NEAR with fee (Erc20FeeProxy).
* @param request the request to pay
*/
export async function payFungibleNearRequest(
request: ClientTypes.IRequestData,
walletConnection: WalletConnection,
amount?: BigNumberish,
callback?: INearTransactionCallback,
): Promise<void> {
validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT);

const { paymentReference, paymentAddress, feeAddress, feeAmount, network } =
getRequestPaymentValues(request);

if (!paymentReference) {
throw new Error('Cannot pay without a paymentReference');
}

if (!network || !NearChains.isChainSupported(network)) {
throw new Error('Should be a Near network');
}
NearChains.assertChainSupported(network);

const amountToPay = getAmountToPay(request, amount).toString();
const version = getPaymentExtensionVersion(request);

if (!(await isReceiverReady(walletConnection, request.currencyInfo.value, paymentAddress))) {
throw new Error(
`The paymentAddress is not registered for the token ${request.currencyInfo.value}`,
);
}

return processNearFungiblePayment(
walletConnection,
network,
amountToPay,
paymentAddress,
paymentReference,
request.currencyInfo.value,
feeAddress || '0x',
feeAmount || 0,
version,
callback,
);
}
6 changes: 3 additions & 3 deletions packages/payment-processor/src/payment/swap-any-to-erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ export function encodeSwapToPayAnyToErc20Request(
signerOrProvider: providers.Provider | Signer = getProvider(),
options: IRequestPaymentOptions,
): string {
const conversionSettings = options?.conversion;
const swapSettings = options?.swap;
const conversionSettings = options.conversion;
const swapSettings = options.swap;

if (!conversionSettings) {
throw new Error(`Conversion Settings are required`);
Expand All @@ -87,7 +87,7 @@ export function encodeSwapToPayAnyToErc20Request(
throw new Error(`Swap Settings are required`);
}
const currencyManager = conversionSettings.currencyManager || CurrencyManager.getDefault();
const network = conversionSettings.currency?.network;
const network = conversionSettings.currency.network;
if (!network) {
throw new Error(`Currency in conversion settings must have a network`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,14 @@ export function encodeSwapToPayErc20FeeRequest(
swapSettings: ISwapSettings,
options?: IRequestPaymentOptions,
): string {
const { network } = request.currencyInfo;
const { paymentReference, paymentAddress, feeAddress, feeAmount, network } =
getRequestPaymentValues(request);
EvmChains.assertChainSupported(network!);

validateErc20FeeProxyRequest(request, options?.amount, options?.feeAmount);

const signer = getSigner(signerOrProvider);
const tokenAddress = request.currencyInfo.value;
const { paymentReference, paymentAddress, feeAddress, feeAmount } =
getRequestPaymentValues(request);
const amountToPay = getAmountToPay(request, options?.amount);
const feeToPay = BigNumber.from(options?.feeAmount || feeAmount || 0);

Expand Down
127 changes: 119 additions & 8 deletions packages/payment-processor/src/payment/utils-near.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
NearConversionNativeTokenPaymentDetector,
NearNativeTokenPaymentDetector,
} from '@requestnetwork/payment-detection';
import { CurrencyTypes } from '@requestnetwork/types';
import { CurrencyTypes, RequestLogicTypes } from '@requestnetwork/types';
import { erc20FeeProxyArtifact } from '@requestnetwork/smart-contracts';

/**
* Callback arguments for the Near web wallet.
Expand All @@ -28,14 +29,29 @@ export const isValidNearAddress = async (nearNetwork: Near, address: string): Pr
export const isNearAccountSolvent = (
amount: BigNumberish,
nearWalletConnection: WalletConnection,
token?: RequestLogicTypes.ICurrency,
): Promise<boolean> => {
return nearWalletConnection
.account()
.state()
.then((state) => {
const balance = BigNumber.from(state?.amount ?? '0');
return balance.gte(amount);
});
if (!token || token.type === RequestLogicTypes.CURRENCY.ETH) {
return nearWalletConnection
.account()
.state()
.then((state) => {
const balance = BigNumber.from(state?.amount ?? '0');
return balance.gte(amount);
});
}
if (token.type === RequestLogicTypes.CURRENCY.ERC20) {
const fungibleContract = new Contract(nearWalletConnection.account(), token.value, {
changeMethods: [],
viewMethods: ['ft_balance_of'],
}) as any;
return fungibleContract
.ft_balance_of({
account_id: nearWalletConnection.account().accountId,
})
.then((balance: string) => BigNumber.from(balance).gte(amount));
}
throw new Error(`isNearAccountSolvent not implemented for ${token.type}`);
};

const GAS_LIMIT_IN_TGAS = 50;
Expand Down Expand Up @@ -148,3 +164,98 @@ export const processNearPaymentWithConversion = async (
throw new Error(`Could not pay Near request. Got ${e.message}`);
}
};

export const processNearFungiblePayment = async (
walletConnection: WalletConnection,
network: CurrencyTypes.NearChainName,
amount: BigNumberish,
to: string,
paymentReference: string,
currencyAddress: string,
feeAddress: string,
feeAmount: BigNumberish,
version = '0.1.0',
callback: INearTransactionCallback | undefined = undefined,
): Promise<void> => {
const fungibleContract = new Contract(walletConnection.account(), currencyAddress, {
changeMethods: ['ft_transfer_call'],
viewMethods: [],
}) as any;

const proxyAddress = erc20FeeProxyArtifact.getAddress(network, version);
await fungibleContract.ft_transfer_call({
args: {
receiver_id: proxyAddress,
amount,
msg: JSON.stringify({
fee_address: feeAddress,
fee_amount: feeAmount,
payment_reference: paymentReference,
to,
}),
},
gas: GAS_LIMIT_CONVERSION_TO_NATIVE,
amount: '1000000000000000000000000 '.toString(), // 1 yoctoNEAR deposit is mandatory for ft_transfer_call
...callback,
});
};

type StorageBalance = {
total: string;
available: string;
};

// min. 0.00125 Ⓝ
const MIN_STORAGE_FOR_FUNGIBLE = '1250000000000000000000';

/**
* Stores the minimum deposit amount on the `paymentAddress` account for `tokenAddress`.
* This does not check the existing deposit, if any, and should be called if `isReceiverReady` is false.
* @param walletConnection
* @param tokenAddress
* @param paymentAddress
*/
export const storageDeposit = async (
walletConnection: WalletConnection,
tokenAddress: string,
paymentAddress: string,
): Promise<void> => {
const fungibleContract = new Contract(walletConnection.account(), tokenAddress, {
changeMethods: ['storage_deposit'],
viewMethods: [],
}) as any;
await fungibleContract.storage_deposit({
args: { account_id: paymentAddress },
value: MIN_STORAGE_FOR_FUNGIBLE,
});
};

/**
* TODO: need to check how I got this error once.
* 'Smart contract panicked: The account account.identifier.near is not registered' if `receiver_id` does not exist.
*
*
*
* This checks that the receiver `paymentAddress` has enough storage on the `tokenAddress` to receive tokens.
*
* It returns false if trying to send tokens to the `paymentAddress` would result in:
*
* - 'Smart contract panicked: The account account.identifier.near is not registered'
*
*/
export const isReceiverReady = async (
walletConnection: WalletConnection,
tokenAddress: string,
paymentAddress: string,
): Promise<boolean> => {
const fungibleContract = new Contract(walletConnection.account(), tokenAddress, {
changeMethods: [],
viewMethods: ['storage_balance_of'],
}) as any;
const storage = (await fungibleContract.storage_balance_of({
args: {
account_id: paymentAddress,
},
})) as StorageBalance | null;
return !!storage && BigNumber.from(storage?.total).gte(MIN_STORAGE_FOR_FUNGIBLE);
};
9 changes: 4 additions & 5 deletions packages/payment-processor/src/payment/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,8 @@ export function getSigner(
}

/**
* Utility to access the payment address, reference,
* and optional feeAmount, feeAddress, expectedFlowRate, expectedStartDate
* of a Request.
* Utility to access payment-related information from a request.
* All data is taken from the request's payment extension, except the network that may be retrieved from the request's currency if needed.
*/
export function getRequestPaymentValues(request: ClientTypes.IRequestData): {
paymentAddress: string;
Expand Down Expand Up @@ -107,7 +106,7 @@ export function getRequestPaymentValues(request: ClientTypes.IRequestData): {
expectedStartDate,
tokensAccepted,
maxRateTimespan,
network,
network: network ?? request.currencyInfo.network,
version: extension.version,
};
}
Expand Down Expand Up @@ -207,7 +206,7 @@ export function validateRequest(
getRequestPaymentValues(request);
let extension = request.extensions[paymentNetworkId];

// FIXME: updating the extension: not needed anymore when "invoicing" will use only ethFeeProxy
// FIXME: updating the extension: not needed anymore when ETH_INPUT_DATA gets deprecated
if (paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT && !extension) {
extension = request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA];
}
Expand Down
Loading

0 comments on commit 982e867

Please sign in to comment.