# Durable plans and signed transaction relay

A **plan** stores the exact proposed transactions for review. **Relay** means submitting transactions a wallet has signed. Center stores the reviewed calls unchanged and tracks their results. It never holds wallet keys, signs transactions, assumes token spending approval, or gives spending permission through an API credential.

The service supports wallets controlled directly by a signing key (**EOAs**), calls forwarded by Relayr, and separately configured smart-wallet requests (**UserOperations**) through EntryPoint v0.7. Direct relay accepts one exact wallet-signed EVM transaction matching one planned call. See the [glossary](https://juicebox.center/api#glossary).

The direct transport supports EIP-155 protected legacy, EIP-2930, and EIP-1559 transactions. Contract creation without a destination, unprotected legacy transactions, blobs, and EIP-7702 authorization-list transactions are rejected. Factory calls with an explicit reviewed destination remain ordinary supported calls. Runtime capabilities report each transport's configured availability, confirmations, and fee bounds. The separate [Relayr sponsorship service](./SPONSORSHIP.md) reports `relayr-prepaid-erc2771` funding requirements; `userOperations` and `sessions` report hosted EntryPoint execution and recurring authority. A route or account binding alone does not establish availability.

## Workflow

The encoded call instructions are **calldata**. **Canonical** block evidence
comes from a block that remains in the chain's accepted history. An
**idempotency key** identifies retries of the same operation; keep it unchanged.

1. A protocol operation or reviewed ABI method produces `RestPlanDraft`: wallet account, exact chain/destination/calldata/native value, ordered dependencies, and canonical block evidence. The authenticated account must own the planned wallet. A bot principal is an API identity, not the wallet signer.
2. `createPlan(actor, draft, idempotencyKey, requestHash)` returns a durable plan ID, commitment, expiry, calls, and redacted step state. The body digest is computed by the authenticated HTTP boundary. An identical idempotency retry returns the original plan, including after its expiry; another body or operation conflicts.
3. `simulateStep(actor, id, stepIndex)` checks dependencies and canonical preparation evidence, then simulates and estimates the exact call as the wallet at a reported canonical block. It never supplies state overrides. Approval prerequisites must already be confirmed and semantically verified when a verifier models them.
4. The wallet signs the exact transaction. `submitStep(actor, id, stepIndex, rawSignedTransaction, idempotencyKey, requestHash)` recovers the signer and validates chain, destination, calldata, value, signature form, nonce, access list bounds, gas, fees, and maximum cost. A new relay requires the signed nonce to equal the account's current pending nonce; future nonce queues and replacements are excluded.
5. The service reconciles the deterministic transaction hash first. If the hash is already observed, it records monitoring state without broadcasting. Otherwise it simulates and estimates against current canonical state, atomically claims a durable dispatch lease, and submits the exact supplied bytes.
6. `refresh(actor, id)` checks receipt status, canonical block hash, configured confirmations, and any installed semantic verifier. Unavailable evidence remains unknown. A successful source-chain receipt does not establish cross-chain settlement.

An EOA transaction signature has no trustworthy signing timestamp. Plan expiry constrains admission to this relay; it is not an onchain deadline and does not prove when a signature was produced. Contract-level deadlines and a separate owner approval bound to an execution commitment are distinct controls. Old signed transactions may remain broadcastable outside this service while their nonce remains unused.

Production installs `authorizeDispatch(plan, stepIndex, transactionHash)` in `TransactionService`. After canonical preflight, it verifies a fresh owner-signed API request or the separate owner approval bound to the exact account, principal, plan commitment, step, and raw transaction hash. A bot API credential and an otherwise valid raw EOA transaction signature do not establish this freshness. The callback returns verified `issuedAt` and `expiresAt` Unix seconds. The service checks the window immediately and the store rechecks it using the admission clock after lock waits: validity is at most five minutes, future issuance tolerance is 30 seconds, and expiry must remain in the future. Read-only observed-hash reconciliation and active-lease replays acquire no new dispatch and need no new owner approval. Capabilities distinguish this configured production mode from a trusted internal service constructed without the callback.

`maximumCost` and the configured `maximumTransactionCost` admission bound cover native value plus gas limit times the signed fee cap. They do not cap or quote additional chain-specific fees. For example, OP Stack chains add L1 data fees and may add operator fees to execution gas charges; see the [OP Stack fee documentation](https://docs.optimism.io/op-stack/transactions/fees). The public `costScope` field makes this limitation explicit. Wallets must account for their chain's full fee model; the relay still runs exact-call gas estimation and canonical-state preflight.

## Persistence, revocation, and retries

`MemoryTransactionStore` is intended for tests or explicitly ephemeral local use. `PostgresTransactionStore` uses migration `005_rest_transactions.sql`, durable plans, actor-scoped idempotency records, and permanent sender/chain/nonce reservation tombstones. Migration `006_rest_sponsorship.sql` supplies the shared plan-step transport reservation table; run all migrations before admitting transaction requests. Public service responses omit signed bytes and lease tokens. The storage layer retains signed bytes solely for exact-hash identity and recovery; operators must protect the database and backups accordingly.

The account authorization check and dispatch claim share the account row lock used by grant revocation. The claim is the admission boundary: revocation prevents new or recovered dispatch claims, while an already claimed, bounded in-flight request may finish. No database transaction stays open during RPC. Expired leases must be reclaimed under current authorization; read-only receipt reconciliation does not reacquire signing or relay authority.

Every step binds permanently to one raw transaction/hash. Every sender/chain/nonce binds globally to one plan step, including across grants. Reusing an idempotency key with a different request conflicts. A lost response, timeout, process crash, or lease expiration does not authorize different calldata, another nonce, or a fee replacement. Reconcile the stored hash. A user may explicitly retry the identical bytes only after a new claim, fresh canonical preflight, and an unchanged pending nonce; an active lease suppresses concurrent dispatch.

Each admitted plan step permanently selects exactly one transport and execution binding under the same account lock. A direct claim binds `signed-eoa-transaction` to the raw transaction hash; Relayr binds `relayr-prepaid-erc2771` to its preparation ID; EntryPoint execution binds `eip4337-user-operation` to its immutable UserOperation record. Competing transports cannot reserve the same step, even with different wallet nonces. Failure, expiry, reorg, and retry do not release this reservation. The memory stores share one `MemoryTransportReservations` instance; the PostgreSQL stores share the durable table. Selecting a different transport after admission requires a new reviewed plan.

Relayr and UserOperation execution are stored as `steps[].externalExecution`, without an invented owner EOA attempt, sender nonce, or signed transaction. Public responses expose `steps[].execution` with the transport, binding ID, chain, and any observed outer transaction hash. `syncExternalExecutions` discovers admitted durable reservations after a crash, including when the original plan had not yet been tagged. Reservation discovery and observation updates do not authorize a new broadcast.

`getPlan` and `refresh` obtain fresh external observations through the installed `ExternalExecutionObserver`. Its Relayr implementation must prove the immutable plan/record binding, exact quoted outer call, and successful expected inner Forwarder execution. The transaction service independently fetches the canonical outer receipt, checks its hash and block against that proof, applies confirmation policy, and evaluates installed semantic predicates against the full receipt logs. A successful outer receipt alone cannot confirm the step. Missing adapters, unavailable proofs, semantic failures, or reorgs block downstream calls. An outer success with a failed inner call is explicitly a failed execution. Original owner EOA nonces remain independent of the sponsor's outer transaction.

`reserveExternalExecution` and `saveExternalExecution` are internal persistence methods, never request-body admission APIs. They validate an existing permanent transport binding, immutable chain/index scope, exact initiating principal, and revision. Owners can discover and refresh their account's bot plans through read access; new financial authority is not acquired. Direct observation updates cannot mutate an external step, and direct submission cannot replace one. Recovery also discovers external-only pending journeys, while repeated plan reads recheck previously confirmed executions.

`recoverPending({limit})` only reconciles pending receipts. It never broadcasts or claims work, including when the initiating grant has expired or been revoked. Owners may read their account's bot plans; bots read only their own principal's plans. New relay mutations remain bound to the plan's original principal.

Recovery uses ascending keyset pagination and returns `nextCursor`. An explicit cursor can resume a worker sweep; a service instance rotates its own cursor when callers omit one. Thus permanently pending older plans do not starve newer journeys. A restart can repeat harmless receipt reads.

HTTP preparation handlers call `findPlanByIdempotency(actor, key, requestHash, 'create-plan')` before repeating upstream work. The idempotency digest binds method, exact raw path/query, content type and exact request body bytes, so a key cannot silently identify another preparation endpoint or reserialized body.

Semantic verification receives complete bounded receipt logs before persistence compacts them. Stored receipts explicitly set `logsStored: false`, preserve `logCount`, and commit the full list as `logsHash = keccak256(UTF8(canonicalJson(logs)))`. Canonical JSON sorts object keys lexicographically, preserves array order, uses JSON string/finite-number/boolean/null encoding, and excludes undefined object properties. Empty stored `logs` therefore means omitted payload, not an assertion that no events occurred. Each refresh obtains and verifies the full canonical receipt again.

## Journeys and partial completion

Plans contain 1–32 calls. Dependencies refer only to distinct earlier calls, yielding a bounded DAG. A dependent call requires canonical successful confirmations and `verified` or explicitly `unmodeled` semantic status for every predecessor. `unknown` or failed modeled semantics block descendants. Current state is refreshed before preflight, and the store rechecks dependencies and revision during claim.

`submitBundle` processes explicitly supplied signed transactions in order and stops on the first unavailable or blocked step. Its response distinguishes partial admission from confirmations. It cannot make a sequence or a multi-chain journey atomic. A submitted predecessor normally leaves its dependent step blocked until confirmation; the caller resumes with the remaining signed steps later.

`transactions_confirmed` means only that the exact stored transactions have canonical successful receipts and any installed semantic predicates have passed. It does not imply destination bridge settlement or complete unmodeled business effects. A reorg invalidates the affected receipt and blocks unsubmitted descendants. Transactions already sent on another chain cannot be recalled. Expired plans require new plans and signatures for future calls; unknown future calldata is never appended to an existing commitment.

## Smart accounts, sponsorship, and session permissions

The implemented `UserOperationService` prepares and publishes EntryPoint v0.7 operations for the pinned Safe7579 stack. It independently verifies account implementation/history, EntryPoint/version/chain, nonce, exact execution calldata, gas and paymaster policy, external signatures, and operation-scoped receipt evidence. Its HTTP routes are `POST /user-operations`, `POST /user-operations/:id/submissions`, and `GET /user-operations/:id`. Create the source plan through `POST /smart-accounts/bindings/:id/plans`. The older `SmartAccountTransportAdapter` interface is not the runtime dispatch mechanism.

Omit `sessionId` for fresh, finite-validity SafeOp signatures from the current Safe-owner threshold over up to sixteen ordered calls. Supply an exact active `sessionId` for a bound bot to sign one approved action with the installed key's EIP-191 signature. API signing remains separate in both cases. Operations outside the session's exact targets, recipients, assets, amounts and validity need a new owner-approved transaction; the API grant cannot expand that policy.

Seven/thirty-day sessions implement cumulative action, gas and payment limits with durable canonical lifecycle evidence. Funds must already be in the smart account or be deposited through a separately approved transaction; an allocation record does not fund or escrow them. Funding and allowance increases require fresh owner approval. See [sessions](SESSIONS.md) for the supported closed actions and whole-wallet reservation/retirement rules.

Hosted owner execution requires the configured verified stack, bundler, gas-only paymaster, provider billing and sponsorship allowance. Session execution additionally requires the reviewed guard matching that paymaster and owner-approved activation. The legacy paymaster/`legacy-v1` guard remain the defaults; the explicit current Pimlico profile uses `current-v2` on Ethereum, Optimism, Base and Arbitrum with the same EntryPoint v0.7. Both guard artifacts are undeployed. Missing session configuration does not disable separately configured owner execution. Follow [execution operations](EXECUTION_OPERATIONS.md); local tests do not create provider credentials, establish billing or activate a production wallet.

UserOperation publication is attempted once after durable nonce and transport reservation. A provider timeout becomes `submission_unknown`; it does not inherit the direct EOA relay's explicit retry/lease behavior. Reconcile the existing operation, scoped inner execution and original plan without automatically publishing again. Safe owner rotation, API unlinking and grant revocation block applicable Center admission but do not remove a legacy onchain session: new owners should explicitly remove old permissions and revoke their enable signatures.

## Verification

`test/rest-transactions.test.ts` exercises local signature recovery, exact calldata/value/chain checks, fee bounds, nonce readiness, unsupported envelopes, response-loss recovery, duplicate dispatch, receipt reorgs, semantic dependency blocking, bundle partial completion, and expiry/revocation. No test contacts a live chain. Store tests cover actor isolation, idempotency, concurrent nonce claims, leases, and shared revocation locks; PostgreSQL integration tests use a dedicated disposable database through `TEST_DATABASE_URL`.
