Skip to content
Open
5 changes: 5 additions & 0 deletions packages/advanced-logic/src/advanced-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import AnyToNear from './extensions/payment-network/near/any-to-near';
import AnyToNearTestnet from './extensions/payment-network/near/any-to-near-testnet';
import NativeToken from './extensions/payment-network/native-token';
import AnyToNative from './extensions/payment-network/any-to-native';
import Erc20TransferableReceivablePaymentNetwork from './extensions/payment-network/erc20/transferable-receivable';

/**
* Module to manage Advanced logic extensions
Expand All @@ -46,6 +47,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
feeProxyContractEth: FeeProxyContractEth;
anyToEthProxy: AnyToEthProxy;
anyToNativeToken: AnyToNative[];
erc20TransferableReceivable: Erc20TransferableReceivablePaymentNetwork;
};

constructor(currencyManager?: ICurrencyManager) {
Expand All @@ -67,6 +69,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
anyToEthProxy: new AnyToEthProxy(currencyManager),
nativeToken: [new NearNative(), new NearTestnetNative()],
anyToNativeToken: [new AnyToNear(currencyManager), new AnyToNearTestnet(currencyManager)],
erc20TransferableReceivable: new Erc20TransferableReceivablePaymentNetwork(),
};
}

Expand Down Expand Up @@ -124,6 +127,8 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: this.extensions.anyToEthProxy,
[ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE_TOKEN]:
this.getAnyToNativeTokenExtensionForActionAndState(extensionAction, requestState),
[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]:
this.extensions.erc20TransferableReceivable,
}[id];

if (!extension) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types';
import { FeeReferenceBasedPaymentNetwork } from '../fee-reference-based';

const CURRENT_VERSION = '0.1.0';

/**
* Implementation of the payment network to pay in ERC20 based on a transferable receivable contract.
*/
export default class Erc20TransferableReceivablePaymentNetwork<
TCreationParameters extends ExtensionTypes.PnFeeReferenceBased.ICreationParameters = ExtensionTypes.PnFeeReferenceBased.ICreationParameters,
> extends FeeReferenceBasedPaymentNetwork<TCreationParameters> {
public constructor(
extensionId: ExtensionTypes.PAYMENT_NETWORK_ID = ExtensionTypes.PAYMENT_NETWORK_ID
.ERC20_TRANSFERABLE_RECEIVABLE,
currentVersion: string = CURRENT_VERSION,
) {
super(extensionId, currentVersion, RequestLogicTypes.CURRENCY.ERC20);
}
}
8 changes: 8 additions & 0 deletions packages/integration-test/test/scheduled/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,13 @@ export const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = {
{} as Extension.PnFeeReferenceBased.IFeeReferenceBased<Extension.PnFeeReferenceBased.ICreationParameters>,
anyToNativeToken:
{} as Extension.PnFeeReferenceBased.IFeeReferenceBased<Extension.PnFeeReferenceBased.ICreationParameters>[],
erc20TransferableReceivable: {
createAddPaymentAddressAction,
createAddRefundAddressAction,
createCreationAction,
// inheritance from declarative
createAddPaymentInstructionAction,
createAddRefundInstructionAction,
} as any as Extension.PnFeeReferenceBased.IFeeReferenceBased<Extension.PnFeeReferenceBased.ICreationParameters>,
},
};
1 change: 1 addition & 0 deletions packages/payment-detection/src/erc20/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { ERC20AddressBasedPaymentDetector } from './address-based';
export { ERC20FeeProxyPaymentDetector } from './fee-proxy-contract';
export { ERC20ProxyPaymentDetector } from './proxy-contract';
export { ERC20TransferableReceivablePaymentDetector } from './transferable-receivable';
10 changes: 7 additions & 3 deletions packages/payment-detection/src/erc20/proxy-info-retriever.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ export default class ProxyERC20InfoRetriever

/**
* Retrieves transfer events for the current contract, address and network.
* @param isTransferable Whether or not the request is expected to be paid
* through a receivable proxy contract
*/
public async getTransferEvents(): Promise<PaymentTypes.ERC20PaymentNetworkEvent[]> {
public async getTransferEvents(
isTransferable = false,
): Promise<PaymentTypes.ERC20PaymentNetworkEvent[]> {
// Create a filter to find all the Transfer logs for the toAddress
const filter = this.contractProxy.filters.TransferWithReference(
null,
Expand Down Expand Up @@ -112,7 +116,7 @@ export default class ProxyERC20InfoRetriever
.filter(
({ parsedLog }) =>
parsedLog.tokenAddress.toLowerCase() === this.tokenContractAddress.toLowerCase() &&
parsedLog.to.toLowerCase() === this.toAddress.toLowerCase(),
(isTransferable || parsedLog.to.toLowerCase() === this.toAddress.toLowerCase()),
)
// Creates the balance events
.map(async ({ parsedLog, blockNumber, transactionHash }) => ({
Expand All @@ -122,7 +126,7 @@ export default class ProxyERC20InfoRetriever
block: blockNumber,
feeAddress: parsedLog.feeAddress || undefined,
feeAmount: parsedLog.feeAmount?.toString() || undefined,
to: this.toAddress,
to: parsedLog.to,
txHash: transactionHash,
},
timestamp: (await this.provider.getBlock(blockNumber || 0)).timestamp,
Expand Down
110 changes: 110 additions & 0 deletions packages/payment-detection/src/erc20/transferable-receivable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { ExtensionTypes, PaymentTypes, RequestLogicTypes } from '@requestnetwork/types';

import { TheGraphInfoRetriever } from '../thegraph';
import { erc20TransferableReceivableArtifact } from '@requestnetwork/smart-contracts';
import { makeGetDeploymentInformation } from '../utils';
import { PaymentNetworkOptions, ReferenceBasedDetectorOptions } from '../types';
import { FeeReferenceBasedDetector } from '../fee-reference-based-detector';
import ProxyERC20InfoRetriever from './proxy-info-retriever';

const ERC20_TRANSFERABLE_RECEIVABLE_CONTRACT_ADDRESS_MAP = {
['0.1.0']: '0.1.0',
};

/**
* Handle payment networks with ERC20 transferable receivable contract extension
*/
export class ERC20TransferableReceivablePaymentDetector extends FeeReferenceBasedDetector<
ExtensionTypes.PnFeeReferenceBased.IFeeReferenceBased,
PaymentTypes.IERC20PaymentEventParameters
> {
private readonly getSubgraphClient: PaymentNetworkOptions['getSubgraphClient'];

/**
* @param extension The advanced logic payment network extensions
*/
public constructor({
advancedLogic,
currencyManager,
getSubgraphClient,
}: ReferenceBasedDetectorOptions & Pick<PaymentNetworkOptions, 'getSubgraphClient'>) {
super(
ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE,
advancedLogic.extensions.erc20TransferableReceivable,
currencyManager,
);
this.getSubgraphClient = getSubgraphClient;
}

/**
* Extracts the balance and events of an address
*
* @private
* @param address Address to check
* @param eventName Indicate if it is an address for payment or refund
* @param network The id of network we want to check
* @param tokenContractAddress the address of the token contract
* @returns The balance and events
*/
protected async extractEvents(
eventName: PaymentTypes.EVENTS_NAMES,
toAddress: string | undefined,
paymentReference: string,
requestCurrency: RequestLogicTypes.ICurrency,
paymentChain: string,
paymentNetwork: ExtensionTypes.IState<ExtensionTypes.PnReferenceBased.ICreationParameters>,
): Promise<PaymentTypes.AllNetworkEvents<PaymentTypes.IERC20PaymentEventParameters>> {
// To satisfy typescript
toAddress;
if (!paymentReference) {
return {
paymentEvents: [],
};
}

const {
address: receivableContractAddress,
creationBlockNumber: receivableCreationBlockNumber,
} = ERC20TransferableReceivablePaymentDetector.getDeploymentInformation(
paymentChain,
paymentNetwork.version,
);

const subgraphClient = this.getSubgraphClient(paymentChain);
if (subgraphClient) {
const graphInfoRetriever = new TheGraphInfoRetriever(subgraphClient, this.currencyManager);
return graphInfoRetriever.getReceivableEvents({
paymentReference,
toAddress: '', // Filtering by payee address does not apply for transferable receivables
contractAddress: receivableContractAddress,
paymentChain,
eventName,
acceptedTokens: [requestCurrency.value],
});
} else {
const transferableReceivableInfoRetriever = new ProxyERC20InfoRetriever(
paymentReference,
receivableContractAddress,
receivableCreationBlockNumber,
requestCurrency.value,
'',
eventName,
paymentChain,
);
const paymentEvents = await transferableReceivableInfoRetriever.getTransferEvents(
true /* isReceivable */,
);
return {
paymentEvents,
};
}
}

/*
* Returns deployment information for the underlying smart contract for a given payment network version
*/
public static getDeploymentInformation = makeGetDeploymentInformation(
erc20TransferableReceivableArtifact,
ERC20_TRANSFERABLE_RECEIVABLE_CONTRACT_ADDRESS_MAP,
);
}
2 changes: 2 additions & 0 deletions packages/payment-detection/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { SuperFluidPaymentDetector } from './erc777/superfluid-detector';
import { EscrowERC20InfoRetriever } from './erc20/escrow-info-retriever';
import { SuperFluidInfoRetriever } from './erc777/superfluid-retriever';
import { PaymentNetworkOptions } from './types';
import { ERC20TransferableReceivablePaymentDetector } from './erc20';

export type { TheGraphClient } from './thegraph';

Expand All @@ -36,6 +37,7 @@ export {
BtcPaymentNetwork,
DeclarativePaymentDetector,
Erc20PaymentNetwork,
ERC20TransferableReceivablePaymentDetector,
EthInputDataPaymentDetector,
EthFeeProxyPaymentDetector,
AnyToERC20PaymentDetector,
Expand Down
2 changes: 2 additions & 0 deletions packages/payment-detection/src/payment-network-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ERC20AddressBasedPaymentDetector,
ERC20FeeProxyPaymentDetector,
ERC20ProxyPaymentDetector,
ERC20TransferableReceivablePaymentDetector,
} from './erc20';
import { SuperFluidPaymentDetector } from './erc777/superfluid-detector';
import { EthFeeProxyPaymentDetector, EthInputDataPaymentDetector } from './eth';
Expand Down Expand Up @@ -48,6 +49,7 @@ const supportedPaymentNetwork: ISupportedPaymentNetworkByCurrency = {
[PN_ID.ERC20_ADDRESS_BASED]: ERC20AddressBasedPaymentDetector,
[PN_ID.ERC20_PROXY_CONTRACT]: ERC20ProxyPaymentDetector,
[PN_ID.ERC20_FEE_PROXY_CONTRACT]: ERC20FeeProxyPaymentDetector,
[PN_ID.ERC20_TRANSFERABLE_RECEIVABLE]: ERC20TransferableReceivablePaymentDetector,
},
},
ETH: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { TheGraphClient } from '../client';
export const theGraphClient: TheGraphClient = {
GetLastSyncedBlock: jest.fn(),
GetPaymentsAndEscrowState: jest.fn(),
GetPaymentsAndEscrowStateForReceivables: jest.fn(),
GetSyncedBlock: jest.fn(),
};
export const getTheGraphClient = () => theGraphClient;
Expand Down
19 changes: 19 additions & 0 deletions packages/payment-detection/src/thegraph/info-retriever.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ export class TheGraphInfoRetriever {
};
}

public async getReceivableEvents(
params: TransferEventsParams,
): Promise<PaymentTypes.AllNetworkEvents<PaymentTypes.IERC20FeePaymentEventParameters>> {
const { payments, escrowEvents } = await this.client.GetPaymentsAndEscrowStateForReceivables({
reference: utils.keccak256(`0x${params.paymentReference}`),
});

params.contractAddress = formatAddress(params.contractAddress, 'contractAddress');
params.acceptedTokens =
params.acceptedTokens?.map((tok) => formatAddress(tok, 'acceptedTokens')) || [];

return {
paymentEvents: payments
.filter((payment) => this.filterPaymentEvents(payment, params))
.map((payment) => this.mapPaymentEvents(payment, params)),
escrowEvents: escrowEvents.map((escrow) => this.mapEscrowEvents(escrow, params)),
};
}

private filterPaymentEvents(payment: PaymentEventResultFragment, params: TransferEventsParams) {
// Check contract address matches expected
if (formatAddress(payment.contractAddress) !== formatAddress(params.contractAddress)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ query GetPaymentsAndEscrowState($reference: Bytes!, $to: Bytes!) {
...EscrowEventResult
}
}

# Receivables can be transferred to different owners, so searching by to could drop balance events.
query GetPaymentsAndEscrowStateForReceivables($reference: Bytes!) {
payments(where: { reference: $reference }, orderBy: timestamp, orderDirection: asc) {
...PaymentEventResult
}
escrowEvents(where: { reference: $reference }, orderBy: timestamp, orderDirection: asc) {
...EscrowEventResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ describe('api/erc20/fee-proxy-contract', () => {
].filter((x) => x.reference.toLowerCase() === reference.toLowerCase()),
escrowEvents: [],
})),
GetPaymentsAndEscrowStateForReceivables: jest.fn(),
GetLastSyncedBlock: jest.fn(),
GetSyncedBlock: jest.fn(),
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const paymentReferenceMock = '01111111111111111111111111111111111111111111111111
/* eslint-disable @typescript-eslint/no-unused-expressions */
describe('api/erc20/proxy-info-retriever', () => {
describe('on localhost', () => {
const paymentAddress = '0xf17f52151ebef6c7334fad080c5704d77216b732';
const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732';

it('can get the localhost balance of an address', async () => {
const infoRetriever = new ProxyERC20InfoRetriever(
Expand Down Expand Up @@ -128,7 +128,7 @@ describe('api/erc20/proxy-info-retriever', () => {

const parameters: PaymentTypes.IERC20FeePaymentEventParameters = event.parameters!;

expect(parameters.to).toBe('0x627306090abab3a6e1400e9345bc60c78a8bef57');
expect(parameters.to).toBe('0x627306090abaB3A6e1400e9345bC60c78a8BEf57');
expect(typeof parameters.block).toBe('number');
expect(typeof parameters.txHash).toBe('string');
expect(parameters.feeAddress).toBe('0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ describe('api/erc20/thegraph-info-retriever', () => {
payments: paymentsMockData[reference] || [],
escrowEvents: [],
})),
GetPaymentsAndEscrowStateForReceivables: jest.fn().mockImplementation(({ reference }) => ({
payments: paymentsMockData[reference] || [],
escrowEvents: [],
})),
GetLastSyncedBlock: jest.fn(),
GetSyncedBlock: jest.fn(),
};
Expand Down
Loading