@molpha/sdk 0.2.0-dev-20261002062009 → 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/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { toPublicKey, addressBytes, toSolanaAddress, getAssociatedTokenAddressSync, TOKEN_PROGRAM_ADDRESS, SYSTEM_PROGRAM_ADDRESS, setComputeUnitLimit, addressFromBytes, findProgramAddressSync } from './chunk-2X22JY7Q.js';
1
+ import { toPublicKey, addressBytes, toSolanaAddress, getAssociatedTokenAddressSync, TOKEN_PROGRAM_ADDRESS, SYSTEM_PROGRAM_ADDRESS, setComputeUnitLimit, setComputeUnitPrice, addressFromBytes, findProgramAddressSync } from './chunk-H77BBZOL.js';
2
2
  import { getAddressEncoder, address, getProgramDerivedAddress } from '@solana/kit';
3
3
  import { keccak_256 } from '@noble/hashes/sha3.js';
4
4
  import { utf8ToBytes, hexToBytes as hexToBytes$1, concatBytes as concatBytes$1, bytesToHex as bytesToHex$1, randomBytes } from '@noble/hashes/utils.js';
5
5
  import { secp256k1 } from '@noble/curves/secp256k1.js';
6
6
  import { gcm } from '@noble/ciphers/aes.js';
7
- import { AnchorProvider, Program } from '@anchor-lang/core';
7
+ import { AnchorProvider, Program, web3 } from '@anchor-lang/core';
8
8
  import BN from 'bn.js';
9
9
  import { ed25519 } from '@noble/curves/ed25519.js';
10
10
 
@@ -313,18 +313,19 @@ function secp256k1PublicKeyFromCoordinates(x, y, label = "secp256k1 public key c
313
313
  }
314
314
  var SELECTION_SEED_DOMAIN = keccak_256(utf8("MOLPHA_SELECTION_V1"));
315
315
  var SELECTION_DERIVE_DOMAIN = keccak_256(utf8("MOLPHA_SELECTION_DERIVE"));
316
+ var SELECTION_WINDOW_MS = 1000n;
316
317
  var MAX_ROUNDS = 65536;
317
318
  var BITMAP_BYTES = 32;
318
319
  var MAX_NODES = BITMAP_BYTES * 8;
319
320
  var U32_MAX = 4294967295;
320
- function deriveSelectionSeed(sourceId, registryVersion, canonicalTimestamp) {
321
+ function deriveSelectionSeed(sourceId, registryVersion, timestamp) {
321
322
  ensureLength(sourceId, 32, "sourceId");
322
323
  return keccak_256(
323
324
  concatBytes(
324
325
  SELECTION_SEED_DOMAIN,
325
326
  sourceId,
326
327
  u32be(registryVersion),
327
- u64be(canonicalTimestamp)
328
+ u64be(BigInt(timestamp) / SELECTION_WINDOW_MS)
328
329
  )
329
330
  );
330
331
  }
@@ -390,7 +391,7 @@ function deriveGroupBitmap(seed, nodeCount, groupSize) {
390
391
  }
391
392
  return bitmap;
392
393
  }
