Skip to content

Commit 4e4c610

Browse files
authored
feat: support private policy (#5)
* feat: support private policy
1 parent 7b9ce33 commit 4e4c610

File tree

6 files changed

+140
-17
lines changed

6 files changed

+140
-17
lines changed

src/paymasterclient.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export type IsSponsorableResponse = {
99
SponsorWebsite: string
1010
}
1111

12+
export type SendRawTransactionOptions = {
13+
UserAgent?: string
14+
}
15+
1216
export enum GaslessTransactionStatus { New = 0, Pending = 1, Confirmed = 2, Failed = 3, Invalid = 4}
1317

1418
export type GaslessTransaction = {
@@ -49,19 +53,84 @@ export type Bundle = {
4953
}
5054

5155
export class PaymasterClient extends ethers.JsonRpcProvider {
52-
constructor(url?: string | FetchRequest, network?: Networkish, options?: JsonRpcApiProviderOptions) {
56+
private privatePolicyUUID?: string
57+
58+
private constructor(
59+
url?: string | FetchRequest,
60+
network?: Networkish,
61+
options?: JsonRpcApiProviderOptions,
62+
privatePolicyUUID?: string
63+
) {
5364
super(url, network, options)
65+
this.privatePolicyUUID = privatePolicyUUID
66+
}
67+
68+
// Static method to create a new standard PaymasterClient
69+
static new(
70+
url?: string | FetchRequest,
71+
network?: Networkish,
72+
options?: JsonRpcApiProviderOptions
73+
): PaymasterClient {
74+
return new PaymasterClient(url, network, options)
75+
}
76+
77+
// Static method to create a new PaymasterClient with private policy
78+
static newPrivatePaymaster(
79+
url: string | FetchRequest,
80+
privatePolicyUUID: string,
81+
network?: Networkish,
82+
options?: JsonRpcApiProviderOptions
83+
): PaymasterClient {
84+
return new PaymasterClient(url, network, options, privatePolicyUUID)
5485
}
5586

5687
async chainID(): Promise<string> {
5788
return await this.send('eth_chainId', [])
5889
}
5990

6091
async isSponsorable(tx: TransactionRequest): Promise<IsSponsorableResponse> {
92+
const policyUUID = this.privatePolicyUUID
93+
if (policyUUID) {
94+
const newConnection = this._getConnection()
95+
newConnection.setHeader("X-MegaFuel-Policy-Uuid", policyUUID)
96+
const provider = new ethers.JsonRpcProvider(
97+
newConnection,
98+
(this as any)._network,
99+
{
100+
staticNetwork: (this as any)._network,
101+
batchMaxCount: (this as any).batchMaxCount,
102+
polling: (this as any).polling
103+
}
104+
)
105+
return await provider.send('pm_isSponsorable', [tx])
106+
}
61107
return await this.send('pm_isSponsorable', [tx])
62108
}
63109

64-
async sendRawTransaction(signedTx: string): Promise<string> {
110+
async sendRawTransaction(signedTx: string, opts: SendRawTransactionOptions = {}): Promise<string> {
111+
const policyUUID = this.privatePolicyUUID
112+
if (opts.UserAgent || this.privatePolicyUUID) {
113+
const newConnection = this._getConnection()
114+
115+
if (opts.UserAgent) {
116+
newConnection.setHeader("User-Agent", opts.UserAgent)
117+
}
118+
if (policyUUID) {
119+
newConnection.setHeader("X-MegaFuel-Policy-Uuid", policyUUID)
120+
}
121+
122+
const provider = new ethers.JsonRpcProvider(
123+
newConnection,
124+
(this as any)._network,
125+
{
126+
staticNetwork: (this as any)._network,
127+
batchMaxCount: (this as any).batchMaxCount,
128+
polling: (this as any).polling
129+
}
130+
)
131+
132+
return await provider.send('eth_sendRawTransaction', [signedTx])
133+
}
65134
return await this.send('eth_sendRawTransaction', [signedTx])
66135
}
67136

@@ -80,4 +149,4 @@ export class PaymasterClient extends ethers.JsonRpcProvider {
80149
async getBundleByUuid(bundleUuid: string): Promise<Bundle> {
81150
return await this.send('pm_getBundleByUuid', [bundleUuid])
82151
}
83-
}
152+
}

src/sponsorclient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,4 @@ export class SponsorClient extends ethers.JsonRpcProvider {
7070
async getPolicySpendData(policyUUID: string): Promise<PolicySpendData> {
7171
return this.send('pm_getPolicySpendData', [policyUUID])
7272
}
73-
}
73+
}

tests/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export const ACCOUNT_ADDRESS = '0xF9A8db17431DD8563747D6FC770297E438Aa12eB'
1616
export const CONTRACT_METHOD = '0xa9059cbb'
1717
export const TOKEN_CONTRACT_ADDRESS = '0xeD24FC36d5Ee211Ea25A80239Fb8C4Cfd80f12Ee'
1818
export const RECIPIENT_ADDRESS = '0xDE08B1Fd79b7016F8DD3Df11f7fa0FbfdF07c941'
19+
export const PRIVATE_POLICY_UUID = "90f1ba4c-1f93-4759-b8a9-da4d59c668b4"

tests/paymaster.spec.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
transformIsSponsorableResponse,
77
transformToGaslessTransaction,
88
delay, transformSponsorTxResponse, transformBundleResponse,
9+
privatePaymasterClient,
910
} from './utils'
10-
import {TOKEN_CONTRACT_ADDRESS, CHAIN_ID, RECIPIENT_ADDRESS} from './env'
11-
import {ethers} from 'ethers'
11+
import { TOKEN_CONTRACT_ADDRESS, CHAIN_ID, RECIPIENT_ADDRESS } from './env'
12+
import { ethers } from 'ethers'
13+
import { SendRawTransactionOptions } from '../src'
1214

1315
let TX_HASH = ''
1416

@@ -96,4 +98,50 @@ describe('paymasterQuery', () => {
9698
expect(sponsorTx.TxHash).toEqual(tx.TxHash)
9799
}, 13000)
98100
})
101+
102+
103+
104+
/**
105+
* Test for checking if a private policy transaction is sponsorable.
106+
*/
107+
describe('isSponsorable', () => {
108+
test('should successfully determine if transaction is sponsorable', async () => {
109+
const tokenContract = new ethers.Contract(TOKEN_CONTRACT_ADDRESS, tokenAbi, wallet)
110+
const tokenAmount = ethers.parseUnits('0', 18)
111+
const nonce = await privatePaymasterClient.getTransactionCount(wallet.address, 'pending')
112+
113+
const transaction = await tokenContract.transfer.populateTransaction(RECIPIENT_ADDRESS.toLowerCase(), tokenAmount)
114+
transaction.from = wallet.address
115+
transaction.nonce = nonce
116+
transaction.gasLimit = BigInt(100000)
117+
transaction.chainId = BigInt(CHAIN_ID)
118+
transaction.gasPrice = BigInt(0)
119+
120+
const safeTransaction = {
121+
...transaction,
122+
gasLimit: transaction.gasLimit.toString(),
123+
chainId: transaction.chainId.toString(),
124+
gasPrice: transaction.gasPrice.toString(),
125+
}
126+
127+
console.log('Prepared transaction:', safeTransaction)
128+
129+
const resRaw = await privatePaymasterClient.isSponsorable(safeTransaction)
130+
const res = transformIsSponsorableResponse(resRaw)
131+
expect(res.Sponsorable).toEqual(true)
132+
133+
const txOpt: SendRawTransactionOptions = {
134+
UserAgent: "TEST USER AGENT"
135+
};
136+
137+
const signedTx = await wallet.signTransaction(safeTransaction)
138+
try {
139+
const tx = await privatePaymasterClient.sendRawTransaction(signedTx,txOpt)
140+
TX_HASH = tx
141+
console.log('Transaction hash received:', TX_HASH)
142+
} catch (error) {
143+
console.error('Transaction failed:', error)
144+
}
145+
}, 100000) // Extends the default timeout as this test involves network calls
146+
})
99147
})

