@molpha/sdk 0.2.0-dev-20261007162054 → 0.2.0-dev-20261010103545

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
@@ -193,30 +193,46 @@ A `programId` that differs from the client's is rejected. Because the hash diffe
193
193
 
194
194
  ### Round timestamp
195
195
 
196
- The request body carries `authSig` and `authTimestamp` but no round timestamp. The gateway assigns the
197
- round's `timestamp` from its own clock, in unix **milliseconds**, on a tick grid
198
- (`floor(now / tickMs) * tickMs`, one second by default), and the result carries it in
199
- `payload.timestamp`. Committee selection reads only the timestamp's one-second window, so
196
+ The request body carries `authSig` and `authTimestamp` but no round timestamp. The gateway stamps the
197
+ round with its own clock, in unix **milliseconds**, floored to a fixed 100 ms tick
198
+ (`floor(nowMs / 100) * 100`), and the result carries the stamp in `payload.timestamp`. The tick is a
199
+ protocol constant, exported as `ROUND_TICK_MS`: it is the same on every gateway and node, it is not
200
+ configurable, and `GET /v1/info` does not report it. Stamping does not wait for a tick boundary, so
201
+ it adds no latency. Committee selection reads only the timestamp's one-second window (ten ticks), so
200
202
  nobody can pick a committee by picking a time.
201
203
 
204
+ - One feed (a source, a quorum and a registry version) runs at most 10 rounds per second, whatever
205
+ the request rate.
206
+ - Requests for the same feed inside one tick share one round: the nodes run it once and every caller
207
+ gets the result. Each caller still spends its own unit of round quota, or its own payment.
208
+ - One consumer gets one round per tick per feed, so at most 10 per second. A second request by the
209
+ same consumer for the same source and quorum inside the tick is refused with HTTP 409; the SDK
210
+ retries it after one tick.
202
211
  - The message the nodes sign is `keccak256(MOLPHA_MESSAGE_V1 || value || sourceId || u32be(registryVersion) || u8(quorum) || u64be(timestamp) || signersBitmap)`.
203
212
  - Chain clocks, epochs and `maxAge`/staleness are in **seconds**: compare with
204
213
  `timestampSeconds(payload.timestamp)` (floored `ts / 1000`). `timestampAgeSeconds(ts, nowSeconds)` saturates at 0.
205
- - 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.
214
+ - The SDK returns `payload.timestamp` exactly as the gateway gave it. Nodes enforce the grid when
215
+ they sign; the on-chain verifiers do not check it.
206
216
 
207
217
  ### Retries and timeouts
208
218
 
209
- `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:
219
+ `requestSignedData` makes up to `maxRetries` attempts (default 6) and chooses the wait before each by why the last one failed (`retryDelayMs`). Every wait carries jitter so clients that failed together do not retry together. The backoff step is a capped exponential: 250 ms, 500 ms, up to 5 s.
210
220
 
211
221
  | Last attempt | Wait before the next |
212
222
  | --- | --- |
213
- | 409 (this consumer already has a round for the source in this tick) | next tick, plus up to half a tick |
214
- | 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 |
215
- | timeout, network error, other 5xx | capped exponential backoff (250 ms up to 5 s), half of it randomized |
223
+ | 409 (this consumer already has a round for the source and quorum in the current tick) | one tick (`ROUND_TICK_MS`, 100 ms), plus up to 20 ms |
224
+ | 503 or 429 (gateway at capacity, or too few nodes accepted the round) | the later of the gateway's `Retry-After` and the backoff step, plus up to one more step |
225
+ | timeout, network error, other 5xx | the backoff step, half of it randomized |
216
226
  | 400, 401, 402, 403 | none: terminal. 403 means the subscription is inactive or its round quota for the term is spent |
217
227
 
218
228
  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.
219
229
 
230
+ The wait after a 409 is a full tick rather than the time left to the next boundary: a full tick puts the retry in a later tick whatever the offset between the client's clock and the gateway's, so the SDK never computes a boundary from the local clock. A caller polling one feed gains nothing by asking more often than once per tick: space the requests at least `ROUND_TICK_MS` apart.
231
+
232
+ A retry is a new request, and the gateway stamps it anew, so it is a **new round**: it spends another unit of round quota or needs another payment, and with a paywalled source another set of payment authorizations. Nothing ties a retry to the attempt before it; there is no idempotency key. Set `maxRetries: 1` to make a single attempt and decide about a retry yourself.
233
+
234
+ The gateway limits the total load: at capacity it answers HTTP 503 with a `Retry-After`, and a consumer over its request rate gets HTTP 429. It also answers 503 when too few nodes accept a round. The SDK treats them alike: it backs off and retries, and throws a `GatewayError` with the status when the attempts run out.
235
+
220
236
  `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.