393
- function deriveSelectionBitmap(sourceId, registryVersion, nodeCount, signaturesRequired, redundancy = 0, ts = Math.floor(Date.now() / 1e3)) {
394
+ function deriveSelectionBitmap(sourceId, registryVersion, nodeCount, signaturesRequired, redundancy = 0, ts = Date.now()) {
394
395
  const seed = deriveSelectionSeed(sourceId, registryVersion, ts);
395
396
  const size = effectiveSelectionSize(signaturesRequired, redundancy, nodeCount);
396
397
  return deriveGroupBitmap(seed, nodeCount, size);
@@ -405,7 +406,7 @@ function encodeRequestAuth(fields) {
405
406
  ensureLength(fields.gateway, 32, "gateway"),
406
407
  toFixedBytes(fields.sourceId, 32, "sourceId"),
407
408
  Uint8Array.of(fields.signaturesRequired),
408
- u64le(fields.timestamp)
409
+ u64le(fields.authTimestamp)
409
410
  );
410
411
  }
411
412
  function hashRequestAuth(fields) {
@@ -774,11 +775,57 @@ function parseGatewayInfo(data) {
774
775
  if (programId !== void 0 && typeof programId !== "string") {
775
776
  throw new Error("GET /v1/info programId must be a base58 string");
776
777
  }
777
- return programId === void 0 ? { gatewayAuthority } : { gatewayAuthority, programId };
778
+ const info = { gatewayAuthority };
779
+ if (programId !== void 0) info.programId = programId;
780
+ const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
781
+ const tickMs = positive(record.tickMs);
782
+ if (tickMs !== void 0) info.tickMs = tickMs;
783
+ const roundTimeoutSeconds = positive(record.roundTimeoutSeconds);
784
+ if (roundTimeoutSeconds !== void 0) info.roundTimeoutSeconds = roundTimeoutSeconds;
785
+ const maxInflightRounds = positive(record.maxInflightRounds);
786
+ if (maxInflightRounds !== void 0) info.maxInflightRounds = maxInflightRounds;
787
+ return info;
778
788
  }
779
789
 
780
790
  // src/gateway/index.ts
781
791
  var ZERO_AUTH_SIG = new Uint8Array(64);
792
+ var authTimestampNow = () => Math.floor(Date.now() / 1e3);
793
+ var DEFAULT_TICK_MS = 1e3;
794
+ function msUntilNextTick(nowMs, tickMs) {
795
+ return tickMs - nowMs % tickMs + 1;
796
+ }
797
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
798
+ var BACKOFF_BASE_MS = 250;
799
+ var BACKOFF_MAX_MS = 5e3;
800
+ var DEFAULT_ROUND_TIMEOUT_MS = 35e3;
801
+ var DEFAULT_MAX_RETRIES = 6;
802
+ function retryDelayMs(cause, failures, nowMs, tickMs, random = Math.random) {
803
+ const nextTick = msUntilNextTick(nowMs, tickMs);
804
+ switch (cause.kind) {
805
+ case "conflict":
806
+ return nextTick + random() * (tickMs / 2);
807
+ case "busy":
808
+ return Math.max(nextTick, cause.retryAfterMs ?? tickMs) + random() * tickMs;
809
+ case "error": {
810
+ const backoff = Math.min(BACKOFF_BASE_MS * 2 ** Math.max(failures - 1, 0), BACKOFF_MAX_MS);
811
+ return Math.max(nextTick, backoff / 2 + random() * (backoff / 2));
812
+ }
813
+ }
814
+ }
815
+ function mergeCause(a, b) {
816
+ if (a.kind === "busy" || b.kind === "busy") {
817
+ const waits = [a, b].flatMap((c) => c.kind === "busy" && c.retryAfterMs !== void 0 ? [c.retryAfterMs] : []);
818
+ return waits.length ? { kind: "busy", retryAfterMs: Math.max(...waits) } : { kind: "busy" };
819
+ }
820
+ if (a.kind === "conflict" || b.kind === "conflict") return { kind: "conflict" };
821
+ return { kind: "error" };
822
+ }
823
+ function retryAfterMs(res) {
824
+ const raw = res.headers.get("retry-after");
825
+ if (raw === null) return void 0;
826
+ const seconds = Number(raw);
827
+ return Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds, 60) * 1e3 : void 0;
828
+ }
782
829
  var DEFAULT_GATEWAY_ENDPOINT = "https://dev-gateway.molpha.io/";
783
830
  var GatewayError = class extends Error {
784
831
  constructor(message, status) {
@@ -801,6 +848,9 @@ var MolphaGateway = class {
801
848
  programIdBytes;
802
849
  /** Gateway PDA per endpoint URL; resolved once per client lifetime. */
803
850
  gatewayPdas = /* @__PURE__ */ new Map();
851
+ /** Advertised timing per endpoint URL, read lazily on the first retry. */
852
+ advertisedInfo = /* @__PURE__ */ new Map();
853
+ random;
804
854
  constructor(endpoints, getRegistrySelectionConfig = async () => {
805
855
  throw new Error(
806
856
  "MolphaGateway requires getRegistrySelectionConfig to request signed data \u2014 pass the current on-chain registry version, redundancy buffer and node count (e.g. () => solana.getRegistrySelectionConfig())"
@@ -821,6 +871,11 @@ var MolphaGateway = class {
821
871
  this.allowUnverifiedNodeKeysForPrivateApi = options.allowUnverifiedNodeKeysForPrivateApi ?? false;
822
872
  this.programId = options.programId ?? MOLPHA_PROGRAM_ID;
823
873
  this.programIdBytes = addressToBytes(this.programId);
874
+ this.random = options.random ?? Math.random;
875
+ }
876
+ /** The configured gateway base URLs, in failover order. */
877
+ endpointUrls() {
878
+ return this.endpoints.map((e) => e.url);
824
879
  }
825
880
  /** Tries endpoints in order; returns the first node list it can fetch. */
826
881
  async getNodes() {
@@ -831,13 +886,12 @@ var MolphaGateway = class {
831
886
  * selection policy, which sizes the eligible set a paid source must fund.
832
887
  *
833
888
  * The `registry` block is advisory: it is whatever the gateway read from the
834
- * chain, and is absent on older gateways or when that read failed. Prefer the
889
+ * chain, and is absent when that read failed. Prefer the
835
890
  * on-chain read (`MolphaSolanaClient.getRegistrySelectionConfig`) whenever a
836
891
  * Solana connection is available — `requestSignedData` already does.
837
892
  */
838
893
  async getNodesInfo() {
839
894
  const data = await this.firstReachableData("/v1/nodes");
840
- if (Array.isArray(data)) return { nodes: data };
841
895
  return {
842
896
  nodes: data.nodes,
843
897
  ...data.registry ? { registry: data.registry } : {}
@@ -890,12 +944,23 @@ var MolphaGateway = class {
890
944
  }
891
945
  /**
892
946
  * Request a threshold-signed data update from the gateway, with retry +
893
- * failover. Per attempt a fresh timestamp yields a fresh selection bitmap; the
894
- * body is POSTed to each endpoint in order until one `completed`s.
947
+ * failover. The body is POSTed to each endpoint in order until one `completed`s.
948
+ *
949
+ * The round's `sourceId` is derived from `apiConfig`. The request carries no round timestamp:
950
+ * the gateway assigns the round's `timestamp` (unix milliseconds) from its own clock
951
+ * on a tick grid, and the committee follows from it. The result carries the assigned
952
+ * timestamp and the signers' bitmap. A retry waits for a later tick, because a round that was
953
+ * already reserved or dispatched cannot run again in the same one (HTTP 409).
954
+ *
955
+ * The request is authorized by a `RequestAuth` signature (`authSig`) over the program id, the
956
+ * endpoint's Gateway PDA, the source id, the quorum and `authTimestamp` (unix seconds, read
957
+ * from the local clock for every attempt). It only proves the caller may spend the
958
+ * subscription's rounds; it is not part of the round, so the signature is recomputed for every
959
+ * endpoint tried.
895
960
  *
896
- * The round's `sourceId` is derived from `apiConfig`. The request authorization
897
- * binds the program id and each gateway's on-chain account, so the auth signature
898
- * is recomputed for every endpoint tried.
961
+ * Because the committee is unknown until the gateway stamps the round, private API
962
+ * secrets are encrypted for **every** node of the registry; the gateway forwards only the
963
+ * selected nodes' envelopes.
899
964
  *
900
965
  * By default this fetches the registry selection config up front and the node
901
966
  * set only when the round needs it (private API encryption, or when the registry
@@ -914,8 +979,8 @@ var MolphaGateway = class {
914
979
  encrypt,
915
980
  sourcePayment,
916
981
  maxAge = 60,
917
- maxRetries = 15,
918
- timeoutMs = 5e3
982
+ maxRetries = DEFAULT_MAX_RETRIES,
983
+ timeoutMs = DEFAULT_ROUND_TIMEOUT_MS
919
984
  } = opts;
920
985
  const signaturesRequired = assertSignaturesRequired(opts.signaturesRequired);
921
986
  const verifyNodeKeys = opts.verifyNodeKeys ?? this.verifyNodeKeys;
@@ -937,17 +1002,19 @@ var MolphaGateway = class {
937
1002
  }
938
1003
  const consumerAuthority = opts.consumerAuthority ?? this.defaultConsumerAuthority ?? subscriptionOwner;
939
1004
  const authSigner = signer ?? this.defaultSigner;
940
- const round = await this.resolveContext(opts.context, encrypt !== void 0);
941
- const { registryVersion, redundancyBuffer } = round;
942
- if (round.nodeCount !== void 0 && round.nodes !== void 0 && round.nodes.length !== round.nodeCount) {
943
- throw new Error(
944
- `Gateway node list has ${round.nodes.length} nodes but registry ${registryVersion} has node_count ${round.nodeCount} \u2014 refresh the round context`
945
- );
946
- }
947
- const nodeCount = round.nodeCount ?? round.nodes?.length;
948
- if (nodeCount === void 0) {
949
- throw new Error("Round context resolved neither nodeCount nor nodes");
950
- }
1005
+ let round = await this.resolveContext(opts.context, encrypt !== void 0);
1006
+ let { registryVersion, redundancyBuffer } = round;
1007
+ const nodeCountOf = (r) => {
1008
+ if (r.nodeCount !== void 0 && r.nodes !== void 0 && r.nodes.length !== r.nodeCount) {
1009
+ throw new Error(
1010
+ `Gateway node list has ${r.nodes.length} nodes but registry ${r.registryVersion} has node_count ${r.nodeCount} \u2014 refresh the round context`
1011
+ );
1012
+ }
1013
+ const n = r.nodeCount ?? r.nodes?.length;
1014
+ if (n === void 0) throw new Error("Round context resolved neither nodeCount nor nodes");
1015
+ return n;
1016
+ };
1017
+ let nodeCount = nodeCountOf(round);
951
1018
  let terms = null;
952
1019
  let authorizations = 0;
953
1020
  let requoted = false;
@@ -970,28 +1037,31 @@ var MolphaGateway = class {
970
1037
  }
971
1038
  }
972
1039
  let lastError;
973
- let lastTimestamp = 0;
1040
+ let tickMs = opts.tickMs;
1041
+ let groupSize = effectiveSelectionSize(signaturesRequired, redundancyBuffer, nodeCount);
1042
+ let registryIndexes = Array.from({ length: nodeCount }, (_, index) => index);
1043
+ let cause = { kind: "error" };
1044
+ let refreshedOnce = false;
1045
+ let retryNow = false;
974
1046
  for (let attempt = 0; attempt < maxRetries; attempt++) {
975
- let timestamp = Math.floor(Date.now() / 1e3);
976
- if (timestamp <= lastTimestamp) timestamp = lastTimestamp + 1;
977
- lastTimestamp = timestamp;
978
- const seed = deriveSelectionSeed(sourceIdBytes, registryVersion, timestamp);
979
- const groupSize = effectiveSelectionSize(signaturesRequired, redundancyBuffer, nodeCount);
980
- const bitmap = deriveGroupBitmap(seed, nodeCount, groupSize);
981
- const indices = selectedIndices(bitmap, nodeCount);
1047
+ if (attempt > 0 && !retryNow) {
1048
+ tickMs ??= await this.advertisedTickMs(timeoutMs) ?? DEFAULT_TICK_MS;
1049
+ await sleep(retryDelayMs(cause, attempt, Date.now(), tickMs, this.random));
1050
+ }
1051
+ retryNow = false;
1052
+ cause = { kind: "error" };
982
1053
  let encKeyBundle;
983
1054
  if (encrypt) {
984
- const selected = selectedNodesForPrivateApiEncryption(round.nodes ?? [], indices);
1055
+ const recipients = selectedNodesForPrivateApiEncryption(round.nodes ?? [], registryIndexes);
985
1056
  if (verifyNodeKeys) {
986
1057
  await verifyNodeKeys({
987
1058
  sourceId,
988
1059
  registryVersion,
989
- timestamp,
990
- selectedIndexes: [...indices],
991
- selectedNodes: selected.map((node) => ({ ...node }))
1060
+ nodeIndexes: [...registryIndexes],
1061
+ nodes: recipients.map((node) => ({ ...node }))
992
1062
  });
993
1063
  }
994
- encKeyBundle = encryptForNodes(requestApiConfig, encrypt.secrets, selected);
1064
+ encKeyBundle = encryptForNodes(requestApiConfig, encrypt.secrets, recipients);
995
1065
  }
996
1066
  let sourcePayments;
997
1067
  if (terms && sourcePayment && authorizations > 0) {
@@ -1004,7 +1074,6 @@ var MolphaGateway = class {
1004
1074
  const baseBody = {
1005
1075
  sourceId,
1006
1076
  registryVersion,
1007
- timestamp,
1008
1077
  maxAge,
1009
1078
  signaturesRequired,
1010
1079
  subscriptionOwner,
@@ -1014,22 +1083,25 @@ var MolphaGateway = class {
1014
1083
  if (encKeyBundle) baseBody.encKeyBundle = encKeyBundle;
1015
1084
  if (sourcePayments) baseBody.sourcePayments = sourcePayments;
1016
1085
  let requote = false;
1086
+ let restartForRefresh = false;
1017
1087
  for (const endpoint of this.endpoints) {
1018
1088
  try {
1019
1089
  let authSig = ZERO_AUTH_SIG;
1090
+ let authTimestamp = authTimestampNow();
1020
1091
  if (authSigner) {
1021
1092
  const gateway = await this.resolveGatewayPda(endpoint, timeoutMs);
1093
+ authTimestamp = authTimestampNow();
1022
1094
  authSig = await authSigner(
1023
1095
  hashRequestAuth({
1024
1096
  programId: this.programIdBytes,
1025
1097
  gateway,
1026
1098
  sourceId: sourceIdBytes,
1027
1099
  signaturesRequired,
1028
- timestamp
1100
+ authTimestamp
1029
1101
  })
1030
1102
  );
1031
1103
  }
1032
- const body = { ...baseBody, authSig: bytesToHex0x(authSig) };
1104
+ const body = { ...baseBody, authSig: bytesToHex0x(authSig), authTimestamp };
1033
1105
  const res = await this.post(
1034
1106
  `${endpoint.url}/v1/round/execute`,
1035
1107
  body,
@@ -1085,17 +1157,27 @@ var MolphaGateway = class {
1085
1157
  break;
1086
1158
  }
1087
1159
  const errorDetail = !res.ok ? await parseGatewayErrorDetail(res) : void 0;
1088
- if (res.status === 400 || res.status === 401) {
1160
+ if (res.status === 400 && !refreshedOnce && !restartForRefresh && /registryVersion.*current version/i.test(errorDetail ?? "")) {
1161
+ restartForRefresh = true;
1162
+ break;
1163
+ }
1164
+ if (res.status === 400 || res.status === 401 || res.status === 403) {
1089
1165
  throw new GatewayError(
1090
1166
  formatGatewayErrorMessage("Gateway rejected request", res.status, errorDetail),
1091
1167
  res.status
1092
1168
  );
1093
1169
  }
1094
- if (res.status === 503) {
1170
+ if (res.status === 503 || res.status === 429) {
1095
1171
  lastError = new GatewayError(
1096
- formatGatewayErrorMessage("Gateway unavailable", 503, errorDetail),
1097
- 503
1172
+ formatGatewayErrorMessage(
1173
+ res.status === 429 ? "Gateway rate limited" : "Gateway unavailable",
1174
+ res.status,
1175
+ errorDetail
1176
+ ),
1177
+ res.status
1098
1178
  );
1179
+ const wait = retryAfterMs(res);
1180
+ cause = mergeCause(cause, wait === void 0 ? { kind: "busy" } : { kind: "busy", retryAfterMs: wait });
1099
1181
  continue;
1100
1182
  }
1101
1183
  if (!res.ok) {
@@ -1103,29 +1185,37 @@ var MolphaGateway = class {
1103
1185
  formatGatewayErrorMessage("Gateway error", res.status, errorDetail),
1104
1186
  res.status
1105
1187
  );
1188
+ if (res.status === 409) cause = mergeCause(cause, { kind: "conflict" });
1106
1189
  continue;
1107
1190
  }
1108
1191
  const json = await res.json();
1109
- const payload = json.data ?? (json.status === "completed" ? json : void 0);
1110
- if (json.status === "completed" && payload) {
1111
- return toResult(payload, {
1192
+ if (json.status === "completed" && json.data) {
1193
+ return toResult(json.data, {
1112
1194
  sourceId,
1113
- registryVersion,
1114
- timestamp,
1115
1195
  signaturesRequired,
1116
- bitmap,
1117
1196
  ...requestApiConfig.aggregation ? { aggregation: requestApiConfig.aggregation } : {}
1118
1197
  });
1119
1198
  }
1120
1199
  lastError = new Error(`Gateway returned status: ${json.status}`);
1121
1200
  } catch (err) {
1122
1201
  if (err instanceof UpstreamPaymentRequiredError) throw err;
1123
- if (err instanceof GatewayError && (err.status === 400 || err.status === 401 || err.status === 402)) {
1202
+ if (err instanceof GatewayError && (err.status === 400 || err.status === 401 || err.status === 402 || err.status === 403)) {
1124
1203
  throw err;
1125
1204
  }
1126
1205
  lastError = err;
1127
1206
  }
1128
1207
  }
1208
+ if (restartForRefresh) {
1209
+ round = await this.resolveContext(void 0, encrypt !== void 0);
1210
+ ({ registryVersion, redundancyBuffer } = round);
1211
+ nodeCount = nodeCountOf(round);
1212
+ groupSize = effectiveSelectionSize(signaturesRequired, redundancyBuffer, nodeCount);
1213
+ registryIndexes = Array.from({ length: nodeCount }, (_, index) => index);
1214
+ refreshedOnce = true;
1215
+ retryNow = true;
1216
+ attempt--;
1217
+ continue;
1218
+ }
1129
1219
  if (requote) continue;
1130
1220
  }
1131
1221
  throw lastError instanceof Error ? lastError : new Error("Gateway round failed after retries");
@@ -1154,6 +1244,27 @@ var MolphaGateway = class {
1154
1244
  if (registry.nodeCount !== void 0) return registry;
1155
1245
  return { ...registry, nodes: await this.getNodes() };
1156
1246
  }
1247
+ /**
1248
+ * The gateway's advertised tick grid, read once per endpoint and only when a retry needs it. Any
1249
+ * failure (an old gateway without `/v1/info`, a timeout) means "not advertised", so the caller
1250
+ * falls back to the default; a failure is not cached.
1251
+ */
1252
+ async advertisedTickMs(timeoutMs) {
1253
+ for (const endpoint of this.endpoints) {
1254
+ let pending = this.advertisedInfo.get(endpoint.url);
1255
+ if (!pending) {
1256
+ pending = this.fetchGatewayInfo(endpoint, Math.min(timeoutMs, 3e3)).catch(() => void 0);
1257
+ this.advertisedInfo.set(endpoint.url, pending);
1258
+ }
1259
+ const info = await pending;
1260
+ if (info === void 0) {
1261
+ this.advertisedInfo.delete(endpoint.url);
1262
+ continue;
1263
+ }
1264
+ if (info.tickMs !== void 0) return info.tickMs;
1265
+ }
1266
+ return void 0;
1267
+ }
1157
1268
  /** Gateway PDA bytes for an endpoint, cached per URL. A failed lookup is not cached. */
1158
1269
  resolveGatewayPda(endpoint, timeoutMs) {
1159
1270
  let pending = this.gatewayPdas.get(endpoint.url);
@@ -1216,14 +1327,11 @@ function assertSignaturesRequired(value) {
1216
1327
  return value;
1217
1328
  }
1218
1329
  function unwrapEnvelope(json, path, status) {
1219
- if (json && typeof json === "object" && "data" in json) {
1220
- const wrapped = json;
1221
- if (wrapped.data === void 0) {
1222
- throw new GatewayError(`GET ${path} returned malformed payload`, status);
1223
- }
1224
- return wrapped.data;
1330
+ const wrapped = json;
1331
+ if (wrapped === null || typeof wrapped !== "object" || wrapped.data === void 0) {
1332
+ throw new GatewayError(`GET ${path} returned malformed payload`, status);
1225
1333
  }
1226
- return json;
1334
+ return wrapped.data;
1227
1335
  }
1228
1336
  function selectedNodesForPrivateApiEncryption(nodes, selectedIndexes) {
1229
1337
  if (!Array.isArray(nodes)) {
@@ -1309,11 +1417,12 @@ function flattenGatewayData(data) {
1309
1417
  if (!nested) return data;
1310
1418
  const { attestation: _nested, ...rest } = data;
1311
1419
  const { payload, signature } = nested;
1420
+ const timestamp = payload?.timestamp ?? payload?.canonicalTimestamp;
1312
1421
  return {
1313
1422
  ...rest,
1314
1423
  ...payload?.sourceId !== void 0 ? { sourceId: payload.sourceId } : {},
1315
1424
  ...payload?.value !== void 0 ? { valuePacked: payload.value } : {},
1316
- ...payload?.canonicalTimestamp !== void 0 ? { timestamp: payload.canonicalTimestamp } : {},
1425
+ ...timestamp !== void 0 ? { timestamp } : {},
1317
1426
  ...payload?.registryVersion !== void 0 ? { registryVersion: payload.registryVersion } : {},
1318
1427
  ...payload?.signaturesRequired !== void 0 ? { signaturesRequired: payload.signaturesRequired } : {},
1319
1428
  ...signature?.signersBitmap !== void 0 ? { signersBitmap: signature.signersBitmap } : {},
@@ -1322,6 +1431,12 @@ function flattenGatewayData(data) {
1322
1431
  };
1323
1432
  }
1324
1433
  function toResult(raw, ctx) {
1434
+ if (raw.attestation) {
1435
+ const { payload, signature } = raw.attestation;
1436
+ if (!payload || !signature) {
1437
+ throw new GatewayError("Gateway response is missing the attestation");
1438
+ }
1439
+ }
1325
1440
  const data = flattenGatewayData(raw);
1326
1441
  if (data.sourceId !== void 0 && normalizeHex(data.sourceId) !== ctx.sourceId) {
1327
1442
  throw new GatewayError(
@@ -1358,33 +1473,41 @@ function toResult(raw, ctx) {
1358
1473
  );
1359
1474
  }
1360
1475
  }
1476
+ const fields = {
1477
+ timestamp: data.timestamp,
1478
+ registryVersion: data.registryVersion,
1479
+ value: data.valuePacked,
1480
+ signersBitmap: data.signersBitmap,
1481
+ s: data.s,
1482
+ commitmentAddr: data.commitmentAddr
1483
+ };
1484
+ for (const [name, field] of Object.entries(fields)) {
1485
+ if (field === void 0 || field === "") {
1486
+ throw new GatewayError(`Gateway response is missing ${name}`);
1487
+ }
1488
+ }
1361
1489
  let value = data.value ?? "";
1362
1490
  if (ctx.aggregation) {
1363
- for (const field of ["valuePacked", "signersBitmap", "s", "commitmentAddr"]) {
1364
- if (!data[field]) {
1365
- throw new GatewayError(`Gateway tolerance response is missing ${field}`);
1366
- }
1367
- }
1368
1491
  try {
1369
- value = formatInt256Decimal(data.valuePacked, ctx.aggregation.numeric.decimals);
1492
+ value = formatInt256Decimal(fields.value, ctx.aggregation.numeric.decimals);
1370
1493
  } catch (err) {
1371
1494
  throw new GatewayError(
1372
- `Gateway tolerance response valuePacked is not an int256 word: ${err instanceof Error ? err.message : String(err)}`
1495
+ `Gateway tolerance response value is not an int256 word: ${err instanceof Error ? err.message : String(err)}`
1373
1496
  );
1374
1497
  }
1375
1498
  }
1376
1499
  return {
1377
1500
  payload: {
1378
1501
  sourceId: ctx.sourceId,
1379
- value: data.valuePacked ?? "",
1380
- canonicalTimestamp: data.timestamp ?? ctx.timestamp,
1381
- registryVersion: data.registryVersion ?? ctx.registryVersion,
1502
+ value: fields.value,
1503
+ timestamp: fields.timestamp,
1504
+ registryVersion: fields.registryVersion,
1382
1505
  signaturesRequired: ctx.signaturesRequired
1383
1506
  },
1384
1507
  signature: {
1385
- signersBitmap: data.signersBitmap ?? bytesToHex(ctx.bitmap),
1386
- s: data.s ?? "",
1387
- commitmentAddr: data.commitmentAddr ?? ""
1508
+ signersBitmap: fields.signersBitmap,
1509
+ s: fields.s,
1510
+ commitmentAddr: fields.commitmentAddr
1388
1511
  },
1389
1512
  value,
1390
1513
  fresh: data.fresh ?? true
@@ -6103,7 +6226,7 @@ var molpha_default = {
6103
6226
  {
6104
6227
  code: 6030,
6105
6228
  name: "FeedNotNewer",
6106
- msg: "canonical_timestamp <= feed.canonical_timestamp"
6229
+ msg: "timestamp <= feed.timestamp"
6107
6230
  },
6108
6231
  {
6109
6232
  code: 6031,
@@ -6248,12 +6371,12 @@ var molpha_default = {
6248
6371
  {
6249
6372
  code: 6059,
6250
6373
  name: "AttestationTooStale",
6251
- msg: "canonical_timestamp is older than now - max_settlement_delay_seconds"
6374
+ msg: "timestamp is older than now - max_settlement_delay_seconds"
6252
6375
  },
6253
6376
  {
6254
6377
  code: 6060,
6255
6378
  name: "AttestationInFuture",
6256
- msg: "canonical_timestamp is further ahead of cluster time than the allowed drift"
6379
+ msg: "timestamp is further ahead of cluster time than the allowed drift"
6257
6380
  },
6258
6381
  {
6259
6382
  code: 6061,
@@ -6378,7 +6501,7 @@ var molpha_default = {
6378
6501
  {
6379
6502
  code: 6085,
6380
6503
  name: "RoundOutsideEpoch",
6381
- msg: "canonical_timestamp is outside the epoch window"
6504
+ msg: "timestamp is outside the epoch window"
6382
6505
  },
6383
6506
  {
6384
6507
  code: 6086,
@@ -6454,6 +6577,11 @@ var molpha_default = {
6454
6577
  code: 6100,
6455
6578
  name: "InvalidRentReceiver",
6456
6579
  msg: "rent receiver must be the account that paid for the closed account"
6580
+ },
6581
+ {
6582
+ code: 6101,
6583
+ name: "InvalidInitialTau",
6584
+ msg: "initial_tau is below the minimum ticket threshold"
6457
6585
  }
6458
6586
  ],
6459
6587
  types: [
@@ -6535,7 +6663,7 @@ var molpha_default = {
6535
6663
  type: "u8"
6536
6664
  },
6537
6665
  {
6538
- name: "canonical_timestamp",
6666
+ name: "timestamp",
6539
6667
  type: "u64"
6540
6668
  }
6541
6669
  ]
@@ -7186,7 +7314,7 @@ var molpha_default = {
7186
7314
  type: "pubkey"
7187
7315
  },
7188
7316
  {
7189
- name: "canonical_timestamp",
7317
+ name: "timestamp",
7190
7318
  type: "u64"
7191
7319
  },
7192
7320
  {
@@ -7232,7 +7360,7 @@ var molpha_default = {
7232
7360
  type: "u32"
7233
7361
  },
7234
7362
  {
7235
- name: "canonical_timestamp",
7363
+ name: "timestamp",
7236
7364
  type: "u64"
7237
7365
  },
7238
7366
  {
@@ -7493,6 +7621,18 @@ var molpha_default = {
7493
7621
  {
7494
7622
  name: "protocol_fee_bps",
7495
7623
  type: "u16"
7624
+ },
7625
+ {
7626
+ name: "initial_tau",
7627
+ docs: [
7628
+ "Ticket threshold of the two genesis epochs: a round is a ticket when the 64-bit prefix of its",
7629
+ "ticket hash is below it, so the sampling probability is `initial_tau / 2^64`. Size it as",
7630
+ "`2^64 * target_tickets_per_epoch / expected_rounds_per_epoch`. `u64::MAX` samples every round",
7631
+ "(every round then costs a ticket transaction until the controller, which moves the",
7632
+ "threshold at most 8x per epoch with a two-epoch lag, brings it down). Must be at least",
7633
+ "`TAU_MIN`."
7634
+ ],
7635
+ type: "u64"
7496
7636
  }
7497
7637
  ]
7498
7638
  }
@@ -8287,7 +8427,7 @@ var molpha_default = {
8287
8427
  {
8288
8428
  name: "max_settlement_delay_seconds",
8289
8429
  docs: [
8290
- "Max age in seconds of `canonical_timestamp` relative to cluster time at attestation",
8430
+ "Max age in seconds of `timestamp` relative to cluster time at attestation",
8291
8431
  "ingress, and the grace period of a superseded registry version."
8292
8432
  ],
8293
8433
  type: "u64"
@@ -9326,7 +9466,17 @@ function planIdFromVariant(variant) {
9326
9466
  }
9327
9467
 
9328
9468
  // src/solana/client.ts
9329
- var DEFAULT_COMPUTE_UNIT_LIMIT = 14e5;
9469
+ var MAX_COMPUTE_UNIT_LIMIT = 14e5;
9470
+ function estimateSubmitComputeUnits(signerCount) {
9471
+ if (!Number.isInteger(signerCount) || signerCount < 1) return MAX_COMPUTE_UNIT_LIMIT;
9472
+ const measured = 43e3 + 9200 * signerCount;
9473
+ return Math.min(Math.round(measured * 1.15) + 1e4, MAX_COMPUTE_UNIT_LIMIT);
9474
+ }
9475
+ var REGISTRY_CACHE_MS = 3e4;
9476
+ var KEY_CACHE_LIMIT = 8192;
9477
+ var PRIORITY_FEE_CACHE_MS = 5e3;
9478
+ var AUTO_FEE_PERCENTILE = 0.75;
9479
+ var AUTO_FEE_CAP_MICRO_LAMPORTS = 1e6;
9330
9480
  var MolphaSolanaClient = class _MolphaSolanaClient {
9331
9481
  constructor(program, provider, programId) {
9332
9482
  this.program = program;
@@ -9336,6 +9486,13 @@ var MolphaSolanaClient = class _MolphaSolanaClient {
9336
9486
  program;
9337
9487
  provider;
9338
9488
  programId;
9489
+ // Registry reads and signer keys are shared by every submit that names the same registry
9490
+ // version, so a fleet of feeds reads them once. Promises are cached, so concurrent submits
9491
+ // coalesce; a failed read is dropped and retried by the next call.
9492
+ registryCache = /* @__PURE__ */ new Map();
9493
+ nodeKeyCache = /* @__PURE__ */ new Map();
9494
+ coalitionCache = /* @__PURE__ */ new Map();
9495
+ priorityFeeCache;
9339
9496
  static create(opts) {
9340
9497
  const programId = toSolanaAddress(opts.programId ?? MOLPHA_PROGRAM_ADDRESS);
9341
9498
  const provider = new AnchorProvider(opts.connection, opts.wallet, {
@@ -9494,12 +9651,16 @@ var MolphaSolanaClient = class _MolphaSolanaClient {
9494
9651
  const { payload, signature: schnorr } = attestation;
9495
9652
  const sourceId = toFixedBytes(payload.sourceId, 32, "sourceId");
9496
9653
  const submitter = this.wallet;
9497
- const registry = await this.fetchRegistry(payload.registryVersion);
9654
+ const registry = await this.fetchRegistryCached(payload.registryVersion);
9498
9655
  const remaining = resolveRemainingAccounts(schnorr.signersBitmap, registry);
9499
9656
  assertSignerCount(remaining.length, payload.signaturesRequired, registry);
9500
- const coalitionKey = opts?.coalitionKey ?? await this.computeSignerCoalitionKey(remaining, registry);
9657
+ const coalitionKey = opts?.coalitionKey ?? await this.computeSignerCoalitionKey(remaining, registry, schnorr.signersBitmap);
9501
9658
  const feed = feedPda(sourceId, payload.signaturesRequired, submitter, this.programId);
9502
- const cuIx = setComputeUnitLimit(opts?.computeUnitLimit ?? DEFAULT_COMPUTE_UNIT_LIMIT);
9659
+ const preInstructions = [
9660
+ setComputeUnitLimit(opts?.computeUnitLimit ?? estimateSubmitComputeUnits(remaining.length))
9661
+ ];
9662
+ const price = await this.resolvePriorityFee(opts?.priorityFeeMicroLamports, feed);
9663
+ if (price > 0) preInstructions.push(setComputeUnitPrice(price));
9503
9664
  const signature = await this.methods.submitAttestation(
9504
9665
  buildSubmitAttestationArgs(attestation, coalitionKey, opts?.rawValue)
9505
9666
  ).accountsPartial({
@@ -9508,26 +9669,80 @@ var MolphaSolanaClient = class _MolphaSolanaClient {
9508
9669
  feed,
9509
9670
  protocolConfig: protocolConfigPda(this.programId),
9510
9671
  systemProgram: SYSTEM_PROGRAM_ADDRESS
9511
- }).remainingAccounts(remaining).preInstructions([cuIx]).rpc();
9672
+ }).remainingAccounts(remaining).preInstructions(preInstructions).rpc();
9512
9673
  return { signature, feed };
9513
9674
  }
9514
- /** Sum of the signers' keys, read from their on-chain `Node` accounts (one batched fetch). */
9515
- async computeSignerCoalitionKey(remaining, registry) {
9516
- const accounts = await this.accounts.node.fetchMultiple(
9517
- remaining.map((meta) => meta.pubkey)
9518
- );
9519
- const keys = accounts.map((account, i) => {
9520
- if (!account) {
9521
- throw new Error(
9522
- `Node account ${remaining[i].pubkey.toBase58()} (registry ${registry.version}) does not exist`
9523
- );
9675
+ /** The registry for `version`, reused for {@link REGISTRY_CACHE_MS}. */
9676
+ fetchRegistryCached(version) {
9677
+ const now = Date.now();
9678
+ const hit = this.registryCache.get(version);
9679
+ if (hit && now - hit.at < REGISTRY_CACHE_MS) return hit.value;
9680
+ const value = this.fetchRegistry(version);
9681
+ this.registryCache.set(version, { at: now, value });
9682
+ value.catch(() => {
9683
+ if (this.registryCache.get(version)?.value === value) this.registryCache.delete(version);
9684
+ });
9685
+ return value;
9686
+ }
9687
+ async resolvePriorityFee(option, feed) {
9688
+ if (option === void 0) return 0;
9689
+ if (option !== "auto") {
9690
+ if (!Number.isFinite(option) || option < 0) {
9691
+ throw new Error('priorityFeeMicroLamports must be a non-negative number or "auto"');
9524
9692
  }
9525
- return {
9526
- x: nodeCoordinate(account, "secp256k1PubkeyX", "secp256k1_pubkey_x"),
9527
- y: nodeCoordinate(account, "secp256k1PubkeyY", "secp256k1_pubkey_y")
9528
- };
9693
+ return Math.floor(option);
9694
+ }
9695
+ const now = Date.now();
9696
+ if (!this.priorityFeeCache || now - this.priorityFeeCache.at >= PRIORITY_FEE_CACHE_MS) {
9697
+ const value = (async () => {
9698
+ try {
9699
+ const recent = await this.provider.connection.getRecentPrioritizationFees({
9700
+ lockedWritableAccounts: [new web3.PublicKey(feed)]
9701
+ });
9702
+ const fees = recent.map((r) => r.prioritizationFee).sort((a, b) => a - b);
9703
+ if (fees.length === 0) return 0;
9704
+ const fee = fees[Math.min(fees.length - 1, Math.floor(fees.length * AUTO_FEE_PERCENTILE))];
9705
+ return Math.min(fee, AUTO_FEE_CAP_MICRO_LAMPORTS);
9706
+ } catch {
9707
+ return 0;
9708
+ }
9709
+ })();
9710
+ this.priorityFeeCache = { at: now, value };
9711
+ }
9712
+ return this.priorityFeeCache.value;
9713
+ }
9714
+ /** Sum of the signers' keys, read from their on-chain `Node` accounts (one batched fetch). */
9715
+ computeSignerCoalitionKey(remaining, registry, signersBitmap) {
9716
+ const setKey = `${registry.version}:${signersBitmap}`;
9717
+ const known = this.coalitionCache.get(setKey);
9718
+ if (known) return known;
9719
+ if (this.coalitionCache.size >= KEY_CACHE_LIMIT) this.coalitionCache.clear();
9720
+ const pending = this.sumSignerKeys(remaining, registry);
9721
+ this.coalitionCache.set(setKey, pending);
9722
+ pending.catch(() => {
9723
+ if (this.coalitionCache.get(setKey) === pending) this.coalitionCache.delete(setKey);
9529
9724
  });
9530
- return computeCoalitionKey(keys);
9725
+ return pending;
9726
+ }
9727
+ async sumSignerKeys(remaining, registry) {
9728
+ const addresses = remaining.map((meta) => meta.pubkey);
9729
+ const missing = addresses.filter((a) => !this.nodeKeyCache.has(a.toBase58()));
9730
+ if (missing.length > 0) {
9731
+ const accounts = await this.accounts.node.fetchMultiple(missing);
9732
+ if (this.nodeKeyCache.size + missing.length > KEY_CACHE_LIMIT) this.nodeKeyCache.clear();
9733
+ accounts.forEach((account, i) => {
9734
+ if (!account) {
9735
+ throw new Error(
9736
+ `Node account ${missing[i].toBase58()} (registry ${registry.version}) does not exist`
9737
+ );
9738
+ }
9739
+ this.nodeKeyCache.set(missing[i].toBase58(), {
9740
+ x: nodeCoordinate(account, "secp256k1PubkeyX", "secp256k1_pubkey_x"),
9741
+ y: nodeCoordinate(account, "secp256k1PubkeyY", "secp256k1_pubkey_y")
9742
+ });
9743
+ });
9744
+ }
9745
+ return computeCoalitionKey(addresses.map((a) => this.nodeKeyCache.get(a.toBase58())));
9531
9746
  }
9532
9747
  /**
9533
9748
  * Read the feed written by `submitter` (default: this wallet) for
@@ -9643,7 +9858,7 @@ function buildSubmitAttestationArgs(attestation, coalitionKey, rawValue) {
9643
9858
  sourceId: Array.from(toFixedBytes(payload.sourceId, 32, "sourceId")),
9644
9859
  registryVersion: payload.registryVersion,
9645
9860
  signaturesRequired: payload.signaturesRequired,
9646
- canonicalTimestamp: new BN(payload.canonicalTimestamp)
9861
+ timestamp: new BN(payload.timestamp)
9647
9862
  },
9648
9863
  signature: {
9649
9864
  aggSigS: Array.from(toFixedBytes(signature.s, 32, "signature.s")),
@@ -9699,16 +9914,16 @@ function nodeCoordinate(account, camelCaseField, snakeCaseField) {
9699
9914
  return toFixedBytes(Uint8Array.from(value), 32, `Node.${camelCaseField}`);
9700
9915
  }
9701
9916
  function selectedNodesForVerifier(args) {
9702
- if (args.selectedIndexes.length === 0) {
9917
+ if (args.nodeIndexes.length === 0) {
9703
9918
  throw new Error("Private API node-key verification requires at least one selected index");
9704
9919
  }
9705
- if (args.selectedNodes.length !== args.selectedIndexes.length) {
9920
+ if (args.nodes.length !== args.nodeIndexes.length) {
9706
9921
  throw new Error(
9707
- `Private API node-key verification expected ${args.selectedIndexes.length} selected nodes, got ${args.selectedNodes.length}`
9922
+ `Private API node-key verification expected ${args.nodeIndexes.length} selected nodes, got ${args.nodes.length}`
9708
9923
  );
9709
9924
  }
9710
9925
  const expected = /* @__PURE__ */ new Set();
9711
- for (const index of args.selectedIndexes) {
9926
+ for (const index of args.nodeIndexes) {
9712
9927
  if (!Number.isInteger(index) || index < 0) {
9713
9928
  throw new Error(`Private API selected index must be a non-negative integer: ${index}`);
9714
9929
  }
@@ -9718,7 +9933,7 @@ function selectedNodesForVerifier(args) {
9718
9933
  expected.add(index);
9719
9934
  }
9720
9935
  const byIndex = /* @__PURE__ */ new Map();
9721
- for (const node of args.selectedNodes) {
9936
+ for (const node of args.nodes) {
9722
9937
  if (!Number.isInteger(node.index) || node.index < 0) {
9723
9938
  throw new Error(
9724
9939
  `Private API selected node index must be a non-negative integer: ${node.index}`
@@ -9732,7 +9947,7 @@ function selectedNodesForVerifier(args) {
9732
9947
  }
9733
9948
  byIndex.set(node.index, node);
9734
9949
  }
9735
- return args.selectedIndexes.map((index) => {
9950
+ return args.nodeIndexes.map((index) => {
9736
9951
  const node = byIndex.get(index);
9737
9952
  if (!node) {
9738
9953
  throw new Error(`Private API selected node is missing for index: ${index}`);
@@ -9763,7 +9978,7 @@ function attestationMessageHash(fields) {
9763
9978
  toFixedBytes(fields.sourceId, 32, "sourceId"),
9764
9979
  u32be(fields.registryVersion),
9765
9980
  u8(fields.signaturesRequired),
9766
- u64be(fields.canonicalTimestamp),
9981
+ u64be(fields.timestamp),
9767
9982
  toFixedBytes(fields.signersBitmap, 32, "signersBitmap")
9768
9983
  )
9769
9984
  );
@@ -9776,10 +9991,20 @@ function attestationMessageHashFromAttestation(attestation) {
9776
9991
  signaturesRequired: payload.signaturesRequired,
9777
9992
  signersBitmap: signature.signersBitmap,
9778
9993
  value: payload.value,
9779
- canonicalTimestamp: payload.canonicalTimestamp
9994
+ timestamp: payload.timestamp
9780
9995
  });
9781
9996
  }
9782
9997
 
9998
+ // src/core/timestamp.ts
9999
+ function timestampSeconds(timestamp) {
10000
+ const ms = typeof timestamp === "bigint" ? timestamp : BigInt(timestamp);
10001
+ if (ms < 0n) throw new RangeError("timestamp must not be negative");
10002
+ return Number(ms / 1000n);
10003
+ }
10004
+ function timestampAgeSeconds(timestamp, nowSeconds) {
10005
+ return Math.max(0, nowSeconds - timestampSeconds(timestamp));
10006
+ }
10007
+
9783
10008
  // src/core/verifyCodes.ts
9784
10009
  var VERIFY_CODES = {
9785
10010
  OK: 0,
@@ -9787,11 +10012,11 @@ var VERIFY_CODES = {
9787
10012
  FEED_WITNESS: 1,
9788
10013
  /** The payload's `registryVersion` does not exist on this verifier. */
9789
10014
  BAD_REGISTRY_VERSION: 2,
9790
- /** Structurally invalid input, or a `canonicalTimestamp` in the future when `maxAge != 0`. */
10015
+ /** Structurally invalid input, or a `timestamp` in the future when `maxAge != 0`. */
9791
10016
  MALFORMED: 3,
9792
- /** `canonicalTimestamp` predates the registry version's activation. */
10017
+ /** `timestamp` predates the registry version's activation. */
9793
10018
  NOT_YET_ACTIVE: 4,
9794
- /** The registry version was superseded more than the grace window before `canonicalTimestamp`. */
10019
+ /** The registry version was superseded more than the grace window before `timestamp`. */
9795
10020
  VERSION_EXPIRED: 5,
9796
10021
  /** Reserved. Never returned. */
9797
10022
  COMPROMISED_QUORUM: 6,
@@ -9835,7 +10060,7 @@ var MOLPHA_VERIFIER_ABI = [
9835
10060
  { name: "sourceId", type: "bytes32", internalType: "bytes32" },
9836
10061
  { name: "registryVersion", type: "uint32", internalType: "uint32" },
9837
10062
  { name: "signaturesRequired", type: "uint8", internalType: "uint8" },
9838
- { name: "canonicalTimestamp", type: "uint64", internalType: "uint64" }
10063
+ { name: "timestamp", type: "uint64", internalType: "uint64" }
9839
10064
  ]
9840
10065
  },
9841
10066
  {
@@ -10008,7 +10233,7 @@ function buildEvmVerifierArgs(attestation, options) {
10008
10233
  sourceId: toFixedHex(signed.sourceId, 32, "sourceId"),
10009
10234
  registryVersion: assertUint2(signed.registryVersion, U32_MAX2, "registryVersion"),
10010
10235
  signaturesRequired: assertUint2(signed.signaturesRequired, U8_MAX, "signaturesRequired"),
10011
- canonicalTimestamp: assertBigUint(signed.canonicalTimestamp, U64_MAX, "canonicalTimestamp")
10236
+ timestamp: assertBigUint(signed.timestamp, U64_MAX, "timestamp")
10012
10237
  };
10013
10238
  const signature = {
10014
10239
  signature: toFixedHex(sig.s, 32, "signature"),
@@ -10030,7 +10255,7 @@ function encodeEvmVerifyCalldata(args) {
10030
10255
  u256beFromBigInt(
10031
10256
  BigInt(assertUint2(payload.signaturesRequired, U8_MAX, "signaturesRequired"))
10032
10257
  ),
10033
- u256beFromBigInt(assertBigUint(payload.canonicalTimestamp, U64_MAX, "canonicalTimestamp")),
10258
+ u256beFromBigInt(assertBigUint(payload.timestamp, U64_MAX, "timestamp")),
10034
10259
  word2(signature.signature, 32, "signature"),
10035
10260
  word2(signature.commitment, 20, "commitment"),
10036
10261
  u256beFromBigInt(assertBigUint(signature.signersBitmap, U256_MAX, "signersBitmap")),
@@ -10160,7 +10385,7 @@ function buildStarknetVerifierArgs(attestation, options) {
10160
10385
  source_id: fixedHexToBigInt(signed.sourceId, 32, "sourceId"),
10161
10386
  registry_version: assertUint3(signed.registryVersion, U32_MAX3, "registryVersion"),
10162
10387
  signatures_required: assertUint3(signed.signaturesRequired, U8_MAX2, "signaturesRequired"),
10163
- canonical_timestamp: assertUint3(signed.canonicalTimestamp, U64_MAX2, "canonicalTimestamp")
10388
+ timestamp: assertUint3(signed.timestamp, U64_MAX2, "timestamp")
10164
10389
  };
10165
10390
  const signature = {
10166
10391
  signature: fixedHexToBigInt(sig.s, 32, "signature"),
@@ -10179,7 +10404,7 @@ function encodeStarknetVerifyCalldata(args) {
10179
10404
  ...u256Felts(payload.source_id, "source_id"),
10180
10405
  toFelt(assertUint3(payload.registry_version, U32_MAX3, "registry_version")),
10181
10406
  toFelt(assertUint3(payload.signatures_required, U8_MAX2, "signatures_required")),
10182
- toFelt(assertUint3(payload.canonical_timestamp, U64_MAX2, "canonical_timestamp")),
10407
+ toFelt(assertUint3(payload.timestamp, U64_MAX2, "timestamp")),
10183
10408
  ...u256Felts(signature.signature, "signature"),
10184
10409
  // An Ethereum-style address, so it must fit 160 bits (the verifier reports a wider one
10185
10410
  // as `MALFORMED`; one past the field prime would not even deserialize).
@@ -10211,7 +10436,139 @@ function parseStarknetVerifyResult(result) {
10211
10436
  return { success, code, reason: verifyCodeName(code) ?? "UNKNOWN" };
10212
10437
  }
10213
10438
 
10439
+ // src/bulk.ts
10440
+ var DEFAULT_CONCURRENCY = 32;
10441
+ var MAX_DEFAULT_CONCURRENCY = 64;
10442
+ var DEFAULT_SUBMIT_CONCURRENCY = 8;
10443
+ var BACKOFF_COOLDOWN_MS = 1e3;
10444
+ var AdaptiveLimiter = class {
10445
+ constructor(initial, max, now = Date.now) {
10446
+ this.max = max;
10447
+ this.now = now;
10448
+ this.max = Math.max(1, max);
10449
+ this.limit_ = Math.min(Math.max(1, Math.floor(initial)), this.max);
10450
+ }
10451
+ max;
10452
+ now;
10453
+ limit_;
10454
+ inFlight = 0;
10455
+ successes = 0;
10456
+ lastCut = -Infinity;
10457
+ waiters = [];
10458
+ get limit() {
10459
+ return this.limit_;
10460
+ }
10461
+ async acquire() {
10462
+ if (this.inFlight < this.limit_) {
10463
+ this.inFlight++;
10464
+ return;
10465
+ }
10466
+ await new Promise((resolve) => this.waiters.push(resolve));
10467
+ }
10468
+ /** Release a slot. `busy` reports that the gateway answered at capacity. */
10469
+ release(outcome) {
10470
+ this.inFlight--;
10471
+ if (outcome === "busy") {
10472
+ this.successes = 0;
10473
+ const t = this.now();
10474
+ if (t - this.lastCut >= BACKOFF_COOLDOWN_MS) {
10475
+ this.limit_ = Math.max(1, Math.floor(this.limit_ / 2));
10476
+ this.lastCut = t;
10477
+ }
10478
+ } else if (outcome === "ok") {
10479
+ if (++this.successes >= this.limit_ && this.limit_ < this.max) {
10480
+ this.limit_++;
10481
+ this.successes = 0;
10482
+ }
10483
+ }
10484
+ while (this.waiters.length > 0 && this.inFlight < this.limit_) {
10485
+ this.inFlight++;
10486
+ this.waiters.shift()();
10487
+ }
10488
+ }
10489
+ };
10490
+ function spreadOffsetMs(sourceIdHex, spreadMs) {
10491
+ if (spreadMs <= 0) return 0;
10492
+ return Number.parseInt(sourceIdHex.slice(0, 8), 16) % spreadMs;
10493
+ }
10494
+ var isBusy = (err) => err instanceof GatewayError && (err.status === 503 || err.status === 429);
10495
+ async function requestMany(sdk, feeds, opts = {}) {
10496
+ const results = new Array(feeds.length);
10497
+ if (feeds.length === 0) return results;
10498
+ const advertised = await sdk.gateway.fetchGatewayInfo(sdkFirstEndpoint(sdk)).catch(() => void 0);
10499
+ const ceiling = opts.concurrency ?? (advertised?.maxInflightRounds ? Math.min(Math.max(1, Math.floor(advertised.maxInflightRounds / 2)), MAX_DEFAULT_CONCURRENCY) : DEFAULT_CONCURRENCY);
10500
+ const requests = new AdaptiveLimiter(ceiling, ceiling);
10501
+ const submits = new AdaptiveLimiter(
10502
+ opts.submitConcurrency ?? DEFAULT_SUBMIT_CONCURRENCY,
10503
+ opts.submitConcurrency ?? DEFAULT_SUBMIT_CONCURRENCY
10504
+ );
10505
+ const context = await sdk.gateway.prepareContext();
10506
+ const submit = opts.submit ?? true;
10507
+ const started = Date.now();
10508
+ const one = async (index) => {
10509
+ const feed = feeds[index];
10510
+ const result = { index, ok: false, ...feed.label !== void 0 ? { label: feed.label } : {} };
10511
+ results[index] = result;
10512
+ try {
10513
+ const wait = spreadOffsetMs(deriveSourceIdString(feed.apiConfig), opts.spreadMs ?? 0) - (Date.now() - started);
10514
+ if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
10515
+ if (opts.signal?.aborted) {
10516
+ result.error = new Error("aborted before the feed was started");
10517
+ return;
10518
+ }
10519
+ await requests.acquire();
10520
+ let outcome = "other";
10521
+ try {
10522
+ result.attestation = await sdk.gateway.requestSignedData({
10523
+ ...opts.request,
10524
+ apiConfig: feed.apiConfig,
10525
+ signaturesRequired: feed.signaturesRequired,
10526
+ context
10527
+ });
10528
+ result.ok = true;
10529
+ outcome = "ok";
10530
+ } catch (err) {
10531
+ result.error = err;
10532
+ outcome = isBusy(err) ? "busy" : "other";
10533
+ } finally {
10534
+ requests.release(outcome);
10535
+ }
10536
+ if (result.ok && submit && result.attestation) {
10537
+ await submits.acquire();
10538
+ try {
10539
+ const sent = await sdk.solana.submitAttestation(result.attestation);
10540
+ result.signature = sent.signature;
10541
+ result.feedAddress = sent.feed;
10542
+ } catch (err) {
10543
+ result.submitError = err;
10544
+ } finally {
10545
+ submits.release("other");
10546
+ }
10547
+ }
10548
+ } finally {
10549
+ opts.onResult?.(result);
10550
+ }
10551
+ };
10552
+ await Promise.all(feeds.map((_, index) => one(index)));
10553
+ return results;
10554
+ }
10555
+ function sdkFirstEndpoint(sdk) {
10556
+ return sdk.gateway.endpointUrls()[0];
10557
+ }
10558
+
10214
10559
  // src/index.ts
10560
+ var SubmitFailedError = class extends Error {
10561
+ constructor(result, cause) {
10562
+ super(
10563
+ `Gateway round succeeded but submitting the attestation failed: ${cause instanceof Error ? cause.message : String(cause)}`
10564
+ );
10565
+ this.result = result;
10566
+ this.cause = cause;
10567
+ this.name = "SubmitFailedError";
10568
+ }
10569
+ result;
10570
+ cause;
10571
+ };
10215
10572
  var MolphaSDK = class {
10216
10573
  gateway;
10217
10574
  solana;
@@ -10241,11 +10598,15 @@ var MolphaSDK = class {
10241
10598
  */
10242
10599
  async requestAndSubmit(opts) {
10243
10600
  const result = await this.gateway.requestSignedData(opts);
10244
- const { signature, feed } = await this.solana.submitAttestation(result);
10245
- return { result, signature, feed };
10601
+ try {
10602
+ const { signature, feed } = await this.solana.submitAttestation(result);
10603
+ return { result, signature, feed };
10604
+ } catch (cause) {
10605
+ throw new SubmitFailedError(result, cause);
10606
+ }
10246
10607
  }
10247
10608
  };
10248
10609
 
10249
- export { AggregationConfigError, DEFAULT_GATEWAY_ENDPOINT, EIP712_DOMAIN_TYPEHASH, GatewayError, 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, MolphaGateway, MolphaSDK, MolphaSolanaClient, PlanType, REQUEST_AUTH_DOMAIN, 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, evmAddressFromPrivateKey, feedPda, formatInt256Decimal, gatewayPda, gatewaySignerFromWallet, getMolphaStarknetVerifierAddress, hashRequestAuth, hexToBytes, nodePda, normalizeEndpoint, normalizeSecp256k1PublicKeyHex, parseEvmVerifyResult, parseGatewayInfo, parseStarknetVerifyResult, parseUpstreamQuote, planIdFromVariant, planPda, planVariant, probeSource, protocolConfigPda, registryPda, registryStatePda, resolveRemainingAccounts, secp256k1PublicKeyFromCoordinates, selectedIndices, signSourcePayments, signerFromKeypair, signersBitmapToDecimal, signersBitmapToStarknetUint256, signersBitmapToUint256, subscriptionPda, toChecksumAddress, toFixedBytes, toFixedHex, transferWithAuthorizationDigest, transferWithAuthorizationHash, u256beFromBigInt, u32be, u32le, u64be, u64le, u8, utf8, validateSuppliedTerms, verifyCodeName };
10610
+ 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_IDL, MOLPHA_PROGRAM_ADDRESS, MOLPHA_PROGRAM_ID, 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 };
10250
10611
  //# sourceMappingURL=index.js.map
10251
10612
  //# sourceMappingURL=index.js.map