Repository navigation
Order reads by tick deadline behind a load balancer; retry sends without a nonce - #320
Merged
Merged
Conversation
majesticwizardcat
marked this pull request as draft
October 19, 2026 14:35
majesticwizardcat
force-pushed
the
feat/lb-stale-reads
branch
2 times, most recently
from
October 23, 2026 10:42
3434384 to
6313994
Compare
…out a nonce
TypeScript SDK: keep the highest `timestamp`/`solution_now` seen per entity and
drop older pages and pushes (positions, balances, triggers, markets); build
balances from the positions push; re-seed of orders keeps rows the stream
already advanced; `since: 0` subscriptions resubscribe with backoff; candle
replays are deduped; `waitForReceipt` treats non-JSON/non-2xx as no receipt;
withdrawals decode `{withdrawals, timestamp}` (and the old array) and subscribe
with `since` from the backfill; the delegated `submit` retries 250ms apart for
10s, re-signs only when no attempt could have been delivered, and otherwise
waits for the receipt and ends with delivery-unknown.
Rust SDK: simple nonce manager (fetch `pending` per send); `send_transaction`
retries by the same rule with `PodSendError::DeliveryUnknown`; re-sign only
when the caller did not fix the nonce; `wait_past_perfect_time` returns errors
instead of swallowing them and decodes the node's null result.
Docs: openapi, guides, design doc and README for the new `timestamp` field,
the withdrawals body and the send retry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y code Both SDKs now wait 250ms, 500ms, then 1s between send attempts (10s budget unchanged), cut each send off after 5s so a hung node cannot consume the budget before the receipt poll, and retry a rejection only when the node returns error code 996 (NonceRejected) instead of matching the reason text. TypeScript: `mint` encodes the faucet's current `mint(to, tokens, uint128 deadline)` ABI (the old bool argument no longer parses on the node; the `depositToClob` option is gone since the faucet always credits the CLOB balance), `PodTxRevertError` carries the error code, and a live test (`npm run test:live`, gated by POD_LIVE_RPC_A/B) drives a round-robin fetch across two fullnodes with one made deaf and asserts no store ever moves backwards and the delegated send still succeeds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The delegated wallet's delivery-unknown outcome carries the tx hash as a field so the app can poll the receipt instead of matching the message. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every REST read sends the highest watermark the entity holds as the `timestamp` query parameter; the node waits for its state to reach it. A 503 with `Retry-After` is retried once (PodBehindError), then the read falls back to today's path. The drop-older check stays for nodes that predate the parameter. The live test now expects zero dropped responses. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The node keeps /v1/bridge/withdrawals as the bare array and serves the
{withdrawals, timestamp} body and the ?timestamp= minimum at /v2.
rest.bridgeWithdrawals reads /v2 and, on a 404, reads the v1 array with
timestampUs 0, so the SDK still works against a node without v2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ails instead of reading without a minimum pod_positions, pod_triggers and pod_markets (re)subscribe with since set to the highest timestamp the source holds, read when each frame is sent, so a reconnect to a lagging node waits for a snapshot at or past what the client holds. A 995 rejection is retried every second without spending the retry budget. readAtLeast retries once after Retry-After and then throws PodBehindError to the resource's own retry path. The keep-highest check stays: concurrent answers for one entity land in either order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nodes deploy before the SDK, so the route exists by the time this SDK ships. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s the minimum block Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
majesticwizardcat
force-pushed
the
feat/lb-stale-reads
branch
from
October 23, 2026 14:36
931153a to
370335a
Compare
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
majesticwizardcat
marked this pull request as ready for review
October 27, 2026 12:11
…ps the hash when it runs out Two hung sends used to end as the transport error, losing the hash of a transaction that may have executed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eipt poll Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A restarted subscription sent since: 0 and accepted older state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
0zzy-o
reviewed
Oct 27, 2026
0zzy-o
left a comment
Contributor
There was a problem hiding this comment.
Looks good, just a comment picked up from the bot probably worth looking at.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wire type declared a token field the node never sends and no symbols, so the public SpotPosition.token was always undefined and the symbols on the wire were dropped; balancesSource had to cast the push entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
0zzy-o
approved these changes
Oct 28, 2026
Merged
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.
Why
Clients read pod through a load balancer across eventually-consistent fullnodes, so a lagging node can answer after a leading one and roll client state backwards. Ticks execute in lockstep on every node, so a tick deadline is a watermark comparable across nodes. The node now stamps every account read and push with
timestamp(deadline of the last executed tick, micros), serves{withdrawals, timestamp}on new/v2/bridge/withdrawalsroutes (v1 unchanged), and returns error code 996 for retryable nonce rejections (node PR pod/pod#191 on Forgejo, ADR 0062). This PR is the SDK side.TypeScript SDK
pod_positionspush (it carries every balances field); the REST re-fetch is gone.since: 0subscriptions resubscribe with bounded backoff on rejection/close; one backoff formula shared by the sync modules.waitForReceipttreats a non-JSON or non-2xx response as "no receipt yet"./v2/bridge/withdrawals[/{account}]({withdrawals, timestamp}), subscribe withsincefrom the page that ended the backfill, and keep that resume point across rejections.submit: no nonce ownership. Attempts wait 250ms, 500ms, then 1s apart, each send is cut off after 5s, the whole submit gives up after 10s. Retried: transport errors, cut-off sends, non-JSON/non-2xx, and node error code 996 (past/future nonce, account pending). Terminal errors are thrown at once. Re-signs with a fresh nonce only when no attempt could have been delivered; otherwise waits for the receipt and ends with a delivery-unknown error.timestampquery parameter (minTimestampUson thePodRestClientmethods); the node waits for its state to reach it. A 503 withRetry-AfterthrowsPodBehindError, retried once, then thrown to the resource's own retry path; a read never falls back to no minimum.pod_positions,pod_triggersandpod_markets(re)subscribe withsinceset to the held watermark, read when each frame is sent (ws.subscribeaccepts a params function). The node waits for it before accepting and rejects with code 995 otherwise; a 995 is resubscribed after 1s without spending the retry budget.mintencodes the faucet's currentmint(to, tokens, uint128 deadline)ABI; the old bool form no longer parses on the node.npm run test:live(gated byPOD_LIVE_RPC_A/POD_LIVE_RPC_B): round-robin fetch across two fullnodes with one made deaf; asserts no store ever moves backwards and the delegated send succeeds. Run byscripts/e2e-node-switch-test.shin the node repo.Rust SDK
pending.send_transactionretries by the same rule (backoff, 5s per-attempt cut-off, 10s budget, code 996); newPodSendError::DeliveryUnknown { tx_hash }(survivesFrom<RpcError>); re-sign only when the caller did not fix the nonce.wait_past_perfect_timereturns errors instead of swallowing them, and decodes the node'snullresult.Public API changes
minTimestampUson thePodRestClientread methods; newPodBehindError;submitthrowsPodDeliveryUnknownErrorwithtxHash.rest.bridgeWithdrawalsreturnsWithdrawalsPage { withdrawals, timestampUs }(was an array); optionaltimestampUsonPositionsSnapshot/Balances;sendRawTransactionthrows on non-2xx / missing result, accepts asignal, andPodTxRevertErrorcarriescode;MintParams/client.mintlosedepositToClob(the faucet always credits the CLOB balance).RecommendedFillersnamesNonceFiller<SimpleNonceManager>;on_urlbounds changed;PodSendErrorgainsDeliveryUnknown(non-exhaustive matches need an arm; the bridge relayer gets one with the rev bump).Tests
vitest 173 tests (+1 live, skipped without nodes),
cargo test -p pod-sdk18 + 3 doctests, fmt and clippy clean. Live test passed 3/3 against a two-fullnode devnet.🤖 Generated with Claude Code