Skip to content

Commit

Permalink
add payment detection for any-to-eth
Browse files Browse the repository at this point in the history
  • Loading branch information
vrolland committed Sep 27, 2021
1 parent 267a143 commit 1d8f309
Show file tree
Hide file tree
Showing 13 changed files with 471 additions and 48 deletions.
66 changes: 66 additions & 0 deletions packages/payment-detection/src/any-to-any-detector.ts
@@ -0,0 +1,66 @@
import { ExtensionTypes, PaymentTypes, RequestLogicTypes } from '@requestnetwork/types';
import Utils from '@requestnetwork/utils';
import FeeReferenceBasedDetector from './fee-reference-based-detector';

import { ICurrencyManager } from '@requestnetwork/currency';

/**
* Abstract class to extend to get the payment balance of conversion requests
*/
export default abstract class AnyToAnyDetector<
TPaymentEventParameters
> extends FeeReferenceBasedDetector<TPaymentEventParameters> {
/**
* @param extension The advanced logic payment network extension, with conversion
* @param extensionType Example : ExtensionTypes.ID.ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ETH_PROXY
*/
public constructor(
protected extension: ExtensionTypes.PnFeeReferenceBased.IFeeReferenceBased,
protected extensionType: ExtensionTypes.ID,
protected currencyManager: ICurrencyManager,
) {
super(extension, extensionType);
}

/**
* Creates the extensions data for the creation of this extension.
* Will set a salt if none is already given
*
* @param paymentNetworkCreationParameters Parameters to create the extension
* @returns The extensionData object
*/
public async createExtensionsDataForCreation(
paymentNetworkCreationParameters: ExtensionTypes.PnAnyToAnyConversion.ICreationParameters,
): Promise<ExtensionTypes.IAction> {
// If no salt is given, generate one
paymentNetworkCreationParameters.salt =
paymentNetworkCreationParameters.salt || (await Utils.crypto.generate8randomBytes());

return this.extension.createCreationAction({
feeAddress: paymentNetworkCreationParameters.feeAddress,
feeAmount: paymentNetworkCreationParameters.feeAmount,
paymentAddress: paymentNetworkCreationParameters.paymentAddress,
refundAddress: paymentNetworkCreationParameters.refundAddress,
network: paymentNetworkCreationParameters.network,
maxRateTimespan: paymentNetworkCreationParameters.maxRateTimespan,
...paymentNetworkCreationParameters,
});
}
/**
* Extracts payment events of an address matching an address and a payment reference
*
* @param address Address to check
* @param eventName Indicate if it is an address for payment or refund
* @param requestCurrency The request currency
* @param paymentReference The reference to identify the payment
* @param paymentNetwork the payment network
* @returns The balance
*/
protected abstract extractEvents(
address: string,
eventName: PaymentTypes.EVENTS_NAMES,
requestCurrency: RequestLogicTypes.ICurrency,
paymentReference: string,
paymentNetwork: ExtensionTypes.IState<any>,
): Promise<PaymentTypes.IPaymentNetworkEvent<TPaymentEventParameters>[]>;
}
127 changes: 127 additions & 0 deletions packages/payment-detection/src/any/any-to-eth-proxy-detector.ts
@@ -0,0 +1,127 @@
import * as SmartContracts from '@requestnetwork/smart-contracts';
import {
AdvancedLogicTypes,
ExtensionTypes,
PaymentTypes,
RequestLogicTypes,
} from '@requestnetwork/types';

import { ICurrencyManager } from '@requestnetwork/currency';

import ProxyInfoRetriever from './any-to-eth-proxy-info-retriever';
import AnyToAnyDetector from '../any-to-any-detector';

// interface of the object indexing the proxy contract version
interface IProxyContractVersion {
[version: string]: string;
}

const PROXY_CONTRACT_ADDRESS_MAP: IProxyContractVersion = {
['0.1.0']: '0.1.0',
};

