@molpha/sdk 0.2.0-dev-20261005112114 → 0.2.0-dev-20261005191253

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.
package/README.md CHANGED
@@ -175,7 +175,22 @@ nobody can pick a committee by picking a time.
175
175
  - The message the nodes sign is `keccak256(MOLPHA_MESSAGE_V1 || value || sourceId || u32be(registryVersion) || u8(quorum) || u64be(timestamp) || signersBitmap)`.
176
176
  - Chain clocks, epochs and `maxAge`/staleness are in **seconds**: compare with
177
177
  `timestampSeconds(payload.timestamp)` (floored `ts / 1000`). `timestampAgeSeconds(ts, nowSeconds)` saturates at 0.
178
- - A round that was already reserved for this consumer, or dispatched to the nodes, cannot run again in the same tick (HTTP 409), so a retry waits for the next one (`tickMs`, default 1000: set it to the gateway's `round.tick_ms`).
178
+ - A round that was already reserved for this consumer, or dispatched to the nodes, cannot run again in the same tick (HTTP 409), so a retry waits for the next one. `tickMs` defaults to the gateway's advertised `tickMs` (read once, on the first retry, from `GET /v1/info`), else 1000.
179
+
180
+ ### Retries and timeouts
181
+
182
+ `requestSignedData` makes up to `maxRetries` attempts (default 6) and chooses the wait before each by why the last one failed (`retryDelayMs`). Every wait reaches at least the next tick, then adds jitter so clients that failed together do not retry together:
183
+
184
+ | Last attempt | Wait before the next |
185
+ | --- | --- |
186
+ | 409 (this consumer already has a round for the source in this tick) | next tick, plus up to half a tick |
187
+ | 503 or 429 (gateway at capacity, or nodes unavailable) | the later of the next tick and the gateway's `Retry-After`, plus up to a tick |
188
+ | timeout, network error, other 5xx | capped exponential backoff (250 ms up to 5 s), half of it randomized |
189
+ | 400, 401, 402, 403 | none: terminal. 403 means the subscription is inactive or its round quota for the term is spent |
190
+
191
+ A 400 saying the `registryVersion` is not the current one (a cached context, or a registry roll between your read and the request) is the exception: the SDK reads the registry afresh and retries once, without spending an attempt.
192
+
193
+ `timeoutMs` defaults to 35 s, above the gateway's own wait for a round (`roundTimeoutSeconds` in `/v1/info`, 30 s by default). A shorter timeout abandons a round the gateway is still running, and the retry then starts another. A source that cannot be fetched is reported as soon as enough nodes have failed (usually well under a second), not after the wait.
179
194
  - Private API secrets are encrypted for every node of the registry (the committee is unknown until the gateway stamps the round); the gateway forwards only the selected nodes' envelopes. `verifyNodeKeys` therefore authenticates all of them.
180
195
 
181
196
  ## Wallet
@@ -311,6 +326,10 @@ If the signed 32-byte value is the keccak digest of a longer preimage, pass `{ r
311
326
  const { signature, feed } = await sdk.solana.submitAttestation(result, { rawValue });
312
327
  ```
313
328
 
329
+ **Compute budget and fees.** The transaction requests `estimateSubmitComputeUnits(signers)` compute units (about `43k + 9.2k` per signer, plus 15% and 10k of margin: roughly 140k at 8 signers), not the 1.4M maximum the old default asked for, which mattered once a priority fee is priced per requested unit. Pass `{ computeUnitLimit }` to override it. No priority fee is attached unless you ask: `{ priorityFeeMicroLamports: 2_000 }` sets a price in micro-lamports per unit, and `{ priorityFeeMicroLamports: "auto" }` uses the 75th percentile of the fees recently paid by writers of that feed (capped at 1 lamport per unit; an unreadable fee market means no fee). Use one of them when submits are dropped under load.
330
+
331
+ **Many feeds.** The client caches what feeds sharing a registry version have in common: the registry read (30 s), each signer `Node` key (never changes) and the coalition key of each signer set, and concurrent submits share one read. Submitting a fleet of feeds therefore costs one registry read and one `Node` read per distinct signer, not per submit. A stale or duplicate round is refused by the program in about 17-20k compute units, before it verifies the signature.
332
+
314
333
  Then read the feed this wallet wrote for that source and quorum:
315
334
 
316
335
  ```ts
@@ -337,6 +356,27 @@ const result = await sdk.gateway.requestSignedData({ apiConfig, signaturesRequir
337
356
  const { signature, feed } = await sdk.solana.submitAttestation(result);
338
357
  ```
339
358
 
359
+ If the round succeeds but submitting fails, `requestAndSubmit` throws a `SubmitFailedError` that carries the signed attestation (`error.result`). The round already consumed quota and the attestation is valid, so submit `error.result` again instead of requesting a new round.
360
+
361
+ ### Many feeds
362
+
363
+ For a large set of feeds use `requestMany` (also exported from the package root) instead of looping over `requestAndSubmit`:
364
+
365
+ ```ts
366
+ import { requestMany } from "@molpha/sdk";
367
+
368
+ const results = await requestMany(sdk, feeds, {
369
+ spreadMs: 1000, // spread starts by source hash so feeds due together do not arrive together
370
+ request: { maxRetries: 6 },
371
+ });
372
+ for (const r of results) {
373
+ if (!r.ok) console.error(r.label, r.error);
374
+ else if (r.submitError) console.warn(r.label, "round ok, submit failed", r.submitError);
375
+ }
376
+ ```
377
+
378
+ It reads the registry inputs once for the whole batch; bounds concurrency, starting from half the gateway's advertised `maxInflightRounds` (at most 64, else 32) and adapting (halving on a busy answer, growing back while requests succeed); runs Solana submits on their own smaller limiter (`submitConcurrency`, default 8); never discards the signed attestation of a round whose submit failed; and returns one result per feed in input order, so a failing feed does not stop the others. Pass `submit: false` to collect attestations only. Throughput is limited by the submit path (one transaction per attestation) long before it is limited by the gateway.
379
+
340
380
  ### Fast requests with a cached context
341
381
 
342
382
  By default every `requestSignedData` call reads the on-chain registry up front
@@ -34,10 +34,13 @@ function getAssociatedTokenAddressSync(mint, owner, tokenProgram = TOKEN_PROGRAM
34
34
  function setComputeUnitLimit(units) {
35
35
  return web3.ComputeBudgetProgram.setComputeUnitLimit({ units });
36
36
  }
37
+ function setComputeUnitPrice(microLamports) {
38
+ return web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports });
39
+ }
37
40
  function keypairFromSecretKey(secretKey) {
38
41
  return web3.Keypair.fromSecretKey(secretKey);
39
42
  }
