Skip to content

Dev/cleanup from gratis#1

Closed
royalf00l wants to merge 3 commits into
mainfrom
dev/cleanup_from_gratis
Closed

Dev/cleanup from gratis#1
royalf00l wants to merge 3 commits into
mainfrom
dev/cleanup_from_gratis

Conversation

@royalf00l

Copy link
Copy Markdown
Collaborator

No description provided.

@royalf00l royalf00l self-assigned this Sep 3, 2024
@royalf00l royalf00l closed this Jan 1, 2025
@royalf00l
royalf00l deleted the dev/cleanup_from_gratis branch January 7, 2025 18:26
peer2f00l added a commit that referenced this pull request Jun 17, 2026
- AGENTS.md: add feed_map.rs + events.rs to the pyth-pro Read/inspect list (#1)
- verifier/README.md: allowed_channel_id is a channel byte, not seconds (#7)
- pull-payloads.mjs: reject non-positive MAX_MESSAGES so the loop can't hang (#3)
- query-solana-trusted-signers.mjs: 15s AbortController timeout on the RPC fetch (#5)
- solana-payload-signer.mjs: reject input that is neither valid hex nor base64 (#6)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
peer2f00l added a commit that referenced this pull request Jun 22, 2026
* feat(pyth-pro): add Pyth Pro (Lazer) ed25519 oracle adapter for NEAR

A push-style NEAR oracle that ingests Pyth Pro (Lazer) solana-format
(ed25519) signed price payloads and re-serves them through the
`pyth-oracle.near` view ABI, so it is a drop-in Pyth oracle for the
market and proxy-oracle.

- verifier (`templar-pyth-pro-verifier`): chain-agnostic verify + parse —
  trusted/unexpired ed25519 signer, signature check, canonical-encoding
  rejection, channel filter, freshness window. Wraps a forked, slimmed
  `pyth-lazer-protocol` pinned by exact rev (bumps are security-sensitive).
- contract (`templar-pyth-pro-adapter-contract`): NEAR cdylib storing
  verified prices and serving the full Pyth read ABI; owner-only admin,
  permissionless payable `update_price_feeds` with per-feed monotonic
  anti-replay and storage-fee/refund; stateless `verify_update` view;
  `BTreeMap`-backed `SignerSet` with structural uniqueness.
- `admin_set_signer(public_key, expires_at_s: Option<u64>)`: Some = add/
  refresh, None = remove (cannot remove the last).
- primitives: `Nanoseconds::{from_micros, as_micros}`.
- docs (README/SPEC/per-crate/TRUSTED_SIGNERS), JS sanity tools, and an
  AGENTS.md high-impact-areas entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: apply CodeRabbit auto-fixes

- AGENTS.md: add feed_map.rs + events.rs to the pyth-pro Read/inspect list (#1)
- verifier/README.md: allowed_channel_id is a channel byte, not seconds (#7)
- pull-payloads.mjs: reject non-positive MAX_MESSAGES so the loop can't hang (#3)
- query-solana-trusted-signers.mjs: 15s AbortController timeout on the RPC fetch (#5)
- solana-payload-signer.mjs: reject input that is neither valid hex nor base64 (#6)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(pyth-pro): address PR #474 review comments

- Bound update_price_feeds work/event size with a configurable, non-zero
  Config.max_feeds_per_update; reject oversized signed bundles up front,
  before any storage write or event emission (CodeRabbit + reviewer: Major).
- Rename VerifyError::NonCanonical -> TrailingBytes and reword its doc/error
  to reflect that it only rejects trailing/non-exhaustive encodings (cursor
  exhaustion), not a full byte round-trip (reviewer P3).
- require_confidence: filter out non-positive confidences as defense-in-depth.
- pull-payloads.mjs: guard JSON.parse and writePayloadFiles so a malformed WS
  frame logs and continues instead of crashing the capture loop.
- Docs + tests: document max_feeds_per_update; cover zero-cap rejection and
  over-cap bundle rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(pyth-pro): atomic admin_upgrade + adopt VersionedState

Add an Owner-only `admin_upgrade(code, migrate_args)` that deploys new
contract code and calls `migrate` in a single Promise batch, so a failed
migration reverts the code deployment too (mirrors the proxy-oracle NEAR
pattern). Guarded with `#[payable]` + `assert_one_yocto()` + `require_owner()`,
matching the contract's other `admin_*` methods.

Adopt `templar_common::versioned_state` now (the contract is not yet deployed,
so there is no migration cost): the live state moves into `State` behind
`VersionedState<State>` (launching at version 1) with `Deref`/`DerefMut`, and
`impl_versioned_state!` generates the private `migrate` entrypoint plus the
`get_stored_state_version` / `get_target_state_version` / `needs_migration`
views. The `migrate` no-op guard is documented in the versioned-state macro.

Tests cover the owner/yocto gate on `admin_upgrade` and a fresh deploy
reporting state version 1 with no pending migration. Cross-version migrate
e2e is deferred until a real v2 exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(pyth-pro): make the adapter contract compile under near-sdk `abi`

CI's `cargo clippy --all-features` and `cargo nextest --workspace` build the
contract with near-sdk's `abi` feature on, which surfaced two compile errors in
the new pyth-pro adapter:

- `#[serde(with = "hex::serde")]` fails schemars codegen (E0573: `hex::serde`
  is a module, not a type). Switch both sites to the split
  `serialize_with`/`deserialize_with` form, matching the rest of the repo.
- The `#[near]`-derived schema on `Config` recurses into the custom `SignerSet`
  field, which had no `JsonSchema`/`BorshSchema` (E0277). Add unconditional
  impls (the `templar-common` `Wad`/`Number` pattern): `BorshSchema` delegates
  to the byte-identical `Vec<([u8; 32], u64)>`, and `JsonSchema` mirrors the
  array-of-objects JSON form via a schema-only DTO — so neither depends on
  `TrustedSigner`'s abi-only schema and both compile in every configuration.
  Add `schemars` as a direct dep (already in-tree via templar-common).

Verified: `--all-features`/abi clippy `-D warnings`, `--workspace --lib`,
wasm32 check, and host tests all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(pyth-pro): enforce no_older_than at ns precision (Codex P2)

`is_fresh` truncated the nanosecond age delta to whole seconds before the
freshness comparison, so a feed up to ~1s older than `age_s` still passed
(e.g. a 1.9s-old feed against `age_s = 1`). Consumers (market/proxy) rely on the
`*_no_older_than` views to enforce `price_maximum_age_s`, so this silently served
stale prices. Compare the full ns delta against `Nanoseconds::from_secs(age_s)`
instead (overflow-safe via saturating_mul). Strictly more correct, and never more
lenient than Pyth's whole-second semantics. Adds a sub-second boundary regression
test.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
peer2f00l added a commit that referenced this pull request Jun 26, 2026
Adds an #[ignore]d near-sandbox integration test (ENG-369 acceptance #1)
that drives the liquidator's own LiquidationExecutor — the migrated
gateway plan/execute path — against a deployed market: deploys market +
mock oracle, funds/registers a supplier+liquidator and a borrower, opens
a borrow, crashes the collateral price, then liquidates and asserts
LiquidationOutcome::Liquidated.

Exposes templar_gateway_testing::sandbox::test_secret_key as pub so
consumers can build a gateway client against the sandbox with the same
key the harness deploys with.

Run with:
  ./script/prebuild-test-contracts.sh
  TEST_CONTRACTS_PREBUILT=1 cargo test -p templar-liquidator \
    --test liquidation_sandbox -- --ignored

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
peer2f00l added a commit that referenced this pull request Jun 30, 2026
…G-369) (#484)

* refactor(accumulator): migrate onto the in-process gateway library

Replace the accumulator's hand-rolled NEAR RPC/transaction plumbing with
templar-gateway-client. Reads go through SigningClient::read
(market.listBorrowPositions, registry.listDeployments,
market.getConfiguration, account.get, contract.getVersion); writes go
through SigningClient::execute (market.applyInterest,
market.accumulateStaticYield), which signs and submits through the
gateway operation driver.

Delete src/rpc.rs entirely (TransactionV0/FunctionCallAction builders,
send_tx with retry/poll, get_access_key_data, view, serialize_and_encode)
and the bespoke 5s nonce cache: near_api's signer sequences nonces for
concurrent submissions. App-level pagination is kept (gateway reads are
single-page) but extracted into a tested collect_paginated helper.

Behavior changes:
- Drop the --timeout/TIMEOUT flag; the gateway client polls to finality
  internally and exposes no per-call timeout.
- Static-yield gating now uses MarketVersion::requires_static_yield_accumulation
  (>= 1.1.0), matching harvest-static-yield, instead of the !is_v1_0_0 hack.

Tests: delete the wiremock suites that mocked the deleted JSON-RPC
plumbing; keep pure-unit coverage for the surviving logic
(collect_paginated continuation, static-account extraction) and the
clap arg-parsing test.

Depends on ENG-389 (bounded MemoryStore retention) for safe long-running
operation.

ENG-368

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(accumulator): surface total discovery failure; reject zero intervals

- list_all_deployments now returns an error when *every* configured
  registry fails, instead of Ok(empty). Startup (`?`) fails loudly and
  the refresh path's `let Ok(..) else` keeps the existing market set,
  rather than silently dropping all accumulators on a transient outage.
  Partial success still returns what was read.
- run_service_with_client validates interval/static_interval/
  registry_refresh_interval/concurrency are all > 0 up front; zero would
  otherwise panic tokio::time::interval / break buffer_unordered mid-run.

Addresses review feedback on #482 (ENG-368).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(accumulator): use shared gateway Network; fold version helper

Address review feedback on #482.

- Network: add a shared `Network` enum to `templar-gateway-client`
  (clap::ValueEnum behind a `clap` feature) and make the accumulator its
  first adopter, removing the bot's duplicate definition. This is the
  off-chain home for the type; remaining tools/services (and the
  templar-common copy) migrate incrementally in follow-up work.
- Helpers: fold `market_version` into a single private
  `supports_static_yield`, dropping a one-caller indirection and the
  public surface. Behaviour (skip markets < 1.1.0, conservative on
  undeterminable version) is unchanged and still mirrors harvest.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(accumulator): reject an empty registry set at startup

The accumulator only discovers markets via registries, so starting with
no registries configured yields a daemon that runs forever doing
nothing. Fail fast with a clear message alongside the existing
interval/concurrency guards.

Addresses review feedback on #482 (ENG-368).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): add proxyOracle.updatePrices write method

Typed write spec + dispatch + near-client method for refreshing a proxy
oracle's cached prices (`update_prices(price_ids)`, 100 TGas, no deposit).
This is the gateway-native equivalent of the liquidator's hand-rolled
proxy-oracle update transaction, needed for the ENG-369 migration.

ENG-369 / ENG-391

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(liquidator): adopt shared gateway Network; add gateway deps

First step of the liquidator gateway migration (ENG-369): add the
templar-gateway-{client,methods-spec,types} dependencies and switch the
`Network` enum from templar_common::utils::Network to the shared
templar_gateway_client::Network (advances ENG-391). RPC/tx plumbing is
migrated in subsequent commits.

ENG-369 / ENG-391

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(liquidator): migrate onto the in-process gateway library

Replace the liquidator's hand-rolled NEAR RPC/transaction plumbing with
templar-gateway-client's SigningClient (ENG-369). One shared SigningClient
replaces the JsonRpcClient + Arc<Signer> + NonceTracker; near_api's signer
sequences nonces internally.

- Reads via client.read: market::{GetBorrowStatus,GetBorrowPosition,
  ListBorrowPositions}, contract::GetVersion, ft::GetBalanceOf,
  proxyOracle::{ListProxies,GetProxy}, lstOracle::{GetOracleId,GetTransformer},
  pyth::ListEmaPricesNoOlderThan, refFinance::GetPools, storage::GetBalanceBounds,
  and contract::ViewFunction for the per-config transformer call.
- Writes via client.execute: market::Liquidate (success determined by the
  operation status); pyth::UpdatePriceFeeds (Hermes VAA hex decoded to bytes;
  the gateway re-hex-encodes for the on-chain call — identical bytes);
  proxyOracle::UpdatePrices; swaps via ft::{Transfer,TransferCall} +
  storage::Deposit + tx::Transfer.
- Deleted the rpc.rs plumbing (view/send_tx/get_access_key_data/NonceTracker/
  list_deployments/...). Kept RpcError/AppError as the crate's error types with
  a From<GatewayError> conversion, so LiquidatorError, notification_kind, and
  the full test suite are unchanged. Dropped near-jsonrpc-primitives and
  near-primitives.
- Business logic (liquidation math, profitability, swap quoting/retry,
  notifier, formatting, HTTP price fetch) is unchanged.

114 tests pass; full clippy gate (--all-features --workspace --tests
-D warnings) clean.

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(liquidator): drop redundant AccountId string round-trip

`call.account_id` is a `near_sdk::AccountId`, which near-sdk re-exports
as `near_account_id::AccountId` — the exact type `contract::ViewFunction`
takes. The `.as_str().parse()` round-tripped a value through a String
back into the same type (re-validating an already-valid account id and
forcing a dead error branch). Pass it directly.

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(liquidator,gateway): address PR #484 review feedback

- Remove X-API-Key/NEAR_API_KEY support from the liquidator (Codex P1).
  The gateway client (near_api) connects from a URL and doesn't attach a
  custom header; operators embed the provider key in NEAR_RPC_URL (e.g.
  FastNear/QuickNode `?apiKey=<key>`). The run scripts now fold
  NEAR_API_KEY into the URL. (Header auth remains reachable for
  Authorization: Bearer providers via near_api RPCEndpoint::with_api_key,
  so no gateway surface is needed.)
- Regenerate gateway/METHODS.md so proxyOracle.updatePrices is listed
  (Codex P2).
- Add StepStatus/TransactionStepRecord::tx_hash() and
  OperationRecord::latest_tx_hash() in gateway-types; use the latter in
  the liquidator's 1-Click swap instead of a hand-rolled step scan.
- Add Debug impls for gateway Client/SigningClient so composed consumer
  types can derive Debug.

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor: shared collect_paginated; typed NEP-297 event parsing

- Extract the duplicated "loop until a short page" pagination helper
  (accumulator + liquidator each had a copy) into
  templar_gateway_client::collect_paginated, generic over the fetcher's
  error type so both anyhow and GatewayResult callers use it. Tests moved
  to the gateway-client crate. (#3 from review)
- Replace the brittle EVENT_JSON substring-matching in the 1-Click swap's
  refund detection with a typed NEP-297 parse: strip the `EVENT_JSON:`
  prefix and deserialize into `Nep297Event`/`FtTransferEvent`, comparing
  typed AccountIds and reading the typed U128 amount. (#6 from review)

Follow-up filed for the MethodSpec constructor ergonomics (ENG-393).

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* test(liquidator): sandbox acceptance test for gateway-driven liquidation

Adds an #[ignore]d near-sandbox integration test (ENG-369 acceptance #1)
that drives the liquidator's own LiquidationExecutor — the migrated
gateway plan/execute path — against a deployed market: deploys market +
mock oracle, funds/registers a supplier+liquidator and a borrower, opens
a borrow, crashes the collateral price, then liquidates and asserts
LiquidationOutcome::Liquidated.

Exposes templar_gateway_testing::sandbox::test_secret_key as pub so
consumers can build a gateway client against the sandbox with the same
key the harness deploys with.

Run with:
  ./script/prebuild-test-contracts.sh
  TEST_CONTRACTS_PREBUILT=1 cargo test -p templar-liquidator \
    --test liquidation_sandbox -- --ignored

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(liquidator): fail liquidations whose receipts reverted

A gateway operation's status reflects only the transaction's final receipt.
`market::Liquidate` is an `ft_transfer_call`, so a liquidation the market
rejects makes the receiver callback panic while NEP-141's `ft_resolve_transfer`
refunds and the top-level transaction still reports `SuccessValue` — the
post-migration check looked only at `OperationStatus::Succeeded`, so a refunded
liquidation was wrongly reported as `Liquidated` (the pre-migration
`check_transaction_success` scanned receipts and caught this).

Restore receipt-level checking: `tx.get` now reports `failed_receipts` (the
accounts whose receipts failed, populated from the execution result's
`receipt_failures()`), and after a top-level success the liquidator fetches it
and treats any failed receipt as a failed liquidation — releasing the reserved
inventory instead of assuming the collateral was seized.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(liquidator,gateway): address CodeRabbit review findings

Gateway:
- tx.get exposes deduped `failed_receipts` (distinct contracts, via BTreeSet).
- OperationRecord::latest_tx_hash picks the highest-`index` step, not the last
  in vector order, so an out-of-order record still resolves correctly.
- collect_paginated takes `NonZeroU32` page size (a zero page would loop
  forever); callers' page-size consts are now NonZeroU32.

Liquidator — gateway write success must check status AND receipts:
- Ref swaps (ft_transfer_call) now reject when any receipt failed, like the
  liquidation executor (a refunded swap no longer reports success).
- execute_liquidation releases reserved inventory if receipt inspection errors.
- Ref storage registration no longer treats a non-success status as
  "already registered" (that genuinely fails); implicit-account creation and
  oneclick storage deposit warn loudly on non-success (refund detection
  backstops the deposit path).
- oneclick: a succeeded deposit must carry a tx hash (never send the operation
  id to the 1-Click API), refund detection now runs unconditionally, and it
  parses NEP-245 `mt_transfer` refunds in addition to NEP-141 `ft_transfer`.

Liquidator — oracle correctness:
- LST detection only treats method-not-found as "not LST"; transient errors
  propagate and aren't cached, so a blip can't permanently misroute an oracle.
- resolve_pyth_update_targets is fallible; transformer/proxy read failures
  propagate instead of silently refreshing the wrong/incomplete feeds.

Liquidator — security & robustness:
- Don't log the secret-bearing NEAR_RPC_URL; run scripts pass it via the
  environment (clap reads NEAR_RPC_URL) instead of argv.
- Floor concurrency at 1 so buffer_unordered can't hang.
- Sandbox test asserts each setup write succeeded.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(liquidator): address CodeRabbit follow-up review findings

- oracle: `resolve_pyth_update_targets` probed `proxy_oracle_cache` directly, so
  a direct caller that hadn't warmed the cache would misclassify a proxy oracle
  as direct Pyth. Use `is_proxy_oracle` (checks the cache first, then probes),
  matching the LST branch and keeping the hot path cheap.
- swap/ref + executor: the failed-receipt guards returned success/None when the
  operation carried no transaction hash, silently skipping receipt inspection on
  an ft_transfer_call. Fail closed instead (the hash is required to inspect
  receipts), matching the 1-Click swap path which already did.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
peer2f00l added a commit that referenced this pull request Jul 2, 2026
…70) (#487)

* refactor(accumulator): migrate onto the in-process gateway library

Replace the accumulator's hand-rolled NEAR RPC/transaction plumbing with
templar-gateway-client. Reads go through SigningClient::read
(market.listBorrowPositions, registry.listDeployments,
market.getConfiguration, account.get, contract.getVersion); writes go
through SigningClient::execute (market.applyInterest,
market.accumulateStaticYield), which signs and submits through the
gateway operation driver.

Delete src/rpc.rs entirely (TransactionV0/FunctionCallAction builders,
send_tx with retry/poll, get_access_key_data, view, serialize_and_encode)
and the bespoke 5s nonce cache: near_api's signer sequences nonces for
concurrent submissions. App-level pagination is kept (gateway reads are
single-page) but extracted into a tested collect_paginated helper.

Behavior changes:
- Drop the --timeout/TIMEOUT flag; the gateway client polls to finality
  internally and exposes no per-call timeout.
- Static-yield gating now uses MarketVersion::requires_static_yield_accumulation
  (>= 1.1.0), matching harvest-static-yield, instead of the !is_v1_0_0 hack.

Tests: delete the wiremock suites that mocked the deleted JSON-RPC
plumbing; keep pure-unit coverage for the surviving logic
(collect_paginated continuation, static-account extraction) and the
clap arg-parsing test.

Depends on ENG-389 (bounded MemoryStore retention) for safe long-running
operation.

ENG-368

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(accumulator): surface total discovery failure; reject zero intervals

- list_all_deployments now returns an error when *every* configured
  registry fails, instead of Ok(empty). Startup (`?`) fails loudly and
  the refresh path's `let Ok(..) else` keeps the existing market set,
  rather than silently dropping all accumulators on a transient outage.
  Partial success still returns what was read.
- run_service_with_client validates interval/static_interval/
  registry_refresh_interval/concurrency are all > 0 up front; zero would
  otherwise panic tokio::time::interval / break buffer_unordered mid-run.

Addresses review feedback on #482 (ENG-368).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(accumulator): use shared gateway Network; fold version helper

Address review feedback on #482.

- Network: add a shared `Network` enum to `templar-gateway-client`
  (clap::ValueEnum behind a `clap` feature) and make the accumulator its
  first adopter, removing the bot's duplicate definition. This is the
  off-chain home for the type; remaining tools/services (and the
  templar-common copy) migrate incrementally in follow-up work.
- Helpers: fold `market_version` into a single private
  `supports_static_yield`, dropping a one-caller indirection and the
  public surface. Behaviour (skip markets < 1.1.0, conservative on
  undeterminable version) is unchanged and still mirrors harvest.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(accumulator): reject an empty registry set at startup

The accumulator only discovers markets via registries, so starting with
no registries configured yields a daemon that runs forever doing
nothing. Fail fast with a clear message alongside the existing
interval/concurrency guards.

Addresses review feedback on #482 (ENG-368).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): add proxyOracle.updatePrices write method

Typed write spec + dispatch + near-client method for refreshing a proxy
oracle's cached prices (`update_prices(price_ids)`, 100 TGas, no deposit).
This is the gateway-native equivalent of the liquidator's hand-rolled
proxy-oracle update transaction, needed for the ENG-369 migration.

ENG-369 / ENG-391

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(liquidator): adopt shared gateway Network; add gateway deps

First step of the liquidator gateway migration (ENG-369): add the
templar-gateway-{client,methods-spec,types} dependencies and switch the
`Network` enum from templar_common::utils::Network to the shared
templar_gateway_client::Network (advances ENG-391). RPC/tx plumbing is
migrated in subsequent commits.

ENG-369 / ENG-391

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(liquidator): migrate onto the in-process gateway library

Replace the liquidator's hand-rolled NEAR RPC/transaction plumbing with
templar-gateway-client's SigningClient (ENG-369). One shared SigningClient
replaces the JsonRpcClient + Arc<Signer> + NonceTracker; near_api's signer
sequences nonces internally.

- Reads via client.read: market::{GetBorrowStatus,GetBorrowPosition,
  ListBorrowPositions}, contract::GetVersion, ft::GetBalanceOf,
  proxyOracle::{ListProxies,GetProxy}, lstOracle::{GetOracleId,GetTransformer},
  pyth::ListEmaPricesNoOlderThan, refFinance::GetPools, storage::GetBalanceBounds,
  and contract::ViewFunction for the per-config transformer call.
- Writes via client.execute: market::Liquidate (success determined by the
  operation status); pyth::UpdatePriceFeeds (Hermes VAA hex decoded to bytes;
  the gateway re-hex-encodes for the on-chain call — identical bytes);
  proxyOracle::UpdatePrices; swaps via ft::{Transfer,TransferCall} +
  storage::Deposit + tx::Transfer.
- Deleted the rpc.rs plumbing (view/send_tx/get_access_key_data/NonceTracker/
  list_deployments/...). Kept RpcError/AppError as the crate's error types with
  a From<GatewayError> conversion, so LiquidatorError, notification_kind, and
  the full test suite are unchanged. Dropped near-jsonrpc-primitives and
  near-primitives.
- Business logic (liquidation math, profitability, swap quoting/retry,
  notifier, formatting, HTTP price fetch) is unchanged.

114 tests pass; full clippy gate (--all-features --workspace --tests
-D warnings) clean.

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(liquidator): drop redundant AccountId string round-trip

`call.account_id` is a `near_sdk::AccountId`, which near-sdk re-exports
as `near_account_id::AccountId` — the exact type `contract::ViewFunction`
takes. The `.as_str().parse()` round-tripped a value through a String
back into the same type (re-validating an already-valid account id and
forcing a dead error branch). Pass it directly.

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(liquidator,gateway): address PR #484 review feedback

- Remove X-API-Key/NEAR_API_KEY support from the liquidator (Codex P1).
  The gateway client (near_api) connects from a URL and doesn't attach a
  custom header; operators embed the provider key in NEAR_RPC_URL (e.g.
  FastNear/QuickNode `?apiKey=<key>`). The run scripts now fold
  NEAR_API_KEY into the URL. (Header auth remains reachable for
  Authorization: Bearer providers via near_api RPCEndpoint::with_api_key,
  so no gateway surface is needed.)
- Regenerate gateway/METHODS.md so proxyOracle.updatePrices is listed
  (Codex P2).
- Add StepStatus/TransactionStepRecord::tx_hash() and
  OperationRecord::latest_tx_hash() in gateway-types; use the latter in
  the liquidator's 1-Click swap instead of a hand-rolled step scan.
- Add Debug impls for gateway Client/SigningClient so composed consumer
  types can derive Debug.

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor: shared collect_paginated; typed NEP-297 event parsing

- Extract the duplicated "loop until a short page" pagination helper
  (accumulator + liquidator each had a copy) into
  templar_gateway_client::collect_paginated, generic over the fetcher's
  error type so both anyhow and GatewayResult callers use it. Tests moved
  to the gateway-client crate. (#3 from review)
- Replace the brittle EVENT_JSON substring-matching in the 1-Click swap's
  refund detection with a typed NEP-297 parse: strip the `EVENT_JSON:`
  prefix and deserialize into `Nep297Event`/`FtTransferEvent`, comparing
  typed AccountIds and reading the typed U128 amount. (#6 from review)

Follow-up filed for the MethodSpec constructor ergonomics (ENG-393).

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* test(liquidator): sandbox acceptance test for gateway-driven liquidation

Adds an #[ignore]d near-sandbox integration test (ENG-369 acceptance #1)
that drives the liquidator's own LiquidationExecutor — the migrated
gateway plan/execute path — against a deployed market: deploys market +
mock oracle, funds/registers a supplier+liquidator and a borrower, opens
a borrow, crashes the collateral price, then liquidates and asserts
LiquidationOutcome::Liquidated.

Exposes templar_gateway_testing::sandbox::test_secret_key as pub so
consumers can build a gateway client against the sandbox with the same
key the harness deploys with.

Run with:
  ./script/prebuild-test-contracts.sh
  TEST_CONTRACTS_PREBUILT=1 cargo test -p templar-liquidator \
    --test liquidation_sandbox -- --ignored

ENG-369

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway-client): pooled multi-key signer + relayer gateway deps

Phase 0 of the relayer gateway migration (ENG-370).

- Add `ClientBuilder::secret_keys(account, keys)`: registers a pooled
  `near_api` signer so an account's writes rotate across multiple keys
  (each with its own nonce sequence). This preserves the relayer's
  nonce-parallel relay throughput once it moves onto the gateway client.
  No `NearTransactionSigner` change needed — it already holds a poolable
  `Arc<Signer>`.
- Add `GatewayError::InvalidSignerKey` and use it for both `secret_key`
  and `secret_keys`; `NearTransaction` ("tx failed") was the wrong
  variant for a bad/missing key at builder time.
- Add gateway deps to the relayer (near-api, templar-gateway-{client,
  core,methods-spec,types}) ahead of the read/oracle-update migration.

ENG-370

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(relayer): route market/oracle reads through the gateway

Phase 1 of the relayer gateway migration (ENG-370): replace the bespoke
read/oracle-resolution layer with gateway typed specs.

- App holds an in-process gateway Client (read-only for now), built from
  --rpc-url. App::new now returns anyhow::Result.
- get_market_prices -> oracle.getPrices (the gateway classifies the oracle
  and applies LST transformers / proxy aggregation server-side, cached).
- load_markets -> registry.listDeployments (paginated via the shared
  collect_paginated) + load_market_data using market.getConfiguration and
  oracle.getPriceResolutionDependencies (oracle kind + per-asset update
  targets), instead of the hand-rolled classification/resolution.
- Delete from near.rs: load_market_accounts, load_market_prices,
  resolve_price{,_with_pyth,_with_lst,_with_proxy}, query_oracle_type,
  resolve_price_identifier, fetch_oracle_request, get_transformer, get_proxy,
  fetch_transformer_input, load_deployments/versions_from_registry, the
  OracleType enum, and the resolution error enums.
- Delete tests/near.rs (transformer_resolution): that logic now lives in the
  gateway, covered by its proxy_oracle/lst_oracle/oracle dispatch tests.

view/view_raw + storage-balance reads remain on near.rs for the relay/UA
path (Phases 2-3). Net: +128 / -626.

ENG-370

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(relayer): drive oracle updates through the gateway

Phase 2 of the relayer gateway migration (ENG-370): rewrite the
oracle-update write path around the gateway driver.

- Spec trait: drop `type Error` + `update_actions(-> Vec<Action>)`; add
  `execute_update(gateway, oracle_id, feed_ids)` which fetches the
  off-chain payload (Hermes VAA / RedStone JS bridge) then submits via
  `gateway.execute` (pyth.updatePriceFeeds / redstone.writePrices). The
  gateway owns signing, submission, and finality.
- oracle/mod.rs Client now holds a relay-bound SigningClient instead of
  the bespoke Near + Cache; UpdateError collapses from 9 variants to 3
  (Fetch / Gateway / NotSucceeded).
- app::update_proxy_prices -> gateway.execute_as(relay,
  proxyOracle.updatePrices). The gateway returns operation status rather
  than the per-price Accepted/Rejected map (same as the liquidator).
- App.gateway is now signing-capable: relay registered via the pooled
  multi-key `secret_keys`, UA registered too; App::new is async. The
  oracle handles get a relay-bound SigningClient.
- get_market_prices reads market.getConfiguration on demand (no MarketData
  lookup) so the read path has no relayer-side state to go stale.
- Tests: redstone integration test drives execute_update via a gateway
  signer; pyth_updates likewise; delete update_actions.rs (it tested the
  removed action-building layer; the fetch logic is now internal).

The update-target grouping still reads MarketData; that, MarketOracleKind,
and the rest of MarketData are removed in Phase 3 with the relay path.
Net: +228 / -382.

ENG-370

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): tx.relayDelegateAction meta-transaction write spec

Adds a gateway write method that submits a NEP-366 signed delegate action
as the signing account (the relayer), which pays its gas — the capability
the relayer's delegate-action relay path needs to move onto the gateway
(ENG-370 Phase 3).

- methods-spec: `tx::RelayDelegateAction { signed_delegate_action }`
  (borsh-encoded SignedDelegateAction as Base64Bytes); registered in the
  write-method list.
- methods-dispatch: PlanWrite decodes the delegate action and plans a
  single transaction to the delegate's sender carrying an
  `Action::Delegate`, signed by the requesting account.
- Regenerate gateway/METHODS.md.

ENG-370

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): expose tokens_burnt on tx.Get

The relayer's allowance accounting reconciles a locked allowance against the
actual NEAR cost of a relayed transaction (tokens burnt), not gas units. Add
`tokens_burnt` to tx.Get's result (summed across the transaction and all its
receipts — the signer's true cost) so the relayer can reconcile through the
gateway and retire its bespoke transaction-status fetch (ENG-370 Phase 3).

Regenerate gateway/METHODS.md.

ENG-370

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(relayer): back the gateway client with PostgresStore

Use the relayer's existing Postgres for the gateway's operation store so
idempotency and replay survive restarts. With the upcoming idempotency-keyed
relay accounting (ENG-370 Phase 3, option A), a relay retried after a crash
will dedupe through the store instead of re-submitting and double-paying gas;
`resume_incomplete_operations` recovers in-flight operations.

The store owns its own tables/migrations (run at startup alongside the
relayer's own migrations); it shares only the database URL.

ENG-370

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): add chain.getGasPrice read

The relayer needs a live gas price to convert gas units into a NEAR
allowance-lock estimate. `near_api` exposes no chain-level gas-price
builder, so model it as a gateway read backed by `near-jsonrpc-client`
against the configured endpoint, keeping consumers free of bespoke RPC.

Only the (stable) gas_price RPC is used; protocol config is deliberately
not exposed — UA reflexive-action gas is handled by attaching max gas in
the gateway's ua.execute and reconciling against actual tokens_burnt.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): add chain.getBlock read

Expose block height + timestamp (nanoseconds) as a typed gateway read,
backed by near-jsonrpc-client against the configured endpoint. The relayer
uses it for the universal-account creation freshness check (replacing its
bespoke block-timestamp RPC); like chain.getGasPrice it keeps consumers off
direct RPC.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(relayer): submit all writes through the gateway; delete bespoke RPC

The "big cut": replace the relayer's bespoke NEAR client (near.rs) and
ad-hoc cache (cache.rs) with the in-process gateway, so every chain
interaction — reads and writes — flows through typed gateway specs.

Submission + allowance accounting (option A):
- Re-key the pending-transaction allowance lock off a relayer-generated
  gateway idempotency key instead of the on-chain hash (DB migration):
  the gateway only surfaces the hash after it submits, too late to lock
  beforehand. The hash is recorded once known so the broom can still
  reconcile. New execute_and_account<S> helper: lock -> execute_request
  -> record hash -> read tx.Get for true tokens_burnt -> settle lock.
- Relay/UA-create/UA-relay/storage-deposit all route through it. Relay
  endpoints are now synchronous (the gateway's execute is atomic), and
  cost is charged from actual tokens_burnt (summed across receipts)
  rather than only the transaction-outcome burn.

Chain reads previously served by near.rs/cache.rs now use gateway specs:
gas price (chain.getGasPrice), block timestamp (chain.getBlock), storage
balance (storage.get*/deposit), account existence (account.get), UA key
(contract.viewFunction). Nonce management is the gateway signer's job.

Drops the protocol-config fetch: UA reflexive-action gas no longer needs
precomputing since the gateway attaches max gas to ua.execute and we
reconcile actual spend. Relay secret keys are now near_api::SecretKey;
the now-dead cache args are removed.

Net -495 LOC. Gateway operations persist in the relayer's Postgres via
PostgresStore for cross-restart idempotency.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(relayer): resolve market config on demand; drop cached MarketData

Remove the load-once MarketData/MarketOracleKind/AssetResolution "kitchen
sink": it cached each market's assets, oracle classification, and price
feeds at startup and never refreshed, so config changes went stale until a
restart.

AccountData now holds only the discovered market-id set plus the relay
allowlist (both still seeded once at startup — the allowlist is a fail-safe
security boundary). Everything else resolves per-request through the
gateway, which caches the underlying reads server-side:
- update_market_prices groups oracle work via market.getConfiguration +
  oracle.getPriceResolutionDependencies (proxy markets via OracleContractKind).
- actions_are_allowed validates a transfer's asset against the market's
  live borrow/collateral assets (now async).
- expand_market_related_contracts widens storage-deposit candidates via
  live config.

The pure request-grouping logic is factored into accumulate_oracle_requests
and unit-tested; the stale-cache grouping tests are removed.

Net -201 LOC.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(gateway-store): confine the operation store to its own schema

The store confines all of its objects — tables, types, and the
`_sqlx_migrations` bookkeeping — to a dedicated Postgres schema (default
`gateway`) via the connection search_path, creating the schema before
migrating. This lets it share a database with another sqlx-migrated
component (the relayer) without their migration tables colliding, keeping
full migration integrity on both sides.

`PostgresStore::new` defaults to the `gateway` schema — safe to make the
default since no gateway-store-backed service is deployed yet — so any
consumer is collision-safe without having to ask; `with_schema` overrides it
(e.g. `"public"` when the store owns the database). The gateway service
inherits the default transparently; the relayer just uses `new`.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(gateway-store): default operation updated_at (NOT NULL had no default)

gateway_operations / gateway_operation_steps declared `updated_at` NOT NULL
but, unlike `created_at`, gave it no default, and the store's inserts don't
set it — so the first live insert failed the not-null constraint. This was
latent until the relayer became the first runtime user of PostgresStore.

Additive migration giving `updated_at` the same `DEFAULT NOW()` as
`created_at`; the store re-inserts an operation on every save, so it tracks
last write.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(gateway): ua.execute forwards signed args verbatim

A universal-account `execute` payload is a user-signed meta-transaction
targeting a specific contract version, so the relay must forward the user's
exact bytes. Taking a typed `ExecuteArgs<Box<[Transaction]>>` re-serialized
the payload through the gateway's model, canonicalizing it — which broke
legacy front-end arg formats (the contract got a string where it expected a
struct) and risked invalidating signatures for any consumer, including the
service's RPC.

`ua.execute` (and the core `UaExecuteArgs`) now take the args as raw
`serde_json::Value`, forwarded verbatim. The relayer uses `ua::Execute`
with the user's original args; verification still runs on the parsed copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(relayer): broom recovers pending ops with no recorded hash

An interrupted relay (crash between locking the allowance and finalizing)
left a pending row whose on-chain hash was never recorded; the broom skipped
hashless rows, so the account's pending slot stayed locked forever, blocking
all future relays until manual DB repair.

Reconcile every pending row against the gateway operation store (keyed by the
relayer's idempotency key), the durable source of truth for an interrupted
operation: it records the signer, the on-chain hash, and survives restarts.
Each row resolves to one of Settle / Release / InFlight via a single cascade,
so there is one reconciliation path rather than a hash-present and a
hash-absent one — and the op record's signer removes the previous
try-each-signer fallback. A min-age floor skips rows still mid-submission.

`PendingTransaction` keeps `operation_key` as its sole identity (no nullable
hash leaking into consumers); `transaction_hash` stays on the row as audit,
recorded by `finalize` (which absorbs the old `attach_transaction_hash`).
Native<->gateway hash conversion is now a total 32-byte move, not a base58
round-trip. Adds `Client::operation_by_idempotency_key`.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): record per-step execution outcome on write results

The driver received the full execution result (return value, per-receipt
logs, gas/tokens burnt) from each write's submission and discarded all but
the hash, forcing callers to re-fetch it with a follow-up tx.get. Capture it
on the step instead: `StepStatus` gains `Succeeded { outcome }` and splits the
old `Failed` into `Reverted { outcome }` (executed on chain, final outcome a
failure — NEAR runs asynchronously, so it may have committed partial state)
and `Rejected` (failed before a recorded execution). The success/failure is
the variant itself, captured once from `near_api`'s `is_success()`; no derived
status field. `OperationRecord` exposes `tokens_burnt()` (summed over executed
steps) and `final_outcome()`.

Logs are grouped per receipt with the executing contract, not flattened, so a
consumer interpreting log content can attribute it safely and receipt
boundaries (including a receipt that emitted none) are preserved.

The outcome is persisted per step (new `outcome jsonb` column on
gateway_operation_steps; reverted vs rejected distinguished by its presence),
so a reloaded operation round-trips.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(relayer): use captured write outcome; reject blocked proxy prices (P2)

With each step's execution outcome on the write result, drop the follow-up
tx.get: `execute_and_account` charges allowance from `operation.tokens_burnt()`
(summed over executed steps, so a reverted tx's gas is still charged), and
`update_proxy_prices` inspects the captured return value — the proxy's
per-price status map — and rejects when any requested price came back
Blocked/ResolveFailed. The proxy transaction succeeds on chain even when a
price is circuit-breaker-blocked, so without this the relayer would relay the
user's action against stale/unavailable prices; this restores the pre-gateway
reject-before-relay behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(liquidator): fail liquidations whose receipts reverted

A gateway operation's status reflects only the transaction's final receipt.
`market::Liquidate` is an `ft_transfer_call`, so a liquidation the market
rejects makes the receiver callback panic while NEP-141's `ft_resolve_transfer`
refunds and the top-level transaction still reports `SuccessValue` — the
post-migration check looked only at `OperationStatus::Succeeded`, so a refunded
liquidation was wrongly reported as `Liquidated` (the pre-migration
`check_transaction_success` scanned receipts and caught this).

Restore receipt-level checking: `tx.get` now reports `failed_receipts` (the
accounts whose receipts failed, populated from the execution result's
`receipt_failures()`), and after a top-level success the liquidator fetches it
and treats any failed receipt as a failed liquidation — releasing the reserved
inventory instead of assuming the collateral was seized.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(liquidator,gateway): address CodeRabbit review findings

Gateway:
- tx.get exposes deduped `failed_receipts` (distinct contracts, via BTreeSet).
- OperationRecord::latest_tx_hash picks the highest-`index` step, not the last
  in vector order, so an out-of-order record still resolves correctly.
- collect_paginated takes `NonZeroU32` page size (a zero page would loop
  forever); callers' page-size consts are now NonZeroU32.

Liquidator — gateway write success must check status AND receipts:
- Ref swaps (ft_transfer_call) now reject when any receipt failed, like the
  liquidation executor (a refunded swap no longer reports success).
- execute_liquidation releases reserved inventory if receipt inspection errors.
- Ref storage registration no longer treats a non-success status as
  "already registered" (that genuinely fails); implicit-account creation and
  oneclick storage deposit warn loudly on non-success (refund detection
  backstops the deposit path).
- oneclick: a succeeded deposit must carry a tx hash (never send the operation
  id to the 1-Click API), refund detection now runs unconditionally, and it
  parses NEP-245 `mt_transfer` refunds in addition to NEP-141 `ft_transfer`.

Liquidator — oracle correctness:
- LST detection only treats method-not-found as "not LST"; transient errors
  propagate and aren't cached, so a blip can't permanently misroute an oracle.
- resolve_pyth_update_targets is fallible; transformer/proxy read failures
  propagate instead of silently refreshing the wrong/incomplete feeds.

Liquidator — security & robustness:
- Don't log the secret-bearing NEAR_RPC_URL; run scripts pass it via the
  environment (clap reads NEAR_RPC_URL) instead of argv.
- Floor concurrency at 1 so buffer_unordered can't hang.
- Sandbox test asserts each setup write succeeded.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(gateway): chain reads via near_api; drop near-jsonrpc-client

The chain client used a second, raw `near-jsonrpc-client` for gas price and
block reads. `near_api::Chain::block()` covers both: a block header carries
`gas_price`, `height`, `timestamp_nanosec`, and `hash`. So:

- `chain.getBlock` now returns `{ height, timestamp_ns, gas_price, hash }`
  (built from one `near_api` block read), and `chain.getGasPrice` is removed —
  `estimate_cost_of_gas` reads the latest block's `gas_price`.
- `near-jsonrpc-client` and `near-primitives` are dropped from gateway-core.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): per-receipt status; persist outcome relationally

`ReceiptOutcome` now carries a `status` (Succeeded/Failed), captured in the
driver from near_api's `receipt_failures()` — so a consumer can detect an inner
receipt failure (e.g. a refunded ft_transfer_call) directly from the write
result. (The failure *message* isn't captured; near_api keeps per-receipt
status private — tracked in ENG-407.)

The execution outcome is now persisted relationally instead of as a jsonb blob,
so the database enforces its shape and evolution is plain ALTER TABLE: scalar
fields (tokens_burnt, total_gas_burnt, return_value) as columns on
gateway_operation_steps, and one row per receipt in a new
gateway_operation_step_receipts child table (contract_id, status, logs text[]).
u128/u64 amounts are stored as lossless decimal text.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(gateway-store): validate schema identifier in with_schema

The schema name is interpolated into DDL (`search_path` and `CREATE SCHEMA`),
which can't be parameterized. Validate it up front as a plain unquoted Postgres
identifier (`[A-Za-z_][A-Za-z0-9_]*`, <= 63 bytes) and reject anything else with
a configuration error, so an empty/invalid/quote-bearing value fails fast at
construction instead of producing malformed or injectable DDL later.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(gateway,relayer): move UA key versioning into the ua.getKey op

`relay::load_ua_key` hand-rolled the contract `get_key` view call and the
`VersionedKeyParameters` (V0 `KeyParameters` / V1 `PayloadExecutionParameters`)
upgrade — duplicating logic that belongs behind the gateway op. Worse, the
gateway client's `get_key` only deserialized V1, so any consumer using the op
directly would have silently dropped legacy (V0) accounts.

Move the versioning into `UniversalAccountClient::get_key`: it deserializes the
versioned response and upgrades V0 to `PayloadExecutionParameters` (using the
account id as the verifying contract), so every gateway consumer gets faithful
parameters for legacy and new accounts. The relayer now calls `ua::GetKey` and
rebuilds the contract type from the wire view for signature verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(gateway,relayer): review-comment cleanups

Mechanical cleanups from the eng-370 self-review:
- operation.rs: split `SubmittedCurrentStep::fail(Option)` into `reverted(outcome)`
  and `rejected()` (callers always passed an inline Some/None).
- OperationPlan::execute(signer, receiver, actions) — defaults wait_until to
  ExecutedOptimistic, collapsing the repeated `single(PlannedTransaction { … })`
  boilerplate across the dispatch impls.
- client: `ClientBuilder::signer` -> `with_signer`.
- broom: `Resolution::InFlight` -> `Defer` (verb, like Settle/Release);
  `resolve`/`reconcile` -> `classify`/`reconcile_pending` (less confusable).
- app: name `group_price_updates`' return (`GroupedPriceUpdates`) instead of a
  same-type 3-tuple.
- AccountData: narrow `resolve_market_ids`/`derive_sda_interactions`/
  `actions_are_allowed` to `&HashSet<AccountId>` (they only use `market_ids`),
  and stop cloning the whole struct in the price-update route (scoped read).
- postgres: hoist the `CurrentStep` import to the top.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(gateway): rename tx.relayDelegateAction -> tx.relaySignedDelegateAction

Name the meta-transaction relay write after the NEAR type it actually carries,
`SignedDelegateAction`, while keeping the "relay" verb. Renames the spec struct
`RelayDelegateAction` -> `RelaySignedDelegateAction` and the RPC method
`tx.relayDelegateAction` -> `tx.relaySignedDelegateAction` (field
`signed_delegate_action` was already accurate). Regenerated METHODS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway): add account.getAccessKey read; route relayer tests through it

Adds a `account.getAccessKey` gateway read (mirrors NEAR's `view_access_key`):
returns the key's `nonce` and permission scope (`FullAccess` /
`FunctionCall { allowance, receiver_id, method_names }`) via `near_api`'s
`Account::access_key`. Wired through `ReadNear::view_access_key` and
`AccountClient::access_key`, registered in `for_each_read_method`, and documented
in METHODS.md.

This closes the last ENG-370 self-review item: the relayer integration test's
`view_access_key` helper hand-rolled a `near_jsonrpc_client` `ViewAccessKey`
query. It now uses the gateway (`account.getAccessKey` for the nonce +
`chain.getBlock` for the block reference), so the tests go through the same typed
specs as the relayer rather than a bespoke RPC client.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(gateway,relayer,liquidator): address clear review follow-ups

Quick/clear items from the latest review pass:
- gateway-core: rename `SubmittedCurrentStep::{succeed,reverted,rejected}` to
  `mark_succeeded`/`mark_reverted`/`mark_rejected` (consistent mood; signals the
  store mutation).
- gateway-types: make the execution-outcome conversion the canonical
  `impl From<ExecutionFinalResult> for ExecutionOutcome` (lives next to the type;
  near-api-types is already a dep), replacing the free fn in the driver.
- gateway-types: `OperationRecord::final_outcome` now selects by `step.index`,
  not vector order, matching `latest_tx_hash` (steps may arrive out of order).
- liquidator: a terminal operation with no transaction hash is now
  `MissingTransactionHash`, not the misleading `TransactionFailed`.
- relayer: propagate the DB-init error in `App::new` instead of `unwrap()`;
  drop a stale gas-price comment; use non-consuming map lookups in
  `get_market_prices` (borrow and collateral may share a price ID).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(gateway,relayer): status-aware UA create + validated delegate-action input

- UA create no longer returns Success for a reverted deploy: `execute_and_account`
  now returns `AccountedExecution { transaction_hash, succeeded }` (accounting is
  still always finalized), and the create route fails when the deploy reverted.
  (The relay routes keep returning the hash — same latent issue, left as-is.)
- `tx.relaySignedDelegateAction` takes a validated `SignedDelegateActionInput`
  instead of raw `Base64Bytes`: it borsh-decodes the NEP-366 payload at the spec
  boundary (custom Deserialize), so malformed input is rejected there and the
  dispatch uses the decoded value via `into_inner()` — no re-validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(relayer): account for non-call actions in reflexive UA gas estimate

Reflexive UA payloads can include non-function-call actions (transfer, add_key,
delete_key); these carry no explicit gas but still burn some, so they previously
contributed 0 to the allowance-lock estimate. Add a configurable flat per-action
overhead (`--ua-reflexive-action-overhead-tgas`, default 1 Tgas) for each
non-call action. The lock is reconciled against actual `tokens_burnt`, so this
only needs to be a safe over-estimate.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(relayer): remove dead oracle update_gas/update_deposit config

`PythConfig`/`RedStoneConfig`'s `update_gas`/`update_deposit` (CLI flags + env
vars) were never read anywhere — the oracle update gas/deposit are set in the
gateway plan (`oracle_write`), and the hardcoded values already equal these
defaults. Rather than wire operator knobs for what are really contract-determined
values (the Pyth deposit is the contract's update fee; gas is just the per-tx
max), drop the dead config. Dynamic Pyth fee (`get_update_fee`) is tracked
separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(gateway): unify chain.getBlock result with the client's BlockSummary

`GetBlockResult` (spec) and `BlockSummary` (core chain client) were identical
field-for-field. Define a single `BlockSummary` in gateway-types (the shared
base both crates depend on) and use it as the `chain.getBlock` output and the
client return, so they can't drift; the dispatch is now a passthrough. (A `From`
impl wasn't possible — both original types are foreign to the dispatch crate, so
the orphan rule forces relocation, not conversion.)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(relayer): close pending-slot leaks + safer relay migration

- set_pending_transaction now claims the account's pending slot *first*
  (`WITH claimed AS (UPDATE … WHERE pending_operation_key IS NULL)`) and inserts
  the transaction row only if the claim succeeded. The old order inserted
  unconditionally then tried to claim, so a contended account leaked an orphaned
  `pending` row that reserved its allowance forever. (.sqlx regenerated.)
- broom: an operation that reached terminal failure with no transaction hash
  (rejected before submission) is now Released, not Deferred — otherwise its slot
  stayed locked forever after a crash between rejection and cleanup.
- relay migration: add the FK as NOT VALID then VALIDATE separately (up + down)
  to avoid a full-table lock; the downgrade now fails (RAISE) if hashless
  transaction rows exist instead of silently deleting pending accounting state.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(gateway): strengthen access-key permission types + test the mapping

Follow-ups on the new account.getAccessKey:
- `AccessKeyPermission::FunctionCall` now uses `AccountId` for `receiver_id` and
  `Vec<ContractMethodName>` for `method_names` instead of raw strings, so the
  public spec doesn't let invalid account ids / method names be representable.
- Extract the near_api -> spec permission mapping into `permission_view` and add
  dispatch tests covering FullAccess, FunctionCall (all carried fields), and the
  invalid-receiver_id rejection.

Also moves the relayer `app` test module to end-of-file to satisfy clippy's
`items_after_test_module` (was failing `clippy -D warnings`).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor: tighten gateway-store queries + trim review-era comment fluff

- gateway-store: replace the manual `query!` + field-by-field `OperationRow`/
  `OperationStepRow` construction (4 sites) with `query_as!`, mapping rows
  directly; consolidate the per-field `#[allow(dead_code)]` audit-column markers
  into one struct-level allow.
- `SignedDelegateActionInput`: correct the doc — it guarantees well-formedness
  (borsh-decodable), not authenticity; signature/expiry are verified on-chain
  (addresses the CodeRabbit overclaim). Drop the redundant `# Errors` doc.
- relay: fix a stale `load_ua_key` doc (it uses `ua.getKey` now, not a generic
  view); join rejection reasons with `.join("\n")` instead of a manual `write!`
  loop; drop a redundant end-of-block comment and an over-explained `From` doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* refactor(relayer,gateway): settle allowances from the gateway record

Make the gateway the single source of truth for the transaction lifecycle
and shrink the relayer to allowances + settlement.

Gateway store:
- save_operation updates status + rewrites steps in place instead of
  re-inserting the operation row, so idempotency_key survives every step
  transition. It was silently nulled on the first save, which broke crash
  recovery and idempotent retries.

Relayer:
- Collapse the bespoke `transaction` table (a status/gas/hash shadow of the
  gateway's own records) into two columns on `account`: pending_operation_key
  and pending_inner_spend. There is only ever one in-flight charge per account.
- Replace set/remove/finalize with lock_pending + settle/release. Settlement
  reads the actual cost from the gateway's OperationRecord (gas always, the
  deposit only on success); idempotency comes from the pending-slot guard.
- release_pending no longer credits allowance (nothing is debited at lock
  time), fixing an allowance-inflation bug on aborted / never-landed charges.
- The broom reconciles terminal operations straight from the gateway record
  (no tx.get). Crash recovery (resume_incomplete_operations) runs once at
  startup, not per sweep, so it can't race the synchronous execute path.
- execute_and_account leaves the charge for the broom on a gateway error (the
  operation may have landed) instead of eagerly deleting it.

Also:
- storage_deposit_top_up errors on a reverted deposit instead of reporting
  success.
- create_account is ON CONFLICT DO NOTHING, so a retried UA create can't brick
  on an orphaned row.
- Rename monitor / UA env vars to match args; trim gateway method docs.

Tests: allowance lock/settle/release (incl. anti-inflation and idempotency),
and an idempotency-key-survives-save regression.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(relayer): harden settlement ordering, startup recovery, and UA gas gate

Follow-ups from the re-review of the gateway accounting refactor.

- execute_and_account: resolve the on-chain hash before settling, so the
  no-transaction error path leaves the charge for the broom to reconcile
  instead of charging and then returning an error. Restores the pre-refactor
  ordering (mutate last).

- Startup recovery (resume_incomplete_operations) is now fatal on failure: the
  container restarts and retries it rather than serving with charges that can
  never settle. Add IGNORE_STARTUP_RECOVERY_FAILURE as an escape hatch for a
  persistently wedged recovery.

- Lock the gas budget the gateway actually commits. The gateway attaches a flat
  300 TGas to the UA `execute` and registry `deploy` plans, but the relayer was
  locking a lower estimate (computed inner gas / 50 TGas), so an under-funded
  account could pass the affordability gate while the relayer paid the larger
  budget. Lock GATEWAY_UA_WRITE_GAS (300 TGas) on both paths and drop the now
  redundant per-action gas estimation and its UA_EXECUTE_TGAS /
  UA_REFLEXIVE_ACTION_OVERHEAD_TGAS config.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(gateway): surface account-not-found as a typed error; fail UA-create closed

The relayer's UA-create existence check treated ANY account-read error as "does
not exist", so a transient RPC failure on an account that actually exists would
let the route proceed to a deploy that reverts on chain and charges the user
gas.

Give callers a typed signal instead: account reads now return
GatewayError::AccountNotFound when (and only when) the node reports the account
does not exist. The node surfaces this inconsistently (typed UnknownAccount vs a
plain message — see near-api's own note), so detection matches the stable RPC
error name in the rendered error, with a regression test over both forms.

UA-create now matches on the typed variant: Ok -> already exists (reject),
AccountNotFound -> proceed, any other error -> fail closed (don't deploy onto a
possibly-existing account).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* ci: provision postgres for the new sqlx::test database tests

The relayer / gateway-store `#[sqlx::test]` tests (first DB tests in the
workspace) provision a database per test and read DATABASE_URL at runtime, so
SQLX_OFFLINE alone isn't enough — they panic with "DATABASE_URL must be set" in
the no-database CI jobs.

- fast-tests: add a postgres service (health-gated) and DATABASE_URL so the
  always-on `--workspace --lib` job can run them.
- script/test.sh: wait for postgres to be healthy (--wait) and export
  DATABASE_URL (respecting an existing value) so the integration job and local
  runs work too.
- compose.dev.yaml: add a postgres healthcheck so `--wait` is meaningful.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* ci: provision postgres for the coverage job's sqlx::test tests too

The coverage job also runs `--workspace --lib`, so it needs the same postgres
service + DATABASE_URL as fast-tests, or the relayer / gateway-store
`#[sqlx::test]` tests fail there when coverage runs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(relayer,gateway): tighten proxy-price validation and recovery of submitted steps

- update_proxy_prices: require EVERY requested price to come back Accepted
  (a missing return value, or a missing / non-accepted entry, is unavailable).
  Iterating only the returned map let an omitted price_id slip through and the
  relayer relay a user action priced against stale data.

- reconcile_submitted_step: on a transient query failure during recovery, leave
  the step Submitted (retry on a later recovery) instead of marking it Rejected.
  Rejecting is terminal — it drops the step from recovery and lets the relayer
  broom release the charge for a transaction whose hash we hold and which may
  have executed on chain.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(gateway): reserve operations before planning to close a charge-release race

A write previously reached the store only after planning (a network
round-trip). During that window the durable idempotency->operation link
did not exist, so the relayer broom could see an in-flight charge with no
matching gateway operation and release it while the request was still
running.

Reserve the operation (idempotency key + identity, no steps) before
planning, so recovery and the broom always observe it and defer. A
reservation is recognized purely by having zero steps -- no extra flag:

- status(): a step-less operation is Pending (reserved), not vacuously
  Succeeded; Succeeded now requires at least one succeeded step.
- OperationStore gains delete_operation; reserving is create_or_get with
  an empty plan, so its signature is unchanged.
- plan_and_complete reserves, plans (dropping the reservation on a
  planning error -- planning is read-only, nothing was submitted), then
  finish_reserved attaches the plan and executes. An empty plan is a
  no-op: the reservation is deleted and no record is left behind.
- Recovery drops a bare reservation (a crash during planning): nothing
  was submitted, so the broom releases the charge.

Removes the now-unused complete_write.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(gateway): single-source submitted-step reconciliation + reservation cleanup

The "did this transaction land?" decision was duplicated across three sites
with divergent rules, which is what drove the repeated, contradictory review
findings. Consolidate it so only reconciliation (querying the chain by hash)
adjudicates an unresolved submitted step:

- Live submit error no longer eager-rejects: the transaction may have
  broadcast before the error (e.g. an RPC timeout), so leave the step
  Submitted and let reconciliation settle it. Rejecting here could release a
  charge for a tx that landed.
- A step left Submitted with no captured outcome (optimistic wait) is in
  flight; execute_remaining_steps now stops on it instead of falling into a
  reject arm. The now-unused mark_rejected is removed.
- query_transaction classifies "unknown to the chain" at the boundary into a
  typed GatewayError::TransactionNotFound (mirrors AccountNotFound).
  Reconciliation marks such a step Rejected (it never landed, releasing the
  charge); any transient/transport error keeps it Submitted to retry. This
  gives never-broadcast submissions a bounded path to terminal.

Also:
- finish_reserved deletes the bare reservation when execution fails before
  persisting any step, so the charge releases immediately instead of waiting
  for startup recovery (factored StoredOperation::is_reservation, reused by
  recovery).
- /relay and /universal_account/relay honor execution.succeeded and report a
  reverted on-chain transaction as a failure, matching UA create.
- README: drop the removed wait_until relay field.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(relayer): harden slim-accounting cutover migration + dev tooling

Address review feedback on the slim-accounting migration and dev setup:
- guard the legacy "transaction" table drop behind a check for un-drained
  pending charges, with the symmetric guard in the down migration
- add a CHECK constraint enforcing the pending-charge completeness invariant
- fix the README post-migration check to test table existence via to_regclass
  rather than counting a table that may already be dropped
- make the relayer wait for a healthy Postgres (compose depends_on condition)
- only start the compose Postgres when DATABASE_URL is unset in test.sh

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* refactor(gateway): persist a planned marker to distinguish reservations from no-ops

Reserve-before-plan writes an operation before planning, so "empty plan" was
overloaded: it meant both a reservation whose planning has not finished and a
genuine no-op whose planning finished with zero steps. Those must be
distinguishable for idempotency — a no-op has to keep resolving to the same
result on retry, not re-plan a possibly-changed world.

Materialize the distinction as a `planned` marker (a nullable planned_at
column on the operation row):
- status(): !planned => Pending; planned + zero steps => terminal Succeeded
- is_reservation() = !planned; delete_reservation only reaps unplanned rows
- finish_reserved persists no-ops instead of deleting them

Rename the store's delete_operation to delete_reservation to match its guarded
semantics, and regenerate the sqlx offline cache.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(gateway): reconcile in-flight operations off startup; classify unknown-account on key reads

Nothing drove a submitted operation to terminal outside startup recovery, so a
live submit timeout left the account's charge locked until the next restart;
and startup recovery could reject a transaction that had merely not yet
propagated, releasing a charge for a tx that may still land.

- add reconcile_operation(key, reject_if_unknown): the relayer's broom now
  drives each in-flight operation toward terminal on every sweep, instead of
  only deferring it. It acts only on charges past MIN_PENDING_AGE, so this
  cannot race the live execute path and an unknown transaction has aged out
  (reject_if_unknown = true).
- startup resume passes reject_if_unknown = false: it may run moments after a
  crash, so it never rejects a still-unknown transaction, leaving it for the
  aged-out broom to resolve.
- view_access_key now maps UnknownAccount to AccountNotFound like view_account,
  so account.getAccessKey distinguishes a missing account from a transient RPC
  failure.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(gateway): drive multi-step ops through reconciliation; reap reservations only at startup

Confine near-api result types to the executor: submit/query now return a
gateway-owned StepOutcome, so the driver is testable with fakes and the two
call sites shrink.

- Item 2: add advance_operation (reconcile -> drive remaining -> save), shared
  by resume_incomplete_operations and reconcile_operation. Both previously
  reconciled a Submitted step then stopped, stranding the rest of a multi-step
  plan. This is the gap our all-single-step relayer traffic never exercised.
- Item 1: reservations are reaped only by startup recovery, where every one is
  provably dead. The broom no longer reaps them (returns them as-is, deferred).
  GatewayService::spawn is now async so recovery runs before serving traffic.
- Item 4: backfill planned_at = created_at for rows predating the column, which
  would otherwise read back as bare reservations.
- Item 5: /relay and the universal-account /relay report success on an on-chain
  revert. Success means the caller's signed transaction landed with execution
  attempted; a revert is the caller's transaction to fix, not a relay failure.

Tests: gateway/store/tests/driver_recovery.rs drives OperationDriver against a
real MemoryStore with fake signing/execution, covering every reconciliation
branch and the reconcile-then-continue regression. Lives in gateway-store to
avoid a gateway-core dev-dependency cycle.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* refactor(gateway): settle writes on Final; age out stuck submitted steps

Address two review follow-ups by removing machinery rather than adding it.

- Finding 3: gateway writes settled on optimistic execution, so relayer
  accounting could charge and report success before finality. Every write now
  waits for Final, and since nothing varied it, wait_until is deleted outright —
  from ContractWriteOptions, PlannedTransaction, the client bridges, the executor
  submit signature, and the step row. This also removes the intermediate-Final /
  last-Optimistic special-casing in step preparation.
- P2: a submitted step whose transaction the chain never records could stay
  in-flight forever for a consumer without the relayer broom (startup recovery
  passed reject_if_unknown = false and never retried). Replace that per-caller
  flag with an age check: reconcile_submitted_step rejects an unknown transaction
  only once it has aged past SUBMITTED_STEP_MAX_AGE. Reconciliation is now
  self-sufficient for every caller, so the reject_if_unknown parameter is gone
  from the driver, client, and broom.

Fold the four unreleased gateway-store migrations this branch introduced into one
(20260629120000_gateway_operation_lifecycle); the dev-merged base is left as-is.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(relayer): settle charges only from a terminal operation

execute_and_account settled unconditionally, assuming execute_request always
drives to a terminal outcome. When the driver leaves a step Submitted (an
in-flight submit), the operation is InProgress with tokens_burnt() == 0, so this
path settled zero and cleared the pending slot — leaving the broom nothing to
reconcile and undercharging if the transaction later finalized.

Rather than special-case that state, make settling a non-terminal operation
impossible: route all settlement through a single Database::resolve_charge, which
settles a terminal operation, releases one that never landed, and leaves a
non-terminal one locked for the broom. The low-level settle is now private, so
resolve_charge is the only caller; the broom's duplicate classifier is removed.

AccountedExecution::succeeded (which conflated reverted with still-pending)
becomes a three-state AccountedStatus, fixing two callers that mishandled a
pending operation: storage-deposit treated it as reverted, and UA create reported
it as a failed deploy.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(relayer): restore known-market allowlist check on /market_prices

/market_prices dropped the accounts.market_ids allowlist check during the
MarketData removal refactor, while /update_prices still enforces it. Restore
parity: reject price queries for markets the relayer has not discovered before
hitting the gateway. Add a market_prices_rejects_unknown_market test mirroring
the update_prices rejection test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* refactor(relayer): unify charge resolution across endpoint and broom

The dispatching endpoint and the broom ran divergent charge-resolution
logic, which is the class of bug that kept recurring. Make
Database::resolve_charge the single rule, taking Option<&OperationRecord>:
a missing operation (planning/presign failure, or a reaped reservation)
releases the slot, a terminal one settles, and a non-terminal one defers.

Both endpoint paths (happy path and the gateway-error path) and the broom
now call this one function identically; the broom no longer carries its own
release_pending branch. This also fixes a real availability bug: a gateway
planning failure deletes the reservation before anything is signed, but the
old error path left the account's single in-flight slot locked until the
broom's delayed (>=60s) sweep, needlessly blocking further requests.

Tests: resolve_charge_releases_missing_operation (None => release) and
planning_failure_releases_the_accounts_slot (EnsureDeposit against a
nonexistent contract fails planning => slot freed immediately).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* test(relayer): pin the lock refreshes pending-charge age invariant

The broom gates on account.updated_at (get_pending_charges filters
updated_at < NOW() - min_age), and the account table's updated_at_trigger
stamps updated_at = NOW() on every update, so lock_pending already refreshes
the lock timestamp. Add a regression test that backdates a stale account,
locks a charge, and asserts the fresh slot is not yet broom-eligible — so the
invariant survives if the trigger is ever removed (the broom must not reclaim
a slot before the gateway persists its operation).

Also document that delete_reservation's reservation-only precondition is
enforced by every store impl (postgres WHERE planned_at IS NULL, memory
is_reservation guard), not merely assumed of the caller.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(gateway,relayer): address CodeRabbit follow-ups

- resolve_charge bills the locked inner_spend only when the operation actually
  executed (final_outcome present), so a planned no-op success clears the slot
  without charging a deposit it never attached. Guarantees no overcharge at the
  settlement point rather than relying on call-site discipline. Adds a no-op
  success test; the success test now uses an outcome-bearing operation.
- Correct the delete_reservation doc: a planned no-op is a terminal success
  retained for idempotency, not a deletable reservation; note the reservation
  precondition is enforced by every impl.
- Make the no-op operation save best-effort, matching the sibling terminal save
  (a completed no-op must not error the caller on a transient store hiccup).
- Log a warn when post-execution reservation cleanup fails, like the
  planning-error path, so a leaked reservation is observable.
- Collapse the aged/fresh submitted-step test pairs into #[rstest] cases.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(gateway,relayer): upgrade-safe migration, fail-fast recovery, RPC key extraction

- Relayer builds its NetworkConfig via NetworkConfigBuilder so a FastNear-style
  ?apiKey= embedded in RPC_URL is routed to the auth header instead of being
  left in the query string (which produced malformed/401 RPC requests). The
  gateway service already used the builder; the relayer had been missed.
- resume_incomplete_operations now returns a summary error when any operation
  fails to advance (or a reservation fails to reap), after attempting them all,
  so App::new's fatal startup-recovery gate and IGNORE_STARTUP_RECOVERY_FAILURE
  actually see per-operation failures instead of the pass reporting success.
  Adds GatewayError::IncompleteRecovery.
- The lifecycle migration refuses a non-empty gateway_operations table (legacy
  rows predate planned_at/submitted_at/outcome and can't be reconstructed),
  turning silent post-upgrade corruption into a loud, safe failure. README
  updated to match (the operation store must start empty; no released consumer).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* refactor(gateway-store): simplify operation store helpers

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* refactor(gateway-store): make operations identity-only

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* refactor(gateway-store): add structural lifecycle schema

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* refactor(gateway-store): persist structural operation state

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* chore(gateway-store): drop stale operation sqlx metadata

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* chore(gateway-store): drop stale step sqlx metadata

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* docs(relayer): update gateway store upgrade notes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(gateway): keep ambiguous submitted operations recoverable

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(gateway-store): track completed operations structurally

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* refactor(relayer): persist pending charges in a ledger

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(relayer): reconcile ledger charges independently

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* docs(relayer): describe gateway charge ledger

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(gateway-store): reject ambiguous current lifecycle rows

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* refactor(relayer): compile pending charge queries with sqlx

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(relayer): fail known-market configuration read errors

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* fix(relayer): remove unused RPC timeout flag

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants