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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
 
@@ -775,7 +775,16 @@ function parseGatewayInfo(data) {
775
775
  if (programId !== void 0 && typeof programId !== "string") {
776
776
  throw new Error("GET /v1/info programId must be a base58 string");
777
777
  }
778
- 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;
779
788
  }
780
789
 
781
790
  // src/gateway/index.ts
@@ -786,6 +795,37 @@ function msUntilNextTick(nowMs, tickMs) {
786
795
  return tickMs - nowMs % tickMs + 1;
787
796
  }
788
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
+ }
789
829
  var DEFAULT_GATEWAY_ENDPOINT = "https://dev-gateway.molpha.io/";
790
830
  var GatewayError = class extends Error {
791
831
  constructor(message, status) {
@@ -808,6 +848,9 @@ var MolphaGateway = class {
808
848
  programIdBytes;
809
849
  /** Gateway PDA per endpoint URL; resolved once per client lifetime. */
810
850
  gatewayPdas = /* @__PURE__ */ new Map();
851
+ /** Advertised timing per endpoint URL, read lazily on the first retry. */
852
+ advertisedInfo = /* @__PURE__ */ new Map();
853
+ random;
811
854
  constructor(endpoints, getRegistrySelectionConfig = async () => {
812
855
  throw new Error(
813
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())"
@@ -828,6 +871,11 @@ var MolphaGateway = class {
828
871
  this.allowUnverifiedNodeKeysForPrivateApi = options.allowUnverifiedNodeKeysForPrivateApi ?? false;
829
872
  this.programId = options.programId ?? MOLPHA_PROGRAM_ID;
830
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);
831
879
  }
832
880
  /** Tries endpoints in order; returns the first node list it can fetch. */
833
881
  async getNodes() {
@@ -931,8 +979,8 @@ var MolphaGateway = class {
931
979
  encrypt,
932
980
  sourcePayment,
933
981
  maxAge = 60,
934
- maxRetries = 15,
935
- timeoutMs = 5e3
982
+ maxRetries = DEFAULT_MAX_RETRIES,
983
+ timeoutMs = DEFAULT_ROUND_TIMEOUT_MS
936
984
  } = opts;
937
985
  const signaturesRequired = assertSignaturesRequired(opts.signaturesRequired);
938
986
  const verifyNodeKeys = opts.verifyNodeKeys ?? this.verifyNodeKeys;
@@ -954,17 +1002,19 @@ var MolphaGateway = class {
954
1002
  }
955
1003
  const consumerAuthority = opts.consumerAuthority ?? this.defaultConsumerAuthority ?? subscriptionOwner;
956
1004
  const authSigner = signer ?? this.defaultSigner;
957
- const round = await this.resolveContext(opts.context, encrypt !== void 0);
958
- const { registryVersion, redundancyBuffer } = round;
959
- if (round.nodeCount !== void 0 && round.nodes !== void 0 && round.nodes.length !== round.nodeCount) {
960
- throw new Error(
961
- `Gateway node list has ${round.nodes.length} nodes but registry ${registryVersion} has node_count ${round.nodeCount} \u2014 refresh the round context`
962
- );
963
- }
964
- const nodeCount = round.nodeCount ?? round.nodes?.length;
965
- if (nodeCount === void 0) {
966
- throw new Error("Round context resolved neither nodeCount nor nodes");
967
- }
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);
968
1018
  let terms = null;
969
1019
  let authorizations = 0;
970
1020
  let requoted = false;
@@ -987,11 +1037,19 @@ var MolphaGateway = class {
987
1037
  }
988
1038
  }
989
1039
  let lastError;
990
- const tickMs = opts.tickMs ?? DEFAULT_TICK_MS;
991
- const groupSize = effectiveSelectionSize(signaturesRequired, redundancyBuffer, nodeCount);
992
- const registryIndexes = Array.from({ length: nodeCount }, (_, index) => index);
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;
993
1046
  for (let attempt = 0; attempt < maxRetries; attempt++) {
994
- if (attempt > 0) await sleep(msUntilNextTick(Date.now(), tickMs));
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" };
995
1053
  let encKeyBundle;
996
1054
  if (encrypt) {
997
1055
  const recipients = selectedNodesForPrivateApiEncryption(round.nodes ?? [], registryIndexes);
@@ -1025,6 +1083,7 @@ var MolphaGateway = class {
1025
1083
  if (encKeyBundle) baseBody.encKeyBundle = encKeyBundle;
1026
1084
  if (sourcePayments) baseBody.sourcePayments = sourcePayments;
1027
1085
  let requote = false;
1086
+ let restartForRefresh = false;
1028
1087
  for (const endpoint of this.endpoints) {
1029
1088
  try {
1030
1089
  let authSig = ZERO_AUTH_SIG;
@@ -1098,17 +1157,27 @@ var MolphaGateway = class {
1098
1157
  break;
1099
1158
  }
1100
1159
  const errorDetail = !res.ok ? await parseGatewayErrorDetail(res) : void 0;
1101
- 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) {
1102
1165
  throw new GatewayError(
1103
1166
  formatGatewayErrorMessage("Gateway rejected request", res.status, errorDetail),
1104
1167
  res.status
1105
1168
  );
1106
1169
  }
1107
- if (res.status === 503) {
1170
+ if (res.status === 503 || res.status === 429) {
1108
1171
  lastError = new GatewayError(
1109
- formatGatewayErrorMessage("Gateway unavailable", 503, errorDetail),
1110
- 503
1172
+ formatGatewayErrorMessage(
1173
+ res.status === 429 ? "Gateway rate limited" : "Gateway unavailable",
1174
+ res.status,
1175
+ errorDetail
1176
+ ),
1177
+ res.status
1111
1178
  );
1179
+ const wait = retryAfterMs(res);
1180
+ cause = mergeCause(cause, wait === void 0 ? { kind: "busy" } : { kind: "busy", retryAfterMs: wait });
1112
1181
  continue;
1113
1182
  }
1114
1183
  if (!res.ok) {
@@ -1116,6 +1185,7 @@ var MolphaGateway = class {
1116
1185
  formatGatewayErrorMessage("Gateway error", res.status, errorDetail),
1117
1186
  res.status
1118
1187
  );
1188
+ if (res.status === 409) cause = mergeCause(cause, { kind: "conflict" });
1119
1189
  continue;
1120
1190
  }
1121
1191
  const json = await res.json();
@@ -1129,12 +1199,23 @@ var MolphaGateway = class {
1129
1199
  lastError = new Error(`Gateway returned status: ${json.status}`);
1130
1200
  } catch (err) {
1131
1201
  if (err instanceof UpstreamPaymentRequiredError) throw err;
1132
- 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)) {
1133
1203
  throw err;
1134
1204
  }
1135
1205
  lastError = err;
1136
1206
  }
1137
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
+ }
1138
1219
  if (requote) continue;
1139
1220
  }
1140
1221
  throw lastError instanceof Error ? lastError : new Error("Gateway round failed after retries");
@@ -1163,6 +1244,27 @@ var MolphaGateway = class {
1163
1244
  if (registry.nodeCount !== void 0) return registry;
1164
1245
  return { ...registry, nodes: await this.getNodes() };
1165
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
+ }
1166
1268
  /** Gateway PDA bytes for an endpoint, cached per URL. A failed lookup is not cached. */
1167
1269
  resolveGatewayPda(endpoint, timeoutMs) {
1168
1270
  let pending = this.gatewayPdas.get(endpoint.url);
@@ -6475,6 +6577,11 @@ var molpha_default = {
6475
6577
  code: 6100,
6476
6578
  name: "InvalidRentReceiver",
6477
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"
6478
6585
  }
6479
6586
  ],
6480
6587
  types: [
@@ -7514,6 +7621,18 @@ var molpha_default = {
7514
7621
  {
7515
7622
  name: "protocol_fee_bps",
7516
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"
7517
7636
  }
7518
7637
  ]
7519
7638
  }
@@ -9347,7 +9466,17 @@ function planIdFromVariant(variant) {
9347
9466
  }
9348
9467
 
9349
9468
  // src/solana/client.ts
9350
- 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;
9351
9480
  var MolphaSolanaClient = class _MolphaSolanaClient {
9352
9481
  constructor(program, provider, programId) {
9353
9482
  this.program = program;
@@ -9357,6 +9486,13 @@ var MolphaSolanaClient = class _MolphaSolanaClient {
9357
9486
  program;
9358
9487
  provider;
9359
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;
9360
9496
  static create(opts) {
9361
9497
  const programId = toSolanaAddress(opts.programId ?? MOLPHA_PROGRAM_ADDRESS);
9362
9498
  const provider = new AnchorProvider(opts.connection, opts.wallet, {
@@ -9515,12 +9651,16 @@ var MolphaSolanaClient = class _MolphaSolanaClient {
9515
9651
  const { payload, signature: schnorr } = attestation;
9516
9652
  const sourceId = toFixedBytes(payload.sourceId, 32, "sourceId");
9517
9653
  const submitter = this.wallet;
9518
- const registry = await this.fetchRegistry(payload.registryVersion);
9654
+ const registry = await this.fetchRegistryCached(payload.registryVersion);
9519
9655
  const remaining = resolveRemainingAccounts(schnorr.signersBitmap, registry);
9520
9656
  assertSignerCount(remaining.length, payload.signaturesRequired, registry);
9521
- const coalitionKey = opts?.coalitionKey ?? await this.computeSignerCoalitionKey(remaining, registry);
9657
+ const coalitionKey = opts?.coalitionKey ?? await this.computeSignerCoalitionKey(remaining, registry, schnorr.signersBitmap);
9522
9658
  const feed = feedPda(sourceId, payload.signaturesRequired, submitter, this.programId);
9523
- 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));
9524
9664
  const signature = await this.methods.submitAttestation(
9525
9665
  buildSubmitAttestationArgs(attestation, coalitionKey, opts?.rawValue)
9526
9666
  ).accountsPartial({
@@ -9529,26 +9669,80 @@ var MolphaSolanaClient = class _MolphaSolanaClient {
9529
9669
  feed,
9530
9670
  protocolConfig: protocolConfigPda(this.programId),
9531
9671
  systemProgram: SYSTEM_PROGRAM_ADDRESS
9532
- }).remainingAccounts(remaining).preInstructions([cuIx]).rpc();
9672
+ }).remainingAccounts(remaining).preInstructions(preInstructions).rpc();
9533
9673
  return { signature, feed };
9534
9674
  }
9535
- /** Sum of the signers' keys, read from their on-chain `Node` accounts (one batched fetch). */
9536
- async computeSignerCoalitionKey(remaining, registry) {
9537
- const accounts = await this.accounts.node.fetchMultiple(
9538
- remaining.map((meta) => meta.pubkey)
9539
- );
9540
- const keys = accounts.map((account, i) => {
9541
- if (!account) {
9542
- throw new Error(
9543
- `Node account ${remaining[i].pubkey.toBase58()} (registry ${registry.version}) does not exist`
9544
- );
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"');
9545
9692
  }
9546
- return {
9547
- x: nodeCoordinate(account, "secp256k1PubkeyX", "secp256k1_pubkey_x"),
9548
- y: nodeCoordinate(account, "secp256k1PubkeyY", "secp256k1_pubkey_y")
9549
- };
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);
9550
9724
  });
9551
- 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())));
9552
9746
  }
9553
9747
  /**
9554
9748
  * Read the feed written by `submitter` (default: this wallet) for
@@ -10242,7 +10436,139 @@ function parseStarknetVerifyResult(result) {
10242
10436
  return { success, code, reason: verifyCodeName(code) ?? "UNKNOWN" };
10243
10437
  }
10244
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
+
10245
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
+ };
10246
10572
  var MolphaSDK = class {
10247
10573
  gateway;
10248
10574
  solana;
@@ -10272,11 +10598,15 @@ var MolphaSDK = class {
10272
10598
  */
10273
10599
  async requestAndSubmit(opts) {
10274
10600
  const result = await this.gateway.requestSignedData(opts);
10275
- const { signature, feed } = await this.solana.submitAttestation(result);
10276
- 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
+ }
10277
10607
  }
10278
10608
  };
10279
10609
 
10280
- 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, SELECTION_WINDOW_MS, 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, msUntilNextTick, nodePda, normalizeEndpoint, normalizeSecp256k1PublicKeyHex, parseEvmVerifyResult, parseGatewayInfo, parseStarknetVerifyResult, parseUpstreamQuote, planIdFromVariant, planPda, planVariant, probeSource, protocolConfigPda, registryPda, registryStatePda, resolveRemainingAccounts, secp256k1PublicKeyFromCoordinates, selectedIndices, signSourcePayments, signerFromKeypair, signersBitmapToDecimal, signersBitmapToStarknetUint256, signersBitmapToUint256, subscriptionPda, timestampAgeSeconds, timestampSeconds, 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 };
10281
10611
  //# sourceMappingURL=index.js.map
10282
10612
  //# sourceMappingURL=index.js.map