40
43
 
41
- export { SYSTEM_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, addressBytes, addressFromBytes, findProgramAddressSync, getAssociatedTokenAddressSync, keypairFromSecretKey, setComputeUnitLimit, toPublicKey, toSolanaAddress };
42
- //# sourceMappingURL=chunk-2X22JY7Q.js.map
43
- //# sourceMappingURL=chunk-2X22JY7Q.js.map
44
+ export { SYSTEM_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, addressBytes, addressFromBytes, findProgramAddressSync, getAssociatedTokenAddressSync, keypairFromSecretKey, setComputeUnitLimit, setComputeUnitPrice, toPublicKey, toSolanaAddress };
45
+ //# sourceMappingURL=chunk-H77BBZOL.js.map
46
+ //# sourceMappingURL=chunk-H77BBZOL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/solana/kit.ts"],"names":[],"mappings":";;;;AAaO,IAAM,qBAAA,GAAwB,QAAQ,6CAA6C;AACnF,IAAM,gCAAA,GAAmC,OAAA;AAAA,EAC9C;AACF,CAAA;AACO,IAAM,sBAAA,GAAyB,QAAQ,kCAAkC;AAEhF,IAAM,iBAAiB,iBAAA,EAAkB;AACzC,IAAM,iBAAiB,iBAAA,EAAkB;AAElC,SAAS,gBAAgB,KAAA,EAA+B;AAC7D,EAAA,OAAO,QAAQ,OAAO,KAAA,KAAU,WAAW,KAAA,GAAQ,KAAA,CAAM,UAAU,CAAA;AACrE;AAEO,SAAS,YAAY,KAAA,EAA2D;AACrF,EAAA,OAAO,iBAAiB,IAAA,CAAK,SAAA,GAAY,QAAQ,IAAI,IAAA,CAAK,UAAU,KAAK,CAAA;AAC3E;AAEO,SAAS,aAAa,KAAA,EAAkC;AAC7D,EAAA,OAAO,WAAW,IAAA,CAAK,cAAA,CAAe,OAAO,eAAA,CAAgB,KAAK,CAAC,CAAC,CAAA;AACtE;AAGO,SAAS,iBAAiB,KAAA,EAA4B;AAC3D,EAAA,OAAO,cAAA,CAAe,OAAO,KAAK,CAAA;AACpC;AAEO,SAAS,sBAAA,CACd,OACA,cAAA,EACS;AACT,EAAA,MAAM,CAAC,GAAG,CAAA,GAAI,IAAA,CAAK,UAAU,sBAAA,CAAuB,KAAA,EAAO,WAAA,CAAY,cAAc,CAAC,CAAA;AACtF,EAAA,OAAO,gBAAgB,GAAG,CAAA;AAC5B;AAUO,SAAS,6BAAA,CACd,IAAA,EACA,KAAA,EACA,YAAA,GAA8B,qBAAA,EACrB;AACT,EAAA,OAAO,sBAAA;AAAA,IACL,CAAC,aAAa,KAAK,CAAA,EAAG,aAAa,YAAY,CAAA,EAAG,YAAA,CAAa,IAAI,CAAC,CAAA;AAAA,IACpE;AAAA,GACF;AACF;AAEO,SAAS,oBAAoB,KAAA,EAAkC;AACpE,EAAA,OAAO,IAAA,CAAK,oBAAA,CAAqB,mBAAA,CAAoB,EAAE,OAAO,CAAA;AAChE;AAEO,SAAS,oBAAoB,aAAA,EAA0C;AAC5E,EAAA,OAAO,IAAA,CAAK,oBAAA,CAAqB,mBAAA,CAAoB,EAAE,eAAe,CAAA;AACxE;AAEO,SAAS,qBAAqB,SAAA,EAAsC;AACzE,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,aAAA,CAAc,SAAS,CAAA;AAC7C","file":"chunk-H77BBZOL.js","sourcesContent":["import { web3 } from \"@anchor-lang/core\";\nimport { address, getAddressDecoder, getAddressEncoder, type Address } from \"@solana/kit\";\n\nexport type SolanaAddress = Address | string | InstanceType<typeof web3.PublicKey>;\nexport type SolanaConnection = InstanceType<typeof web3.Connection>;\nexport type SolanaKeypair = InstanceType<typeof web3.Keypair>;\nexport type SolanaInstruction = InstanceType<typeof web3.TransactionInstruction>;\nexport type SolanaAccountMeta = {\n pubkey: InstanceType<typeof web3.PublicKey>;\n isSigner: boolean;\n isWritable: boolean;\n};\n\nexport const TOKEN_PROGRAM_ADDRESS = address(\"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA\");\nexport const ASSOCIATED_TOKEN_PROGRAM_ADDRESS = address(\n \"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL\",\n);\nexport const SYSTEM_PROGRAM_ADDRESS = address(\"11111111111111111111111111111111\");\n\nconst addressEncoder = getAddressEncoder();\nconst addressDecoder = getAddressDecoder();\n\nexport function toSolanaAddress(value: SolanaAddress): Address {\n return address(typeof value === \"string\" ? value : value.toBase58());\n}\n\nexport function toPublicKey(value: SolanaAddress): InstanceType<typeof web3.PublicKey> {\n return value instanceof web3.PublicKey ? value : new web3.PublicKey(value);\n}\n\nexport function addressBytes(value: SolanaAddress): Uint8Array {\n return Uint8Array.from(addressEncoder.encode(toSolanaAddress(value)));\n}\n\n/** Base58 `Address` from 32 raw bytes (e.g. a `Registry.nodes[i]` entry). */\nexport function addressFromBytes(bytes: Uint8Array): Address {\n return addressDecoder.decode(bytes);\n}\n\nexport function findProgramAddressSync(\n seeds: Uint8Array[],\n programAddress: SolanaAddress,\n): Address {\n const [pda] = web3.PublicKey.findProgramAddressSync(seeds, toPublicKey(programAddress));\n return toSolanaAddress(pda);\n}\n\nexport function findProgramPublicKeySync(\n seeds: Uint8Array[],\n programAddress: SolanaAddress,\n): InstanceType<typeof web3.PublicKey> {\n const [pda] = web3.PublicKey.findProgramAddressSync(seeds, toPublicKey(programAddress));\n return pda;\n}\n\nexport function getAssociatedTokenAddressSync(\n mint: SolanaAddress,\n owner: SolanaAddress,\n tokenProgram: SolanaAddress = TOKEN_PROGRAM_ADDRESS,\n): Address {\n return findProgramAddressSync(\n [addressBytes(owner), addressBytes(tokenProgram), addressBytes(mint)],\n ASSOCIATED_TOKEN_PROGRAM_ADDRESS,\n );\n}\n\nexport function setComputeUnitLimit(units: number): SolanaInstruction {\n return web3.ComputeBudgetProgram.setComputeUnitLimit({ units });\n}\n\nexport function setComputeUnitPrice(microLamports: number): SolanaInstruction {\n return web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports });\n}\n\nexport function keypairFromSecretKey(secretKey: Uint8Array): SolanaKeypair {\n return web3.Keypair.fromSecretKey(secretKey);\n}\n\nexport function generateKeypair(): SolanaKeypair {\n return web3.Keypair.generate();\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Address } from '@solana/kit';
2
2
  import { Wallet, Idl, AnchorProvider } from '@anchor-lang/core';
3
- import { U as UpstreamQuote, A as APIConfig, a as AssetDomain, b as UpstreamTerms, E as EvmSigner, R as RegistrySelectionConfig, S as Signer, N as NodeKeyVerifier, c as Node, d as NodesInfo, e as SourcePaymentOptions, f as Attestation, g as SolanaAccountMeta, h as SolanaConnection, i as SolanaAddress, j as NodeKeyVerifierArgs, k as AggregationConfig, M as MolphaWallet } from './wallet-LqCFWwV4.js';
4
- export { l as AttestationPayload, m as EncKeyBundle, n as NumericConfig, o as RegistryInfo, p as SchnorrSignature, q as gatewaySignerFromWallet, s as signerFromKeypair } from './wallet-LqCFWwV4.js';
3
+ import { U as UpstreamQuote, A as APIConfig, a as AssetDomain, b as UpstreamTerms, E as EvmSigner, S as Signer, c as SourcePaymentOptions, R as RegistrySelectionConfig, N as Node, d as NodeKeyVerifier, e as NodesInfo, f as Attestation, g as SolanaAccountMeta, h as SolanaConnection, i as SolanaAddress, j as NodeKeyVerifierArgs, k as AggregationConfig, M as MolphaWallet } from './wallet--IGHoCtG.js';
4
+ export { l as AttestationPayload, m as EncKeyBundle, n as NumericConfig, o as RegistryInfo, p as SchnorrSignature, q as gatewaySignerFromWallet, s as signerFromKeypair } from './wallet--IGHoCtG.js';
5
5
  import BN from 'bn.js';
6
6
 
7
7
  /**
@@ -27,6 +27,12 @@ interface GatewayInfo {
27
27
  gatewayAuthority: string;
28
28
  /** Program id the gateway settles against; must match the client's when present. */
