@getomnichain/omnichain 0.3.6 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +80 -1
  2. package/dist/cjs/index.js +1 -0
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/rango_chain_names.js +209 -0
  5. package/dist/cjs/rango_chain_names.js.map +1 -0
  6. package/dist/cjs/solana/solana_chain.js +89 -25
  7. package/dist/cjs/solana/solana_chain.js.map +1 -1
  8. package/dist/cjs/solana/solana_transaction_status.js +39 -3
  9. package/dist/cjs/solana/solana_transaction_status.js.map +1 -1
  10. package/dist/cjs/utxo/btc/tools/bitcoin_core.tool.js +129 -0
  11. package/dist/cjs/utxo/btc/tools/bitcoin_core.tool.js.map +1 -1
  12. package/dist/cjs/utxo/btc/tools/esplora.tool.js +84 -0
  13. package/dist/cjs/utxo/btc/tools/esplora.tool.js.map +1 -1
  14. package/dist/cjs/utxo/utxo_chain.js +18 -49
  15. package/dist/cjs/utxo/utxo_chain.js.map +1 -1
  16. package/dist/cjs/utxo/utxo_transaction_status.js +20 -16
  17. package/dist/cjs/utxo/utxo_transaction_status.js.map +1 -1
  18. package/dist/esm/index.d.ts +1 -0
  19. package/dist/esm/index.d.ts.map +1 -1
  20. package/dist/esm/index.js +1 -0
  21. package/dist/esm/index.js.map +1 -1
  22. package/dist/esm/rango_chain_names.d.ts +76 -0
  23. package/dist/esm/rango_chain_names.d.ts.map +1 -0
  24. package/dist/esm/rango_chain_names.js +171 -0
  25. package/dist/esm/rango_chain_names.js.map +1 -0
  26. package/dist/esm/solana/solana_chain.d.ts.map +1 -1
  27. package/dist/esm/solana/solana_chain.js +90 -26
  28. package/dist/esm/solana/solana_chain.js.map +1 -1
  29. package/dist/esm/solana/solana_transaction_status.d.ts +40 -2
  30. package/dist/esm/solana/solana_transaction_status.d.ts.map +1 -1
  31. package/dist/esm/solana/solana_transaction_status.js +38 -2
  32. package/dist/esm/solana/solana_transaction_status.js.map +1 -1
  33. package/dist/esm/utxo/btc/tools/bitcoin_core.tool.d.ts +4 -1
  34. package/dist/esm/utxo/btc/tools/bitcoin_core.tool.d.ts.map +1 -1
  35. package/dist/esm/utxo/btc/tools/bitcoin_core.tool.js +129 -0
  36. package/dist/esm/utxo/btc/tools/bitcoin_core.tool.js.map +1 -1
  37. package/dist/esm/utxo/btc/tools/esplora.tool.d.ts +2 -1
  38. package/dist/esm/utxo/btc/tools/esplora.tool.d.ts.map +1 -1
  39. package/dist/esm/utxo/btc/tools/esplora.tool.js +84 -0
  40. package/dist/esm/utxo/btc/tools/esplora.tool.js.map +1 -1
  41. package/dist/esm/utxo/tools/raw_transaction_provider.d.ts +2 -1
  42. package/dist/esm/utxo/tools/raw_transaction_provider.d.ts.map +1 -1
  43. package/dist/esm/utxo/utxo.d.ts +27 -0
  44. package/dist/esm/utxo/utxo.d.ts.map +1 -1
  45. package/dist/esm/utxo/utxo_chain.d.ts.map +1 -1
  46. package/dist/esm/utxo/utxo_chain.js +18 -49
  47. package/dist/esm/utxo/utxo_chain.js.map +1 -1
  48. package/dist/esm/utxo/utxo_transaction_status.d.ts +21 -16
  49. package/dist/esm/utxo/utxo_transaction_status.d.ts.map +1 -1
  50. package/dist/esm/utxo/utxo_transaction_status.js +20 -16
  51. package/dist/esm/utxo/utxo_transaction_status.js.map +1 -1
  52. package/docs/README.md +1 -0
  53. package/docs/solana.md +23 -0
  54. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -8,7 +8,77 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
- _Nothing yet. Add entries here as PRs merge; on release, rename this section to `[X.Y.Z] — YYYY-MM-DD` and open a fresh empty `[Unreleased]` above it._
11
+ ## [0.5.1] — 2026-09-22
12
+
13
+ Surfaces Solana finality on `SolanaTransactionStatus` (RIN-296, SDK half of RIN-285). Non-blocking pollers can now read one status snapshot and see how close to finality a tx is, matching the pattern EVM (`blockNumber` + tip derivation) and UTXO (RIN-195's carried `confirmations`) already use.
14
+
15
+ ### Added
16
+
17
+ - **`SolanaTransactionStatus.slot: number | null`** — from `getTransaction`'s `slot` on the success path; from `getSignatureStatus`'s `slot` on the ledger-pruned fallback.
18
+ - **`SolanaTransactionStatus.confirmations: number | null`** — the raw RPC count for non-final states, normalised to the exported `SOLANA_FINALIZED_CONFIRMATIONS = 32` when the RPC returns `null` (rooted, per the Solana RPC contract). `null` when the sig-status read was unavailable.
19
+ - **`SolanaTransactionStatus.confirmationStatus: 'processed' | 'confirmed' | 'finalized' | null`** — the raw RPC string.
20
+ - **`SolanaTransactionStatus.signers: readonly string[]`** — base58 signer keys in message order (the first `header.numRequiredSignatures` `staticAccountKeys`). Mirrors omnichain-py `signer_keys = account_keys[: header.num_required_signatures]` at `impl/solana/base.py:1175`. Empty on the ledger-pruned fallback and on `notFound`.
21
+ - **`SOLANA_FINALIZED_CONFIRMATIONS = 32`** — exported constant.
22
+ - **`SolanaConfirmationStatus`** — union of the three RPC states.
23
+
24
+ ### Changed
25
+
26
+ - **`SolanaChain.getTransactionStatus`** fires `getTransaction` and `getSignatureStatus` in parallel via `Promise.allSettled` (RIN-296 D1) — the common Success case pays one round-trip of latency instead of two. Sig-status failure on the Success path degrades the four new fields to `null`/`[]` rather than failing the whole read (D2). The ledger-pruned Pending branch carries whatever finality it has (D3).
27
+
28
+ ### Note
29
+
30
+ - No `blockNumber` field. Solana slots are not block heights (skipped slots exist) and `getChainTipHeight` returns a block height (0.3.3 fix); consumers deriving finality from `blockNumber` on other families must gate Solana on `confirmations`.
31
+ - **Additive only.** All new fields optional in the init with safe defaults (`null` / `[]`); every pre-0.5.1 `SolanaTransactionStatus.successful/failed/pending/notFound` call site compiles unchanged.
32
+ - **Note on version bump.** RIN-285 R4 specifies a **minor** bump. This release ships as a **patch** (0.5.0 → 0.5.1) per requester decision. Under SemVer this is a documented divergence — the change is additive to the public surface (new optional fields, new exported constant/type) and a minor bump would be more strictly correct.
33
+
34
+ ---
35
+
36
+ ## [0.5.0] — 2026-09-21
37
+
38
+ Adds a single canonical bidirectional map between omnichain chain ids and Rango's uppercase `blockchains[].name` identifiers. Removes the need for downstream consumers that talk to Rango to maintain their own partial copies of the mapping.
39
+
40
+ ### Added
41
+
42
+ - **`CHAIN_ID_TO_RANGO_NAME: ReadonlyMap<number, string>`** on `rango_chain_names.ts` — primary map for every omnichain chain that appears in Rango's `/meta`. Sourced from `GET https://public-api.rango.exchange/basic/meta`, snapshot dated 2026-09-21.
43
+ - **`RANGO_NAME_TO_CHAIN_ID: ReadonlyMap<string, number>`** — the inverse map, keyed by uppercase name.
44
+ - **`NOT_ON_RANGO: ReadonlySet<number>`** — explicit "not on Rango" decisions for testnets + mainnet chains omnichain declares but Rango's `/meta` does not carry (e.g. `MANTLE`, `OPBNB`, `SEI_EVM`, `WORLD_CHAIN`, `WANCHAIN`, `ABSTRACT`, `INK`, `BOB`, `ZKLINK_NOVA`, `TEMPO`). Each id must appear in the map or here; the coverage test fails otherwise, and `prepublishOnly` now runs the suite so an undecided chain id cannot ship.
45
+ - **`rangoNameForChainId(chainId)`** and **`chainIdForRangoName(name)`** — accessors. The name-side normalises `trim() + toUpperCase()` and returns `undefined` for any non-string input, so callers looping over untrusted Rango feed rows can skip bad entries instead of aborting.
46
+ - **Fixture** `test/fixtures/rango_meta_blockchains.json` — compact wrapper `{fetchedAt, source, blockchains: [{name, chainId, type}, …]}` of Rango's `/meta`, ~8 KB. The map's substance is verified against it: every mapped name exists, every positive-id row's parsed `chainId` (hex `0x…` or decimal) equals the omnichain id, every non-positive mapped id lives on the family predicate the fixture type implies (`SOLANA→isSolana`, `SUI→isSui`, `XRPL→isXrpl`, `STELLAR→isStellar`, `TON→isTon`, `TRANSFER→isUtxo`; TRON is positive and covered by the hex check), and no `NOT_ON_RANGO` id is actually present in the fixture. Refreshing the map becomes "replace fixture, rerun tests."
47
+ - **Tests** in `test/rango_chain_names.spec.ts`: bijection, no-duplicate names, name-format invariant (`/^[A-Z0-9_]+$/`), critical anchors (BTC=-1, SOLANA=-2000, ETH, BSC, ARBITRUM, BASE, LINEA, AVAX_CCHAIN, LTC, DOGE, BCH, TRON), case/whitespace normalisation, unknown-and-non-string handling, disjoint map/`NOT_ON_RANGO`, exhaustive `chain_ids.ts` coverage, and the three fixture-backed feed checks.
48
+
49
+ ### Note
50
+
51
+ - Aliases (alternate Rango spellings for the same chain) are out of scope. If a consumer hits one in the wild, opening a follow-up to add it is a separate decision.
52
+
53
+ ---
54
+
55
+ ## [0.4.0] — 2026-09-16
56
+
57
+ Brings TS `UtxoTransactionStatus` in line with the Python SDK's already-shipped UTXO shape: per-input `{address, value}` surfaced, and `balanceChanges` becomes net per-address (matching EVM/Solana). Every hydration detail lives in the per-tool provider, mirroring Python's `AbstractUtxoTool.get_tx`.
58
+
59
+ ### Added
60
+
61
+ - **`UtxoTransaction` interface** on `utxo/utxo.ts` — the intermediate type returned by the tool layer, mirroring Python's `class UtxoTransaction`: `{ inputs, outputs, netChangesHr: Record<string, Decimal>, size, vsize, confirmations, confirmationDatetime }`.
62
+ - **`UtxoTransactionInput` type** — `{ txid, vout, scriptPubkeyHex, address, valueSats, valueBtcHr, coinbase? }`. Both `valueSats` (bigint, `@deprecated`) and `valueBtcHr` (Decimal) are populated this release; the deprecated `valueSats` is scheduled for removal in a future major, with `valueBtcHr` renamed → `value` at that point to align with Python. Mirrors the `AssetBalanceChange.balanceChangeMr`/`Hr` idiom.
63
+ - **`UtxoRawTransactionProvider.getTransactionWithInputs(txid)`** — new provider method returning a fully-hydrated `UtxoTransaction`. Every adapter is responsible for hydration per its API, matching Python's per-tool approach.
64
+ - **`EsploraTool.getTransactionWithInputs`** — parses inline `vin[*].prevout` from Esplora's `/tx/{txid}` response; zero extra RPCs.
65
+ - **`BitcoinCoreTool.getTransactionWithInputs`** — hydrates via a hybrid path that works on every Core version: whichever verbosity the caller requests (`bitcoinCoreVerbose?: 1 | 2`, default `1`), the adapter uses any `vin[*].prevout` the response inlines and runs a batched `getrawtransaction verbose=1` walk for the vins that do not carry a prevout (older Core, mempool txs, pruned-block txs, Core < 25). Zero extra RPCs when block undo data is available; N extra otherwise. Matches Python's `impl/utxo/tools/bitcoin_core.py:453` cost profile on the fallback path. Setting `bitcoinCoreVerbose: 2` on Core ≥ 25 is the recommended production configuration for confirmed-heavy status polling.
66
+ - **`UtxoTransactionStatus.inputs`** — `readonly UtxoTransactionInput[] | null`. Populated when the provider tool hydrated every non-coinbase input; `null` (with `inputsUnresolvedReason: 'pending'`) on `Pending` statuses whose provider skips hydration.
67
+ - **`UtxoTransactionStatus.inputsUnresolvedReason`** — `'provider_error' | 'parent_missing' | 'pending' | null`.
68
+ - **`UtxoInputsUnresolvedReason`** type on `utxo/utxo.ts`.
69
+ - **8 new RIN-226 unit tests** covering: inputs order + dual units, net semantics on a mixed-owner tx, self-send fee-only debit, no legacy accessor, hydration failure → thrown `RpcError`, coinbase input shape, `Pending` + hydration unavailable, `valueSats`/`valueBtcHr` agreement.
70
+
71
+ ### Changed (breaking on the UTXO surface)
72
+
73
+ - **`UtxoTransactionStatus.balanceChanges`** is now **net per-address** (from `UtxoTransaction.netChangesHr`) on `Success` statuses — inputs debited, outputs credited. Previously gross output credits per receiving address. Matches `EvmTransactionStatus`, `SolanaTransactionStatus`, Python's UTXO status, and Clydner's BlockCypher parser.
74
+ - On confirmed + hydration failure, `UtxoChain.getTransactionStatus` **throws `ChainError(RpcError)`** rather than returning a `Success` with `null` balance changes (preserves the base `TransactionStatus(Success)` invariant that requires `balanceChanges` present).
75
+ - Rewrote the `UtxoTransactionStatus` class docstring — the "gross output credits" caveat and the "`inputs` … comes back in the 2C UTXO port" TODO are removed.
76
+ - No `outputCreditsByAddress()` accessor and no `grossOutputCreditsLegacy` field: deposit detectors that only need who-got-credited iterate `status.outputs[]` directly (Python has neither).
77
+
78
+ ### Note
79
+
80
+ - Migration: consumers that summed positive UTXO credits from `balanceChanges` need to switch to iterating `status.outputs[]` per address. A hot-wallet withdrawal that previously read as a large positive credit (from the change output) now reads as a net debit for the fee — the correct sign.
81
+ - Companion cards to file: rango-intents BTC-8 workaround deletion (drop `enrichInputTxFromChain` and the parent-tx resolver; consume `status.inputs` + iterate `status.outputs[]` for deposit detection) and Clydner BlockCypher-parser netting (drop the parser's own netting or document why it stays because it ingests BlockCypher payloads directly, not SDK statuses).
12
82
 
13
83
  ---
14
84
 
@@ -324,6 +394,15 @@ Initial npm release of `@getomnichain/omnichain`. Replaces prior vendored-submod
324
394
 
325
395
  ---
326
396
 
397
+ [0.5.1]: https://github.com/getomnichain/omnichain/releases/tag/v0.5.1
398
+ [0.5.0]: https://github.com/getomnichain/omnichain/releases/tag/v0.5.0
399
+ [0.4.0]: https://github.com/getomnichain/omnichain/releases/tag/v0.4.0
400
+ [0.3.6]: https://github.com/getomnichain/omnichain/releases/tag/v0.3.6
401
+ [0.3.5]: https://github.com/getomnichain/omnichain/releases/tag/v0.3.5
402
+ [0.3.4]: https://github.com/getomnichain/omnichain/releases/tag/v0.3.4
403
+ [0.3.3]: https://github.com/getomnichain/omnichain/releases/tag/v0.3.3
404
+ [0.3.2]: https://github.com/getomnichain/omnichain/releases/tag/v0.3.2
405
+ [0.3.1]: https://github.com/getomnichain/omnichain/releases/tag/v0.3.1
327
406
  [0.3.0]: https://github.com/getomnichain/omnichain/releases/tag/v0.3.0
328
407
  [0.2.1]: https://github.com/getomnichain/omnichain/releases/tag/v0.2.1
329
408
  [0.2.0]: https://github.com/getomnichain/omnichain/releases/tag/v0.2.0
package/dist/cjs/index.js CHANGED
@@ -24,6 +24,7 @@ __exportStar(require("./errors.js"), exports);
24
24
  __exportStar(require("./is_address.decorator.js"), exports);
25
25
  __exportStar(require("./network_type.js"), exports);
26
26
  __exportStar(require("./priority.js"), exports);
27
+ __exportStar(require("./rango_chain_names.js"), exports);
27
28
  __exportStar(require("./token.js"), exports);
28
29
  __exportStar(require("./transaction_status.js"), exports);
29
30
  __exportStar(require("./unsigned_transaction.js"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4DAA0C;AAC1C,+CAA6B;AAC7B,uDAAqC;AACrC,+DAA6C;AAC7C,kDAAgC;AAChC,iDAA+B;AAC/B,8CAA4B;AAC5B,4DAA0C;AAC1C,oDAAkC;AAClC,gDAA8B;AAC9B,6CAA2B;AAC3B,0DAAwC;AACxC,4DAA0C;AAC1C,iDAA+B;AAC/B,oDAAkC;AAClC,iDAA+B;AAC/B,kDAAgC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4DAA0C;AAC1C,+CAA6B;AAC7B,uDAAqC;AACrC,+DAA6C;AAC7C,kDAAgC;AAChC,iDAA+B;AAC/B,8CAA4B;AAC5B,4DAA0C;AAC1C,oDAAkC;AAClC,gDAA8B;AAC9B,yDAAuC;AACvC,6CAA2B;AAC3B,0DAAwC;AACxC,4DAA0C;AAC1C,iDAA+B;AAC/B,oDAAkC;AAClC,iDAA+B;AAC/B,kDAAgC"}
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NOT_ON_RANGO = exports.RANGO_NAME_TO_CHAIN_ID = exports.CHAIN_ID_TO_RANGO_NAME = void 0;
37
+ exports.rangoNameForChainId = rangoNameForChainId;
38
+ exports.chainIdForRangoName = chainIdForRangoName;
39
+ const C = __importStar(require("./chain_ids.js"));
40
+ const MAX_RANGO_NAME_LENGTH = 64;
41
+ /**
42
+ * Bidirectional lookup between omnichain chain ids and Rango's canonical
43
+ * uppercase blockchain names (the `blockchains[].name` field of Rango's
44
+ * `/meta` response).
45
+ *
46
+ * TypeScript-only module — no `omnichain-py` counterpart. `chain_ids.ts`
47
+ * mirrors the Python catalogue, but the Rango-name mapping is a
48
+ * TS-consumer concern and does not exist on the Python side.
49
+ *
50
+ * Source of truth: `GET https://public-api.rango.exchange/basic/meta`.
51
+ * The snapshot backing this module lives at
52
+ * `test/fixtures/rango_meta_blockchains.json`; its `fetchedAt` field is
53
+ * the single source-of-date. Refresh via `npm run rango:refresh`.
54
+ *
55
+ * Downstream consumers that talk to Rango should import from here
56
+ * instead of maintaining a local copy. `test/rango_chain_names.spec.ts`
57
+ * fails the suite whenever `chain_ids.ts` gains a new id that is
58
+ * neither mapped here nor listed in `NOT_ON_RANGO`, so a new chain
59
+ * forces an explicit decision. That suite runs on every PR/push via
60
+ * `.github/workflows/test.yml` and on `npm publish` via
61
+ * `prepublishOnly`. When the `test` check is required on `main` via
62
+ * branch protection, consumers vendoring omnichain from `main` inherit
63
+ * that check as their guarantee; until then, the workflow signals but
64
+ * does not block.
65
+ *
66
+ * Aliases (alternate spellings Rango may use in other endpoints) are
67
+ * deliberately out of scope; if a consumer hits one, add it as a
68
+ * separate decision.
69
+ */
70
+ exports.CHAIN_ID_TO_RANGO_NAME = new Map([
71
+ [C.CHAIN_ID_BITCOIN_MAINNET, 'BTC'],
72
+ [C.CHAIN_ID_LITECOIN_MAINNET, 'LTC'],
73
+ [C.CHAIN_ID_DOGECOIN_MAINNET, 'DOGE'],
74
+ [C.CHAIN_ID_DASH_MAINNET, 'DASH'],
75
+ [C.CHAIN_ID_ZCASH_MAINNET, 'ZCASH'],
76
+ [C.CHAIN_ID_BITCOIN_CASH_MAINNET, 'BCH'],
77
+ [C.CHAIN_ID_SOLANA_MAINNET, 'SOLANA'],
78
+ [C.CHAIN_ID_SUI_MAINNET, 'SUI'],
79
+ [C.CHAIN_ID_XRPL_MAINNET, 'XRPL'],
80
+ [C.CHAIN_ID_STELLAR_MAINNET, 'STELLAR'],
81
+ [C.CHAIN_ID_TON_MAINNET, 'TON'],
82
+ [C.CHAIN_ID_TRON_MAINNET, 'TRON'],
83
+ [C.CHAIN_ID_ETHEREUM, 'ETH'],
84
+ [C.CHAIN_ID_OPTIMISM, 'OPTIMISM'],
85
+ [C.CHAIN_ID_CRONOS, 'CRONOS'],
86
+ [C.CHAIN_ID_BNB_CHAIN, 'BSC'],
87
+ [C.CHAIN_ID_OKX_CHAIN, 'OKC'],
88
+ [C.CHAIN_ID_GNOSIS, 'GNOSIS'],
89
+ [C.CHAIN_ID_UNICHAIN, 'UNICHAIN'],
90
+ [C.CHAIN_ID_POLYGON, 'POLYGON'],
91
+ [C.CHAIN_ID_MONAD, 'MONAD'],
92
+ [C.CHAIN_ID_SONIC, 'SONIC'],
93
+ [C.CHAIN_ID_SHIMMER, 'SHIMMER'],
94
+ [C.CHAIN_ID_XLAYER, 'XLAYER'],
95
+ [C.CHAIN_ID_FANTOM, 'FANTOM'],
96
+ [C.CHAIN_ID_BOBA, 'BOBA'],
97
+ [C.CHAIN_ID_ZKSYNC, 'ZKSYNC'],
98
+ [C.CHAIN_ID_STABLE, 'STABLE'],
99
+ [C.CHAIN_ID_HYPER_EVM, 'HYPEREVM'],
100
+ [C.CHAIN_ID_METIS, 'METIS'],
101
+ [C.CHAIN_ID_POLYGON_ZKEVM, 'POLYGONZK'],
102
+ [C.CHAIN_ID_MOONBEAM, 'MOONBEAM'],
103
+ [C.CHAIN_ID_MOONRIVER, 'MOONRIVER'],
104
+ [C.CHAIN_ID_SONEIUM, 'SONEIUM'],
105
+ [C.CHAIN_ID_CITREA, 'CITREA'],
106
+ [C.CHAIN_ID_MEGA_ETH, 'MEGAETH'],
107
+ [C.CHAIN_ID_ZETACHAIN, 'ZETA_CHAIN'],
108
+ [C.CHAIN_ID_BASE, 'BASE'],
109
+ [C.CHAIN_ID_IOTA_EVM, 'IOTA'],
110
+ [C.CHAIN_ID_PLASMA, 'PLASMA'],
111
+ [C.CHAIN_ID_MODE, 'MODE'],
112
+ [C.CHAIN_ID_ARBITRUM, 'ARBITRUM'],
113
+ [C.CHAIN_ID_CELO, 'CELO'],
114
+ [C.CHAIN_ID_AVALANCHE_C_CHAIN, 'AVAX_CCHAIN'],
115
+ [C.CHAIN_ID_LINEA, 'LINEA'],
116
+ [C.CHAIN_ID_BERA_CHAIN, 'BERACHAIN'],
117
+ [C.CHAIN_ID_BLAST, 'BLAST'],
118
+ [C.CHAIN_ID_TAIKO, 'TAIKO'],
119
+ [C.CHAIN_ID_SCROLL, 'SCROLL'],
120
+ [C.CHAIN_ID_KATANA, 'KATANA'],
121
+ [C.CHAIN_ID_ZORA, 'ZORA'],
122
+ [C.CHAIN_ID_AURORA, 'AURORA'],
123
+ ]);
124
+ /**
125
+ * Inverse of `CHAIN_ID_TO_RANGO_NAME`, keyed by uppercase name.
126
+ * Prefer `chainIdForRangoName` for case- and whitespace-tolerant lookup.
127
+ */
128
+ exports.RANGO_NAME_TO_CHAIN_ID = new Map(Array.from(exports.CHAIN_ID_TO_RANGO_NAME, ([id, name]) => [name, id]));
129
+ /**
130
+ * Explicit decisions for chain ids omnichain declares but Rango's `/meta`
131
+ * does not carry, verified against the same snapshot as
132
+ * `CHAIN_ID_TO_RANGO_NAME`.
133
+ *
134
+ * Every id in `chain_ids.ts` must appear in either `CHAIN_ID_TO_RANGO_NAME`
135
+ * or here; the coverage test fails the PR check and `npm publish` (via
136
+ * `prepublishOnly`) otherwise. Adding an id to `chain_ids.ts` without a
137
+ * decision is a bug.
138
+ *
139
+ * Grouped by reason:
140
+ * - Testnets/devnets Rango's `/meta` intentionally omits.
141
+ * - Mainnet EVM chains not on Rango's `/meta` as of the snapshot; add to
142
+ * `CHAIN_ID_TO_RANGO_NAME` once Rango starts publishing them.
143
+ */
144
+ exports.NOT_ON_RANGO = new Set([
145
+ // testnets/devnets — Rango's /meta never carries these.
146
+ C.CHAIN_ID_BITCOIN_TESTNET,
147
+ C.CHAIN_ID_BITCOIN_SIGNET,
148
+ C.CHAIN_ID_SOLANA_TESTNET,
149
+ C.CHAIN_ID_SOLANA_DEVNET,
150
+ C.CHAIN_ID_SUI_TESTNET,
151
+ C.CHAIN_ID_SUI_DEVNET,
152
+ C.CHAIN_ID_XRPL_TESTNET,
153
+ C.CHAIN_ID_STELLAR_TESTNET,
154
+ C.CHAIN_ID_TON_TESTNET,
155
+ C.CHAIN_ID_TRON_SHASTA,
156
+ C.CHAIN_ID_GOERLI,
157
+ C.CHAIN_ID_SEPOLIA,
158
+ C.CHAIN_ID_CELO_SEPOLIA,
159
+ // Mainnet, absent from /meta as of the fixture snapshot — move to the map when Rango publishes them.
160
+ C.CHAIN_ID_OPBNB,
161
+ C.CHAIN_ID_WORLD_CHAIN,
162
+ C.CHAIN_ID_WANCHAIN,
163
+ C.CHAIN_ID_SEI_EVM,
164
+ C.CHAIN_ID_ABSTRACT,
165
+ C.CHAIN_ID_TEMPO,
166
+ C.CHAIN_ID_MANTLE,
167
+ C.CHAIN_ID_INK,
168
+ C.CHAIN_ID_BOB,
169
+ C.CHAIN_ID_ZKLINK_NOVA,
170
+ ]);
171
+ /**
172
+ * Rango's canonical uppercase blockchain name for a given omnichain chain
173
+ * id, or `undefined` when the chain is not on Rango, when the input is not
174
+ * a real number (`NaN`), or when it is `null` / `undefined`.
175
+ *
176
+ * `bigint` input is rejected at runtime and returns `undefined`. Callers
177
+ * bridging from `ethers` v6's `Network.chainId: bigint` should narrow with
178
+ * `Number(x)` and check `Number.isSafeInteger` first.
179
+ */
180
+ function rangoNameForChainId(chainId) {
181
+ if (typeof chainId !== 'number' || Number.isNaN(chainId))
182
+ return undefined;
183
+ return exports.CHAIN_ID_TO_RANGO_NAME.get(chainId);
184
+ }
185
+ /**
186
+ * Omnichain chain id for a Rango blockchain name. Input is normalised
187
+ * (`trim()` + `toUpperCase()`) so `' bsc '`, `'BSC'`, `'Bsc'` all resolve
188
+ * to `56`. Returns `undefined` for an unknown name, for any non-string
189
+ * input, for raw input longer than 64 characters (rejected before any
190
+ * per-character work — the bound is several times the longest name in
191
+ * the snapshot, so it is a DoS guard, not a contract), or for input
192
+ * carrying non-ASCII characters (Unicode-aware `toUpperCase` folds
193
+ * `'ſ' → 'S'`
194
+ * and `'ı' → 'I'`, which would resolve homoglyph inputs to real chain
195
+ * ids — the accessor fails closed instead). Callers looping over
196
+ * untrusted Rango feed rows can skip bad entries without a `TypeError`.
197
+ */
198
+ function chainIdForRangoName(name) {
199
+ if (typeof name !== 'string')
200
+ return undefined;
201
+ if (name.length > MAX_RANGO_NAME_LENGTH)
202
+ return undefined;
203
+ for (let i = 0; i < name.length; i++) {
204
+ if (name.charCodeAt(i) > 0x7f)
205
+ return undefined;
206
+ }
207
+ return exports.RANGO_NAME_TO_CHAIN_ID.get(name.trim().toUpperCase());
208
+ }
209
+ //# sourceMappingURL=rango_chain_names.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rango_chain_names.js","sourceRoot":"","sources":["../../rango_chain_names.ts"],"names":[],"mappings":"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"}
@@ -587,56 +587,85 @@ class SolanaChain extends chain_base_ts_1.Chain {
587
587
  catch {
588
588
  scrubUrl = null;
589
589
  }
590
- let tx;
591
- try {
592
- tx = await connection.getTransaction(txHash, {
590
+ // RIN-296 D1: fetch `getTransaction` and `getSignatureStatus` in
591
+ // parallel so the common (Success) case pays one round-trip of
592
+ // latency instead of two. `getSignatureStatus` populates the four
593
+ // new finality fields (slot / confirmations / confirmationStatus /
594
+ // signers) on the status; `slot` and `signers` also come from
595
+ // `getTransaction` when it succeeds.
596
+ const [txResult, sigResult] = await Promise.allSettled([
597
+ connection.getTransaction(txHash, {
593
598
  commitment: 'confirmed',
594
599
  maxSupportedTransactionVersion: 0,
595
- });
596
- }
597
- catch (err) {
600
+ }),
601
+ connection.getSignatureStatus(txHash, { searchTransactionHistory: true }),
602
+ ]);
603
+ if (txResult.status === 'rejected') {
604
+ const err = txResult.reason;
598
605
  const rawMsg = err instanceof Error ? err.message : String(err);
599
606
  throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Failed to read Solana tx ${txHash}: ${rawMsg}`, scrubUrl), { chainId: this.chainId, txHash }, (0, errors_ts_1.sanitizeCause)(err, scrubUrl));
600
607
  }
608
+ const tx = txResult.value;
609
+ // RIN-296 D2: sig-status failure on the Success path degrades to
610
+ // `null` fields rather than throwing — a working tx read must not
611
+ // fail because a metadata enrichment call failed.
612
+ const sigValue = sigResult.status === 'fulfilled' ? (sigResult.value?.value ?? null) : null;
613
+ const confirmationStatus = extractSolanaConfirmationStatus(sigValue?.confirmationStatus);
614
+ const confirmations = normaliseSolanaConfirmations(sigValue);
601
615
  if (!tx) {
602
- // Full tx not fetchable; fall back to signature status. Consume BOTH
603
- // fields (`confirmationStatus` + `err`) — the previous fix that
604
- // always returned Pending on settled-but-unfetchable let a settled-
605
- // FAILED tx poll indefinitely.
606
- let sig;
607
- try {
608
- sig = await connection.getSignatureStatus(txHash, { searchTransactionHistory: true });
609
- }
610
- catch (err) {
616
+ // Full tx not fetchable; use signature status. Consume BOTH fields
617
+ // (`confirmationStatus` + `err`) — always returning Pending on
618
+ // settled-but-unfetchable would let a settled-FAILED tx poll
619
+ // indefinitely.
620
+ if (sigResult.status === 'rejected') {
621
+ const err = sigResult.reason;
611
622
  const rawMsg = err instanceof Error ? err.message : String(err);
612
623
  throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.RpcError, (0, errors_ts_1.sanitizeMessage)(`Failed to read Solana signature status ${txHash}: ${rawMsg}`, scrubUrl), { chainId: this.chainId, txHash }, (0, errors_ts_1.sanitizeCause)(err, scrubUrl));
613
624
  }
614
- if (!sig || !sig.value)
625
+ if (!sigValue)
615
626
  return solana_transaction_status_ts_1.SolanaTransactionStatus.notFound(this.chainId);
616
- const settled = sig.value.confirmationStatus === 'finalized' ||
617
- sig.value.confirmationStatus === 'confirmed';
618
- if (settled && sig.value.err) {
619
- // Fees are not reconstructable from a sig-status only — factory
620
- // now accepts fees: null for exactly this case.
627
+ const slot = sigValue.slot ?? null;
628
+ const settled = confirmationStatus === 'finalized' || confirmationStatus === 'confirmed';
629
+ if (settled && sigValue.err) {
621
630
  return solana_transaction_status_ts_1.SolanaTransactionStatus.failed({
622
631
  chainId: this.chainId,
623
632
  inclusionAt: null,
624
- error: { code: 'REVERTED', reason: JSON.stringify(sig.value.err) },
633
+ error: { code: 'REVERTED', reason: JSON.stringify(sigValue.err) },
625
634
  fees: null,
635
+ slot,
636
+ confirmations,
637
+ confirmationStatus,
626
638
  });
627
639
  }
628
640
  // settled-Success without a fetchable body OR still propagating —
629
641
  // keep polling. NotFound here would misreport a settled deposit.
630
- return solana_transaction_status_ts_1.SolanaTransactionStatus.pending(this.chainId);
642
+ // RIN-296 D3: pass finality through so consumers polling on
643
+ // Pending see the tx's position toward finality.
644
+ return solana_transaction_status_ts_1.SolanaTransactionStatus.pending(this.chainId, {
645
+ slot,
646
+ confirmations,
647
+ confirmationStatus,
648
+ });
631
649
  }
650
+ // RIN-296 R2: `slot` and `signers` are read from the `getTransaction`
651
+ // response the SDK already fetches. `signers` = the first
652
+ // `numRequiredSignatures` static account keys, matching
653
+ // omnichain-py `signer_keys = account_keys[: header.num_required_signatures]`.
654
+ const slot = tx.slot;
655
+ const accounts = tx.transaction.message.staticAccountKeys.map((k) => k.toBase58());
656
+ const signers = accounts.slice(0, tx.transaction.message.header.numRequiredSignatures);
632
657
  if (!tx.meta) {
633
658
  // A returned tx without meta is a valid RPC response shape (some
634
659
  // very old txs or unusual nodes). Report Pending so consumers keep
635
660
  // polling rather than throwing from a status read; matches the
636
661
  // _decodeBalanceChanges guard at the same file.
637
- return solana_transaction_status_ts_1.SolanaTransactionStatus.pending(this.chainId);
662
+ return solana_transaction_status_ts_1.SolanaTransactionStatus.pending(this.chainId, {
663
+ slot,
664
+ confirmations,
665
+ confirmationStatus,
666
+ signers,
667
+ });
638
668
  }
639
- const accounts = tx.transaction.message.staticAccountKeys.map((k) => k.toBase58());
640
669
  const feePayer = accounts[0] ?? '';
641
670
  if (feePayer.length === 0) {
642
671
  throw new errors_ts_1.ChainError(errors_ts_1.ChainErrorKinds.TransactionDecodeFailed, `Solana tx ${txHash} has no fee_payer account`, { chainId: this.chainId, txHash });
@@ -659,6 +688,10 @@ class SolanaChain extends chain_base_ts_1.Chain {
659
688
  inclusionAt,
660
689
  error: { code: 'REVERTED', reason: JSON.stringify(tx.meta.err) },
661
690
  fees,
691
+ slot,
692
+ confirmations,
693
+ confirmationStatus,
694
+ signers,
662
695
  });
663
696
  }
664
697
  // Wrap the decoder so a malformed uiTokenAmount.amount / pre/postBalance
@@ -679,6 +712,10 @@ class SolanaChain extends chain_base_ts_1.Chain {
679
712
  inclusionAt,
680
713
  balanceChanges,
681
714
  fees,
715
+ slot,
716
+ confirmations,
717
+ confirmationStatus,
718
+ signers,
682
719
  });
683
720
  }
684
721
  async getChainTipHeight() {
@@ -1297,6 +1334,33 @@ function signatureBase58FromBytes(txBytes) {
1297
1334
  return bs58encode(sigBytes);
1298
1335
  }
1299
1336
  const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
1337
+ /**
1338
+ * Narrow `getSignatureStatuses` `confirmationStatus` (a raw string) to
1339
+ * the known union, falling back to `null` for unknown values so a
1340
+ * future upstream widening doesn't lie about the type.
1341
+ */
1342
+ function extractSolanaConfirmationStatus(raw) {
1343
+ if (raw === 'processed' || raw === 'confirmed' || raw === 'finalized')
1344
+ return raw;
1345
+ return null;
1346
+ }
1347
+ /**
1348
+ * Normalise `getSignatureStatuses` `value.confirmations` to a monotone
1349
+ * count consumers can gate on. Per the RPC contract, `null` means
1350
+ * rooted (past `finalized`); the SDK maps that to
1351
+ * {@link SOLANA_FINALIZED_CONFIRMATIONS} so `confirmations >= threshold`
1352
+ * checks behave the way callers expect. Passing a null `sigValue`
1353
+ * (sig-status unavailable — a metadata-enrichment failure on the
1354
+ * success path) returns `null` so consumers can distinguish "unknown"
1355
+ * from any real count.
1356
+ */
1357
+ function normaliseSolanaConfirmations(sigValue) {
1358
+ if (sigValue === null)
1359
+ return null;
1360
+ if (sigValue.confirmations !== null)
1361
+ return sigValue.confirmations;
1362
+ return solana_transaction_status_ts_1.SOLANA_FINALIZED_CONFIRMATIONS;
1363
+ }
1300
1364
  function parseSolanaSignature(raw) {
1301
1365
  const hexCandidate = raw.startsWith('0x') ? raw.slice(2) : raw;
1302
1366
  if (/^[0-9a-fA-F]+$/.test(hexCandidate) && hexCandidate.length === 128) {