Skip to content

Commit 5e3b73b

Browse files
authored
feat: remove fee sponsoring on nibi (NibiruChain#2530)
1 parent 61e01e2 commit 5e3b73b

14 files changed

Lines changed: 24 additions & 222 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,6 @@ AGENTS.md
363363
precompile.test
364364
tx_log.json
365365
passkey-bundler/.tmp
366+
evm-e2e/.nibid-*/
367+
evm-e2e/.passkey-*-privkey.txt
368+
evm-e2e/.passkey-*-wallet.json

evm-e2e/passkey-sdk/src/local-bundler.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ const provider = new JsonRpcProvider(RPC_URL)
4747
const wallet = new Wallet(PRIVATE_KEY, provider)
4848
const ENTRY_POINT_ABI = [
4949
"function handleOps((address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature)[] ops, address payable beneficiary)",
50-
"function depositTo(address) payable",
5150
]
5251
const FACTORY_ABI = [
5352
"function createAccount(bytes32 _qx, bytes32 _qy) returns (address account)",
@@ -157,8 +156,6 @@ async function handleRpcRequest(
157156
return createResult(payload.id, handleGetUserOpReceipt(payload.params ?? []))
158157
case "passkey_createAccount":
159158
return createResult(payload.id, await handleCreatePasskeyAccount(payload.params ?? []))
160-
case "passkey_fundAccount":
161-
return createResult(payload.id, await handleFundAccount(payload.params ?? []))
162159
case "passkey_getLogs":
163160
return createResult(payload.id, handleGetLogs(payload.params ?? []))
164161
default:
@@ -183,16 +180,7 @@ async function handleSendUserOperation(params: any[], chainId: bigint): Promise<
183180
nonce: rpcUserOp.nonce,
184181
})
185182

186-
let bundlerNonce = await provider.getTransactionCount(wallet.address, "pending")
187-
188-
// Simple pre-fund: deposit the required amount into EntryPoint for this sender.
189-
const requiredPrefund =
190-
(userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas) * userOp.maxFeePerGas
191-
if (requiredPrefund > 0n) {
192-
console.log(`Prefunding sender ${rpcUserOp.sender} with ${requiredPrefund} wei in EntryPoint`)
193-
const prefundTx = await entryPoint.depositTo(rpcUserOp.sender, { value: requiredPrefund, nonce: bundlerNonce++ })
194-
await prefundTx.wait()
195-
}
183+
const bundlerNonce = await provider.getTransactionCount(wallet.address, "pending")
196184

197185
const tx = await entryPoint.handleOps([userOp], wallet.address, {
198186
gasLimit: userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas + 200000n,
@@ -268,18 +256,6 @@ async function handleCreatePasskeyAccount(params: any[]): Promise<{ account: str
268256
return { account, txHash: tx.hash }
269257
}
270258

271-
async function handleFundAccount(params: any[]): Promise<{ txHash: string }> {
272-
const to = params[0] as string | undefined
273-
const amount = params[1] ? BigInt(params[1] as string) : 1_000_000_000_000_000_000n
274-
if (!to) {
275-
throw new Error("passkey_fundAccount requires target address")
276-
}
277-
const tx = await wallet.sendTransaction({ to, value: amount })
278-
const receipt = await tx.wait()
279-
console.log(`Funded ${to} with ${amount} wei from bundler wallet (tx ${receipt?.hash ?? tx.hash})`)
280-
return { txHash: receipt?.hash ?? tx.hash }
281-
}
282-
283259
function readBody(req: IncomingMessage): Promise<string> {
284260
return new Promise((resolve, reject) => {
285261
let data = ""

evm-e2e/passkey-sdk/src/passkey-e2e.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ async function main() {
6464
await txCreate.wait()
6565

6666
const account = new Contract(predictedAccount, ACCOUNT_ABI, provider)
67-
const entryPointContract = new Contract(
68-
ENTRY_POINT,
69-
["function depositTo(address account) payable", "function balanceOf(address account) view returns (uint256)"],
70-
wallet,
71-
)
7267

7368
// 3) Fund the PasskeyAccount.
7469
const fundTx = await wallet.sendTransaction({
@@ -102,20 +97,6 @@ async function main() {
10297
maxPriorityFeePerGas: maxPriority,
10398
}
10499

105-
const requiredPrefund =
106-
(userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas) * userOp.maxFeePerGas
107-
console.log("Depositing prefund:", formatEther(requiredPrefund), "NIBI")
108-
const depositTx = await entryPointContract.depositTo(predictedAccount, {
109-
value: requiredPrefund,
110-
nonce: deployerNonce++,
111-
})
112-
await depositTx.wait()
113-
console.log(
114-
"EntryPoint deposit before bundling:",
115-
formatEther(await entryPointContract.balanceOf(predictedAccount)),
116-
"NIBI",
117-
)
118-
119100
const chainId = BigInt((await provider.getNetwork()).chainId)
120101
const userOpHash = getUserOpHash(userOp, ENTRY_POINT, chainId)
121102
const { r, s } = signUserOpHash(userOpHash, nodePasskey.privKey)
@@ -146,10 +127,8 @@ async function main() {
146127
// 6) Confirm nonce + balance changes on-chain.
147128
const nonceAfter = (await account.nonce()) as bigint
148129
const balanceAfter = await provider.getBalance(predictedAccount)
149-
const depositAfter = await entryPointContract.balanceOf(predictedAccount)
150130
console.log("Nonce after:", nonceAfter.toString())
151131
console.log("Account balance after:", formatEther(balanceAfter), "NIBI")
152-
console.log("EntryPoint deposit after:", formatEther(depositAfter), "NIBI")
153132

154133
if (nonceAfter === onChainNonce + 1n && balanceAfter < balanceBefore) {
155134
console.log("✅ Passkey ERC-4337 flow completed successfully")

passkey-app/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ now talks to an ERC-4337 bundler (not a custom RPC method) for passkey-signed us
88

99
```bash
1010
# From nibiru/ root
11-
just passkey-demo # starts localnet (if needed), deploys EntryPoint + PasskeyAccountFactory, writes .env.local, starts bundler on :4337 (logs in logs/passkey-bundler.log), funds bundler from guard-cream dev key
11+
just passkey-demo # starts localnet (if needed), deploys EntryPoint + PasskeyAccountFactory, writes .env.local, starts bundler on :4337 (logs in logs/passkey-bundler.log)
1212

1313
# Then launch the UI
1414
cd passkey-app && npm install && npm run dev
@@ -29,9 +29,9 @@ Open http://localhost:5173. The connection panel is prefilled from `.env.local`
2929
bundler URL if needed (defaults to http://127.0.0.1:4337). Flow:
3030

3131
1) Create/load a passkey (WebAuthn).
32-
2) Click “Create passkey account” — this asks the bundler to call `createAccount(qx,qy)` on the factory (gas paid by the
33-
bundler dev key), then auto-sets `from` from the `AccountCreated` event.
34-
3) Fund that address from the localnet validator if needed (e.g. `nibid tx bank send validator <passkey_addr> 1000000000000000000unibi --yes --fees 750000unibi --gas 300000 --chain-id nibiru-localnet-0 --node http://localhost:26657`).
32+
2) Click “Create passkey account” — this asks the bundler to call `createAccount(qx,qy)` on the factory, then auto-sets
33+
`from` from the `AccountCreated` event.
34+
3) Bundler sponsorship is disabled. Fund that address manually if needed (e.g. `nibid tx bank send validator <passkey_addr> 1000000000000000000unibi --yes --fees 750000unibi --gas 300000 --chain-id nibiru-localnet-0 --node http://localhost:26657`).
3535
4) Use “Send NIBI (prefilled)” for a quick native transfer, or “Open custom transaction” for calldata trades.
3636
5) Deposit flow uses `PerpVaultEvmInterface.deposit`; fill vault address, collateral ERC20, and wasm msg if needed.
3737

passkey-app/src/App.tsx

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ import { makePublicClient } from './lib/passkeyClient'
77
import { clearPasskey, loadPasskey, savePasskey } from './lib/storage'
88
import { derivePasskeyAddress } from './lib/passkeyAddress'
99
import { encodeExecute, PASSKEY_ACCOUNT_ABI } from './lib/passkeyAccount'
10-
import { createPasskeyAccount, fundAccount, fetchBundlerLogs, sendUserOperation } from './lib/bundler'
10+
import { createPasskeyAccount, fetchBundlerLogs, sendUserOperation } from './lib/bundler'
1111
import { defaultUserOp, getUserOpHash, toRpcUserOperation } from './lib/userop'
1212
import { fromBase64Url } from './lib/base64'
1313
import { getBundlerHealth, getRpcHealth } from './lib/health'
1414
import { Sidebar } from './components/Sidebar'
1515
import { Step } from './components/Step'
16-
import { fundFromDevAccount } from './lib/faucet'
1716

1817
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
1918
const envRpc = import.meta.env.VITE_RPC_URL as string | undefined
@@ -51,7 +50,6 @@ function App() {
5150
const [balance, setBalance] = useState<string>('0')
5251
const [bundlerBalance, setBundlerBalance] = useState<string>('0')
5352
const [bundlerLogs, setBundlerLogs] = useState<BundlerLogEntry[]>([])
54-
const [isFunding, setIsFunding] = useState(false)
5553
const [isDeployed, setIsDeployed] = useState(false)
5654
const [serviceHealth, setServiceHealth] = useState<{ rpc: ServiceHealth; bundler: ServiceHealth }>({
5755
rpc: { status: 'checking' },
@@ -468,54 +466,27 @@ function App() {
468466

469467
<Step
470468
number={3}
471-
title="Fund Account"
469+
title="Funding (Optional)"
472470
description={
473471
<>
474472
<p>
475-
Smart contract accounts need gas to pay for transactions (unless using a Paymaster).
473+
Bundler fee sponsorship is disabled.
476474
</p>
477475
<p>
478-
We'll use the Bundler's faucet to send some testnet NIBI to your new account.
476+
If this account needs native tokens for non-gasless flows, fund it directly from a wallet or CLI.
479477
</p>
480478
</>
481479
}
482480
isActive={isDeployed}
483481
isCompleted={BigInt(balance) > 0}
484482
>
485-
<button
486-
onClick={async () => {
487-
if (!fromAddress || fromAddress === ZERO_ADDRESS) return
488-
try {
489-
setIsFunding(true)
490-
const desiredAmount = 100000000000000000n // 0.1 NIBI
491-
const bundlerBal = BigInt(bundlerBalance || '0')
492-
if (bundlerBal < desiredAmount + 20000000000000000n) {
493-
updateStatus('Bundler is underfunded. Top up the bundler signer first.')
494-
return
495-
}
496-
497-
updateStatus(`Requesting transfer of ${desiredAmount} wei from bundler dev account...`)
498-
const txHash = await fundFromDevAccount(config, fromAddress, desiredAmount)
499-
updateStatus(`Funded tx: ${txHash}`)
500-
} catch (err: any) {
501-
console.error(err)
502-
updateStatus(err?.message ?? 'Funding failed')
503-
} finally {
504-
setIsFunding(false)
505-
}
506-
}}
507-
disabled={isFunding || !isDeployed}
508-
>
509-
{isFunding ? 'Funding…' : 'Fund from Dev Account'}
510-
</button>
511-
{BigInt(balance) === 0n && (
512-
<div style={{ marginTop: '12px', fontSize: '14px', color: '#64748b' }}>
513-
<p>If the faucet fails, send NIBI manually to:</p>
514-
<code style={{ background: '#f1f5f9', padding: '4px 8px', borderRadius: '4px', wordBreak: 'break-all' }}>
515-
{fromAddress}
516-
</code>
517-
</div>
518-
)}
483+
<div style={{ marginTop: '12px', fontSize: '14px', color: '#64748b' }}>
484+
<p>No bundler funding RPC is available.</p>
485+
<p>Fund manually if needed:</p>
486+
<code style={{ background: '#f1f5f9', padding: '4px 8px', borderRadius: '4px', wordBreak: 'break-all' }}>
487+
{fromAddress}
488+
</code>
489+
</div>
519490
</Step>
520491

521492
<Step
@@ -534,7 +505,7 @@ function App() {
534505
</ol>
535506
</>
536507
}
537-
isActive={isDeployed && BigInt(balance) > 0}
508+
isActive={isDeployed}
538509
>
539510
<div className="grid">
540511
<div style={{ border: '1px solid #e2e8f0', padding: '16px', borderRadius: '8px' }}>

passkey-bundler/README.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
# Passkey Bundler
22

33
Lightweight ERC-4337 bundler focused on passkey-backed accounts on Nibiru, aligned with `bundler-prd.md`. It exposes
4-
JSON-RPC on port `4337` by default, performs validation, prefunding, and queue-based submission to the configured
5-
EntryPoint, and ships with health and metrics endpoints for operations.
4+
JSON-RPC on port `4337` by default, performs validation and queue-based submission to the configured EntryPoint, and
5+
ships with health and metrics endpoints for operations.
66

77
## Features
88
- JSON-RPC: `eth_chainId`, `eth_supportedEntryPoints`, `eth_sendUserOperation`, `eth_getUserOperationReceipt`.
9-
- Passkey helpers: `passkey_createAccount(qx,qy,factory?)`, `passkey_fundAccount(address,amountWei)`,
10-
`passkey_getLogs(limit)`.
9+
- Passkey helpers: `passkey_createAccount(qx,qy,factory?)`, `passkey_getLogs(limit)`.
1110
- Validation: entry point and chain ID enforcement, userOp schema checks, rate limiting, optional API key auth
1211
(`x-api-key` or `Authorization: Bearer <key>`).
1312
- Queue + retries: in-memory FIFO queue (tie-breaker `maxPriorityFeePerGas`), configurable concurrency, retries with
1413
gas bumping and nonce management.
15-
- Prefunding: optional EntryPoint `depositTo` top-ups with configurable ceiling.
1614
- Observability: `/healthz`, `/readyz`, `/metrics` (Prometheus), structured logs kept in a rolling buffer.
1715
- Storage: in-memory receipt/log store with configurable retention (intended to be swapped for SQLite/Postgres later).
1816

@@ -54,8 +52,6 @@ node dist/index.js
5452
`FINALITY_BLOCKS` (default `2`).
5553
- `VALIDATION_ENABLED`: run `simulateValidation` before enqueue (defaults to `true` in testnet mode).
5654
- `ENABLE_PASSKEY_HELPERS`: enable `passkey_*` helper RPC methods (defaults to `false` in testnet mode).
57-
- `PREFUND_ENABLED` (default `true` in dev, `false` in testnet), `MAX_PREFUND_WEI` (default `5e18`),
58-
`PREFUND_ALLOWLIST` (comma-separated sender addresses; required if `PREFUND_ENABLED=true` in testnet mode).
5955
- `RECEIPT_LIMIT` (default `1000`), `RECEIPT_POLL_INTERVAL_MS` (default `5000`).
6056

6157
Health: `GET /healthz` (process + RPC reachability), `GET /readyz` (RPC synced, signer nonce). Metrics: `GET /metrics`

passkey-bundler/src/config.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ const baseSchema = z.object({
2828
validationEnabled: z.boolean().default(false),
2929
gasBumpPercent: z.number().nonnegative().default(15),
3030
gasBumpWei: z.bigint({ coerce: true }).optional(),
31-
prefundEnabled: z.boolean().default(true),
32-
maxPrefundWei: z.bigint({ coerce: true }).default(5_000_000_000_000_000_000n), // 5 ETH
33-
prefundAllowlist: z.array(hexAddress).default([]),
3431
submissionTimeoutMs: z.number().int().positive().default(45_000),
3532
finalityBlocks: z.number().int().positive().default(2),
3633
receiptLimit: z.number().int().positive().default(1000),
@@ -49,7 +46,6 @@ export function loadConfig(): BundlerConfig {
4946
config = {
5047
...config,
5148
authRequired: merged.authRequired ?? true,
52-
prefundEnabled: merged.prefundEnabled ?? false,
5349
enablePasskeyHelpers: merged.enablePasskeyHelpers ?? false,
5450
validationEnabled: merged.validationEnabled ?? true,
5551
}
@@ -60,9 +56,6 @@ export function loadConfig(): BundlerConfig {
6056
if (config.authRequired && config.apiKeys.length === 0) {
6157
throw new Error("Testnet mode requires API keys (set BUNDLER_API_KEYS) or disable authRequired")
6258
}
63-
if (config.prefundEnabled && config.prefundAllowlist.length === 0) {
64-
throw new Error("Testnet mode with prefundEnabled requires PREFUND_ALLOWLIST (comma-separated sender addresses)")
65-
}
6659
}
6760

6861
return config
@@ -105,11 +98,6 @@ function envOverrides(): RawConfig {
10598
validationEnabled: toBool(process.env.VALIDATION_ENABLED),
10699
gasBumpPercent: process.env.GAS_BUMP ? Number(process.env.GAS_BUMP) : undefined,
107100
gasBumpWei: toBigInt(process.env.GAS_BUMP_WEI),
108-
prefundEnabled: toBool(process.env.PREFUND_ENABLED),
109-
maxPrefundWei: toBigInt(process.env.MAX_PREFUND_WEI),
110-
prefundAllowlist: process.env.PREFUND_ALLOWLIST
111-
? process.env.PREFUND_ALLOWLIST.split(",").map((v) => v.trim()).filter(Boolean)
112-
: undefined,
113101
submissionTimeoutMs: toInt(process.env.SUBMISSION_TIMEOUT_MS),
114102
finalityBlocks: toInt(process.env.FINALITY_BLOCKS),
115103
receiptLimit: toInt(process.env.RECEIPT_LIMIT),

passkey-bundler/src/entryPoint.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const USER_OP_TUPLE =
55

66
export const ENTRY_POINT_V06_ABI = [
77
`function handleOps(${USER_OP_TUPLE}[] ops, address payable beneficiary)`,
8-
"function depositTo(address account) payable",
98
"function balanceOf(address account) view returns (uint256)",
109
`function simulateValidation(${USER_OP_TUPLE} userOp)`,
1110
`function simulateHandleOp(${USER_OP_TUPLE} op, address target, bytes targetCallData)`,
@@ -31,4 +30,3 @@ export function extractRevertData(err: unknown): string | undefined {
3130
undefined
3231
)
3332
}
34-

passkey-bundler/src/index.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,6 @@ async function handleRpcRequest(payload: any, ctx: RpcRequestContext): Promise<o
245245
case "passkey_createAccount":
246246
if (!config.enablePasskeyHelpers) return createError(payload.id, -32601, `Method ${method} not found`)
247247
return createResult(payload.id, await handleCreatePasskeyAccount(payload.params ?? [], wallet))
248-
case "passkey_fundAccount":
249-
if (!config.enablePasskeyHelpers) return createError(payload.id, -32601, `Method ${method} not found`)
250-
return createResult(payload.id, await handleFundAccount(payload.params ?? [], wallet))
251248
case "passkey_getLogs":
252249
if (!config.enablePasskeyHelpers) return createError(payload.id, -32601, `Method ${method} not found`)
253250
return createResult(payload.id, await handleGetLogs(payload.params ?? [], store))
@@ -395,15 +392,6 @@ async function handleCreatePasskeyAccount(params: any[], wallet: Wallet): Promis
395392
return { account, txHash: receipt?.hash ?? tx.hash }
396393
}
397394

398-
async function handleFundAccount(params: any[], wallet: Wallet): Promise<{ txHash: string }> {
399-
const to = params[0] as string | undefined
400-
const amount = params[1] ? BigInt(params[1] as string) : 1_000_000_000_000_000_000n
401-
if (!to) throw new Error("passkey_fundAccount requires target address")
402-
const tx = await wallet.sendTransaction({ to, value: amount })
403-
const receipt = await tx.wait()
404-
return { txHash: receipt?.hash ?? tx.hash }
405-
}
406-
407395
async function handleGetLogs(params: any[], store: BundlerStore) {
408396
const limit = Number(params?.[0] ?? 100)
409397
if (!Number.isFinite(limit) || limit <= 0) return []

passkey-bundler/src/metrics.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ export class Metrics {
66
readonly rpcFailures: Counter
77
readonly userOpSuccess: Counter
88
readonly userOpFailed: Counter
9-
readonly prefundAttempts: Counter
10-
readonly prefundFailures: Counter
119
readonly queueDepth: Gauge
1210
readonly submissionDuration: Histogram
1311

@@ -42,18 +40,6 @@ export class Metrics {
4240
labelNames: ["reason"],
4341
})
4442

45-
this.prefundAttempts = new Counter({
46-
name: "bundler_prefund_attempts_total",
47-
help: "Prefund attempts performed before handleOps",
48-
registers: [this.registry],
49-
})
50-
51-
this.prefundFailures = new Counter({
52-
name: "bundler_prefund_failures_total",
53-
help: "Prefund attempts that failed",
54-
registers: [this.registry],
55-
})
56-
5743
this.queueDepth = new Gauge({
5844
name: "bundler_queue_depth",
5945
help: "Current queue depth for pending user operations",

0 commit comments

Comments
 (0)