diff --git a/.changeset/pink-moons-admire.md b/.changeset/pink-moons-admire.md new file mode 100644 index 00000000..7f0a41a1 --- /dev/null +++ b/.changeset/pink-moons-admire.md @@ -0,0 +1,5 @@ +--- +"@rhinestone/sdk": patch +--- + +Support custom bundler / paymaster / provider transport using `factory` prop diff --git a/bun.lock b/bun.lock index 57d760cf..b2018826 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { diff --git a/src/accounts/json-rpc/index.test.ts b/src/accounts/json-rpc/index.test.ts index a3c1a2cd..6aa32cfd 100644 --- a/src/accounts/json-rpc/index.test.ts +++ b/src/accounts/json-rpc/index.test.ts @@ -1,6 +1,6 @@ +import { http } from 'viem' import { base, mainnet } from 'viem/chains' import { describe, expect, test } from 'vitest' - import { createTransport } from './index' describe('JSON-RPC', () => { @@ -23,6 +23,16 @@ describe('JSON-RPC', () => { expect(transport).toBeDefined() }) + test('Factory', () => { + const viemTransport = http('https://my-rpc.example.com') + const transport = createTransport(mainnet, { + type: 'factory', + getTransport: (_chainId: number) => viemTransport, + }) + expect(transport).toBeDefined() + expect(transport).toBe(viemTransport) + }) + test('Custom throws error when URL not configured for chain', () => { expect(() => createTransport(mainnet, { diff --git a/src/accounts/json-rpc/index.ts b/src/accounts/json-rpc/index.ts index e8e9173c..e9868e95 100644 --- a/src/accounts/json-rpc/index.ts +++ b/src/accounts/json-rpc/index.ts @@ -20,6 +20,9 @@ function createTransport(chain: Chain, provider?: ProviderConfig): Transport { const customUrl = getCustomUrl(chain.id, provider.urls) return http(customUrl) } + case 'factory': { + return provider.getTransport(chain.id) + } } } diff --git a/src/accounts/utils.ts b/src/accounts/utils.ts index 98057076..d412b9e0 100644 --- a/src/accounts/utils.ts +++ b/src/accounts/utils.ts @@ -1,4 +1,4 @@ -import type { Address, Client, Hex } from 'viem' +import type { Address, Client, Hex, Transport } from 'viem' import { concatHex, encodeAbiParameters, @@ -227,21 +227,33 @@ async function getAccountNonce( } function getBundlerClient(config: RhinestoneConfig, client: Client) { - function getBundlerEndpoint(config: BundlerConfig, chainId: number) { + function getBundlerTransport(config: BundlerConfig, chainId: number) { switch (config.type) { case 'pimlico': - return `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${config.apiKey}` + return http( + `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${config.apiKey}`, + ) case 'biconomy': - return `https://bundler.biconomy.io/api/v3/${chainId}/${config.apiKey}` + return http( + `https://bundler.biconomy.io/api/v3/${chainId}/${config.apiKey}`, + ) + case 'factory': + return config.getTransport(chainId) } } - function getPaymasterEndpoint(config: PaymasterConfig, chainId: number) { + function getPaymasterTransport(config: PaymasterConfig, chainId: number) { switch (config.type) { case 'pimlico': - return `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${config.apiKey}` + return http( + `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${config.apiKey}`, + ) case 'biconomy': - return `https://paymaster.biconomy.io/api/v2/${chainId}/${config.apiKey}` + return http( + `https://paymaster.biconomy.io/api/v2/${chainId}/${config.apiKey}`, + ) + case 'factory': + return config.getTransport(chainId) } } @@ -251,42 +263,31 @@ function getBundlerClient(config: RhinestoneConfig, client: Client) { throw new Error('Chain id is required') } - const endpoint = bundler - ? getBundlerEndpoint(bundler, chainId) - : `https://public.pimlico.io/v2/${chainId}/rpc` - const paymasterEndpoint = paymaster - ? getPaymasterEndpoint(paymaster, chainId) + const transport = bundler + ? getBundlerTransport(bundler, chainId) + : http(`https://public.pimlico.io/v2/${chainId}/rpc`) + const paymasterTransport = paymaster + ? getPaymasterTransport(paymaster, chainId) : undefined return createBundlerClient({ client, - transport: http(endpoint), - paymaster: paymasterEndpoint + transport: transport, + paymaster: paymasterTransport ? createPaymasterClient({ - transport: http(paymasterEndpoint), + transport: paymasterTransport, }) : undefined, userOperation: { - estimateFeesPerGas: () => getGasPriceEstimate(endpoint), + estimateFeesPerGas: () => getGasPriceEstimate(transport), }, }) } -async function getGasPriceEstimate(bundlerUrl: string) { - const response = await fetch(bundlerUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: Date.now(), - // TODO do not rely on vendor-specific methods - method: 'pimlico_getUserOperationGasPrice', - params: [], - }), - }) - - const json = (await response.json()) as UserOperationGasPriceResponse +async function getGasPriceEstimate(bundlerTransport: Transport) { + const json = (await bundlerTransport({}).request({ + method: 'pimlico_getUserOperationGasPrice', + params: [], + })) as UserOperationGasPriceResponse return { maxFeePerGas: BigInt(json.result.fast.maxFeePerGas), diff --git a/src/types.ts b/src/types.ts index 824124c6..b01659e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { Account, Address, Chain, Hex } from 'viem' +import type { Account, Address, Chain, Hex, Transport } from 'viem' import type { WebAuthnAccount } from 'viem/account-abstraction' import type { ModuleType } from './modules/common' import type { EnableSessionData } from './modules/validators/smart-sessions' @@ -88,16 +88,30 @@ type ProviderConfig = type: 'custom' urls: Record } + | { + type: 'factory' + getTransport: (chainId: number) => Transport + } -interface BundlerConfig { - type: 'pimlico' | 'biconomy' - apiKey: string -} +type BundlerConfig = + | { + type: 'pimlico' | 'biconomy' + apiKey: string + } + | { + type: 'factory' + getTransport: (chainId: number) => Transport + } -interface PaymasterConfig { - type: 'pimlico' | 'biconomy' - apiKey: string -} +type PaymasterConfig = + | { + type: 'pimlico' | 'biconomy' + apiKey: string + } + | { + type: 'factory' + getTransport: (chainId: number) => Transport + } type OwnerSet = | OwnableValidatorConfig