Dev/cleanup from gratis#1
Closed
royalf00l wants to merge 3 commits into
Closed
Conversation
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.