diff --git a/packages/payment-detection/codegen-tron.yml b/packages/payment-detection/codegen-tron.yml new file mode 100644 index 0000000000..f413ae0109 --- /dev/null +++ b/packages/payment-detection/codegen-tron.yml @@ -0,0 +1,10 @@ +overwrite: true +schema: 'src/thegraph/queries/tron/schema.graphql' +documents: src/thegraph/queries/tron/*.graphql +generates: + src/thegraph/generated/graphql-tron.ts: + plugins: + - 'typescript' + - 'typescript-operations' + - 'typescript-graphql-request' + - 'typescript-document-nodes' diff --git a/packages/payment-detection/package.json b/packages/payment-detection/package.json index 53363b73d3..1a9a8762f9 100644 --- a/packages/payment-detection/package.json +++ b/packages/payment-detection/package.json @@ -38,7 +38,7 @@ "prepare": "yarn run build", "test": "jest --runInBand", "test:watch": "yarn test --watch", - "codegen": "graphql-codegen --config codegen.yml ; graphql-codegen --config codegen-superfluid.yml; graphql-codegen --config codegen-near.yml" + "codegen": "graphql-codegen --config codegen.yml ; graphql-codegen --config codegen-superfluid.yml; graphql-codegen --config codegen-near.yml; graphql-codegen --config codegen-tron.yml" }, "dependencies": { "@requestnetwork/currency": "0.30.0", diff --git a/packages/payment-detection/src/erc20/fee-proxy-contract.ts b/packages/payment-detection/src/erc20/fee-proxy-contract.ts index 80b694c72e..d648149c1f 100644 --- a/packages/payment-detection/src/erc20/fee-proxy-contract.ts +++ b/packages/payment-detection/src/erc20/fee-proxy-contract.ts @@ -161,7 +161,10 @@ export class ERC20FeeProxyPaymentDetector< protected getTheGraphInfoRetriever( paymentChain: TChain, - subgraphClient: TheGraphClient | TheGraphClient, + subgraphClient: + | TheGraphClient + | TheGraphClient + | TheGraphClient, ): TheGraphInfoRetriever | NearInfoRetriever { const graphInfoRetriever = EvmChains.isChainSupported(paymentChain) ? new TheGraphInfoRetriever(subgraphClient as TheGraphClient, this.currencyManager) diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index 42227f874d..d8950d80b0 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -16,6 +16,7 @@ import { getTheGraphClientUrl, getTheGraphEvmClient, getTheGraphNearClient, + getTheGraphTronClient, } from './thegraph'; import { calculateEscrowState, @@ -30,6 +31,7 @@ import { unpadAmountFromChainlink, } from './utils'; import { NearConversionNativeTokenPaymentDetector, NearNativeTokenPaymentDetector } from './near'; +import { TronERC20FeeProxyPaymentDetector, TronInfoRetriever } from './tron'; import { FeeReferenceBasedDetector } from './fee-reference-based-detector'; import { SuperFluidPaymentDetector } from './erc777/superfluid-detector'; import { EscrowERC20InfoRetriever } from './erc20/escrow-info-retriever'; @@ -55,6 +57,8 @@ export { SuperFluidPaymentDetector, NearNativeTokenPaymentDetector, NearConversionNativeTokenPaymentDetector, + TronERC20FeeProxyPaymentDetector, + TronInfoRetriever, EscrowERC20InfoRetriever, SuperFluidInfoRetriever, MetaDetector, @@ -65,6 +69,7 @@ export { getTheGraphClientUrl, getTheGraphEvmClient, getTheGraphNearClient, + getTheGraphTronClient, parseLogArgs, padAmountForChainlink, unpadAmountFromChainlink, diff --git a/packages/payment-detection/src/payment-network-factory.ts b/packages/payment-detection/src/payment-network-factory.ts index b88a2689d9..7e5e48de7a 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -24,6 +24,7 @@ import { SuperFluidPaymentDetector } from './erc777/superfluid-detector'; import { EthFeeProxyPaymentDetector, EthInputDataPaymentDetector } from './eth'; import { AnyToERC20PaymentDetector, AnyToEthFeeProxyPaymentDetector } from './any'; import { NearConversionNativeTokenPaymentDetector, NearNativeTokenPaymentDetector } from './near'; +import { TronERC20FeeProxyPaymentDetector } from './tron'; import { getPaymentNetworkExtension } from './utils'; import { getTheGraphClient } from './thegraph'; import { getDefaultProvider } from 'ethers'; @@ -55,6 +56,13 @@ const supportedPaymentNetwork: ISupportedPaymentNetworkByCurrency = { 'near-testnet': { [PN_ID.ERC20_FEE_PROXY_CONTRACT]: ERC20FeeProxyPaymentDetector, }, + // TRON chains + tron: { + [PN_ID.ERC20_FEE_PROXY_CONTRACT]: TronERC20FeeProxyPaymentDetector, + }, + nile: { + [PN_ID.ERC20_FEE_PROXY_CONTRACT]: TronERC20FeeProxyPaymentDetector, + }, '*': { [PN_ID.ERC20_ADDRESS_BASED]: ERC20AddressBasedPaymentDetector, diff --git a/packages/payment-detection/src/thegraph/client.ts b/packages/payment-detection/src/thegraph/client.ts index f5700ae013..36fe418add 100644 --- a/packages/payment-detection/src/thegraph/client.ts +++ b/packages/payment-detection/src/thegraph/client.ts @@ -1,9 +1,10 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { CurrencyTypes } from '@requestnetwork/types'; -import { NearChains } from '@requestnetwork/currency'; +import { NearChains, TronChains } from '@requestnetwork/currency'; import { GraphQLClient } from 'graphql-request'; import { Block_Height, getSdk, Maybe } from './generated/graphql'; import { getSdk as getNearSdk } from './generated/graphql-near'; +import { getSdk as getTronSdk } from './generated/graphql-tron'; const THE_GRAPH_STUDIO_URL = 'https://api.studio.thegraph.com/query/67444/request-payments-$NETWORK/version/latest'; @@ -20,6 +21,13 @@ const THE_GRAPH_URL_MANTLE = const THE_GRAPH_URL_CORE = 'https://thegraph.coredao.org/subgraphs/name/requestnetwork/request-payments-core'; +// TRON Substreams-powered subgraph URLs +const THE_GRAPH_URL_TRON = + 'https://api.studio.thegraph.com/query/67444/request-payments-tron/version/latest'; + +const THE_GRAPH_URL_TRON_NILE = + 'https://api.studio.thegraph.com/query/67444/request-payments-tron-nile/version/latest'; + const THE_GRAPH_EXPLORER_SUBGRAPH_ID: Partial> = { ['arbitrum-one']: '3MtDdHbzvBVNBpzUTYXGuDDLgTd1b8bPYwoH1Hdssgp9', avalanche: 'A27V4PeZdKHeyuBkehdBJN8cxNtzVpXvYoqkjHUHRCFp', @@ -47,11 +55,13 @@ const THE_GRAPH_EXPLORER_SUBGRAPH_ID: Partial = (TChain extends CurrencyTypes.NearChainName ? ReturnType + : TChain extends CurrencyTypes.TronChainName + ? ReturnType : ReturnType) & { options?: TheGraphQueryOptions; }; @@ -104,6 +114,9 @@ export const getTheGraphClient = ( ) => { const url = getTheGraphClientUrl(network, options); if (!url) return; + if (TronChains.isChainSupported(network)) { + return getTheGraphTronClient(url, options); + } return NearChains.isChainSupported(network) ? getTheGraphNearClient(url, options) : getTheGraphEvmClient(url, options); @@ -127,6 +140,15 @@ export const getTheGraphNearClient = (url: string, options?: TheGraphClientOptio return sdk; }; +export const getTheGraphTronClient = (url: string, options?: TheGraphClientOptions) => { + const [clientOptions, queryOptions] = extractClientOptions(url, options); + const sdk: TheGraphClient = getTronSdk( + new GraphQLClient(url, clientOptions), + ); + sdk.options = queryOptions; + return sdk; +}; + export const getTheGraphClientUrl = ( network: CurrencyTypes.ChainName, options?: TheGraphClientOptions, @@ -155,6 +177,10 @@ export const getTheGraphClientUrl = ( return THE_GRAPH_URL_MANTLE_TESTNET; case chain === 'core': return THE_GRAPH_URL_CORE; + case chain === 'tron': + return THE_GRAPH_URL_TRON; + case chain === 'nile': + return THE_GRAPH_URL_TRON_NILE; default: return shouldUseTheGraphExplorer ? theGraphExplorerUrl : theGraphStudioUrl; } diff --git a/packages/payment-detection/src/thegraph/queries/tron/GetTronPayments.graphql b/packages/payment-detection/src/thegraph/queries/tron/GetTronPayments.graphql new file mode 100644 index 0000000000..0d4a0d79b7 --- /dev/null +++ b/packages/payment-detection/src/thegraph/queries/tron/GetTronPayments.graphql @@ -0,0 +1,45 @@ +# Getting TRC20 payments from the ERC20FeeProxy contract on TRON +query GetTronPayments( + $reference: Bytes! + $to: String! + $tokenAddress: String! + $contractAddress: String! +) { + payments( + where: { + reference: $reference + to: $to + tokenAddress: $tokenAddress + contractAddress: $contractAddress + } + orderBy: timestamp + orderDirection: asc + ) { + amount + block + txHash + feeAmount + feeAddress + from + timestamp + tokenAddress + } +} + +# Getting TRC20 payments without token address filter (for any token) +query GetTronPaymentsAnyToken($reference: Bytes!, $to: String!, $contractAddress: String!) { + payments( + where: { reference: $reference, to: $to, contractAddress: $contractAddress } + orderBy: timestamp + orderDirection: asc + ) { + amount + block + txHash + feeAmount + feeAddress + from + timestamp + tokenAddress + } +} diff --git a/packages/payment-detection/src/thegraph/queries/tron/graphql.config.yml b/packages/payment-detection/src/thegraph/queries/tron/graphql.config.yml new file mode 100644 index 0000000000..f731d57f3c --- /dev/null +++ b/packages/payment-detection/src/thegraph/queries/tron/graphql.config.yml @@ -0,0 +1,2 @@ +# Using local schema until the subgraph is deployed to The Graph Studio +schema: ../../../../../substreams-tron/schema.graphql diff --git a/packages/payment-detection/src/thegraph/queries/tron/schema.graphql b/packages/payment-detection/src/thegraph/queries/tron/schema.graphql new file mode 100644 index 0000000000..591bee29a3 --- /dev/null +++ b/packages/payment-detection/src/thegraph/queries/tron/schema.graphql @@ -0,0 +1,69 @@ +# GraphQL schema for Request Network TRON payments +# This schema is used for generating TypeScript types for The Graph queries + +type Query { + payments( + where: Payment_filter + orderBy: Payment_orderBy + orderDirection: OrderDirection + ): [Payment!]! +} + +enum OrderDirection { + asc + desc +} + +enum Payment_orderBy { + timestamp + block + amount +} + +input Payment_filter { + reference: Bytes + to: String + tokenAddress: String + contractAddress: String +} + +type Payment { + "Unique identifier: txHash-logIndex" + id: ID! + + "The TRC20 token contract address" + tokenAddress: String! + + "The payment recipient address" + to: String! + + "The payment amount" + amount: BigInt! + + "The indexed payment reference (keccak256 hash)" + reference: Bytes! + + "The fee amount" + feeAmount: BigInt! + + "The fee recipient address" + feeAddress: String + + "The sender address" + from: String! + + "Block number" + block: Int! + + "Block timestamp (Unix seconds)" + timestamp: Int! + + "Transaction hash" + txHash: String! + + "The ERC20FeeProxy contract address" + contractAddress: String! +} + +scalar BigInt +scalar Bytes diff --git a/packages/payment-detection/src/tron/index.ts b/packages/payment-detection/src/tron/index.ts new file mode 100644 index 0000000000..7356bcf3bd --- /dev/null +++ b/packages/payment-detection/src/tron/index.ts @@ -0,0 +1,3 @@ +export { TronERC20FeeProxyPaymentDetector } from './tron-fee-proxy-detector'; +export { TronInfoRetriever } from './tron-info-retriever'; +export type { TronPaymentEvent } from './tron-info-retriever'; diff --git a/packages/payment-detection/src/tron/tron-fee-proxy-detector.ts b/packages/payment-detection/src/tron/tron-fee-proxy-detector.ts new file mode 100644 index 0000000000..c96be94a69 --- /dev/null +++ b/packages/payment-detection/src/tron/tron-fee-proxy-detector.ts @@ -0,0 +1,128 @@ +import { erc20FeeProxyArtifact } from '@requestnetwork/smart-contracts'; +import { + CurrencyTypes, + ExtensionTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import { TronChains, isSameChain } from '@requestnetwork/currency'; + +import { ERC20FeeProxyPaymentDetectorBase } from '../erc20/fee-proxy-contract'; +import { NetworkNotSupported } from '../balance-error'; +import { ReferenceBasedDetectorOptions, TGetSubGraphClient } from '../types'; +import { TronInfoRetriever, TronPaymentEvent } from './tron-info-retriever'; +import { TheGraphClient } from '../thegraph'; + +/** + * Handle payment networks with ERC20 fee proxy contract extension on TRON chains + */ +export class TronERC20FeeProxyPaymentDetector extends ERC20FeeProxyPaymentDetectorBase< + ExtensionTypes.PnFeeReferenceBased.IFeeReferenceBased, + TronPaymentEvent +> { + private readonly getSubgraphClient: TGetSubGraphClient; + protected readonly network: CurrencyTypes.TronChainName | undefined; + + constructor({ + advancedLogic, + currencyManager, + getSubgraphClient, + network, + }: ReferenceBasedDetectorOptions & { + network?: CurrencyTypes.TronChainName; + getSubgraphClient: TGetSubGraphClient; + }) { + super( + ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, + advancedLogic.getFeeProxyContractErc20ForNetwork(network) ?? + advancedLogic.extensions.feeProxyContractErc20, + currencyManager, + ); + this.getSubgraphClient = getSubgraphClient; + this.network = network; + } + + /** + * Gets the deployment information for the ERC20FeeProxy contract on TRON + */ + public static getDeploymentInformation( + network: CurrencyTypes.VMChainName, + _paymentNetworkVersion?: string, + ): { address: string; creationBlockNumber: number; contractVersion: string } { + void _paymentNetworkVersion; // Parameter kept for API compatibility + + // Validate that the network is a TRON chain + if (network !== 'tron' && network !== 'nile') { + throw new Error( + `TronERC20FeeProxyPaymentDetector only supports TRON networks, got: ${network}`, + ); + } + + // For TRON, we use the 'tron' version of the artifact + const tronNetwork = network as CurrencyTypes.TronChainName; + const address = erc20FeeProxyArtifact.getAddress(tronNetwork, 'tron'); + const creationBlockNumber = + tronNetwork === 'tron' + ? 79216121 // TRON mainnet + : 63208782; // Nile testnet + + return { address, creationBlockNumber, contractVersion: 'tron' }; + } + + /** + * Extracts the payment events of a request on TRON + */ + protected async extractEvents( + eventName: PaymentTypes.EVENTS_NAMES, + toAddress: string | undefined, + paymentReference: string, + requestCurrency: RequestLogicTypes.ICurrency, + paymentChain: CurrencyTypes.TronChainName, + _paymentNetwork: ExtensionTypes.IState, + ): Promise> { + void _paymentNetwork; // Parameter required by parent class signature + // Validate that the payment chain is a supported TRON chain + if (!TronChains.isChainSupported(paymentChain)) { + throw new NetworkNotSupported( + `Unsupported TRON network '${paymentChain}' for TRON payment detector`, + ); + } + + if (this.network && !isSameChain(paymentChain, this.network)) { + throw new NetworkNotSupported( + `Unsupported network '${paymentChain}' for payment detector instantiated with '${this.network}'`, + ); + } + + if (!toAddress) { + return { + paymentEvents: [], + }; + } + + const { address: proxyContractAddress } = + TronERC20FeeProxyPaymentDetector.getDeploymentInformation(paymentChain); + + const subgraphClient = this.getSubgraphClient( + paymentChain, + ) as TheGraphClient; + + if (!subgraphClient) { + throw new Error( + `Could not get a TheGraph-based info retriever for TRON chain ${paymentChain}. ` + + `Ensure the TRON Substreams-powered subgraph is deployed and accessible.`, + ); + } + + const infoRetriever = new TronInfoRetriever(subgraphClient); + + return infoRetriever.getTransferEvents({ + eventName, + paymentReference, + toAddress, + contractAddress: proxyContractAddress, + paymentChain, + acceptedTokens: [requestCurrency.value], + }); + } +} diff --git a/packages/payment-detection/src/tron/tron-info-retriever.ts b/packages/payment-detection/src/tron/tron-info-retriever.ts new file mode 100644 index 0000000000..5cee4b2d37 --- /dev/null +++ b/packages/payment-detection/src/tron/tron-info-retriever.ts @@ -0,0 +1,78 @@ +import { CurrencyTypes, PaymentTypes } from '@requestnetwork/types'; +import { utils } from 'ethers'; +import type { TheGraphClient } from '../thegraph'; +import type { GetTronPaymentsQuery } from '../thegraph/generated/graphql-tron'; +import { ITheGraphBaseInfoRetriever, TransferEventsParams } from '../types'; + +/** + * TRON payment event parameters + */ +export interface TronPaymentEvent extends PaymentTypes.IERC20FeePaymentEventParameters { + txHash: string; +} + +/** + * TheGraph info retriever for ERC20FeeProxy payments on TRON + * Retrieves TransferWithReferenceAndFee events from the TRON Substreams-powered subgraph + */ +export class TronInfoRetriever implements ITheGraphBaseInfoRetriever { + constructor(protected readonly client: TheGraphClient) {} + + public async getTransferEvents( + params: TransferEventsParams, + ): Promise> { + const { paymentReference, toAddress, contractAddress, acceptedTokens } = params; + + if (acceptedTokens && acceptedTokens.length > 1) { + throw new Error(`TronInfoRetriever does not support multiple accepted tokens.`); + } + + // Hash the payment reference as done in EVM subgraphs + const hashedReference = utils.keccak256(`0x${paymentReference}`); + + let payments: GetTronPaymentsQuery['payments']; + + if (acceptedTokens?.length === 1) { + const result = await this.client.GetTronPayments({ + reference: hashedReference, + to: toAddress, + tokenAddress: acceptedTokens[0], + contractAddress, + }); + payments = result.payments; + } else { + const result = await this.client.GetTronPaymentsAnyToken({ + reference: hashedReference, + to: toAddress, + contractAddress, + }); + payments = result.payments; + } + + return { + paymentEvents: payments.map((p: GetTronPaymentsQuery['payments'][0]) => + this.mapPaymentEvent(p, params), + ), + }; + } + + private mapPaymentEvent( + payment: GetTronPaymentsQuery['payments'][0], + params: TransferEventsParams, + ): PaymentTypes.IPaymentNetworkEvent { + return { + amount: String(payment.amount), + name: params.eventName, + timestamp: payment.timestamp, + parameters: { + feeAmount: payment.feeAmount ? String(payment.feeAmount) : undefined, + txHash: payment.txHash, + block: payment.block, + to: params.toAddress, + from: payment.from, + feeAddress: payment.feeAddress ?? undefined, + tokenAddress: payment.tokenAddress, + }, + }; + } +} diff --git a/packages/payment-detection/test/tron/tron-fee-proxy-detector.test.ts b/packages/payment-detection/test/tron/tron-fee-proxy-detector.test.ts new file mode 100644 index 0000000000..5aa7fd5dad --- /dev/null +++ b/packages/payment-detection/test/tron/tron-fee-proxy-detector.test.ts @@ -0,0 +1,189 @@ +import { + CurrencyTypes, + ExtensionTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import { TronERC20FeeProxyPaymentDetector } from '../../src/tron/tron-fee-proxy-detector'; + +describe('TronERC20FeeProxyPaymentDetector', () => { + describe('getDeploymentInformation', () => { + it('should return correct address for TRON mainnet', () => { + const info = TronERC20FeeProxyPaymentDetector.getDeploymentInformation('tron'); + expect(info.address).toBe('TCUDPYnS9dH3WvFEaE7wN7vnDa51J4R4fd'); + expect(info.creationBlockNumber).toBe(79216121); + }); + + it('should return correct address for Nile testnet', () => { + const info = TronERC20FeeProxyPaymentDetector.getDeploymentInformation('nile'); + expect(info.address).toBe('THK5rNmrvCujhmrXa5DB1dASepwXTr9cJs'); + expect(info.creationBlockNumber).toBe(63208782); + }); + }); + + describe('constructor', () => { + const mockAdvancedLogic = { + getFeeProxyContractErc20ForNetwork: jest.fn().mockReturnValue(undefined), + extensions: { + feeProxyContractErc20: { + id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + version: '0.1.0', + }, + }, + }; + + const mockCurrencyManager = { + from: jest.fn(), + fromStorageCurrency: jest.fn(), + }; + + const mockGetSubgraphClient = jest.fn(); + + it('should create detector for TRON network', () => { + const detector = new TronERC20FeeProxyPaymentDetector({ + advancedLogic: mockAdvancedLogic as any, + currencyManager: mockCurrencyManager as any, + getSubgraphClient: mockGetSubgraphClient, + network: 'tron', + }); + + expect(detector).toBeInstanceOf(TronERC20FeeProxyPaymentDetector); + }); + + it('should create detector for Nile network', () => { + const detector = new TronERC20FeeProxyPaymentDetector({ + advancedLogic: mockAdvancedLogic as any, + currencyManager: mockCurrencyManager as any, + getSubgraphClient: mockGetSubgraphClient, + network: 'nile', + }); + + expect(detector).toBeInstanceOf(TronERC20FeeProxyPaymentDetector); + }); + }); + + describe('extractEvents', () => { + const mockPayment = { + amount: '1000000', + block: 63208800, + txHash: 'abc123def456', + feeAmount: '10000', + feeAddress: 'TFeeAddress1234567890123456789012', + from: 'TFromAddress1234567890123456789012', + timestamp: 1700000000, + tokenAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + }; + + const mockSubgraphClient = { + GetTronPayments: jest.fn().mockResolvedValue({ payments: [mockPayment] }), + GetTronPaymentsAnyToken: jest.fn().mockResolvedValue({ payments: [mockPayment] }), + options: {}, + }; + + const mockAdvancedLogic = { + getFeeProxyContractErc20ForNetwork: jest.fn().mockReturnValue(undefined), + extensions: { + feeProxyContractErc20: { + id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + version: '0.1.0', + }, + }, + }; + + const mockCurrencyManager = { + from: jest.fn(), + fromStorageCurrency: jest.fn().mockReturnValue({ + symbol: 'USDT', + decimals: 6, + network: 'tron', + address: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + }), + }; + + it('should throw error when subgraph client is not available', async () => { + const mockGetSubgraphClient = jest.fn().mockReturnValue(undefined); + + const detector = new TronERC20FeeProxyPaymentDetector({ + advancedLogic: mockAdvancedLogic as any, + currencyManager: mockCurrencyManager as any, + getSubgraphClient: mockGetSubgraphClient, + network: 'tron', + }); + + // Access protected method through casting + const extractEvents = (detector as any).extractEvents.bind(detector); + + await expect( + extractEvents( + PaymentTypes.EVENTS_NAMES.PAYMENT, + 'TToAddress12345678901234567890123', + 'paymentref123', + { + value: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + network: 'tron', + } as RequestLogicTypes.ICurrency, + 'tron' as CurrencyTypes.TronChainName, + { version: '0.1.0' } as ExtensionTypes.IState, + ), + ).rejects.toThrow('Could not get a TheGraph-based info retriever'); + }); + + it('should return empty events when toAddress is undefined', async () => { + const mockGetSubgraphClient = jest.fn().mockReturnValue(mockSubgraphClient); + + const detector = new TronERC20FeeProxyPaymentDetector({ + advancedLogic: mockAdvancedLogic as any, + currencyManager: mockCurrencyManager as any, + getSubgraphClient: mockGetSubgraphClient, + network: 'tron', + }); + + // Access protected method through casting + const extractEvents = (detector as any).extractEvents.bind(detector); + + const result = await extractEvents( + PaymentTypes.EVENTS_NAMES.PAYMENT, + undefined, + 'paymentref123', + { + value: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + network: 'tron', + } as RequestLogicTypes.ICurrency, + 'tron' as CurrencyTypes.TronChainName, + { version: '0.1.0' } as ExtensionTypes.IState, + ); + + expect(result.paymentEvents).toHaveLength(0); + }); + + it('should throw NetworkNotSupported for unsupported chains', async () => { + const mockGetSubgraphClient = jest.fn().mockReturnValue(mockSubgraphClient); + + const detector = new TronERC20FeeProxyPaymentDetector({ + advancedLogic: mockAdvancedLogic as any, + currencyManager: mockCurrencyManager as any, + getSubgraphClient: mockGetSubgraphClient, + network: 'tron', + }); + + // Access protected method through casting + const extractEvents = (detector as any).extractEvents.bind(detector); + + await expect( + extractEvents( + PaymentTypes.EVENTS_NAMES.PAYMENT, + 'TToAddress12345678901234567890123', + 'paymentref123', + { + value: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + network: 'nile', + } as RequestLogicTypes.ICurrency, + 'nile' as CurrencyTypes.TronChainName, // Different from instantiated network + { version: '0.1.0' } as ExtensionTypes.IState, + ), + ).rejects.toThrow("Unsupported network 'nile' for payment detector instantiated with 'tron'"); + }); + }); +}); diff --git a/packages/payment-detection/test/tron/tron-info-retriever.test.ts b/packages/payment-detection/test/tron/tron-info-retriever.test.ts new file mode 100644 index 0000000000..83e1260105 --- /dev/null +++ b/packages/payment-detection/test/tron/tron-info-retriever.test.ts @@ -0,0 +1,111 @@ +import { PaymentTypes } from '@requestnetwork/types'; +import { TronInfoRetriever } from '../../src/tron/tron-info-retriever'; + +describe('TronInfoRetriever', () => { + const mockPayment = { + amount: '1000000', + block: 63208800, + txHash: 'abc123def456', + feeAmount: '10000', + feeAddress: 'TFeeAddress1234567890123456789012', + from: 'TFromAddress1234567890123456789012', + timestamp: 1700000000, + tokenAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + }; + + const createMockClient = (payments: (typeof mockPayment)[]) => ({ + GetTronPayments: jest.fn().mockResolvedValue({ payments }), + GetTronPaymentsAnyToken: jest.fn().mockResolvedValue({ payments }), + options: {}, + }); + + describe('getTransferEvents', () => { + it('should retrieve payment events with token filter', async () => { + const mockClient = createMockClient([mockPayment]); + const retriever = new TronInfoRetriever(mockClient as any); + + const result = await retriever.getTransferEvents({ + paymentReference: 'abc123', + toAddress: 'TToAddress12345678901234567890123', + contractAddress: 'TCUDPYnS9dH3WvFEaE7wN7vnDa51J4R4fd', + paymentChain: 'tron', + eventName: PaymentTypes.EVENTS_NAMES.PAYMENT, + acceptedTokens: ['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'], + }); + + expect(mockClient.GetTronPayments).toHaveBeenCalled(); + expect(result.paymentEvents).toHaveLength(1); + expect(result.paymentEvents[0].amount).toBe('1000000'); + expect(result.paymentEvents[0].name).toBe(PaymentTypes.EVENTS_NAMES.PAYMENT); + expect(result.paymentEvents[0].parameters?.feeAmount).toBe('10000'); + expect(result.paymentEvents[0].parameters?.txHash).toBe('abc123def456'); + }); + + it('should retrieve payment events without token filter', async () => { + const mockClient = createMockClient([mockPayment]); + const retriever = new TronInfoRetriever(mockClient as any); + + const result = await retriever.getTransferEvents({ + paymentReference: 'abc123', + toAddress: 'TToAddress12345678901234567890123', + contractAddress: 'TCUDPYnS9dH3WvFEaE7wN7vnDa51J4R4fd', + paymentChain: 'tron', + eventName: PaymentTypes.EVENTS_NAMES.PAYMENT, + }); + + expect(mockClient.GetTronPaymentsAnyToken).toHaveBeenCalled(); + expect(result.paymentEvents).toHaveLength(1); + }); + + it('should return empty array when no payments found', async () => { + const mockClient = createMockClient([]); + const retriever = new TronInfoRetriever(mockClient as any); + + const result = await retriever.getTransferEvents({ + paymentReference: 'abc123', + toAddress: 'TToAddress12345678901234567890123', + contractAddress: 'TCUDPYnS9dH3WvFEaE7wN7vnDa51J4R4fd', + paymentChain: 'tron', + eventName: PaymentTypes.EVENTS_NAMES.PAYMENT, + acceptedTokens: ['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'], + }); + + expect(result.paymentEvents).toHaveLength(0); + }); + + it('should throw error for multiple accepted tokens', async () => { + const mockClient = createMockClient([]); + const retriever = new TronInfoRetriever(mockClient as any); + + await expect( + retriever.getTransferEvents({ + paymentReference: 'abc123', + toAddress: 'TToAddress12345678901234567890123', + contractAddress: 'TCUDPYnS9dH3WvFEaE7wN7vnDa51J4R4fd', + paymentChain: 'tron', + eventName: PaymentTypes.EVENTS_NAMES.PAYMENT, + acceptedTokens: [ + 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + 'TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8', + ], + }), + ).rejects.toThrow('TronInfoRetriever does not support multiple accepted tokens'); + }); + + it('should handle refund events correctly', async () => { + const mockClient = createMockClient([mockPayment]); + const retriever = new TronInfoRetriever(mockClient as any); + + const result = await retriever.getTransferEvents({ + paymentReference: 'abc123', + toAddress: 'TToAddress12345678901234567890123', + contractAddress: 'TCUDPYnS9dH3WvFEaE7wN7vnDa51J4R4fd', + paymentChain: 'tron', + eventName: PaymentTypes.EVENTS_NAMES.REFUND, + acceptedTokens: ['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'], + }); + + expect(result.paymentEvents[0].name).toBe(PaymentTypes.EVENTS_NAMES.REFUND); + }); + }); +});