221
237
 
222
238
  ## Wallet
@@ -917,7 +933,7 @@ Current scope:
917
933
 
918
934
  - Solana subscription and extend flow;
919
935
  - deterministic source id and attestation message hashing;
920
- - gateway signed-data requests (failover, tick-aware retries, per-gateway request auth, context cache);
936
+ - gateway signed-data requests (failover, retries with backoff, per-gateway request auth, context cache);
921
937
  - Solana attestation submission and feed/registry reads;
922
938
  - private API encryption helpers (pre-production);
923
939
  - caller-funded x402 payments for paywalled API sources (Base USDC, pre-production);
package/dist/index.d.ts CHANGED
@@ -40,8 +40,6 @@ interface GatewayInfo {
40
40
  gatewayAuthority: string;
41
41
  /** Program id the gateway settles against; must match the client's when present. */
42
42
  programId?: string;
43
- /** The gateway's round timestamp grid in milliseconds (`round.tick_ms`), when advertised. */
44
- tickMs?: number;
45
43
  /** Longest the gateway waits for a round, in seconds (`node.agg_wait_seconds`), when advertised. */
46
44
  roundTimeoutSeconds?: number;
47
45
  /** Rounds the gateway runs at once (`limits.max_inflight_rounds`); 0 or absent: not advertised. */
@@ -165,17 +163,12 @@ interface RequestSignedDataOptions {
165
163
  /** Max accepted value age in seconds. Default 60. */
166
164
  maxAge?: number;
167
165
  /**
168
- * Max attempts. Each retry waits for a later gateway tick, plus jitter, and for longer after a
169
- * busy answer (`Retry-After`) or a failure that is not a conflict (capped exponential backoff);
170
- * see {@link retryDelayMs}. Default {@link DEFAULT_MAX_RETRIES}.
166
+ * Max attempts. The wait before each retry depends on why the last attempt failed: one round
167
+ * tick after a duplicate (HTTP 409), the gateway's `Retry-After` or a capped exponential backoff
168
+ * after a busy answer, the backoff after anything else; see {@link retryDelayMs}. Every attempt
169
+ * the gateway accepts spends another unit of round quota. Default {@link DEFAULT_MAX_RETRIES}.
171
170
  */
172
171
  maxRetries?: number;
173
- /**
174
- * The gateway's tick grid in milliseconds (its `round.tick_ms`). A retry waits for the start of
175
- * the next tick so it is a new round, not a duplicate of the last. Default: the gateway's
176
- * advertised `tickMs` (read once, on the first retry), else 1000.
177
- */
178
- tickMs?: number;
179
172
  /**
180
173
  * Per-request timeout in ms. Default {@link DEFAULT_ROUND_TIMEOUT_MS}, above the gateway's own
181
174
  * wait for a round: a shorter timeout abandons rounds the gateway is still running and the retry
@@ -228,6 +221,8 @@ interface MolphaGatewayOptions {
228
221
  allowUnverifiedNodeKeysForPrivateApi?: boolean;
229
222
  /** Source of randomness in [0, 1) for retry jitter; defaults to `Math.random` (tests only). */
230
223
  random?: () => number;
224
+ /** Waits `ms` before a retry; defaults to a `setTimeout` timer (tests only). */
225
+ sleep?: (ms: number) => Promise<void>;
231
226
  }
232
227
  /**
233
228
  * The slow-changing inputs a `requestSignedData` round binds to. Fetch once with
@@ -237,8 +232,6 @@ interface RoundContext extends RegistrySelectionConfig {
237
232
  /** Full registry node set: private API secrets are encrypted for every node in it. */
238
233
  nodes: Node[];
239
234
  }
240
- /** Milliseconds from `nowMs` to the start of the next tick, plus a millisecond of margin. */
241
- declare function msUntilNextTick(nowMs: number, tickMs: number): number;
242
235
  /**
243
236
  * Default per-request timeout: above the gateway's own wait for a round (`node.agg_wait_seconds`,
244
237
  * 30 s by default), so the client does not abandon a round the gateway is still running and then
@@ -249,9 +242,10 @@ declare const DEFAULT_ROUND_TIMEOUT_MS = 35000;
249
242
  declare const DEFAULT_MAX_RETRIES = 6;
250
243
  /**
251
244
  * Why an attempt failed, as far as it decides how long to wait before the next one:
252
- * - `conflict`: HTTP 409, this consumer already has a round for the source in this tick;
253
- * - `busy`: HTTP 503 or 429, the gateway is at capacity or a node set is unavailable, optionally
254
- * with the gateway's `Retry-After`;
245
+ * - `conflict`: HTTP 409, this consumer already has a round for the source and quorum in the
246
+ * current round tick;
247
+ * - `busy`: HTTP 503 or 429, the gateway is at capacity, or too few nodes accepted the round
248
+ * (unavailable, or busy), optionally with the gateway's `Retry-After`;
255
249
  * - `error`: anything else that may be transient (timeout, network, other 5xx).
256
250
  */