29
29
  programId?: string;
30
+ /** The gateway's round timestamp grid in milliseconds (`round.tick_ms`), when advertised. */
31
+ tickMs?: number;
32
+ /** Longest the gateway waits for a round, in seconds (`node.agg_wait_seconds`), when advertised. */
33
+ roundTimeoutSeconds?: number;
34
+ /** Rounds the gateway runs at once (`limits.max_inflight_rounds`); 0 or absent: not advertised. */
35
+ maxInflightRounds?: number;
30
36
  }
31
37
  declare function normalizeEndpoint(input: GatewayEndpointInput): GatewayEndpoint;
32
38
  /** 32-byte form of a base58 address. */
@@ -145,14 +151,23 @@ interface RequestSignedDataOptions {
145
151
  sourcePayment?: SourcePaymentOptions;
146
152
  /** Max accepted value age in seconds. Default 60. */
147
153
  maxAge?: number;
148
- /** Max attempts; each retry waits for a later gateway tick. Default 15. */
154
+ /**
155
+ * Max attempts. Each retry waits for a later gateway tick, plus jitter, and for longer after a
156
+ * busy answer (`Retry-After`) or a failure that is not a conflict (capped exponential backoff);
157
+ * see {@link retryDelayMs}. Default {@link DEFAULT_MAX_RETRIES}.
158
+ */
149
159
  maxRetries?: number;
150
160
  /**
151
161
  * The gateway's tick grid in milliseconds (its `round.tick_ms`). A retry waits for the start of
152
- * the next tick so it is a new round, not a duplicate of the last. Default 1000.
162
+ * the next tick so it is a new round, not a duplicate of the last. Default: the gateway's
163
+ * advertised `tickMs` (read once, on the first retry), else 1000.
153
164
  */
154
165
  tickMs?: number;
155
- /** Per-request timeout in ms. Default 5000. */
166
+ /**
167
+ * Per-request timeout in ms. Default {@link DEFAULT_ROUND_TIMEOUT_MS}, above the gateway's own
168
+ * wait for a round: a shorter timeout abandons rounds the gateway is still running and the retry
169
+ * then starts another.
170
+ */
156
171
  timeoutMs?: number;
157
172
  /**
158
173
  * Pre-fetched round inputs. Any field present here skips its network/on-chain
@@ -198,6 +213,8 @@ interface MolphaGatewayOptions {
198
213
  * may use gateway-provided node keys without authentication. Defaults to false.
199
214
  */
200
215
  allowUnverifiedNodeKeysForPrivateApi?: boolean;
216
+ /** Source of randomness in [0, 1) for retry jitter; defaults to `Math.random` (tests only). */
217
+ random?: () => number;
201
218
  }
202
219
  /**
203
220
  * The slow-changing inputs a `requestSignedData` round binds to. Fetch once with
@@ -209,6 +226,40 @@ interface RoundContext extends RegistrySelectionConfig {
209
226
  }
210
227
  /** Milliseconds from `nowMs` to the start of the next tick, plus a millisecond of margin. */
211
228
  declare function msUntilNextTick(nowMs: number, tickMs: number): number;
229
+ /**
230
+ * Default per-request timeout: above the gateway's own wait for a round (`node.agg_wait_seconds`,
231
+ * 30 s by default), so the client does not abandon a round the gateway is still running and then
232
+ * start a second one. A gateway advertises its value as `roundTimeoutSeconds` in `GET /v1/info`.
233
+ */
234
+ declare const DEFAULT_ROUND_TIMEOUT_MS = 35000;
235
+ /** Default number of attempts. */
236
+ declare const DEFAULT_MAX_RETRIES = 6;
237
+ /**
238
+ * Why an attempt failed, as far as it decides how long to wait before the next one:
239
+ * - `conflict`: HTTP 409, this consumer already has a round for the source in this tick;
240
+ * - `busy`: HTTP 503 or 429, the gateway is at capacity or a node set is unavailable, optionally
241
+ * with the gateway's `Retry-After`;
242
+ * - `error`: anything else that may be transient (timeout, network, other 5xx).
243
+ */
244
+ type RetryCause = {
245
+ kind: "conflict";
246
+ } | {
247
+ kind: "busy";
248
+ retryAfterMs?: number;
249
+ } | {
250
+ kind: "error";
251
+ };
252
+ /**
253
+ * How long to wait before the next attempt, in ms. Every delay reaches at least the start of the
254
+ * next tick (a retry inside the tick that just failed would be a duplicate round), then adds jitter
255
+ * so clients that failed together do not retry together:
256
+ * - conflict: the next tick plus up to half a tick;
257
+ * - busy: the later of the next tick and the gateway's `Retry-After` (a tick if absent), plus up to
258
+ * a tick;
259
+ * - error: capped exponential backoff (250 ms, 500 ms, ... 5 s) with half of it randomized.
260
+ * `failures` is the number of attempts that have failed so far (1 after the first).
261
+ */
262
+ declare function retryDelayMs(cause: RetryCause, failures: number, nowMs: number, tickMs: number, random?: () => number): number;
212
263
  /** Default gateway base URL when `endpoints` is omitted. */
213
264
  declare const DEFAULT_GATEWAY_ENDPOINT = "https://dev-gateway.molpha.io/";
