Skip to content

Tags: avnu-labs/paymaster

Tags

v1.6.0

Toggle v1.6.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🐛 Tell refused submissions apart from uncertain ones (171) (#95)

* 🐛 Persist the relayer nonce on delayed release in the Redis lock

unlock_with_expiry only extended the lock key: the nonce invalidated before a
delayed release was dropped, so the next lock reused the stale cached nonce
after the cooldown. Store the nonce and extend the lock in one atomic
pipeline, so the cooldown never applies with a stale nonce.

* 🐛 Tell refused submissions apart from uncertain ones (171)

Every execution failure answered 156 (TRANSACTION_EXECUTION_ERROR), including
a timeout or gateway error while submitting the transaction and duplicate
requests. A client could not know whether the transaction might still land,
and acting on a 156 (e.g. pushing a refund) could race it.

- Add TRANSACTION_SUBMISSION_UNCERTAIN (171), the only error meaning the
  transaction may have been broadcast: any other error guarantees the request
  broadcast nothing. Its data tells a duplicate request apart from an
  uncertain broadcast, which carries the relayer transaction hash
- Classify submission errors: only the refusals declared by
  starknet_addInvokeTransaction and requests that never reached a node
  (connection failure, every endpoint circuit broken) are refusals, anything
  else becomes an UncertainSubmission carrying the prepared transaction hash
- Propagate UncertainSubmission through the relayer and execution errors
- On an uncertain submission, invalidate the relayer nonce and release the
  relayer with a delay, as for an invalid nonce. Treat an underpriced
  replacement as an invalid nonce, as a pending transaction holds it
- Make the duplicate filter admission atomic and return a reservation, released
  only when the request fails without anything broadcast
- Share the estimate and execute step between both execute endpoints
- Document 171 in the OpenRPC specification

v1.5.0

Toggle v1.5.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🐛 Read the privacy pool fee from the pool instead of configuration (#93)

* 🐛 Read the privacy pool fee from the pool instead of configuration

The relayer pre-transferred privacy.pool_fee_amount from configuration while
the forwarder approves the pool for get_fee_amount() read on-chain. If the pool
raised its fee, every private transaction reverted; if it lowered it (mainnet
went from 6 to 4 STRK in September 2026), the difference stayed in the forwarder
on every transaction.

Build and execute now resolve the fee from the pool for private transactions.
The configured amount keeps its role as the on/off switch (0 disables pool fee
handling) and becomes the fallback when the pool cannot be read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ♻️ Cache the privacy pool fee and remove its configuration

- Add SyncValue::read_and_revalidate: serve a stale value and refresh it in
  the background (single in-flight refresh), fetch inline only once expired
- Read get_fee_amount() through the starknet clients, typed, instrumented and
  cached per pool for 30s
- Propagate errors instead of silently falling back to a configured fee
- Remove privacy.pool_fee_amount: the fee comes from the pool and a pool
  without fee returns 0; existing configurations still parse
- Resolve the fee in one place for build and execute, rejecting private
  transactions when no privacy pool is configured
- Drop the parsing unit tests and redundant comments

---------

Co-authored-by: l-henri <22731646+l-henri@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

v1.4.8

Toggle v1.4.8's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
⬆️ Bump moka from 0.12.13 to 0.12.15 (#84)

Bumps [moka](https://github.com/moka-rs/moka) from 0.12.13 to 0.12.15.
- [Release notes](https://github.com/moka-rs/moka/releases)
- [Changelog](https://github.com/moka-rs/moka/blob/main/CHANGELOG.md)
- [Commits](moka-rs/moka@v0.12.13...v0.12.15)

---
updated-dependencies:
- dependency-name: moka
  dependency-version: 0.12.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

v1.4.7

Toggle v1.4.7's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🐛 Tolerate screening attestation suffix in apply_actions calldata (#82)

The privacy pool upgrade added a trailing `screening: Option<ScreeningAttestation>`
argument to `apply_actions`, so the calldata now ends with an extra `[0x1]` (None)
or `[0x0, issued_at, sig_r, sig_s]` (Some) after the action span.

`parse_server_actions` assumed the calldata was only `Span<ServerAction>` and
rejected any trailing felt with `TrailingData`, so every private transaction that
reaches the parser failed with CALLDATA_PARSING (rpc code 166).

Consume the optional suffix after the action span instead of rejecting it. The
paymaster only relays the attestation verbatim (the contract verifies it), so we
just skip it. Pre-screening pools emit no suffix (remaining() == 0), so the old
behaviour is unchanged — backward compatible, and forward compatible for when
mainnet receives the same upgrade.

v1.4.6

Toggle v1.4.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🐛 Cap declared L2 gas bound at the sequencer gateway limit (#81)

Heavy sponsored transactions (e.g. a Garaga UltraHonk proof verification) were
rejected pre-broadcast by the Starknet sequencer gateway with "Max gas amount is
too high ... maximum allowed gas amount: 1200000000" (surfaced as RPC code 156),
even though they only consume a fraction of that.

The declared `max_l2_gas` bound is the estimate padded by the x1.5 gas
multiplier, which can exceed the sequencer's stateless `max_l2_gas_amount` cap
(1.2B on mainnet) while actual consumption stays well under it.

- Cap the declared L2 gas bound at a configurable `max_l2_gas_amount` (default
  1_200_000_000, exactly the mainnet sequencer limit; the gateway check is
  strict `>`, so declaring exactly it is accepted): min(estimate * 1.5, cap).
  L1 gas and L1 data gas keep their full x1.5 padding.
- Reject early, before locking a relayer and broadcasting, when even the bare
  estimate (+10% safety) cannot fit under the cap, surfacing a clear
  MaxL2GasAmountExceeded error (new RPC code 169) instead of a paid out-of-gas
  revert.
- Make the cap configurable via the `starknet` config block (serde default) so
  it can be tuned per network without a redeploy, since the sequencer value may
  change over time.

v1.4.5

Toggle v1.4.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🐛 Don't cache validation overhead when contract is not yet deployed (#79

)

`ValidationGasOverhead::fetch` used to convert every error from
`client.call(get_signers)` into `Ok(Self::none())`, and the caller
(`resolve_gas_overhead`) cached the result indefinitely. As a side
effect, any address queried while its contract did not yet exist —
typically during a `deploy_and_invoke` flow that deploys the wallet in
the same transaction it is estimating — got permanently marked as
"no overhead". Once the wallet was deployed and the user did follow-up
sponsored invokes, the cached `none()` was returned instead of the
Braavos overhead, the resource bound on `l2_gas` was set ~5x too low,
and Braavos passkey transactions reverted with
`Insufficient max L2Gas`.

Distinguish error variants in `fetch`:

- successful response → cache (Braavos or none, based on shape)
- contract-level execution error (account exists but does not expose
  `get_signers`, e.g. Argent/OZ) → cache as `none()`
- `ContractNotFound` (wallet not yet deployed, the case behind this
  bug) → return `none()` for the current request but skip the cache so
  the next request retries the detection
- any other transient RPC error (timeout, rate-limit, ...) → same as
  `ContractNotFound`: do not cache

Also extract the response-shape classification into a pure function
(`from_get_signers_response`) and add unit tests covering the on-chain
shapes observed for a classic Braavos (4 felts) and a Braavos with a
passkey/secp256r1 signer (5 felts).

v1.4.4

Toggle v1.4.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
⚡ Speed up paymaster_getSupportedTokens fanout (#78)

With 32 supported_tokens the fanout in `Client::fetch_tokens` used a
concurrency of 8, requiring 4 sequential rounds. Bumped the executor
limit to 32 so all token prices fetch in a single round.

Also pre-fetch STRK once before spawning the workers: every per-token
price computation needs the STRK price, and on a cold cache the 32
concurrent workers were each issuing their own redundant STRK call.

Reduces the endpoint p95 from ~2s to ~500-700ms in observed traces.

v1.4.3

Toggle v1.4.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🐛 Fix Insufficient pool fee payment when gas_token == STRK (#77)

When the user pays the privacy pool fee in STRK as gas_token, the forwarder's
balance_before snapshot already includes the relayer's pre-transferred STRK.
The pool's transferFrom then pulls that exact amount back, so `received` ends
up `pool_fee` short of `gas_amount` and the assertion reverts with
"Insufficient pool fee payment". USDC / ETH gas tokens are unaffected because
the snapshot tracks a different token from the pretransfer.

Fix in `forwarder.cairo`: when `gas_token_address == STRK_ADDRESS`, subtract
the pool fee (now returned by `approve_pool_fee`) from `balance_before` so the
delta correctly represents only the user's payment. Behaviour for non-STRK
gas tokens is unchanged.

Also drop the provider_fee_overhead on the pool fee when paying in STRK
(`build.rs`): the overhead exists to cover STRK / gas-token price drift between
build and execute, which is irrelevant when the gas token is STRK itself.
Saves the user ~0.6 STRK per private tx on mainnet.

v1.4.2

Toggle v1.4.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🐛 Fix privacy pool fee flow (approve + overhead + STRK pre-fund) (#76)

* Approve privacy pool to pull STRK fee in forwarder

The last call of a private batch targets a privacy pool whose `apply_actions`
collects its fee via `transferFrom(forwarder, fee_collector, fee)` on STRK,
independent of the user's gas token. Without an allowance, that transfer
reverts on the ERC20 check.

- Add `IPrivacyPool` dispatcher (`get_fee_amount`) and a 10_000 STRK safety cap.
- Hardcode the canonical STRK address on Starknet; `approve_pool_fee` always
  approves the pool on STRK, never on the user's gas token.
- Wire the helper into `execute_private` and `execute_private_sponsored`.
- Add a minimal `MockPrivacyPool` and tests covering the fee-limit guard.

* Apply only provider overhead to the privacy pool fee

PR #72 applied `max_fee_multiplier` (~3x) to the pool fee, which caused
users to approve triple the amount they actually owed and hit
"Insufficient ERC20 balance" on execute.

The pool fee is a fixed amount in STRK read from the pool, so it does not
need the large safety multiplier that guards against gas-fee volatility —
only a small overhead to cover STRK / gas-token price drift between build
and execute. Use `compute_paid_fee_in_strk` (~1.1x via `provider_fee_overhead`)
for the pool portion; keep `compute_max_fee_in_strk` on the gas portion.

* Pre-fund forwarder with STRK from relayer for privacy pool fee

The privacy pool collects its fee via `transferFrom(forwarder, fee_collector, fee)`
in STRK. When the user pays in a non-STRK gas token (USDC, etc.), the forwarder
has no STRK balance for the pool to pull, so `apply_actions` reverts.

The relayer already holds STRK (refilled by the gas-tank → relayers rebalancing),
so we prepend a `strk.transfer(forwarder, pool_fee_amount)` call before the
forwarder call in both private and private-sponsored flows. The relayer is
reimbursed when the forwarder forwards the user's gas-token payment to the gas
tank, closing the loop via the existing rebalancing.

Only emitted when `privacy_pool_fee_amount > 0`, so non-pool-fee transactions
are unchanged.

v1.4.1

Toggle v1.4.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
🐛 Apply max fee overhead to privacy pool fee (#72)

Pool fee was added raw on top of the multiplied gas fee, leaving no
headroom for STRK/token price drift between build and execute. With a
non-STRK pool_fee_token (e.g. USDC), this caused POOL_FEE_TOO_LOW (167)
on paymaster_executeTransaction. Fold pool fee into the total before
applying the max_fee_multiplier, and apply the same overhead to the
sponsored-mode fee_action_amount.