257
251
  type RetryCause = {
@@ -263,16 +257,17 @@ type RetryCause = {
263
257
  kind: "error";
264
258
  };
265
259
  /**
266
- * How long to wait before the next attempt, in ms. Every delay reaches at least the start of the
267
- * next tick (a retry inside the tick that just failed would be a duplicate round), then adds jitter
268
- * so clients that failed together do not retry together:
269
- * - conflict: the next tick plus up to half a tick;
270
- * - busy: the later of the next tick and the gateway's `Retry-After` (a tick if absent), plus up to
271
- * a tick;
272
- * - error: capped exponential backoff (250 ms, 500 ms, ... 5 s) with half of it randomized.
273
- * `failures` is the number of attempts that have failed so far (1 after the first).
260
+ * How long to wait before the next attempt, in ms. Each delay carries jitter, so clients that
261
+ * failed together do not retry together:
262
+ * - conflict: one full round tick ({@link ROUND_TICK_MS}) plus up to 20 ms. The gateway stamps a
263
+ * round with the tick it is in, so a retry a full tick later is in a later tick whatever the
264
+ * offset between the two clocks: no boundary is computed from the local clock;
265
+ * - busy: the later of the gateway's `Retry-After` and the backoff step, plus up to one more step;
266
+ * - error: the backoff step, half of it randomized.
267
+ * The backoff step is a capped exponential (250 ms, 500 ms, ... 5 s). `failures` is the number of
268
+ * attempts that have failed so far (1 after the first).
274
269
  */
275
- declare function retryDelayMs(cause: RetryCause, failures: number, nowMs: number, tickMs: number, random?: () => number): number;
270
+ declare function retryDelayMs(cause: RetryCause, failures: number, random?: () => number): number;
276
271
 
277
272
  /** Thrown for terminal gateway errors (400/401) — never retried. */
278
273
  declare class GatewayError extends Error {
@@ -292,9 +287,8 @@ declare class MolphaGateway {
292
287
  private readonly programIdBytes;
293
288
  /** Gateway PDA per endpoint URL; resolved once per client lifetime. */
294
289
  private readonly gatewayPdas;
295
- /** Advertised timing per endpoint URL, read lazily on the first retry. */
296
- private readonly advertisedInfo;
297
290
  private readonly random;
291
+ private readonly sleep;
298
292
  constructor(endpoints?: GatewayEndpointInput | GatewayEndpointInput[], getRegistrySelectionConfig?: () => Promise<RegistrySelectionConfig>, defaultSigner?: Signer,
299
293
  /**
300
294
  * Either a default subscription owner (base58) or gateway options. A string is the
@@ -337,10 +331,13 @@ declare class MolphaGateway {
337
331
  * failover. The body is POSTed to each endpoint in order until one `completed`s.
338
332
  *
339
333
  * The round's `sourceId` is derived from `apiConfig`. The request carries no round timestamp:
340
- * the gateway assigns the round's `timestamp` (unix milliseconds) from its own clock
341
- * on a tick grid, and the committee follows from it. The result carries the assigned
342
- * timestamp and the signers' bitmap. A retry waits for a later tick, because a round that was
343
- * already reserved or dispatched cannot run again in the same one (HTTP 409).
334
+ * the gateway stamps the round with its own clock floored to the round tick (`timestamp`, unix
335
+ * milliseconds, a multiple of {@link ROUND_TICK_MS}), and the committee follows from it. The
336
+ * result carries the assigned timestamp and the signers' bitmap; the timestamp is taken as the
337
+ * gateway gave it. Requests for the same source, quorum and registry version inside one tick
338
+ * share one round. One consumer gets one round per tick per source and quorum: a second request
339
+ * in the tick is refused (HTTP 409) and retried one tick later. A retry is stamped anew, so it
340
+ * is a new round that spends another unit of round quota.
344
341
  *
345
342
  * The request is authorized by a `RequestAuth` signature (`authSig`) over the program id, the
346
343
  * endpoint's Gateway PDA, the source id, the quorum and `authTimestamp` (unix seconds, read
@@ -370,12 +367,6 @@ declare class MolphaGateway {
370
367
  * encryption) or when nothing else can tell us the node count.
371
368
  */
372
369
  private resolveContext;
373
- /**
374
- * The gateway's advertised tick grid, read once per endpoint and only when a retry needs it. Any
375
- * failure (an old gateway without `/v1/info`, a timeout) means "not advertised", so the caller
376
- * falls back to the default; a failure is not cached.
377
- */
378
- private advertisedTickMs;
379
370
  /** Gateway PDA bytes for an endpoint, cached per URL. A failed lookup is not cached. */
380
371
  private resolveGatewayPda;
381
372
  private lookupGatewayPda;
@@ -877,6 +868,15 @@ declare function deriveSelectionBitmap(sourceId: Uint8Array, registryVersion: nu
877
868
  * EVM `block.timestamp`), epoch windows and `maxAge`/staleness are measured in SECONDS, so
878
869
  * anything that compares the two must go through {@link timestampSeconds}.
879
870
  */
871
+ /**
872
+ * The round tick in milliseconds: the gateway stamps a round with its clock floored to this grid
873
+ * (`floor(nowMs / ROUND_TICK_MS) * ROUND_TICK_MS`) and nodes reject a round whose timestamp is off
874
+ * it. A feed (source, quorum, registry version) therefore runs at most ten rounds per second, and
875
+ * every selection window (`SELECTION_WINDOW_MS`, one second) holds exactly ten ticks. A
876
+ * protocol constant, the same in every gateway and node: not configuration, and not reported by
877
+ * `GET /v1/info`.
878
+ */
879
+ declare const ROUND_TICK_MS = 100;
880
880
  /** `timestamp` (unix ms) as whole unix seconds, floored. */
881
881
  declare function timestampSeconds(timestamp: number | bigint): number;
882
882
  /**
@@ -1467,8 +1467,8 @@ interface BulkOptions {
1467
1467
  */
1468
1468
  concurrency?: number;
1469
1469
  /**
1470
- * Spread request starts over this many ms by a hash of the feed's source id. Feeds that are due on
1471
- * the same tick otherwise arrive together and queue at the gateway. Default 0 (no spreading).
1470
+ * Spread request starts over this many ms by a hash of the feed's source id. Feeds that are due at
1471
+ * the same moment otherwise arrive together and queue at the gateway. Default 0 (no spreading).
1472
1472
  */
1473
1473
  spreadMs?: number;
1474
1474
  /** Submit each attestation to Solana after its round. Default true. */
@@ -1592,4 +1592,4 @@ declare class MolphaSDK {
1592
1592
  }>;
1593
1593
  }
1594
1594
 
1595
- 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_DEV_VERIFIERS_PROVISIONAL, MOLPHA_IDL, MOLPHA_PROGRAM_ADDRESS, MOLPHA_PROGRAM_ID, MOLPHA_SDK_PROFILE, 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 };
1595
+ 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_DEV_VERIFIERS_PROVISIONAL, MOLPHA_IDL, MOLPHA_PROGRAM_ADDRESS, MOLPHA_PROGRAM_ID, MOLPHA_SDK_PROFILE, 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, ROUND_TICK_MS, 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, 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 };
package/dist/index.js CHANGED
@@ -401,6 +401,17 @@ function deriveSelectionBitmap(sourceId, registryVersion, nodeCount, signaturesR
401
401
  const size = effectiveSelectionSize(signaturesRequired, redundancy, nodeCount);
402
402
  return deriveGroupBitmap(seed, nodeCount, size);
403
403
  }
404
+
405
+ // src/core/timestamp.ts
406
+ var ROUND_TICK_MS = 100;
407
+ function timestampSeconds(timestamp) {
408
+ const ms = typeof timestamp === "bigint" ? timestamp : BigInt(timestamp);
409
+ if (ms < 0n) throw new RangeError("timestamp must not be negative");
410
+ return Number(ms / 1000n);
411
+ }
412
+ function timestampAgeSeconds(timestamp, nowSeconds) {
413
+ return Math.max(0, nowSeconds - timestampSeconds(timestamp));
414
+ }
404
415
  var REQUEST_AUTH_DOMAIN = utf8("MOLPHA_REQAUTH_V1");
405
416
  function encodeRequestAuth(fields) {
406
417
  if (!Number.isInteger(fields.signaturesRequired) || fields.signaturesRequired < 1 || fields.signaturesRequired > 255) {
@@ -680,7 +691,17 @@ function validateSuppliedTerms(terms, resource, assetDomain) {
680
691
  return selectTerms(
681
692
  {
682
693
  x402Version: terms.x402Version,
683
- accepts: [terms.requirements]
694
+ accepts: [terms.requirements],
695
+ // Re-validation rebuilds the terms from a bare envelope, so the source's payment-identifier
696
+ // declaration has to be carried through it or the supplied terms would pay without one.
697
+ ...terms.paymentIdentifier ? {
698
+ extensions: {
699
+ "payment-identifier": {
700
+ info: terms.paymentIdentifier.info,
701
+ ...terms.paymentIdentifier.schema !== void 0 ? { schema: terms.paymentIdentifier.schema } : {}
702
+ }
703
+ }
704
+ } : {}
684
705
  },
685
706
  resource,
686
707
  assetDomain
@@ -798,8 +819,6 @@ function parseGatewayInfo(data) {
798
819
  const info = { gatewayAuthority };
799
820
  if (programId !== void 0) info.programId = programId;
800
821
  const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
801
- const tickMs = positive(record.tickMs);
802
- if (tickMs !== void 0) info.tickMs = tickMs;
803
822
  const roundTimeoutSeconds = positive(record.roundTimeoutSeconds);
804
823
  if (roundTimeoutSeconds !== void 0) info.roundTimeoutSeconds = roundTimeoutSeconds;
805
824
  const maxInflightRounds = positive(record.maxInflightRounds);
@@ -810,26 +829,21 @@ function parseGatewayInfo(data) {
810
829
  // src/gateway/index.ts
811
830
  var ZERO_AUTH_SIG = new Uint8Array(64);
812
831
  var authTimestampNow = () => Math.floor(Date.now() / 1e3);
813
- var DEFAULT_TICK_MS = 1e3;
814
- function msUntilNextTick(nowMs, tickMs) {
815
- return tickMs - nowMs % tickMs + 1;
816
- }
817
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
832
+ var timerSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
833
+ var CONFLICT_JITTER_MS = 20;
818
834
  var BACKOFF_BASE_MS = 250;
819
835
  var BACKOFF_MAX_MS = 5e3;
820
836
  var DEFAULT_ROUND_TIMEOUT_MS = 35e3;
821
837
  var DEFAULT_MAX_RETRIES = 6;
822
- function retryDelayMs(cause, failures, nowMs, tickMs, random = Math.random) {
823
- const nextTick = msUntilNextTick(nowMs, tickMs);
838
+ function retryDelayMs(cause, failures, random = Math.random) {
839
+ const backoff = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(failures - 1, 0), BACKOFF_MAX_MS);
824
840
  switch (cause.kind) {
825
841
  case "conflict":
826
- return nextTick + random() * (tickMs / 2);
842
+ return ROUND_TICK_MS + random() * CONFLICT_JITTER_MS;
827
843
  case "busy":
828
- return Math.max(nextTick, cause.retryAfterMs ?? tickMs) + random() * tickMs;
829
- case "error": {
830
- const backoff = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(failures - 1, 0), BACKOFF_MAX_MS);
831
- return Math.max(nextTick, backoff / 2 + random() * (backoff / 2));
832
- }
844
+ return Math.max(cause.retryAfterMs ?? 0, backoff) + random() * backoff;
845
+ case "error":
846
+ return backoff / 2 + random() * (backoff / 2);
833
847
  }
834
848
  }
835
849
  function mergeCause(a, b) {
@@ -867,9 +881,8 @@ var MolphaGateway = class {
867
881
  programIdBytes;
868
882
  /** Gateway PDA per endpoint URL; resolved once per client lifetime. */
869
883
  gatewayPdas = /* @__PURE__ */ new Map();
870
- /** Advertised timing per endpoint URL, read lazily on the first retry. */
871
- advertisedInfo = /* @__PURE__ */ new Map();
872
884
  random;
885
+ sleep;
873
886
  constructor(endpoints, getRegistrySelectionConfig = async () => {
874
887
  throw new Error(
875
888
  "MolphaGateway requires getRegistrySelectionConfig to request signed data \u2014 pass the current on-chain registry version, redundancy buffer and node count (e.g. () => solana.getRegistrySelectionConfig())"
@@ -891,6 +904,7 @@ var MolphaGateway = class {
891
904
  this.programId = options.programId ?? MOLPHA_PROGRAM_ID;
892
905
  this.programIdBytes = addressToBytes(this.programId);
893
906
  this.random = options.random ?? Math.random;
907
+ this.sleep = options.sleep ?? timerSleep;
894
908
  }
895
909
  /** The configured gateway base URLs, in failover order. */
896
910
  endpointUrls() {
@@ -966,10 +980,13 @@ var MolphaGateway = class {
966
980
  * failover. The body is POSTed to each endpoint in order until one `completed`s.
967
981
  *
968
982
  * The round's `sourceId` is derived from `apiConfig`. The request carries no round timestamp:
969
- * the gateway assigns the round's `timestamp` (unix milliseconds) from its own clock
970
- * on a tick grid, and the committee follows from it. The result carries the assigned
971
- * timestamp and the signers' bitmap. A retry waits for a later tick, because a round that was
972
- * already reserved or dispatched cannot run again in the same one (HTTP 409).
983
+ * the gateway stamps the round with its own clock floored to the round tick (`timestamp`, unix
984
+ * milliseconds, a multiple of {@link ROUND_TICK_MS}), and the committee follows from it. The
985
+ * result carries the assigned timestamp and the signers' bitmap; the timestamp is taken as the
986
+ * gateway gave it. Requests for the same source, quorum and registry version inside one tick
987
+ * share one round. One consumer gets one round per tick per source and quorum: a second request
988
+ * in the tick is refused (HTTP 409) and retried one tick later. A retry is stamped anew, so it
989
+ * is a new round that spends another unit of round quota.
973
990
  *
974
991
  * The request is authorized by a `RequestAuth` signature (`authSig`) over the program id, the
975
992
  * endpoint's Gateway PDA, the source id, the quorum and `authTimestamp` (unix seconds, read
@@ -1056,7 +1073,6 @@ var MolphaGateway = class {
1056
1073
  }
1057
1074
  }
1058
1075
  let lastError;
1059
- let tickMs = opts.tickMs;
1060
1076
  let groupSize = effectiveSelectionSize(signaturesRequired, redundancyBuffer, nodeCount);
1061
1077
  let registryIndexes = Array.from({ length: nodeCount }, (_, index) => index);
1062
1078
  let cause = { kind: "error" };
@@ -1064,8 +1080,7 @@ var MolphaGateway = class {
1064
1080
  let retryNow = false;
1065
1081
  for (let attempt = 0; attempt < maxRetries; attempt++) {
1066
1082
  if (attempt > 0 && !retryNow) {
1067
- tickMs ??= await this.advertisedTickMs(timeoutMs) ?? DEFAULT_TICK_MS;
1068
- await sleep(retryDelayMs(cause, attempt, Date.now(), tickMs, this.random));
1083
+ await this.sleep(retryDelayMs(cause, attempt, this.random));
1069
1084
  }
1070
1085
  retryNow = false;
1071
1086
  cause = { kind: "error" };
@@ -1263,27 +1278,6 @@ var MolphaGateway = class {
1263
1278
  if (registry.nodeCount !== void 0) return registry;
1264
1279
  return { ...registry, nodes: await this.getNodes() };
1265
1280
  }
1266
- /**
1267
- * The gateway's advertised tick grid, read once per endpoint and only when a retry needs it. Any
1268
- * failure (an old gateway without `/v1/info`, a timeout) means "not advertised", so the caller
1269
- * falls back to the default; a failure is not cached.
1270
- */
1271
- async advertisedTickMs(timeoutMs) {
1272
- for (const endpoint of this.endpoints) {
1273
- let pending = this.advertisedInfo.get(endpoint.url);
1274
- if (!pending) {
1275
- pending = this.fetchGatewayInfo(endpoint, Math.min(timeoutMs, 3e3)).catch(() => void 0);
1276
- this.advertisedInfo.set(endpoint.url, pending);
1277
- }
1278
- const info = await pending;
1279
- if (info === void 0) {
1280
- this.advertisedInfo.delete(endpoint.url);
1281
- continue;
1282
- }
1283
- if (info.tickMs !== void 0) return info.tickMs;
1284
- }
1285
- return void 0;
1286
- }
1287
1281
  /** Gateway PDA bytes for an endpoint, cached per URL. A failed lookup is not cached. */
1288
1282
  resolveGatewayPda(endpoint, timeoutMs) {
1289
1283
  let pending = this.gatewayPdas.get(endpoint.url);
@@ -1620,7 +1614,7 @@ var molpha_default = {
1620
1614
  name: "molpha",
1621
1615
  version: "0.1.0",
1622
1616
  spec: "0.1.0",
1623
- description: "Molpha Oracle Solana Program - Merged from molpha-solana and molpha-feed"
1617
+ description: "Molpha Oracle Solana Program"
1624
1618
  },
1625
1619
  instructions: [
1626
1620
  {
@@ -3079,6 +3073,16 @@ var molpha_default = {
3079
3073
  writable: true,
3080
3074
  signer: true
3081
3075
  },
3076
+ {
3077
+ name: "program",
3078
+ docs: [
3079
+ "This program. Its account names the ProgramData account holding the upgrade authority."
3080
+ ],
3081
+ address: "chivcFQgxzwkpLvW41PV431HQ4dYpaW3povQH3AdpQt"
3082
+ },
3083
+ {
3084
+ name: "program_data"
3085
+ },
3082
3086
  {
3083
3087
  name: "token_program",
3084
3088
  address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
@@ -5726,19 +5730,6 @@ var molpha_default = {
5726
5730
  84
5727
5731
  ]
5728
5732
  },
5729
- {
5730
- name: "DelegateStatusChanged",
5731
- discriminator: [
5732
- 188,
5733
- 202,
5734
- 137,
5735
- 41,
5736
- 54,
5737
- 213,
5738
- 14,
5739
- 92
5740
- ]
5741
- },
5742
5733
  {
5743
5734
  name: "EpochClosed",
5744
5735
  discriminator: [
@@ -6601,6 +6592,16 @@ var molpha_default = {
6601
6592
  code: 6101,
6602
6593
  name: "InvalidInitialTau",
6603
6594
  msg: "initial_tau is below the minimum ticket threshold"
6595
+ },
6596
+ {
6597
+ code: 6102,
6598
+ name: "InvalidProgramData",
6599
+ msg: "program data account is not this program's"
6600
+ },
6601
+ {
6602
+ code: 6103,
6603
+ name: "NotUpgradeAuthority",
6604
+ msg: "initialize must be signed by the program's upgrade authority"
6604
6605
  }
6605
6606
  ],
6606
6607
  types: [
@@ -6683,6 +6684,9 @@ var molpha_default = {
6683
6684
  },
6684
6685
  {
6685
6686
  name: "timestamp",
6687
+ docs: [
6688
+ "Gateway-assigned round time in unix **milliseconds**."
6689
+ ],
6686
6690
  type: "u64"
6687
6691
  }
6688
6692
  ]
@@ -6898,34 +6902,6 @@ var molpha_default = {
6898
6902
  ]
6899
6903
  }
6900
6904
  },
6901
- {
6902
- name: "DelegateStatusChanged",
6903
- type: {
6904
- kind: "struct",
6905
- fields: [
6906
- {
6907
- name: "delegate",
6908
- type: "pubkey"
6909
- },
6910
- {
6911
- name: "owner",
6912
- type: "pubkey"
6913
- },
6914
- {
6915
- name: "old_status",
6916
- type: "u8"
6917
- },
6918
- {
6919
- name: "new_status",
6920
- type: "u8"
6921
- },
6922
- {
6923
- name: "updated_at",
6924
- type: "i64"
6925
- }
6926
- ]
6927
- }
6928
- },
6929
6905
  {
6930
6906
  name: "EpochClosed",
6931
6907
  type: {
@@ -7334,6 +7310,9 @@ var molpha_default = {
7334
7310
  },
7335
7311
  {
7336
7312
  name: "timestamp",
7313
+ docs: [
7314
+ "Unix milliseconds. Updates are strictly ordered on it, so two rounds in one second order."
7315
+ ],
7337
7316
  type: "u64"
7338
7317
  },
7339
7318
  {
@@ -10014,16 +9993,6 @@ function attestationMessageHashFromAttestation(attestation) {
10014
9993
  });
10015
9994
  }
10016
9995
 
10017
- // src/core/timestamp.ts
10018
- function timestampSeconds(timestamp) {
10019
- const ms = typeof timestamp === "bigint" ? timestamp : BigInt(timestamp);
10020
- if (ms < 0n) throw new RangeError("timestamp must not be negative");
10021
- return Number(ms / 1000n);
10022
- }
10023
- function timestampAgeSeconds(timestamp, nowSeconds) {
10024
- return Math.max(0, nowSeconds - timestampSeconds(timestamp));
10025
- }
10026
-
10027
9996
  // src/core/verifyCodes.ts
10028
9997
  var VERIFY_CODES = {
10029
9998
  OK: 0,
@@ -10622,6 +10591,6 @@ var MolphaSDK = class {
10622
10591
  }
10623
10592
  };
10624
10593
 
10625
- export { AdaptiveLimiter, AggregationConfigError, DEFAULT_GATEWAY_ENDPOINT, DEFAULT_MAX_RETRIES, DEFAULT_ROUND_TIMEOUT_MS, EIP712_DOMAIN_TYPEHASH, GatewayError, INT256_MAX, INT256_MIN, MESSAGE_PREFIX, MIN_TOLERANCE_SIGNATURES, MOLPHA_DEV_VERIFIERS_PROVISIONAL, MOLPHA_IDL, MOLPHA_PROGRAM_ADDRESS, MOLPHA_PROGRAM_ID, MOLPHA_SDK_PROFILE, MOLPHA_VERIFIER_ABI, MOLPHA_VERIFIER_ADDRESS, MOLPHA_VERIFIER_STARKNET_ADDRESSES, MOLPHA_VERIFIER_STARKNET_SEPOLIA, MolphaGateway, MolphaSDK, MolphaSolanaClient, PlanType, REQUEST_AUTH_DOMAIN, SELECTION_WINDOW_MS, SubmitFailedError, TRANSFER_WITH_AUTHORIZATION_TYPEHASH, UpstreamPaymentRequiredError, VERIFY_CODES, 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, gatewaySignerFromWallet, getMolphaStarknetVerifierAddress, hashRequestAuth, hexToBytes, msUntilNextTick, nodePda, normalizeEndpoint, normalizeSecp256k1PublicKeyHex, parseEvmVerifyResult, parseGatewayInfo, parseStarknetVerifyResult, parseUpstreamQuote, planIdFromVariant, planPda, planVariant, probeSource, protocolConfigPda, registryPda, registryStatePda, requestMany, resolveRemainingAccounts, retryDelayMs, secp256k1PublicKeyFromCoordinates, selectedIndices, signSourcePayments, signerFromKeypair, signersBitmapToDecimal, signersBitmapToStarknetUint256, signersBitmapToUint256, spreadOffsetMs, subscriptionPda, timestampAgeSeconds, timestampSeconds, toChecksumAddress, toFixedBytes, toFixedHex, transferWithAuthorizationDigest, transferWithAuthorizationHash, u256beFromBigInt, u32be, u32le, u64be, u64le, u8, utf8, validateSuppliedTerms, verifyCodeName };
10594
+ export { AdaptiveLimiter, AggregationConfigError, DEFAULT_GATEWAY_ENDPOINT, DEFAULT_MAX_RETRIES, DEFAULT_ROUND_TIMEOUT_MS, EIP712_DOMAIN_TYPEHASH, GatewayError, INT256_MAX, INT256_MIN, MESSAGE_PREFIX, MIN_TOLERANCE_SIGNATURES, MOLPHA_DEV_VERIFIERS_PROVISIONAL, MOLPHA_IDL, MOLPHA_PROGRAM_ADDRESS, MOLPHA_PROGRAM_ID, MOLPHA_SDK_PROFILE, MOLPHA_VERIFIER_ABI, MOLPHA_VERIFIER_ADDRESS, MOLPHA_VERIFIER_STARKNET_ADDRESSES, MOLPHA_VERIFIER_STARKNET_SEPOLIA, MolphaGateway, MolphaSDK, MolphaSolanaClient, PlanType, REQUEST_AUTH_DOMAIN, ROUND_TICK_MS, SELECTION_WINDOW_MS, SubmitFailedError, TRANSFER_WITH_AUTHORIZATION_TYPEHASH, UpstreamPaymentRequiredError, VERIFY_CODES, 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, gatewaySignerFromWallet, getMolphaStarknetVerifierAddress, hashRequestAuth, hexToBytes, nodePda, normalizeEndpoint, normalizeSecp256k1PublicKeyHex, parseEvmVerifyResult, parseGatewayInfo, parseStarknetVerifyResult, parseUpstreamQuote, planIdFromVariant, planPda, planVariant, probeSource, protocolConfigPda, registryPda, registryStatePda, requestMany, resolveRemainingAccounts, retryDelayMs, secp256k1PublicKeyFromCoordinates, selectedIndices, signSourcePayments, signerFromKeypair, signersBitmapToDecimal, signersBitmapToStarknetUint256, signersBitmapToUint256, spreadOffsetMs, subscriptionPda, timestampAgeSeconds, timestampSeconds, toChecksumAddress, toFixedBytes, toFixedHex, transferWithAuthorizationDigest, transferWithAuthorizationHash, u256beFromBigInt, u32be, u32le, u64be, u64le, u8, utf8, validateSuppliedTerms, verifyCodeName };
10626
10595
  //# sourceMappingURL=index.js.map
10627
10596
  //# sourceMappingURL=index.js.map