diff --git a/src/paymasterclient.ts b/src/paymasterclient.ts index fc58ad8..c095f97 100644 --- a/src/paymasterclient.ts +++ b/src/paymasterclient.ts @@ -9,6 +9,10 @@ export type IsSponsorableResponse = { SponsorWebsite: string } +export type SendRawTransactionOptions = { + UserAgent?: string +} + export enum GaslessTransactionStatus { New = 0, Pending = 1, Confirmed = 2, Failed = 3, Invalid = 4} export type GaslessTransaction = { @@ -49,8 +53,35 @@ export type Bundle = { } export class PaymasterClient extends ethers.JsonRpcProvider { - constructor(url?: string | FetchRequest, network?: Networkish, options?: JsonRpcApiProviderOptions) { + private privatePolicyUUID?: string + + private constructor( + url?: string | FetchRequest, + network?: Networkish, + options?: JsonRpcApiProviderOptions, + privatePolicyUUID?: string + ) { super(url, network, options) + this.privatePolicyUUID = privatePolicyUUID + } + + // Static method to create a new standard PaymasterClient + static new( + url?: string | FetchRequest, + network?: Networkish, + options?: JsonRpcApiProviderOptions + ): PaymasterClient { + return new PaymasterClient(url, network, options) + } + + // Static method to create a new PaymasterClient with private policy + static newPrivatePaymaster( + url: string | FetchRequest, + privatePolicyUUID: string, + network?: Networkish, + options?: JsonRpcApiProviderOptions + ): PaymasterClient { + return new PaymasterClient(url, network, options, privatePolicyUUID) } async chainID(): Promise { @@ -58,10 +89,48 @@ export class PaymasterClient extends ethers.JsonRpcProvider { } async isSponsorable(tx: TransactionRequest): Promise { + const policyUUID = this.privatePolicyUUID + if (policyUUID) { + const newConnection = this._getConnection() + newConnection.setHeader("X-MegaFuel-Policy-Uuid", policyUUID) + const provider = new ethers.JsonRpcProvider( + newConnection, + (this as any)._network, + { + staticNetwork: (this as any)._network, + batchMaxCount: (this as any).batchMaxCount, + polling: (this as any).polling + } + ) + return await provider.send('pm_isSponsorable', [tx]) + } return await this.send('pm_isSponsorable', [tx]) } - async sendRawTransaction(signedTx: string): Promise { + async sendRawTransaction(signedTx: string, opts: SendRawTransactionOptions = {}): Promise { + const policyUUID = this.privatePolicyUUID + if (opts.UserAgent || this.privatePolicyUUID) { + const newConnection = this._getConnection() + + if (opts.UserAgent) { + newConnection.setHeader("User-Agent", opts.UserAgent) + } + if (policyUUID) { + newConnection.setHeader("X-MegaFuel-Policy-Uuid", policyUUID) + } + + const provider = new ethers.JsonRpcProvider( + newConnection, + (this as any)._network, + { + staticNetwork: (this as any)._network, + batchMaxCount: (this as any).batchMaxCount, + polling: (this as any).polling + } + ) + + return await provider.send('eth_sendRawTransaction', [signedTx]) + } return await this.send('eth_sendRawTransaction', [signedTx]) } @@ -80,4 +149,4 @@ export class PaymasterClient extends ethers.JsonRpcProvider { async getBundleByUuid(bundleUuid: string): Promise { return await this.send('pm_getBundleByUuid', [bundleUuid]) } -} +} \ No newline at end of file diff --git a/src/sponsorclient.ts b/src/sponsorclient.ts index ad1bded..ebd64f1 100644 --- a/src/sponsorclient.ts +++ b/src/sponsorclient.ts @@ -70,4 +70,4 @@ export class SponsorClient extends ethers.JsonRpcProvider { async getPolicySpendData(policyUUID: string): Promise { return this.send('pm_getPolicySpendData', [policyUUID]) } -} +} \ No newline at end of file diff --git a/tests/env.ts b/tests/env.ts index 4b63a3f..acb3036 100644 --- a/tests/env.ts +++ b/tests/env.ts @@ -16,3 +16,4 @@ export const ACCOUNT_ADDRESS = '0xF9A8db17431DD8563747D6FC770297E438Aa12eB' export const CONTRACT_METHOD = '0xa9059cbb' export const TOKEN_CONTRACT_ADDRESS = '0xeD24FC36d5Ee211Ea25A80239Fb8C4Cfd80f12Ee' export const RECIPIENT_ADDRESS = '0xDE08B1Fd79b7016F8DD3Df11f7fa0FbfdF07c941' +export const PRIVATE_POLICY_UUID = "90f1ba4c-1f93-4759-b8a9-da4d59c668b4" diff --git a/tests/paymaster.spec.ts b/tests/paymaster.spec.ts index d1eed25..ff2f652 100644 --- a/tests/paymaster.spec.ts +++ b/tests/paymaster.spec.ts @@ -6,9 +6,11 @@ import { transformIsSponsorableResponse, transformToGaslessTransaction, delay, transformSponsorTxResponse, transformBundleResponse, + privatePaymasterClient, } from './utils' -import {TOKEN_CONTRACT_ADDRESS, CHAIN_ID, RECIPIENT_ADDRESS} from './env' -import {ethers} from 'ethers' +import { TOKEN_CONTRACT_ADDRESS, CHAIN_ID, RECIPIENT_ADDRESS } from './env' +import { ethers } from 'ethers' +import { SendRawTransactionOptions } from '../src' let TX_HASH = '' @@ -96,4 +98,50 @@ describe('paymasterQuery', () => { expect(sponsorTx.TxHash).toEqual(tx.TxHash) }, 13000) }) + + + + /** + * Test for checking if a private policy transaction is sponsorable. + */ + describe('isSponsorable', () => { + test('should successfully determine if transaction is sponsorable', async () => { + const tokenContract = new ethers.Contract(TOKEN_CONTRACT_ADDRESS, tokenAbi, wallet) + const tokenAmount = ethers.parseUnits('0', 18) + const nonce = await privatePaymasterClient.getTransactionCount(wallet.address, 'pending') + + const transaction = await tokenContract.transfer.populateTransaction(RECIPIENT_ADDRESS.toLowerCase(), tokenAmount) + transaction.from = wallet.address + transaction.nonce = nonce + transaction.gasLimit = BigInt(100000) + transaction.chainId = BigInt(CHAIN_ID) + transaction.gasPrice = BigInt(0) + + const safeTransaction = { + ...transaction, + gasLimit: transaction.gasLimit.toString(), + chainId: transaction.chainId.toString(), + gasPrice: transaction.gasPrice.toString(), + } + + console.log('Prepared transaction:', safeTransaction) + + const resRaw = await privatePaymasterClient.isSponsorable(safeTransaction) + const res = transformIsSponsorableResponse(resRaw) + expect(res.Sponsorable).toEqual(true) + + const txOpt: SendRawTransactionOptions = { + UserAgent: "TEST USER AGENT" + }; + + const signedTx = await wallet.signTransaction(safeTransaction) + try { + const tx = await privatePaymasterClient.sendRawTransaction(signedTx,txOpt) + TX_HASH = tx + console.log('Transaction hash received:', TX_HASH) + } catch (error) { + console.error('Transaction failed:', error) + } + }, 100000) // Extends the default timeout as this test involves network calls + }) }) diff --git a/tests/sponsor.spec.ts b/tests/sponsor.spec.ts index 33dfcba..1264c4f 100644 --- a/tests/sponsor.spec.ts +++ b/tests/sponsor.spec.ts @@ -1,7 +1,7 @@ -import {describe, expect, test} from '@jest/globals' -import {sponsorClient} from './utils' -import {WhitelistType} from '../src' -import {POLICY_UUID, ACCOUNT_ADDRESS, CONTRACT_METHOD} from './env' +import { describe, expect, test } from '@jest/globals' +import { WhitelistType } from '../src' +import { POLICY_UUID, ACCOUNT_ADDRESS, CONTRACT_METHOD } from './env' +import { sponsorClient } from './utils' /** * Test suite for Sponsor API methods involving whitelist management and spend data retrieval. @@ -158,10 +158,13 @@ describe('sponsorQuery', () => { * Tests retrieving user spend data. */ describe('getUserSpendData', () => { - test('should return null for spend data when user has none', async () => { - const res = await sponsorClient.getUserSpendData(ACCOUNT_ADDRESS, POLICY_UUID) + test('should return not null for user spend data', async () => { + const res = await sponsorClient.getUserSpendData( + ACCOUNT_ADDRESS, + POLICY_UUID + ) - expect(res).toBeNull() + expect(res).not.toBeNull() console.log('User spend data:', res) }) }) @@ -192,4 +195,4 @@ describe('sponsorQuery', () => { console.log('Re-addition to FromAccountWhitelist response:', res) }) }) -}) +}) \ No newline at end of file diff --git a/tests/utils.ts b/tests/utils.ts index b18e51b..4619567 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -7,16 +7,18 @@ import { Bundle, GaslessTransactionStatus, } from '../src' -import {CHAIN_ID, SPONSOR_URL, CHAIN_URL, PAYMASTER_URL, PRIVATE_KEY, TOKEN_CONTRACT_ADDRESS} from './env' +import {CHAIN_ID, SPONSOR_URL, CHAIN_URL, PAYMASTER_URL, PRIVATE_KEY, TOKEN_CONTRACT_ADDRESS, PRIVATE_POLICY_UUID} from './env' import {ethers} from 'ethers' -export const sponsorClient = new SponsorClient(SPONSOR_URL, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))}) + +export const sponsorClient = new SponsorClient(SPONSOR_URL+"/"+CHAIN_ID, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))}) // Provider for assembling the transaction (e.g., testnet) export const assemblyProvider = new ethers.JsonRpcProvider(CHAIN_URL) // Provider for sending the transaction (e.g., could be a different network or provider) -export const paymasterClient = new PaymasterClient(PAYMASTER_URL) +export const paymasterClient = PaymasterClient.new(PAYMASTER_URL, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))}) +export const privatePaymasterClient = PaymasterClient.newPrivatePaymaster(SPONSOR_URL+"/"+CHAIN_ID, PRIVATE_POLICY_UUID, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))}) export const wallet = new ethers.Wallet(PRIVATE_KEY, assemblyProvider) // ERC20 token ABI (only including the transfer function)