feat(pyth): support Pyth-priced iAssets in transaction builders - #47
Conversation
Every iAsset on mainnet is now priced via Pyth (DeferredValidation), so
every price-dependent write tool refused to build a transaction: open_cdp,
withdraw_cdp, mint_cdp, redeem_cdp, freeze_cdp, leverage_cdp and redeem_rob
all threw "Pyth-priced operations are not yet supported by this server".
The SDK builders already accept a signed Pyth message and the Pyth state
OutRef as trailing arguments; both are served by the analytics API:
GET /v3/assets/{iasset}/{collateral}/price -> pythPayload (signed message)
GET /v3/pyth-state/utxo -> Pyth state OutRef
resolvePriceSource() resolves whichever oracle an asset uses — an oracle-NFT
OutRef, or a freshly fetched Pyth message plus state OutRef — so the tools
handle both without branching. A price update older than the on-chain 280s
delay window is rejected up front, since a transaction built from it could
never be submitted, and the price plus its submission deadline are reported
back in the transaction summary for hardware-wallet signing flows.
get_pyth_price and get_oracle_price now return the live price alongside the
on-chain feed configuration instead of only pointing at the Pyth Lazer API.
Refs INDY-131
Reported by an integrator running the HTTP transport in production: - The server held a single transport for the whole process, so a client that restarted could never rejoin — initialize returned "Server already initialized" and every other call demanded the lost Mcp-Session-Id, with a process restart the only way out. Each client now gets its own transport and MCP server, keyed by session id and dropped on close; an initialize always starts a fresh session, even when the client replays a session id this process no longer knows. - HOST was ignored and the listener always bound 0.0.0.0, which is a poor default for a process holding a Blockfrost key. HOST now selects the bind address, and MCP_PORT is accepted as an alias for PORT. - engines said Node >=18, but undici needs the File global from Node 20 and the server crashes at startup on 18 with "File is not defined". Refs INDY-131
The indigo-sdk acceptance tests resolve the Pyth state UTxO straight from the chain — it is whichever UTxO holds the pythStateAssetClass NFT named in system params. resolvePythStateOref() now uses that as a fallback when the analytics endpoint is unavailable, so a momentary outage there does not stop transactions being built. New wiring tests assert that each of open_cdp, withdraw_cdp, mint_cdp, redeem_cdp, freeze_cdp, leverage_cdp and redeem_rob hands the SDK builder the Pyth message and state OutRef in the trailing positions, passes undefined for the price oracle alongside them, and reports the price and submission deadline in the transaction summary — with deposit_cdp checked to confirm price-free builders are untouched. Refs INDY-131
Building a Pyth-priced CDP transaction against mainnet fails on the first completion attempt: RedeemerBuilder: Coin selection had to be updated after building redeemers, possibly leading to incorrect indices. Try setting a minimum fee of 1115401 lovelaces. Script-heavy transactions shift coin selection once execution units are known, which invalidates the redeemer indices. Lucid reports the fee that makes selection stable, so buildUnsignedTx now retries once with it. The retry assembles a fresh TxBuilder rather than completing the failed one again — a builder that failed to complete still holds its mints and redeemers, and reusing it fails with "Duplicate Mint Asset" instead. Verified end to end on mainnet: open_cdp for iUSD now returns unsigned CBOR with both Pyth withdrawals (the state withdraw script and the iUSD feed validator), the Pyth state and feed script-ref reference inputs, a 280s validity window and evaluated execution units for all five redeemers. Refs INDY-131
iADA is a live iAsset — the indexer lists it, it has a Pyth feed in system params (iADA/.), its price endpoint serves signed payloads, and four iADA CDPs exist on mainnet — but AssetParam omitted it, so every tool taking an asset refused it. Verified against mainnet after adding it: get_pyth_price and get_oracle_price both return 1.000000000000 for iADA/ADA, the latter reading the on-chain collateral-asset datum. The server also advertised version 0.2.0 in `initialize` and on /health while package.json said 0.3.0, which misleads anyone diagnosing a version problem — exactly the situation this branch came out of. README now documents the 280s Pyth submission deadline and the summary.pyth block alongside the write tools, since a client that signs slowly needs to rebuild rather than submit. Refs INDY-131
|
Pushed a fifth commit ( iADA was unreachable. The server misreported its version. README. Documents the 280s Pyth submission deadline and the Suite is 137 passing; typecheck, lint, format and build clean. Separately: |
Hardcoding it is what let `initialize` and /health drift to 0.2.0 against a 0.3.0 package; esbuild inlines the value at build time, so a release bump is now a single-file change.
Closes INDY-131.
Reported by an integrator (Discord ticket-2633) building a Python MCP client against
@indigoprotocol/indigo-mcp@0.3.0:open_cdpfailed for every iAsset tested. Since effectively every iAsset on mainnet is now Pyth-priced, no CDP could be opened through the MCP at all.Pyth pricing
@indigo-labs/indigo-sdkalready accepts a signed Pyth message and the Pyth state OutRef as trailing arguments on the affected builders — the server just never supplied them. Both come from the analytics API:GET /v3/assets/{iasset}/{collateral}/price→pythPayload, the signed Solana-format messageGET /v3/pyth-state/utxo→ the Pyth state OutRefNew
src/utils/pyth.tswraps both.resolvePriceSource()returns whichever oracle an asset uses — an oracle-NFT OutRef, or a freshly fetched Pyth message plus state OutRef — soopen_cdp,withdraw_cdp,mint_cdp,redeem_cdp,freeze_cdp,leverage_cdpandredeem_robno longer branch on it and thePYTH_UNSUPPORTEDerror is gone.Two details worth review:
submitBeforedeadline are returned in the transaction summary — the reporter signs on a hardware wallet, so knowing the deadline matters.pythStateAssetClassNFT). That is used as a fallback when the analytics endpoint is unavailable.get_pyth_priceandget_oracle_pricenow return the live price alongside the on-chain feed configuration.Minimum-fee retry
Building a Pyth-priced CDP transaction against mainnet failed on the first completion attempt:
Script-heavy transactions shift coin selection once execution units are known, which invalidates redeemer indices.
buildUnsignedTxretries once at the fee Lucid suggests, assembling a freshTxBuilder— completing the failed one again fails withDuplicate Mint Asset, since it still holds its mints and redeemers. Without this, Pyth support alone would still not produce a usable transaction.Transport and config fixes from the same report
initializereturned "Server already initialized" and every other call demanded the lostMcp-Session-Id. Each client now gets its own transport and MCP server, keyed by session id and dropped on close; aninitializealways starts a fresh session, even when the client replays a session id this process no longer knows.HOSTandMCP_PORT. The listener always bound0.0.0.0regardless ofHOST— a poor default for a process holding a Blockfrost key.HOSTnow selects the bind address andMCP_PORTis accepted as an alias forPORT.engines.node→>=20.undicineeds theFileglobal from Node 20; on Node 18 the server crashed at startup withReferenceError: File is not definedinstead of a clear version error.Verification
Mainnet, through the HTTP transport:
Decoding the body: two withdrawals (the Pyth state
withdraw_scriptand the iUSDpythFeedValHash), the Pyth state and feed script-ref reference inputs, a 280-slot validity window, theCDP+iUSDmint, and evaluated execution units on all five redeemers — meaning the Plutus scripts, both Pyth withdrawal scripts included, ran and passed.Also checked without a wallet: for all six iAssets the payload decodes with the SDK's own decoder and the price derived from it matches the analytics
priceto within 1e-11; the state UTxO and all six feed validator script refs are unspent on-chain with hashes matching system params.Session behaviour verified live: concurrent sessions, a restarted client re-initializing while replaying a stale session id, 404 for unknown sessions and 400 for missing ones.
25 new tests (Pyth helpers, builder-argument wiring for all seven tools, the min-fee retry). Suite 136 passing; typecheck, lint, format and build clean.