/**
* Handle payment networks with ETH input data extension
*/
export default class ETHFeeProxyDetector extends AnyToAnyDetector<PaymentTypes.IETHPaymentEventParameters> {
/**
* @param extension The advanced logic payment network extensions
*/
public constructor({
advancedLogic,
currencyManager,
}: {
advancedLogic: AdvancedLogicTypes.IAdvancedLogic;
currencyManager: ICurrencyManager;
}) {
super(
advancedLogic.extensions.feeProxyContractEth,
ExtensionTypes.ID.PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT,
currencyManager,
);
}

/**
* Extracts payment events of an address matching an address and a payment reference
*
* @param address Address to check
* @param eventName Indicate if it is an address for payment or refund
* @param requestCurrency The request currency
* @param paymentReference The reference to identify the payment
* @param paymentNetwork the payment network
* @returns The balance
*/
protected async extractEvents(
address: string,
eventName: PaymentTypes.EVENTS_NAMES,
requestCurrency: RequestLogicTypes.ICurrency,
paymentReference: string,
paymentNetwork: ExtensionTypes.IState<any>,
): Promise<PaymentTypes.ETHPaymentNetworkEvent[]> {
const network = requestCurrency.network;
if (!network) {
throw Error('requestCurrency.network must be defined');
}

const { ethFeeProxyContract, conversionProxyContract } = await this.safeGetProxiesArtifacts(
network,
paymentNetwork.version,
);

if (!ethFeeProxyContract) {
throw Error('ETH fee proxy contract not found');
}
if (!conversionProxyContract) {
throw Error('ETH conversion proxy contract not found');
}

const currency = this.currencyManager.fromStorageCurrency(requestCurrency);
if (!currency) {
throw Error('requestCurrency not found in currency manager');
}

const proxyInfoRetriever = new ProxyInfoRetriever(
currency,
paymentReference,
conversionProxyContract.address,
conversionProxyContract.creationBlockNumber,
ethFeeProxyContract.address,
ethFeeProxyContract.creationBlockNumber,
address,
eventName,
network,
paymentNetwork.values?.maxRateTimespan,
);

return await proxyInfoRetriever.getTransferEvents();
}

/*
* Fetches events from the Ethereum Proxy, or returns null
*/
private async safeGetProxiesArtifacts(network: string, paymentNetworkVersion: string) {
const contractVersion = PROXY_CONTRACT_ADDRESS_MAP[paymentNetworkVersion];
let ethFeeProxyContract = null;
let conversionProxyContract = null;

try {
ethFeeProxyContract = SmartContracts.ethConversionArtifact.getDeploymentInformation(
network,
contractVersion,
);
} catch (error) {
console.warn(error);
}

try {
conversionProxyContract = SmartContracts.ethereumFeeProxyArtifact.getDeploymentInformation(
network,
contractVersion,
);
} catch (error) {
console.warn(error);
}

return { ethFeeProxyContract, conversionProxyContract };
}
}
174 changes: 174 additions & 0 deletions packages/payment-detection/src/any/any-to-eth-proxy-info-retriever.ts
@@ -0,0 +1,174 @@
import { CurrencyDefinition } from '@requestnetwork/currency';
import { PaymentTypes } from '@requestnetwork/types';
import { BigNumber, ethers } from 'ethers';
import { getDefaultProvider } from '../provider';
import { parseLogArgs, unpadAmountFromChainlink } from '../utils';

// The conversion proxy smart contract ABI fragment containing TransferWithConversionAndReference event
const ethConversionProxyContractAbiFragment = [
'event TransferWithConversionAndReference(uint256 amount, address currency, bytes indexed paymentReference, uint256 feeAmount, uint256 maxRateTimespan)',
];

// The ETH proxy smart contract ABI fragment containing TransferWithReference event
const ethFeeProxyContractAbiFragment = [
'event TransferWithReferenceAndFee(address to,uint256 amount,bytes indexed paymentReference,uint256 feeAmount,address feeAddress)',
];

/** TransferWithConversionAndReference event */
type TransferWithConversionAndReferenceArgs = {
amount: BigNumber;
currency: string;
paymentReference: string;
feeAmount: BigNumber;
maxRateTimespan: BigNumber;
};

/** TransferWithReferenceAndFee event */
type TransferWithReferenceAndFeeArgs = {
to: string;
amount: BigNumber;
paymentReference: string;
feeAmount: BigNumber;
feeAddress: string;
};

