Skip to content

Commit

Permalink
feat: support private policy (#5)
Browse files Browse the repository at this point in the history
* feat: support private policy
  • Loading branch information
BarryTong98 authored Oct 16, 2024
1 parent 7b9ce33 commit 4e4c610
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 17 deletions.
75 changes: 72 additions & 3 deletions src/paymasterclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -49,19 +53,84 @@ 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<string> {
return await this.send('eth_chainId', [])
}

async isSponsorable(tx: TransactionRequest): Promise<IsSponsorableResponse> {
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<string> {
async sendRawTransaction(signedTx: string, opts: SendRawTransactionOptions = {}): Promise<string> {
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])
}

Expand All @@ -80,4 +149,4 @@ export class PaymasterClient extends ethers.JsonRpcProvider {
async getBundleByUuid(bundleUuid: string): Promise<Bundle> {
return await this.send('pm_getBundleByUuid', [bundleUuid])
}
}
}
2 changes: 1 addition & 1 deletion src/sponsorclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ export class SponsorClient extends ethers.JsonRpcProvider {
async getPolicySpendData(policyUUID: string): Promise<PolicySpendData> {
return this.send('pm_getPolicySpendData', [policyUUID])
}
}
}
1 change: 1 addition & 0 deletions tests/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
52 changes: 50 additions & 2 deletions tests/paymaster.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''

Expand Down Expand Up @@ -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
})
})
19 changes: 11 additions & 8 deletions tests/sponsor.spec.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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)
})
})
Expand Down Expand Up @@ -192,4 +195,4 @@ describe('sponsorQuery', () => {
console.log('Re-addition to FromAccountWhitelist response:', res)
})
})
})
})
8 changes: 5 additions & 3 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 4e4c610

Please sign in to comment.