tests/sponsor.spec.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {describe, expect, test} from '@jest/globals'
2-
import {sponsorClient} from './utils'
3-
import {WhitelistType} from '../src'
4-
import {POLICY_UUID, ACCOUNT_ADDRESS, CONTRACT_METHOD} from './env'
1+
import { describe, expect, test } from '@jest/globals'
2+
import { WhitelistType } from '../src'
3+
import { POLICY_UUID, ACCOUNT_ADDRESS, CONTRACT_METHOD } from './env'
4+
import { sponsorClient } from './utils'
55

66
/**
77
* Test suite for Sponsor API methods involving whitelist management and spend data retrieval.
@@ -158,10 +158,13 @@ describe('sponsorQuery', () => {
158158
* Tests retrieving user spend data.
159159
*/
160160
describe('getUserSpendData', () => {
161-
test('should return null for spend data when user has none', async () => {
162-
const res = await sponsorClient.getUserSpendData(ACCOUNT_ADDRESS, POLICY_UUID)
161+
test('should return not null for user spend data', async () => {
162+
const res = await sponsorClient.getUserSpendData(
163+
ACCOUNT_ADDRESS,
164+
POLICY_UUID
165+
)
163166

164-
expect(res).toBeNull()
167+
expect(res).not.toBeNull()
165168
console.log('User spend data:', res)
166169
})
167170
})
@@ -192,4 +195,4 @@ describe('sponsorQuery', () => {
192195
console.log('Re-addition to FromAccountWhitelist response:', res)
193196
})
194197
})
195-
})
198+
})

tests/utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import {
77
Bundle,
88
GaslessTransactionStatus,
99
} from '../src'
10-
import {CHAIN_ID, SPONSOR_URL, CHAIN_URL, PAYMASTER_URL, PRIVATE_KEY, TOKEN_CONTRACT_ADDRESS} from './env'
10+
import {CHAIN_ID, SPONSOR_URL, CHAIN_URL, PAYMASTER_URL, PRIVATE_KEY, TOKEN_CONTRACT_ADDRESS, PRIVATE_POLICY_UUID} from './env'
1111
import {ethers} from 'ethers'
1212

13-
export const sponsorClient = new SponsorClient(SPONSOR_URL, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))})
13+
14+
export const sponsorClient = new SponsorClient(SPONSOR_URL+"/"+CHAIN_ID, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))})
1415

1516
// Provider for assembling the transaction (e.g., testnet)
1617
export const assemblyProvider = new ethers.JsonRpcProvider(CHAIN_URL)
1718

1819
// Provider for sending the transaction (e.g., could be a different network or provider)
19-
export const paymasterClient = new PaymasterClient(PAYMASTER_URL)
20+
export const paymasterClient = PaymasterClient.new(PAYMASTER_URL, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))})
21+
export const privatePaymasterClient = PaymasterClient.newPrivatePaymaster(SPONSOR_URL+"/"+CHAIN_ID, PRIVATE_POLICY_UUID, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))})
2022

2123
export const wallet = new ethers.Wallet(PRIVATE_KEY, assemblyProvider)
2224
// ERC20 token ABI (only including the transfer function)

0 commit comments

Comments
 (0)