/**
* Retrieves a list of payment events from a payment reference, a destination address, a token address and a proxy contract
*/
export default class AnyToEthProxyInfoRetriever
implements PaymentTypes.IPaymentNetworkInfoRetriever<PaymentTypes.ETHPaymentNetworkEvent> {
public contractConversionProxy: ethers.Contract;
public contractETHFeeProxy: ethers.Contract;
public provider: ethers.providers.Provider;

/**
* @param requestCurrency The request currency
* @param paymentReference The reference to identify the payment
* @param conversionProxyContractAddress The address of the proxy contract
* @param conversionProxyCreationBlockNumber The block that created the proxy contract
* @param toAddress Address of the balance we want to check
* @param eventName Indicate if it is an address for payment or refund
* @param network The Ethereum network to use
*/
constructor(
private requestCurrency: CurrencyDefinition,
private paymentReference: string,
private conversionProxyContractAddress: string,
private conversionProxyCreationBlockNumber: number,
private ethFeeProxyContractAddress: string,
private ethFeeProxyCreationBlockNumber: number,
private toAddress: string,
private eventName: PaymentTypes.EVENTS_NAMES,
private network: string,
private maxRateTimespan: number = 0,
) {
// Creates a local or default provider
this.provider = getDefaultProvider(this.network);

// Setup the conversion proxy contract interface
this.contractConversionProxy = new ethers.Contract(
this.conversionProxyContractAddress,
ethConversionProxyContractAbiFragment,
this.provider,
);

this.contractETHFeeProxy = new ethers.Contract(
this.ethFeeProxyContractAddress,
ethFeeProxyContractAbiFragment,
this.provider,
);
}

/**
* Retrieves transfer events from the payment proxy and conversion proxy.
* Logs from both proxies are matched by transaction hash, as both proxies should
* be called in one transaction.
*
* The conversion proxy's logs are used to compute the amounts in request currency (typically fiat).
* The payment proxy's logs are used the same way as for a pn-fee-proxy request.
*/
public async getTransferEvents(): Promise<PaymentTypes.ETHPaymentNetworkEvent[]> {
// Create a filter to find all the Fee Transfer logs with the payment reference
const conversionFilter = this.contractConversionProxy.filters.TransferWithConversionAndReference(
null,
null,
'0x' + this.paymentReference,
) as ethers.providers.Filter;
conversionFilter.fromBlock = this.conversionProxyCreationBlockNumber;
conversionFilter.toBlock = 'latest';

// Get the fee proxy contract event logs
const conversionLogs = await this.provider.getLogs(conversionFilter);

// Create a filter to find all the Fee Transfer logs with the payment reference
const feeFilter = this.contractETHFeeProxy.filters.TransferWithReferenceAndFee(
null,
null,
'0x' + this.paymentReference,
null,
null,
) as ethers.providers.Filter;
feeFilter.fromBlock = this.ethFeeProxyCreationBlockNumber;
feeFilter.toBlock = 'latest';

// Get the fee proxy contract event logs
const feeLogs = await this.provider.getLogs(feeFilter);

// Parses, filters and creates the events from the logs with the payment reference
const eventPromises = conversionLogs
// Parses the logs
.map((log) => {
const parsedConversionLog = this.contractConversionProxy.interface.parseLog(log);
const proxyLog = feeLogs.find((l) => l.transactionHash === log.transactionHash);
if (!proxyLog) {
throw new Error('proxy log not found');
}
const parsedProxyLog = this.contractETHFeeProxy.interface.parseLog(proxyLog);
return {
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
conversionLog: parseLogArgs<TransferWithConversionAndReferenceArgs>(parsedConversionLog),
proxyLog: parseLogArgs<TransferWithReferenceAndFeeArgs>(parsedProxyLog),
};
})
// Keeps only the log with the right token and the right destination address
// With ethers v5, the criteria below can be added to the conversionFilter (PROT-1234)
.filter(
({ conversionLog, proxyLog }) =>
// check the rate timespan
this.maxRateTimespan >= conversionLog.maxRateTimespan.toNumber() &&
// check the requestCurrency
this.requestCurrency.hash.toLowerCase() === conversionLog.currency.toLowerCase() &&
// check to address
proxyLog.to.toLowerCase() === this.toAddress.toLowerCase(),
)
// Creates the balance events
.map(async ({ conversionLog, proxyLog, blockNumber, transactionHash }) => {
const requestCurrency = this.requestCurrency;

const amount = unpadAmountFromChainlink(conversionLog.amount, requestCurrency).toString();
const feeAmount = unpadAmountFromChainlink(
conversionLog.feeAmount,
requestCurrency,
).toString();

return {
amount,
name: this.eventName,
parameters: {
block: blockNumber,
feeAddress: proxyLog.feeAddress || undefined,
feeAmount,
feeAmountInCrypto: proxyLog.feeAmount.toString() || undefined,
amountInCrypto: proxyLog.amount.toString(),
to: this.toAddress,
txHash: transactionHash,
maxRateTimespan: conversionLog.maxRateTimespan.toString(),
},
timestamp: (await this.provider.getBlock(blockNumber || 0)).timestamp,
};
});

return Promise.all(eventPromises);
}
}

0 comments on commit 1d8f309

Please sign in to comment.