214
265
  /** Thrown for terminal gateway errors (400/401) — never retried. */
@@ -229,12 +280,17 @@ declare class MolphaGateway {
229
280
  private readonly programIdBytes;
230
281
  /** Gateway PDA per endpoint URL; resolved once per client lifetime. */
231
282
  private readonly gatewayPdas;
283
+ /** Advertised timing per endpoint URL, read lazily on the first retry. */
284
+ private readonly advertisedInfo;
285
+ private readonly random;
232
286
  constructor(endpoints?: GatewayEndpointInput | GatewayEndpointInput[], getRegistrySelectionConfig?: () => Promise<RegistrySelectionConfig>, defaultSigner?: Signer,
233
287
  /**
234
288
  * Either a default subscription owner (base58) or gateway options. A string is the
235
289
  * shorthand for `{ defaultSubscriptionOwner }` used by standalone callers/tests.
236
290
  */
237
291
  defaultSubscriptionOwnerOrOptions?: string | MolphaGatewayOptions, defaultConsumerAuthority?: string);
292
+ /** The configured gateway base URLs, in failover order. */
293
+ endpointUrls(): string[];
238
294
  /** Tries endpoints in order; returns the first node list it can fetch. */
239
295
  getNodes(): Promise<Node[]>;
240
296
  /**
@@ -302,6 +358,12 @@ declare class MolphaGateway {
302
358
  * encryption) or when nothing else can tell us the node count.
303
359
  */
304
360
  private resolveContext;
361
+ /**
362
+ * The gateway's advertised tick grid, read once per endpoint and only when a retry needs it. Any
363
+ * failure (an old gateway without `/v1/info`, a timeout) means "not advertised", so the caller
364
+ * falls back to the default; a failure is not cached.
365
+ */
366
+ private advertisedTickMs;
305
367
  /** Gateway PDA bytes for an endpoint, cached per URL. A failed lookup is not cached. */
306
368
  private resolveGatewayPda;
307
369
  private lookupGatewayPda;
@@ -394,6 +456,17 @@ declare function planIdFromVariant(variant: Record<string, unknown>): PlanId;
394
456
  * `Program` over the vendored IDL (program `3d01170`, "Epoch settlements").
395
457
  */
396
458
 
459
+ /**
460
+ * Compute units to request for a `submit_attestation` carrying `signerCount` signatures.
461
+ *
462
+ * The program's LiteSVM benchmark measures the whole transaction at about `43k + 9.1k` units per
463
+ * signer (119k at 8 signers, 155k at 12, 208k at 18), so the old flat 1.4M request was 7-10 times
464
+ * what a typical aggregate uses. That matters once a priority fee is attached, because it is
465
+ * priced per requested unit, and for how the scheduler packs blocks. The estimate adds 15% and a
466
+ * fixed 10k: the program's selection check costs a little more on some registries than the
467
+ * benchmark's, and a limit that is hit fails the transaction for good.
468
+ */
469
+ declare function estimateSubmitComputeUnits(signerCount: number): number;
397
470
  type Commitment$1 = NonNullable<ConstructorParameters<typeof AnchorProvider>[2]>["commitment"];
398
471
  interface SubscribeResult {
399
472
  signature: string;
@@ -478,7 +551,18 @@ interface SubmitAttestationArgs {
478
551
  };
479
552
  }
480
553
  interface SubmitAttestationOptions {
554
+ /**
555
+ * Compute-unit limit. Defaults to {@link estimateSubmitComputeUnits} for the aggregate's signer
556
+ * count, not the 1.4M maximum.
557
+ */
481
558
  computeUnitLimit?: number;
559
+ /**
560
+ * Priority fee in micro-lamports per compute unit. A number is used as given; `"auto"` takes the
561
+ * 75th percentile of the fees recently paid by transactions that wrote the feed, capped at 1
562
+ * lamport per unit. Omitted: no priority fee, which is right on a quiet cluster. Raise it (or
563
+ * use `"auto"`) when submits are dropped under load.
564
+ */
565
+ priorityFeeMicroLamports?: number | "auto";
482
566
  /**
483
567
  * Precomputed signer coalition key. When omitted, the client fetches signer `Node`
484
568
  * accounts and sums their secp256k1 keys ({@link computeCoalitionKey}).
@@ -501,6 +585,10 @@ declare class MolphaSolanaClient {
501
585
  private readonly program;
502
586
  private readonly provider;
503
587
  readonly programId: Address;
588
+ private readonly registryCache;
589
+ private readonly nodeKeyCache;
590
+ private readonly coalitionCache;
591
+ private priorityFeeCache?;
504
592
  private constructor();
505
593
  static create(opts: CreateClientOpts): MolphaSolanaClient;
506
594
  private get wallet();
@@ -568,8 +656,12 @@ declare class MolphaSolanaClient {
568
656
  * `keccak256(rawValue)`; {@link buildSubmitAttestationArgs} validates that before send.
569
657
  */
570
658
  submitAttestation(attestation: Attestation, opts?: SubmitAttestationOptions): Promise<SubmitResult>;
659
+ /** The registry for `version`, reused for {@link REGISTRY_CACHE_MS}. */
660
+ private fetchRegistryCached;
661
+ private resolvePriorityFee;
571
662
  /** Sum of the signers' keys, read from their on-chain `Node` accounts (one batched fetch). */
572
663
  private computeSignerCoalitionKey;
664
+ private sumSignerKeys;
573
665
  /**
574
666
  * Read the feed written by `submitter` (default: this wallet) for
575
667
  * `(sourceId, signaturesRequired)`, or `null` before its first submit.
@@ -1331,6 +1423,98 @@ declare const subscriptionPda: (owner: SolanaAddress, programId: SolanaAddress)
1331
1423
  */
1332
1424
  declare function feedPda(sourceId: Uint8Array, signaturesRequired: number, submitter: SolanaAddress, programId: SolanaAddress): Address;
1333
1425
 
1426
+ /**
1427
+ * Drive many feeds through the gateway (and optionally on to Solana) without overloading it.
1428
+ *
1429
+ * The consumer is the party that pushes updates and pays for them, so the cost and the burst
1430
+ * behaviour of a large set of feeds is the consumer's to control. {@link requestMany} does the parts
1431
+ * that matter at volume:
1432
+ *
1433
+ * - the registry inputs are read once and shared by every request, not once per feed;
1434
+ * - concurrency is bounded and adapts: it starts from the gateway's advertised capacity, halves
1435
+ * when the gateway answers busy (503/429) and creeps back up while requests succeed (AIMD);
1436
+ * - request starts are spread over a window by a hash of each feed's source id, so feeds that fall
1437
+ * due together do not arrive together, and the same feed always lands at the same offset;
1438
+ * - submitting to Solana runs on its own, smaller limiter, pipelined behind the requests;
1439
+ * - a failed submit never discards the signed attestation the gateway returned (and quota paid for).
1440
+ *
1441
+ * Each request still has the single-request retry policy (backoff with jitter, `Retry-After`, a stale
1442
+ * registry refreshed once; see {@link retryDelayMs}).
1443
+ */
1444
+
1445
+ /** One feed to update. */
1446
+ interface BulkFeed {
1447
+ apiConfig: APIConfig;
1448
+ signaturesRequired: number;
1449
+ /** Echoed in the result; for the caller's own bookkeeping. */
1450
+ label?: string;
1451
+ }
1452
+ interface BulkOptions {
1453
+ /**
1454
+ * Most gateway requests in flight. The limit adapts below this ceiling. Default: half the gateway's
1455
+ * advertised `maxInflightRounds` (at most 64), or 32 when it advertises none.
1456
+ */
1457
+ concurrency?: number;
1458
+ /**
1459
+ * Spread request starts over this many ms by a hash of the feed's source id. Feeds that are due on
1460
+ * the same tick otherwise arrive together and queue at the gateway. Default 0 (no spreading).
1461
+ */
1462
+ spreadMs?: number;
1463
+ /** Submit each attestation to Solana after its round. Default true. */
1464
+ submit?: boolean;
1465
+ /** Most Solana submits in flight. Default 8. */
1466
+ submitConcurrency?: number;
1467
+ /**
1468
+ * Options applied to every request (subscription owner, signer, `maxRetries`, `timeoutMs`, ...).
1469
+ * `apiConfig`, `signaturesRequired` and `context` are set per feed.
1470
+ */
1471
+ request?: Omit<RequestSignedDataOptions, "apiConfig" | "signaturesRequired" | "context">;
1472
+ /** Called as each feed finishes, in completion order. */
1473
+ onResult?: (result: BulkResult) => void;
1474
+ /** Stops starting new feeds; those already started finish. */
1475
+ signal?: AbortSignal;
1476
+ }
1477
+ interface BulkResult {
1478
+ /** Position in the input array. */
1479
+ index: number;
1480
+ label?: string;
1481
+ /** The gateway round succeeded (submitting is reported separately in `submitError`). */
1482
+ ok: boolean;
1483
+ /** The signed attestation, whenever the round succeeded, including when the submit failed. */
1484
+ attestation?: Attestation;
1485
+ /** Transaction signature and feed account, when submitted. */
1486
+ signature?: string;
1487
+ feedAddress?: Address;
1488
+ /** Why the round failed. */
1489
+ error?: unknown;
1490
+ /** Why submitting failed after a successful round; `attestation` is still valid. */
1491
+ submitError?: unknown;
1492
+ }
1493
+ /**
1494
+ * An adaptive concurrency limit: additive increase on success, multiplicative decrease on a busy
1495
+ * answer. Waiters are served in arrival order.
1496
+ */
1497
+ declare class AdaptiveLimiter {
1498
+ private readonly max;
1499
+ private readonly now;
1500
+ private limit_;
1501
+ private inFlight;
1502
+ private successes;
1503
+ private lastCut;
1504
+ private readonly waiters;
1505
+ constructor(initial: number, max: number, now?: () => number);
1506
+ get limit(): number;
1507
+ acquire(): Promise<void>;
1508
+ /** Release a slot. `busy` reports that the gateway answered at capacity. */
1509
+ release(outcome: "ok" | "busy" | "other"): void;
1510
+ }
1511
+ /** Deterministic start offset for a feed: the same source id always lands at the same point. */
1512
+ declare function spreadOffsetMs(sourceIdHex: string, spreadMs: number): number;
1513
+ /**
1514
+ * Update many feeds. Results are returned in input order; a feed that fails does not stop the others.
1515
+ */
1516
+ declare function requestMany(sdk: MolphaSDK, feeds: BulkFeed[], opts?: BulkOptions): Promise<BulkResult[]>;
1517
+
1334
1518
  /**
1335
1519
  * Vendored Molpha Anchor IDL (`target/idl/molpha.json` from the program repo).
1336
1520
  *
@@ -1368,6 +1552,20 @@ interface MolphaSDKOptions {
1368
1552
  idl?: Idl;
1369
1553
  commitment?: Commitment;
1370
1554
  }
1555
+ /**
1556
+ * Thrown by {@link MolphaSDK.requestAndSubmit} when the gateway round succeeded but submitting the
1557
+ * attestation to Solana failed. The round consumed subscription quota and the signed attestation is
1558
+ * valid, so it is carried here: submit `error.result` again (it stays valid until its freshness
1559
+ * window passes, and re-submitting it is safe) instead of requesting, and paying for, a new round.
1560
+ */
1561
+ declare class SubmitFailedError extends Error {
1562
+ /** The signed attestation the gateway returned. */
1563
+ readonly result: Attestation;
1564
+ readonly cause: unknown;
1565
+ constructor(
1566
+ /** The signed attestation the gateway returned. */
1567
+ result: Attestation, cause: unknown);
1568
+ }
1371
1569
  declare class MolphaSDK {
1372
1570
  readonly gateway: MolphaGateway;
1373
1571
  readonly solana: MolphaSolanaClient;
@@ -1383,4 +1581,4 @@ declare class MolphaSDK {
1383
1581
  }>;
1384
1582
  }
1385
1583
 
1386
- export { APIConfig, AggregationConfig, AggregationConfigError, AssetDomain, Attestation, type AttestationMessageFields, type BuildEvmVerifierArgsOptions, type BuildStarknetVerifierArgsOptions, type CoalitionKey, DEFAULT_GATEWAY_ENDPOINT, EIP712_DOMAIN_TYPEHASH, type Eip712Domain, type EvmAttestation, type EvmAttestationPayload, type EvmSchnorrSignature, EvmSigner, type EvmVerifierArgs, type EvmVerifyResult, type FeedAccount, type GatewayEndpoint, type GatewayEndpointInput, GatewayError, type GatewayInfo, INT256_MAX, INT256_MIN, MESSAGE_PREFIX, MIN_TOLERANCE_SIGNATURES, MOLPHA_IDL, MOLPHA_PROGRAM_ADDRESS, MOLPHA_PROGRAM_ID, MOLPHA_VERIFIER_ABI, MOLPHA_VERIFIER_ADDRESS, MOLPHA_VERIFIER_STARKNET_ADDRESSES, MOLPHA_VERIFIER_STARKNET_SEPOLIA, type MolphaEvmNetwork, MolphaGateway, type MolphaGatewayOptions, MolphaSDK, type MolphaSDKOptions, MolphaSolanaClient, type MolphaStarknetNetwork, MolphaWallet, Node, NodeKeyVerifier, NodeKeyVerifierArgs, NodesInfo, type PlanId, type PlanInfo, PlanType, REQUEST_AUTH_DOMAIN, RegistrySelectionConfig, type RegistryStateView, type RegistryView, type RequestAuthFields, type RequestSignedDataOptions, type RoundContext, SELECTION_WINDOW_MS, type Secp256k1KeyInput, Signer, SourcePaymentOptions, type StarknetAttestation, type StarknetAttestationPayload, type StarknetFeltLike, type StarknetSchnorrSignature, type StarknetVerifierArgs, type StarknetVerifyResult, type SubmitAttestationArgs, type SubmitAttestationOptions, type SubmitResult, type SubscribeResult, type SubscriptionInfo, TRANSFER_WITH_AUTHORIZATION_TYPEHASH, type TransferAuthorization, UpstreamPaymentRequiredError, UpstreamQuote, UpstreamTerms, VERIFY_CODES, type VerifyCode, type VerifyCodeName, addressToBytes, assertAggregationQuorum, attestationMessageHash, attestationMessageHashFromAttestation, base64ToBytes, bigIntFromBytesBe, bitmapBitSet, bitmapToIndices, buildEvmVerifierArgs, buildStarknetVerifierArgs, buildSubmitAttestationArgs, bytesToBase64, bytesToHex, bytesToHex0x, canonicalizeAPIConfig, canonicalizeAggregation, commitmentAddressToStarknetFelt, computeCoalitionKey, concatBytes, createEvmSignerFromPrivateKey, decodeInt256, deriveGatewayPda, deriveGatewayPdaAddress, deriveGroupBitmap, deriveSelectionBitmap, deriveSelectionSeed, deriveSourceId, deriveSourceIdString, domainSeparator, effectiveSelectionSize, eligibleSetSize, encodeEvmVerifyCalldata, encodeInt256, encodeInt256Decimal, encodeRequestAuth, encodeStarknetVerifyCalldata, ensureLength, evmAddressFromPrivateKey, feedPda, formatInt256Decimal, gatewayPda, getMolphaStarknetVerifierAddress, hashRequestAuth, hexToBytes, msUntilNextTick, nodePda, normalizeEndpoint, normalizeSecp256k1PublicKeyHex, parseEvmVerifyResult, parseGatewayInfo, parseStarknetVerifyResult, parseUpstreamQuote, planIdFromVariant, planPda, planVariant, probeSource, protocolConfigPda, registryPda, registryStatePda, resolveRemainingAccounts, secp256k1PublicKeyFromCoordinates, selectedIndices, signSourcePayments, signersBitmapToDecimal, signersBitmapToStarknetUint256, signersBitmapToUint256, subscriptionPda, timestampAgeSeconds, timestampSeconds, toChecksumAddress, toFixedBytes, toFixedHex, transferWithAuthorizationDigest, transferWithAuthorizationHash, u256beFromBigInt, u32be, u32le, u64be, u64le, u8, utf8, validateSuppliedTerms, verifyCodeName };
1584
+ export { APIConfig, AdaptiveLimiter, AggregationConfig, AggregationConfigError, AssetDomain, Attestation, type AttestationMessageFields, type BuildEvmVerifierArgsOptions, type BuildStarknetVerifierArgsOptions, type BulkFeed, type BulkOptions, type BulkResult, type CoalitionKey, DEFAULT_GATEWAY_ENDPOINT, DEFAULT_MAX_RETRIES, DEFAULT_ROUND_TIMEOUT_MS, EIP712_DOMAIN_TYPEHASH, type Eip712Domain, type EvmAttestation, type EvmAttestationPayload, type EvmSchnorrSignature, EvmSigner, type EvmVerifierArgs, type EvmVerifyResult, type FeedAccount, type GatewayEndpoint, type GatewayEndpointInput, GatewayError, type GatewayInfo, INT256_MAX, INT256_MIN, MESSAGE_PREFIX, MIN_TOLERANCE_SIGNATURES, MOLPHA_IDL, MOLPHA_PROGRAM_ADDRESS, MOLPHA_PROGRAM_ID, MOLPHA_VERIFIER_ABI, MOLPHA_VERIFIER_ADDRESS, MOLPHA_VERIFIER_STARKNET_ADDRESSES, MOLPHA_VERIFIER_STARKNET_SEPOLIA, type MolphaEvmNetwork, MolphaGateway, type MolphaGatewayOptions, MolphaSDK, type MolphaSDKOptions, MolphaSolanaClient, type MolphaStarknetNetwork, MolphaWallet, Node, NodeKeyVerifier, NodeKeyVerifierArgs, NodesInfo, type PlanId, type PlanInfo, PlanType, REQUEST_AUTH_DOMAIN, RegistrySelectionConfig, type RegistryStateView, type RegistryView, type RequestAuthFields, type RequestSignedDataOptions, type RetryCause, type RoundContext, SELECTION_WINDOW_MS, type Secp256k1KeyInput, Signer, SourcePaymentOptions, type StarknetAttestation, type StarknetAttestationPayload, type StarknetFeltLike, type StarknetSchnorrSignature, type StarknetVerifierArgs, type StarknetVerifyResult, type SubmitAttestationArgs, type SubmitAttestationOptions, SubmitFailedError, type SubmitResult, type SubscribeResult, type SubscriptionInfo, TRANSFER_WITH_AUTHORIZATION_TYPEHASH, type TransferAuthorization, UpstreamPaymentRequiredError, UpstreamQuote, UpstreamTerms, VERIFY_CODES, type VerifyCode, type VerifyCodeName, addressToBytes, assertAggregationQuorum, attestationMessageHash, attestationMessageHashFromAttestation, base64ToBytes, bigIntFromBytesBe, bitmapBitSet, bitmapToIndices, buildEvmVerifierArgs, buildStarknetVerifierArgs, buildSubmitAttestationArgs, bytesToBase64, bytesToHex, bytesToHex0x, canonicalizeAPIConfig, canonicalizeAggregation, commitmentAddressToStarknetFelt, computeCoalitionKey, concatBytes, createEvmSignerFromPrivateKey, decodeInt256, deriveGatewayPda, deriveGatewayPdaAddress, deriveGroupBitmap, deriveSelectionBitmap, deriveSelectionSeed, deriveSourceId, deriveSourceIdString, domainSeparator, effectiveSelectionSize, eligibleSetSize, encodeEvmVerifyCalldata, encodeInt256, encodeInt256Decimal, encodeRequestAuth, encodeStarknetVerifyCalldata, ensureLength, estimateSubmitComputeUnits, evmAddressFromPrivateKey, feedPda, formatInt256Decimal, gatewayPda, getMolphaStarknetVerifierAddress, hashRequestAuth, hexToBytes, msUntilNextTick, nodePda, normalizeEndpoint, normalizeSecp256k1PublicKeyHex, parseEvmVerifyResult, parseGatewayInfo, parseStarknetVerifyResult, parseUpstreamQuote, planIdFromVariant, planPda, planVariant, probeSource, protocolConfigPda, registryPda, registryStatePda, requestMany, resolveRemainingAccounts, retryDelayMs, secp256k1PublicKeyFromCoordinates, selectedIndices, signSourcePayments, signersBitmapToDecimal, signersBitmapToStarknetUint256, signersBitmapToUint256, spreadOffsetMs, subscriptionPda, timestampAgeSeconds, timestampSeconds, toChecksumAddress, toFixedBytes, toFixedHex, transferWithAuthorizationDigest, transferWithAuthorizationHash, u256beFromBigInt, u32be, u32le, u64be, u64le, u8, utf8, validateSuppliedTerms, verifyCodeName };