signet.js 0.2.0 → 0.3.0

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.cjs CHANGED
@@ -2695,24 +2695,33 @@ const EMIT_CPI_INSTRUCTION_DISCRIMINATOR = Buffer.from([
2695
2695
  ]);
2696
2696
  var CpiEventParser = class {
2697
2697
  /**
2698
- * Parse CPI events from a transaction (emit_cpi! pattern)
2698
+ * Parse CPI events from an already-fetched transaction (emit_cpi! pattern).
2699
2699
  */
2700
- static async parseCpiEvents(connection, signature, targetProgramId, program) {
2700
+ static parseCpiEventsFromTransaction(tx, targetProgramId, program) {
2701
2701
  const events$1 = [];
2702
+ if (!tx?.meta?.innerInstructions) return events$1;
2703
+ for (const innerIxSet of tx.meta.innerInstructions) for (const instruction of innerIxSet.instructions) {
2704
+ if (!("programId" in instruction) || !("data" in instruction)) continue;
2705
+ if (instruction.programId.toString() !== targetProgramId) continue;
2706
+ const parsedEvent = this.parseInstruction(instruction.data, program);
2707
+ if (parsedEvent) events$1.push(parsedEvent);
2708
+ }
2709
+ return events$1;
2710
+ }
2711
+ /**
2712
+ * Fetch a transaction by signature and parse its CPI events.
2713
+ * Used by the subscription path where only the signature is available.
2714
+ */
2715
+ static async fetchAndParseCpiEvents(connection, signature, targetProgramId, program) {
2702
2716
  try {
2703
2717
  const tx = await connection.getParsedTransaction(signature, {
2704
2718
  commitment: "confirmed",
2705
2719
  maxSupportedTransactionVersion: 0
2706
2720
  });
2707
- if (!tx?.meta?.innerInstructions) return events$1;
2708
- for (const innerIxSet of tx.meta.innerInstructions) for (const instruction of innerIxSet.instructions) {
2709
- if (!("programId" in instruction) || !("data" in instruction)) continue;
2710
- if (instruction.programId.toString() !== targetProgramId) continue;
2711
- const parsedEvent = this.parseInstruction(instruction.data, program);
2712
- if (parsedEvent) events$1.push(parsedEvent);
2713
- }
2714
- } catch {}
2715
- return events$1;
2721
+ return this.parseCpiEventsFromTransaction(tx, targetProgramId, program);
2722
+ } catch {
2723
+ return [];
2724
+ }
2716
2725
  }
2717
2726
  /**
2718
2727
  * Parse CPI event from instruction data
@@ -2742,7 +2751,7 @@ var CpiEventParser = class {
2742
2751
  return connection.onLogs(program.programId, (logs, context) => {
2743
2752
  if (logs.err) return;
2744
2753
  (async () => {
2745
- const events$1 = await this.parseCpiEvents(connection, logs.signature, program.programId.toString(), program);
2754
+ const events$1 = await this.fetchAndParseCpiEvents(connection, logs.signature, program.programId.toString(), program);
2746
2755
  for (const event of events$1) {
2747
2756
  const handler = eventHandlers.get(event.name);
2748
2757
  if (handler) try {
@@ -2842,15 +2851,15 @@ var ChainSignatureContract$1 = class extends ChainSignatureContract {
2842
2851
  }
2843
2852
  /**
2844
2853
  * Sends a transaction to the program to request a signature, then
2845
- * polls for the signature result. If the signature is not found within the retry
2846
- * parameters, it will throw an error.
2854
+ * races a WebSocket listener against polling backfill to find the result.
2855
+ * If the signature is not found within the timeout, it will throw an error.
2847
2856
  */
2848
2857
  async sign(args, options) {
2849
2858
  const algo = options?.sign?.algo ?? "";
2850
2859
  const dest = options?.sign?.dest ?? "";
2851
2860
  const params = options?.sign?.params ?? "";
2852
2861
  const delay = options?.retry?.delay ?? 5e3;
2853
- const retryCount = options?.retry?.retryCount ?? 12;
2862
+ const timeoutMs = delay * (options?.retry?.retryCount ?? 12);
2854
2863
  if (options?.remainingAccounts?.filter((acc) => acc.isSigner)?.some((acc) => !options?.remainingSigners?.some((signer) => signer.publicKey.equals(acc.pubkey)))) throw new Error("All accounts marked as signers must have a corresponding signer");
2855
2864
  const requestId = this.getRequestId(args, {
2856
2865
  algo,
@@ -2868,24 +2877,38 @@ var ChainSignatureContract$1 = class extends ChainSignatureContract {
2868
2877
  const transaction = new __solana_web3_js.Transaction().add(instruction);
2869
2878
  transaction.feePayer = this.provider.wallet.publicKey;
2870
2879
  const hash = await this.sendAndConfirmWithoutWebSocket(transaction, options?.remainingSigners);
2880
+ const controller = new AbortController();
2881
+ const successPromise = this.waitForEvent({
2882
+ eventName: "signatureRespondedEvent",
2883
+ requestId,
2884
+ signer: this.programId,
2885
+ afterSignature: hash,
2886
+ timeoutMs,
2887
+ backfillIntervalMs: delay,
2888
+ signal: controller.signal
2889
+ });
2890
+ const errorPromise = this.waitForEvent({
2891
+ eventName: "signatureErrorEvent",
2892
+ requestId,
2893
+ signer: this.programId,
2894
+ afterSignature: hash,
2895
+ timeoutMs,
2896
+ backfillIntervalMs: delay,
2897
+ signal: controller.signal
2898
+ }).then((err) => {
2899
+ throw new SignatureContractError(err.error, requestId, { hash });
2900
+ });
2901
+ successPromise.catch(() => {});
2902
+ errorPromise.catch(() => {});
2871
2903
  try {
2872
- const pollResult = await this.pollForRequestId({
2873
- requestId,
2874
- payload: args.payload,
2875
- path: args.path,
2876
- afterSignature: hash,
2877
- options: {
2878
- delay,
2879
- retryCount
2880
- }
2881
- });
2882
- if (!pollResult) throw new SignatureNotFoundError(requestId, { hash });
2883
- if ("error" in pollResult) throw new SignatureContractError(pollResult.error, requestId, { hash });
2884
- if (!await verifyRecoveredAddress(pollResult, args.payload, this.requesterAddress, args.path, this, args.key_version)) throw new SigningError(requestId, { hash }, /* @__PURE__ */ new Error("Signature verification failed: recovered address does not match expected address"));
2885
- return pollResult;
2904
+ const result = await Promise.race([successPromise, errorPromise]);
2905
+ if (!await verifyRecoveredAddress(result, args.payload, this.requesterAddress, args.path, this, args.key_version)) throw new SigningError(requestId, { hash }, /* @__PURE__ */ new Error("Signature verification failed: recovered address does not match expected address"));
2906
+ return result;
2886
2907
  } catch (error) {
2887
2908
  if (error instanceof SignatureNotFoundError || error instanceof SignatureContractError) throw error;
2888
2909
  else throw new SigningError(requestId, { hash }, error instanceof Error ? error : void 0);
2910
+ } finally {
2911
+ controller.abort();
2889
2912
  }
2890
2913
  }
2891
2914
  async sendAndConfirmWithoutWebSocket(transaction, signers) {
@@ -2909,70 +2932,147 @@ var ChainSignatureContract$1 = class extends ChainSignatureContract {
2909
2932
  throw new __solana_web3_js.TransactionExpiredTimeoutError(signature, timeout / 1e3);
2910
2933
  }
2911
2934
  /**
2912
- * Polls for signature or error events matching the given requestId starting from the solana transaction with signature afterSignature.
2913
- * Returns a signature, error data, or undefined if nothing is found.
2935
+ * Waits for a specific event matching the given requestId by combining
2936
+ * a WebSocket listener (real-time) with polling backfill (resilience).
2914
2937
  */
2915
- async pollForRequestId({ requestId, payload: _payload, path: _path, afterSignature, options }) {
2916
- const delay = options?.delay ?? 5e3;
2917
- const retryCount = options?.retryCount ?? 12;
2918
- let lastCheckedSignature = afterSignature;
2919
- for (let i = 0; i < retryCount; i++) {
2920
- try {
2921
- const signatures = await this.connection.getSignaturesForAddress(this.programId, {
2922
- until: lastCheckedSignature,
2923
- limit: 50
2924
- }, "confirmed");
2925
- if (signatures.length > 0) lastCheckedSignature = signatures[signatures.length - 1].signature;
2926
- for (const sig of signatures) {
2927
- const tx = await this.connection.getParsedTransaction(sig.signature, {
2928
- commitment: "confirmed",
2929
- maxSupportedTransactionVersion: 0
2938
+ async waitForEvent(options) {
2939
+ const { eventName, requestId, signer, afterSignature, timeoutMs = 6e4, backfillIntervalMs = 5e3, backfillLimit = 50, signal } = options;
2940
+ return await new Promise((resolve, reject) => {
2941
+ let settled = false;
2942
+ const seenSignatures = /* @__PURE__ */ new Set();
2943
+ let lastCheckedSignature = afterSignature;
2944
+ const cleanupFns = [];
2945
+ const cleanup = () => {
2946
+ for (const fn of cleanupFns) try {
2947
+ fn();
2948
+ } catch {}
2949
+ };
2950
+ const settle = (action) => {
2951
+ if (settled) return;
2952
+ settled = true;
2953
+ cleanup();
2954
+ action();
2955
+ };
2956
+ const processEvent = (name, data, txSignature) => {
2957
+ if (settled) return false;
2958
+ if (txSignature && seenSignatures.has(txSignature)) return false;
2959
+ if (txSignature) seenSignatures.add(txSignature);
2960
+ if (name !== eventName) return false;
2961
+ const result = this.mapEventForName(eventName, data, requestId);
2962
+ if (result !== void 0) {
2963
+ settle(() => {
2964
+ resolve(result);
2930
2965
  });
2931
- if (tx?.meta?.logMessages) {
2932
- const result = await this.parseLogsForEvents(tx.meta.logMessages, requestId, sig.signature);
2933
- if (result) return result;
2934
- }
2966
+ return true;
2935
2967
  }
2936
- } catch (error) {
2937
- console.error("Error checking for events:", error);
2938
- }
2939
- if (i < retryCount - 1) {
2940
- console.log(`Retrying get signature: ${i + 1}/${retryCount}`);
2941
- await new Promise((resolve) => setTimeout(resolve, delay));
2968
+ return false;
2969
+ };
2970
+ if (signal) {
2971
+ if (signal.aborted) {
2972
+ settle(() => {
2973
+ reject(signal.reason ?? /* @__PURE__ */ new Error("Aborted"));
2974
+ });
2975
+ return;
2976
+ }
2977
+ const onAbort = () => {
2978
+ settle(() => {
2979
+ reject(signal.reason ?? /* @__PURE__ */ new Error("Aborted"));
2980
+ });
2981
+ };
2982
+ signal.addEventListener("abort", onAbort, { once: true });
2983
+ cleanupFns.push(() => {
2984
+ signal.removeEventListener("abort", onAbort);
2985
+ });
2942
2986
  }
2943
- }
2987
+ const timeoutId = setTimeout(() => {
2988
+ settle(() => {
2989
+ reject(new SignatureNotFoundError(requestId));
2990
+ });
2991
+ }, timeoutMs);
2992
+ cleanupFns.push(() => {
2993
+ clearTimeout(timeoutId);
2994
+ });
2995
+ const parser = new __coral_xyz_anchor.EventParser(this.program.programId, this.program.coder);
2996
+ const subId = this.connection.onLogs(signer, (logs, _context) => {
2997
+ if (settled) return;
2998
+ if (logs.err) return;
2999
+ for (const evt of parser.parseLogs(logs.logs)) {
3000
+ if (!evt) continue;
3001
+ if (processEvent(evt.name, evt.data, logs.signature)) return;
3002
+ }
3003
+ CpiEventParser.fetchAndParseCpiEvents(this.connection, logs.signature, this.programId.toString(), this.program).then((cpiEvents) => {
3004
+ for (const event of cpiEvents) if (processEvent(event.name, event.data, logs.signature)) return;
3005
+ });
3006
+ }, "confirmed");
3007
+ cleanupFns.push(() => {
3008
+ this.connection.removeOnLogsListener(subId);
3009
+ });
3010
+ const runBackfill = async () => {
3011
+ if (settled) return;
3012
+ try {
3013
+ const signatures = await this.connection.getSignaturesForAddress(signer, {
3014
+ until: lastCheckedSignature,
3015
+ limit: backfillLimit
3016
+ }, "confirmed");
3017
+ if (signatures.length > 0) lastCheckedSignature = signatures[0].signature;
3018
+ for (const sig of signatures) {
3019
+ if (settled) return;
3020
+ if (seenSignatures.has(sig.signature)) continue;
3021
+ const tx = await this.connection.getParsedTransaction(sig.signature, {
3022
+ commitment: "confirmed",
3023
+ maxSupportedTransactionVersion: 0
3024
+ });
3025
+ if (!tx) continue;
3026
+ const cpiEvents = CpiEventParser.parseCpiEventsFromTransaction(tx, this.programId.toString(), this.program);
3027
+ for (const event of cpiEvents) if (processEvent(event.name, event.data, sig.signature)) return;
3028
+ const logs = tx.meta?.logMessages;
3029
+ if (logs) for (const evt of parser.parseLogs(logs)) {
3030
+ if (!evt) continue;
3031
+ if (processEvent(evt.name, evt.data, sig.signature)) return;
3032
+ }
3033
+ }
3034
+ } catch {}
3035
+ };
3036
+ runBackfill();
3037
+ const intervalId = setInterval(() => {
3038
+ runBackfill();
3039
+ }, backfillIntervalMs);
3040
+ cleanupFns.push(() => {
3041
+ clearInterval(intervalId);
3042
+ });
3043
+ });
2944
3044
  }
2945
- /**
2946
- * Parses transaction logs for signature or error events.
2947
- */
2948
- async parseLogsForEvents(logs, requestId, signature) {
2949
- const cpiEvents = await CpiEventParser.parseCpiEvents(this.connection, signature, this.programId.toString(), this.program);
2950
- for (const event of cpiEvents) {
2951
- const mapped = this.mapEventToResult(event.name, event.name === "signatureRespondedEvent" ? event.data : event.data, requestId);
2952
- if (mapped) return mapped;
2953
- }
2954
- const parser = new __coral_xyz_anchor.EventParser(this.program.programId, this.program.coder);
2955
- for (const evt of parser.parseLogs(logs)) {
2956
- if (!evt) continue;
2957
- const mapped = this.mapEventToResult(evt.name, evt.name === "signatureRespondedEvent" ? evt.data : evt.data, requestId);
2958
- if (mapped) return mapped;
2959
- }
3045
+ mapRespondToResult(data, requestId) {
3046
+ if ("0x" + Buffer.from(data.requestId).toString("hex") !== requestId) return void 0;
3047
+ return {
3048
+ r: Buffer.from(data.signature.bigR.x).toString("hex"),
3049
+ s: Buffer.from(data.signature.s).toString("hex"),
3050
+ v: data.signature.recoveryId + 27
3051
+ };
2960
3052
  }
2961
- mapEventToResult(name, data, requestId) {
3053
+ mapRespondErrorToResult(data, requestId) {
2962
3054
  const eventRequestIdHex = "0x" + Buffer.from(data.requestId).toString("hex");
2963
- if (name === "signatureRespondedEvent" && eventRequestIdHex === requestId) {
2964
- const d = data;
2965
- return {
2966
- r: Buffer.from(d.signature.bigR.x).toString("hex"),
2967
- s: Buffer.from(d.signature.s).toString("hex"),
2968
- v: d.signature.recoveryId + 27
2969
- };
2970
- }
2971
- if (name === "signatureErrorEvent" && eventRequestIdHex === requestId) return {
3055
+ if (eventRequestIdHex !== requestId) return void 0;
3056
+ return {
2972
3057
  requestId: eventRequestIdHex,
2973
3058
  error: data.error
2974
3059
  };
2975
3060
  }
3061
+ mapRespondBidirectionalToResult(data, requestId) {
3062
+ if ("0x" + Buffer.from(data.requestId).toString("hex") !== requestId) return void 0;
3063
+ return {
3064
+ serializedOutput: data.serializedOutput,
3065
+ signature: data.signature
3066
+ };
3067
+ }
3068
+ mapEventForName(eventName, data, requestId) {
3069
+ switch (eventName) {
3070
+ case "signatureRespondedEvent": return this.mapRespondToResult(data, requestId);
3071
+ case "signatureErrorEvent": return this.mapRespondErrorToResult(data, requestId);
3072
+ case "respondBidirectionalEvent": return this.mapRespondBidirectionalToResult(data, requestId);
3073
+ default: return;
3074
+ }
3075
+ }
2976
3076
  /**
2977
3077
  * Generates the request ID for a signature request allowing to track the response.
2978
3078
  */
@@ -2992,46 +3092,6 @@ var ChainSignatureContract$1 = class extends ChainSignatureContract {
2992
3092
  chainId: KDF_CHAIN_IDS.SOLANA
2993
3093
  });
2994
3094
  }
2995
- /**
2996
- * Subscribes to program events using Anchor's EventParser for regular events,
2997
- * and CPI parsing for emit_cpi!-emitted events. Returns an unsubscribe fn.
2998
- */
2999
- async subscribeToEvents(handlers) {
3000
- const commitment = "confirmed";
3001
- const cpiHandlers = /* @__PURE__ */ new Map();
3002
- if (handlers.onSignatureResponded) {
3003
- const onSignatureResponded = handlers.onSignatureResponded;
3004
- cpiHandlers.set("signatureRespondedEvent", async (e, s) => {
3005
- await onSignatureResponded(e, s);
3006
- });
3007
- }
3008
- if (handlers.onSignatureError) {
3009
- const onSignatureError = handlers.onSignatureError;
3010
- cpiHandlers.set("signatureErrorEvent", async (e, s) => {
3011
- await onSignatureError(e, s);
3012
- });
3013
- }
3014
- const cpiSubId = CpiEventParser.subscribeToCpiEvents(this.connection, this.program, cpiHandlers);
3015
- const parser = new __coral_xyz_anchor.EventParser(this.program.programId, this.program.coder);
3016
- const subId = this.connection.onLogs(this.program.programId, (logs, context) => {
3017
- if (logs.err) return;
3018
- for (const evt of parser.parseLogs(logs.logs)) {
3019
- if (!evt) continue;
3020
- if (evt.name === "signatureRespondedEvent") {
3021
- const onSignatureResponded = handlers.onSignatureResponded;
3022
- if (onSignatureResponded) onSignatureResponded(evt.data, context.slot);
3023
- }
3024
- if (evt.name === "signatureErrorEvent") {
3025
- const onSignatureError = handlers.onSignatureError;
3026
- if (onSignatureError) onSignatureError(evt.data, context.slot);
3027
- }
3028
- }
3029
- }, commitment);
3030
- return async () => {
3031
- await this.connection.removeOnLogsListener(subId);
3032
- await this.connection.removeOnLogsListener(cpiSubId);
3033
- };
3034
- }
3035
3095
  };
3036
3096
 
3037
3097
  //#endregion
package/dist/index.d.cts CHANGED
@@ -1550,16 +1550,17 @@ interface Signature {
1550
1550
  s: number[];
1551
1551
  recoveryId: number;
1552
1552
  }
1553
- interface SignatureRespondedEvent {
1554
- requestId: number[];
1555
- responder: PublicKey;
1553
+ interface RespondBidirectionalData {
1554
+ serializedOutput: Buffer;
1556
1555
  signature: Signature;
1557
1556
  }
1558
- interface SignatureErrorEvent {
1559
- requestId: number[];
1560
- responder: PublicKey;
1561
- error: string;
1557
+ type ChainSignaturesEventName = 'signatureRespondedEvent' | 'signatureErrorEvent' | 'respondBidirectionalEvent';
1558
+ interface EventResultMap {
1559
+ signatureRespondedEvent: RSVSignature;
1560
+ signatureErrorEvent: SignatureErrorData;
1561
+ respondBidirectionalEvent: RespondBidirectionalData;
1562
1562
  }
1563
+ type EventResult<E extends ChainSignaturesEventName> = EventResultMap[E];
1563
1564
  //#endregion
1564
1565
  //#region src/contracts/solana/ChainSignaturesContract.d.ts
1565
1566
  declare class ChainSignatureContract$1 extends ChainSignatureContract {
@@ -1608,8 +1609,8 @@ declare class ChainSignatureContract$1 extends ChainSignatureContract {
1608
1609
  }): Promise<TransactionInstruction>;
1609
1610
  /**
1610
1611
  * Sends a transaction to the program to request a signature, then
1611
- * polls for the signature result. If the signature is not found within the retry
1612
- * parameters, it will throw an error.
1612
+ * races a WebSocket listener against polling backfill to find the result.
1613
+ * If the signature is not found within the timeout, it will throw an error.
1613
1614
  */
1614
1615
  sign(args: SignArgs, options?: Partial<SignOptions> & {
1615
1616
  remainingAccounts?: AccountMeta[];
@@ -1617,39 +1618,27 @@ declare class ChainSignatureContract$1 extends ChainSignatureContract {
1617
1618
  }): Promise<RSVSignature>;
1618
1619
  private sendAndConfirmWithoutWebSocket;
1619
1620
  /**
1620
- * Polls for signature or error events matching the given requestId starting from the solana transaction with signature afterSignature.
1621
- * Returns a signature, error data, or undefined if nothing is found.
1621
+ * Waits for a specific event matching the given requestId by combining
1622
+ * a WebSocket listener (real-time) with polling backfill (resilience).
1622
1623
  */
1623
- pollForRequestId({
1624
- requestId,
1625
- payload: _payload,
1626
- path: _path,
1627
- afterSignature,
1628
- options
1629
- }: {
1624
+ waitForEvent<E extends ChainSignaturesEventName>(options: {
1625
+ eventName: E;
1630
1626
  requestId: string;
1631
- payload: number[];
1632
- path: string;
1633
- afterSignature: string;
1634
- options?: RetryOptions;
1635
- }): Promise<RSVSignature | SignatureErrorData | undefined>;
1636
- /**
1637
- * Parses transaction logs for signature or error events.
1638
- */
1639
- private parseLogsForEvents;
1640
- private mapEventToResult;
1627
+ signer: PublicKey;
1628
+ afterSignature?: string;
1629
+ timeoutMs?: number;
1630
+ backfillIntervalMs?: number;
1631
+ backfillLimit?: number;
1632
+ signal?: AbortSignal;
1633
+ }): Promise<EventResult<E>>;
1634
+ private mapRespondToResult;
1635
+ private mapRespondErrorToResult;
1636
+ private mapRespondBidirectionalToResult;
1637
+ private mapEventForName;
1641
1638
  /**
1642
1639
  * Generates the request ID for a signature request allowing to track the response.
1643
1640
  */
1644
1641
  getRequestId(args: SignArgs, options?: SignOptions['sign']): string;
1645
- /**
1646
- * Subscribes to program events using Anchor's EventParser for regular events,
1647
- * and CPI parsing for emit_cpi!-emitted events. Returns an unsubscribe fn.
1648
- */
1649
- subscribeToEvents(handlers: {
1650
- onSignatureResponded?: (event: SignatureRespondedEvent, slot: number) => Promise<void> | void;
1651
- onSignatureError?: (event: SignatureErrorEvent, slot: number) => Promise<void> | void;
1652
- }): Promise<() => Promise<void>>;
1653
1642
  }
1654
1643
  declare namespace index_d_exports$4 {
1655
1644
  export { ChainSignatureContract$1 as ChainSignatureContract, ChainSignaturesProject, utils };
package/dist/index.d.ts CHANGED
@@ -1548,16 +1548,17 @@ interface Signature {
1548
1548
  s: number[];
1549
1549
  recoveryId: number;
1550
1550
  }
1551
- interface SignatureRespondedEvent {
1552
- requestId: number[];
1553
- responder: PublicKey;
1551
+ interface RespondBidirectionalData {
1552
+ serializedOutput: Buffer;
1554
1553
  signature: Signature;
1555
1554
  }
1556
- interface SignatureErrorEvent {
1557
- requestId: number[];
1558
- responder: PublicKey;
1559
- error: string;
1555
+ type ChainSignaturesEventName = 'signatureRespondedEvent' | 'signatureErrorEvent' | 'respondBidirectionalEvent';
1556
+ interface EventResultMap {
1557
+ signatureRespondedEvent: RSVSignature;
1558
+ signatureErrorEvent: SignatureErrorData;
1559
+ respondBidirectionalEvent: RespondBidirectionalData;
1560
1560
  }
1561
+ type EventResult<E extends ChainSignaturesEventName> = EventResultMap[E];
1561
1562
  //#endregion
1562
1563
  //#region src/contracts/solana/ChainSignaturesContract.d.ts
1563
1564
  declare class ChainSignatureContract$1 extends ChainSignatureContract {
@@ -1606,8 +1607,8 @@ declare class ChainSignatureContract$1 extends ChainSignatureContract {
1606
1607
  }): Promise<TransactionInstruction>;
1607
1608
  /**
1608
1609
  * Sends a transaction to the program to request a signature, then
1609
- * polls for the signature result. If the signature is not found within the retry
1610
- * parameters, it will throw an error.
1610
+ * races a WebSocket listener against polling backfill to find the result.
1611
+ * If the signature is not found within the timeout, it will throw an error.
1611
1612
  */
1612
1613
  sign(args: SignArgs, options?: Partial<SignOptions> & {
1613
1614
  remainingAccounts?: AccountMeta[];
@@ -1615,39 +1616,27 @@ declare class ChainSignatureContract$1 extends ChainSignatureContract {
1615
1616
  }): Promise<RSVSignature>;
1616
1617
  private sendAndConfirmWithoutWebSocket;
1617
1618
  /**
1618
- * Polls for signature or error events matching the given requestId starting from the solana transaction with signature afterSignature.
1619
- * Returns a signature, error data, or undefined if nothing is found.
1619
+ * Waits for a specific event matching the given requestId by combining
1620
+ * a WebSocket listener (real-time) with polling backfill (resilience).
1620
1621
  */
1621
- pollForRequestId({
1622
- requestId,
1623
- payload: _payload,
1624
- path: _path,
1625
- afterSignature,
1626
- options
1627
- }: {
1622
+ waitForEvent<E extends ChainSignaturesEventName>(options: {
1623
+ eventName: E;
1628
1624
  requestId: string;
1629
- payload: number[];
1630
- path: string;
1631
- afterSignature: string;
1632
- options?: RetryOptions;
1633
- }): Promise<RSVSignature | SignatureErrorData | undefined>;
1634
- /**
1635
- * Parses transaction logs for signature or error events.
1636
- */
1637
- private parseLogsForEvents;
1638
- private mapEventToResult;
1625
+ signer: PublicKey;
1626
+ afterSignature?: string;
1627
+ timeoutMs?: number;
1628
+ backfillIntervalMs?: number;
1629
+ backfillLimit?: number;
1630
+ signal?: AbortSignal;
1631
+ }): Promise<EventResult<E>>;
1632
+ private mapRespondToResult;
1633
+ private mapRespondErrorToResult;
1634
+ private mapRespondBidirectionalToResult;
1635
+ private mapEventForName;
1639
1636
  /**
1640
1637
  * Generates the request ID for a signature request allowing to track the response.
1641
1638
  */
1642
1639
  getRequestId(args: SignArgs, options?: SignOptions['sign']): string;
1643
- /**
1644
- * Subscribes to program events using Anchor's EventParser for regular events,
1645
- * and CPI parsing for emit_cpi!-emitted events. Returns an unsubscribe fn.
1646
- */
1647
- subscribeToEvents(handlers: {
1648
- onSignatureResponded?: (event: SignatureRespondedEvent, slot: number) => Promise<void> | void;
1649
- onSignatureError?: (event: SignatureErrorEvent, slot: number) => Promise<void> | void;
1650
- }): Promise<() => Promise<void>>;
1651
1640
  }
1652
1641
  declare namespace index_d_exports$4 {
1653
1642
  export { ChainSignatureContract$1 as ChainSignatureContract, ChainSignaturesProject, utils };
package/dist/index.js CHANGED
@@ -2652,24 +2652,33 @@ const EMIT_CPI_INSTRUCTION_DISCRIMINATOR = Buffer.from([
2652
2652
  ]);
2653
2653
  var CpiEventParser = class {
2654
2654
  /**
2655
- * Parse CPI events from a transaction (emit_cpi! pattern)
2655
+ * Parse CPI events from an already-fetched transaction (emit_cpi! pattern).
2656
2656
  */
2657
- static async parseCpiEvents(connection, signature, targetProgramId, program) {
2657
+ static parseCpiEventsFromTransaction(tx, targetProgramId, program) {
2658
2658
  const events$1 = [];
2659
+ if (!tx?.meta?.innerInstructions) return events$1;
2660
+ for (const innerIxSet of tx.meta.innerInstructions) for (const instruction of innerIxSet.instructions) {
2661
+ if (!("programId" in instruction) || !("data" in instruction)) continue;
2662
+ if (instruction.programId.toString() !== targetProgramId) continue;
2663
+ const parsedEvent = this.parseInstruction(instruction.data, program);
2664
+ if (parsedEvent) events$1.push(parsedEvent);
2665
+ }
2666
+ return events$1;
2667
+ }
2668
+ /**
2669
+ * Fetch a transaction by signature and parse its CPI events.
2670
+ * Used by the subscription path where only the signature is available.
2671
+ */
2672
+ static async fetchAndParseCpiEvents(connection, signature, targetProgramId, program) {
2659
2673
  try {
2660
2674
  const tx = await connection.getParsedTransaction(signature, {
2661
2675
  commitment: "confirmed",
2662
2676
  maxSupportedTransactionVersion: 0
2663
2677
  });
2664
- if (!tx?.meta?.innerInstructions) return events$1;
2665
- for (const innerIxSet of tx.meta.innerInstructions) for (const instruction of innerIxSet.instructions) {
2666
- if (!("programId" in instruction) || !("data" in instruction)) continue;
2667
- if (instruction.programId.toString() !== targetProgramId) continue;
2668
- const parsedEvent = this.parseInstruction(instruction.data, program);
2669
- if (parsedEvent) events$1.push(parsedEvent);
2670
- }
2671
- } catch {}
2672
- return events$1;
2678
+ return this.parseCpiEventsFromTransaction(tx, targetProgramId, program);
2679
+ } catch {
2680
+ return [];
2681
+ }
2673
2682
  }
2674
2683
  /**
2675
2684
  * Parse CPI event from instruction data
@@ -2699,7 +2708,7 @@ var CpiEventParser = class {
2699
2708
  return connection.onLogs(program.programId, (logs, context) => {
2700
2709
  if (logs.err) return;
2701
2710
  (async () => {
2702
- const events$1 = await this.parseCpiEvents(connection, logs.signature, program.programId.toString(), program);
2711
+ const events$1 = await this.fetchAndParseCpiEvents(connection, logs.signature, program.programId.toString(), program);
2703
2712
  for (const event of events$1) {
2704
2713
  const handler = eventHandlers.get(event.name);
2705
2714
  if (handler) try {
@@ -2799,15 +2808,15 @@ var ChainSignatureContract$1 = class extends ChainSignatureContract {
2799
2808
  }
2800
2809
  /**
2801
2810
  * Sends a transaction to the program to request a signature, then
2802
- * polls for the signature result. If the signature is not found within the retry
2803
- * parameters, it will throw an error.
2811
+ * races a WebSocket listener against polling backfill to find the result.
2812
+ * If the signature is not found within the timeout, it will throw an error.
2804
2813
  */
2805
2814
  async sign(args, options) {
2806
2815
  const algo = options?.sign?.algo ?? "";
2807
2816
  const dest = options?.sign?.dest ?? "";
2808
2817
  const params = options?.sign?.params ?? "";
2809
2818
  const delay = options?.retry?.delay ?? 5e3;
2810
- const retryCount = options?.retry?.retryCount ?? 12;
2819
+ const timeoutMs = delay * (options?.retry?.retryCount ?? 12);
2811
2820
  if (options?.remainingAccounts?.filter((acc) => acc.isSigner)?.some((acc) => !options?.remainingSigners?.some((signer) => signer.publicKey.equals(acc.pubkey)))) throw new Error("All accounts marked as signers must have a corresponding signer");
2812
2821
  const requestId = this.getRequestId(args, {
2813
2822
  algo,
@@ -2825,24 +2834,38 @@ var ChainSignatureContract$1 = class extends ChainSignatureContract {
2825
2834
  const transaction = new Transaction().add(instruction);
2826
2835
  transaction.feePayer = this.provider.wallet.publicKey;
2827
2836
  const hash = await this.sendAndConfirmWithoutWebSocket(transaction, options?.remainingSigners);
2837
+ const controller = new AbortController();
2838
+ const successPromise = this.waitForEvent({
2839
+ eventName: "signatureRespondedEvent",
2840
+ requestId,
2841
+ signer: this.programId,
2842
+ afterSignature: hash,
2843
+ timeoutMs,
2844
+ backfillIntervalMs: delay,
2845
+ signal: controller.signal
2846
+ });
2847
+ const errorPromise = this.waitForEvent({
2848
+ eventName: "signatureErrorEvent",
2849
+ requestId,
2850
+ signer: this.programId,
2851
+ afterSignature: hash,
2852
+ timeoutMs,
2853
+ backfillIntervalMs: delay,
2854
+ signal: controller.signal
2855
+ }).then((err) => {
2856
+ throw new SignatureContractError(err.error, requestId, { hash });
2857
+ });
2858
+ successPromise.catch(() => {});
2859
+ errorPromise.catch(() => {});
2828
2860
  try {
2829
- const pollResult = await this.pollForRequestId({
2830
- requestId,
2831
- payload: args.payload,
2832
- path: args.path,
2833
- afterSignature: hash,
2834
- options: {
2835
- delay,
2836
- retryCount
2837
- }
2838
- });
2839
- if (!pollResult) throw new SignatureNotFoundError(requestId, { hash });
2840
- if ("error" in pollResult) throw new SignatureContractError(pollResult.error, requestId, { hash });
2841
- if (!await verifyRecoveredAddress(pollResult, args.payload, this.requesterAddress, args.path, this, args.key_version)) throw new SigningError(requestId, { hash }, /* @__PURE__ */ new Error("Signature verification failed: recovered address does not match expected address"));
2842
- return pollResult;
2861
+ const result = await Promise.race([successPromise, errorPromise]);
2862
+ if (!await verifyRecoveredAddress(result, args.payload, this.requesterAddress, args.path, this, args.key_version)) throw new SigningError(requestId, { hash }, /* @__PURE__ */ new Error("Signature verification failed: recovered address does not match expected address"));
2863
+ return result;
2843
2864
  } catch (error) {
2844
2865
  if (error instanceof SignatureNotFoundError || error instanceof SignatureContractError) throw error;
2845
2866
  else throw new SigningError(requestId, { hash }, error instanceof Error ? error : void 0);
2867
+ } finally {
2868
+ controller.abort();
2846
2869
  }
2847
2870
  }
2848
2871
  async sendAndConfirmWithoutWebSocket(transaction, signers) {
@@ -2866,70 +2889,147 @@ var ChainSignatureContract$1 = class extends ChainSignatureContract {
2866
2889
  throw new TransactionExpiredTimeoutError(signature, timeout / 1e3);
2867
2890
  }
2868
2891
  /**
2869
- * Polls for signature or error events matching the given requestId starting from the solana transaction with signature afterSignature.
2870
- * Returns a signature, error data, or undefined if nothing is found.
2892
+ * Waits for a specific event matching the given requestId by combining
2893
+ * a WebSocket listener (real-time) with polling backfill (resilience).
2871
2894
  */
2872
- async pollForRequestId({ requestId, payload: _payload, path: _path, afterSignature, options }) {
2873
- const delay = options?.delay ?? 5e3;
2874
- const retryCount = options?.retryCount ?? 12;
2875
- let lastCheckedSignature = afterSignature;
2876
- for (let i = 0; i < retryCount; i++) {
2877
- try {
2878
- const signatures = await this.connection.getSignaturesForAddress(this.programId, {
2879
- until: lastCheckedSignature,
2880
- limit: 50
2881
- }, "confirmed");
2882
- if (signatures.length > 0) lastCheckedSignature = signatures[signatures.length - 1].signature;
2883
- for (const sig of signatures) {
2884
- const tx = await this.connection.getParsedTransaction(sig.signature, {
2885
- commitment: "confirmed",
2886
- maxSupportedTransactionVersion: 0
2895
+ async waitForEvent(options) {
2896
+ const { eventName, requestId, signer, afterSignature, timeoutMs = 6e4, backfillIntervalMs = 5e3, backfillLimit = 50, signal } = options;
2897
+ return await new Promise((resolve, reject) => {
2898
+ let settled = false;
2899
+ const seenSignatures = /* @__PURE__ */ new Set();
2900
+ let lastCheckedSignature = afterSignature;
2901
+ const cleanupFns = [];
2902
+ const cleanup = () => {
2903
+ for (const fn of cleanupFns) try {
2904
+ fn();
2905
+ } catch {}
2906
+ };
2907
+ const settle = (action) => {
2908
+ if (settled) return;
2909
+ settled = true;
2910
+ cleanup();
2911
+ action();
2912
+ };
2913
+ const processEvent = (name, data, txSignature) => {
2914
+ if (settled) return false;
2915
+ if (txSignature && seenSignatures.has(txSignature)) return false;
2916
+ if (txSignature) seenSignatures.add(txSignature);
2917
+ if (name !== eventName) return false;
2918
+ const result = this.mapEventForName(eventName, data, requestId);
2919
+ if (result !== void 0) {
2920
+ settle(() => {
2921
+ resolve(result);
2887
2922
  });
2888
- if (tx?.meta?.logMessages) {
2889
- const result = await this.parseLogsForEvents(tx.meta.logMessages, requestId, sig.signature);
2890
- if (result) return result;
2891
- }
2923
+ return true;
2892
2924
  }
2893
- } catch (error) {
2894
- console.error("Error checking for events:", error);
2895
- }
2896
- if (i < retryCount - 1) {
2897
- console.log(`Retrying get signature: ${i + 1}/${retryCount}`);
2898
- await new Promise((resolve) => setTimeout(resolve, delay));
2925
+ return false;
2926
+ };
2927
+ if (signal) {
2928
+ if (signal.aborted) {
2929
+ settle(() => {
2930
+ reject(signal.reason ?? /* @__PURE__ */ new Error("Aborted"));
2931
+ });
2932
+ return;
2933
+ }
2934
+ const onAbort = () => {
2935
+ settle(() => {
2936
+ reject(signal.reason ?? /* @__PURE__ */ new Error("Aborted"));
2937
+ });
2938
+ };
2939
+ signal.addEventListener("abort", onAbort, { once: true });
2940
+ cleanupFns.push(() => {
2941
+ signal.removeEventListener("abort", onAbort);
2942
+ });
2899
2943
  }
2900
- }
2944
+ const timeoutId = setTimeout(() => {
2945
+ settle(() => {
2946
+ reject(new SignatureNotFoundError(requestId));
2947
+ });
2948
+ }, timeoutMs);
2949
+ cleanupFns.push(() => {
2950
+ clearTimeout(timeoutId);
2951
+ });
2952
+ const parser = new EventParser(this.program.programId, this.program.coder);
2953
+ const subId = this.connection.onLogs(signer, (logs, _context) => {
2954
+ if (settled) return;
2955
+ if (logs.err) return;
2956
+ for (const evt of parser.parseLogs(logs.logs)) {
2957
+ if (!evt) continue;
2958
+ if (processEvent(evt.name, evt.data, logs.signature)) return;
2959
+ }
2960
+ CpiEventParser.fetchAndParseCpiEvents(this.connection, logs.signature, this.programId.toString(), this.program).then((cpiEvents) => {
2961
+ for (const event of cpiEvents) if (processEvent(event.name, event.data, logs.signature)) return;
2962
+ });
2963
+ }, "confirmed");
2964
+ cleanupFns.push(() => {
2965
+ this.connection.removeOnLogsListener(subId);
2966
+ });
2967
+ const runBackfill = async () => {
2968
+ if (settled) return;
2969
+ try {
2970
+ const signatures = await this.connection.getSignaturesForAddress(signer, {
2971
+ until: lastCheckedSignature,
2972
+ limit: backfillLimit
2973
+ }, "confirmed");
2974
+ if (signatures.length > 0) lastCheckedSignature = signatures[0].signature;
2975
+ for (const sig of signatures) {
2976
+ if (settled) return;
2977
+ if (seenSignatures.has(sig.signature)) continue;
2978
+ const tx = await this.connection.getParsedTransaction(sig.signature, {
2979
+ commitment: "confirmed",
2980
+ maxSupportedTransactionVersion: 0
2981
+ });
2982
+ if (!tx) continue;
2983
+ const cpiEvents = CpiEventParser.parseCpiEventsFromTransaction(tx, this.programId.toString(), this.program);
2984
+ for (const event of cpiEvents) if (processEvent(event.name, event.data, sig.signature)) return;
2985
+ const logs = tx.meta?.logMessages;
2986
+ if (logs) for (const evt of parser.parseLogs(logs)) {
2987
+ if (!evt) continue;
2988
+ if (processEvent(evt.name, evt.data, sig.signature)) return;
2989
+ }
2990
+ }
2991
+ } catch {}
2992
+ };
2993
+ runBackfill();
2994
+ const intervalId = setInterval(() => {
2995
+ runBackfill();
2996
+ }, backfillIntervalMs);
2997
+ cleanupFns.push(() => {
2998
+ clearInterval(intervalId);
2999
+ });
3000
+ });
2901
3001
  }
2902
- /**
2903
- * Parses transaction logs for signature or error events.
2904
- */
2905
- async parseLogsForEvents(logs, requestId, signature) {
2906
- const cpiEvents = await CpiEventParser.parseCpiEvents(this.connection, signature, this.programId.toString(), this.program);
2907
- for (const event of cpiEvents) {
2908
- const mapped = this.mapEventToResult(event.name, event.name === "signatureRespondedEvent" ? event.data : event.data, requestId);
2909
- if (mapped) return mapped;
2910
- }
2911
- const parser = new EventParser(this.program.programId, this.program.coder);
2912
- for (const evt of parser.parseLogs(logs)) {
2913
- if (!evt) continue;
2914
- const mapped = this.mapEventToResult(evt.name, evt.name === "signatureRespondedEvent" ? evt.data : evt.data, requestId);
2915
- if (mapped) return mapped;
2916
- }
3002
+ mapRespondToResult(data, requestId) {
3003
+ if ("0x" + Buffer.from(data.requestId).toString("hex") !== requestId) return void 0;
3004
+ return {
3005
+ r: Buffer.from(data.signature.bigR.x).toString("hex"),
3006
+ s: Buffer.from(data.signature.s).toString("hex"),
3007
+ v: data.signature.recoveryId + 27
3008
+ };
2917
3009
  }
2918
- mapEventToResult(name, data, requestId) {
3010
+ mapRespondErrorToResult(data, requestId) {
2919
3011
  const eventRequestIdHex = "0x" + Buffer.from(data.requestId).toString("hex");
2920
- if (name === "signatureRespondedEvent" && eventRequestIdHex === requestId) {
2921
- const d = data;
2922
- return {
2923
- r: Buffer.from(d.signature.bigR.x).toString("hex"),
2924
- s: Buffer.from(d.signature.s).toString("hex"),
2925
- v: d.signature.recoveryId + 27
2926
- };
2927
- }
2928
- if (name === "signatureErrorEvent" && eventRequestIdHex === requestId) return {
3012
+ if (eventRequestIdHex !== requestId) return void 0;
3013
+ return {
2929
3014
  requestId: eventRequestIdHex,
2930
3015
  error: data.error
2931
3016
  };
2932
3017
  }
3018
+ mapRespondBidirectionalToResult(data, requestId) {
3019
+ if ("0x" + Buffer.from(data.requestId).toString("hex") !== requestId) return void 0;
3020
+ return {
3021
+ serializedOutput: data.serializedOutput,
3022
+ signature: data.signature
3023
+ };
3024
+ }
3025
+ mapEventForName(eventName, data, requestId) {
3026
+ switch (eventName) {
3027
+ case "signatureRespondedEvent": return this.mapRespondToResult(data, requestId);
3028
+ case "signatureErrorEvent": return this.mapRespondErrorToResult(data, requestId);
3029
+ case "respondBidirectionalEvent": return this.mapRespondBidirectionalToResult(data, requestId);
3030
+ default: return;
3031
+ }
3032
+ }
2933
3033
  /**
2934
3034
  * Generates the request ID for a signature request allowing to track the response.
2935
3035
  */
@@ -2949,46 +3049,6 @@ var ChainSignatureContract$1 = class extends ChainSignatureContract {
2949
3049
  chainId: KDF_CHAIN_IDS.SOLANA
2950
3050
  });
2951
3051
  }
2952
- /**
2953
- * Subscribes to program events using Anchor's EventParser for regular events,
2954
- * and CPI parsing for emit_cpi!-emitted events. Returns an unsubscribe fn.
2955
- */
2956
- async subscribeToEvents(handlers) {
2957
- const commitment = "confirmed";
2958
- const cpiHandlers = /* @__PURE__ */ new Map();
2959
- if (handlers.onSignatureResponded) {
2960
- const onSignatureResponded = handlers.onSignatureResponded;
2961
- cpiHandlers.set("signatureRespondedEvent", async (e, s) => {
2962
- await onSignatureResponded(e, s);
2963
- });
2964
- }
2965
- if (handlers.onSignatureError) {
2966
- const onSignatureError = handlers.onSignatureError;
2967
- cpiHandlers.set("signatureErrorEvent", async (e, s) => {
2968
- await onSignatureError(e, s);
2969
- });
2970
- }
2971
- const cpiSubId = CpiEventParser.subscribeToCpiEvents(this.connection, this.program, cpiHandlers);
2972
- const parser = new EventParser(this.program.programId, this.program.coder);
2973
- const subId = this.connection.onLogs(this.program.programId, (logs, context) => {
2974
- if (logs.err) return;
2975
- for (const evt of parser.parseLogs(logs.logs)) {
2976
- if (!evt) continue;
2977
- if (evt.name === "signatureRespondedEvent") {
2978
- const onSignatureResponded = handlers.onSignatureResponded;
2979
- if (onSignatureResponded) onSignatureResponded(evt.data, context.slot);
2980
- }
2981
- if (evt.name === "signatureErrorEvent") {
2982
- const onSignatureError = handlers.onSignatureError;
2983
- if (onSignatureError) onSignatureError(evt.data, context.slot);
2984
- }
2985
- }
2986
- }, commitment);
2987
- return async () => {
2988
- await this.connection.removeOnLogsListener(subId);
2989
- await this.connection.removeOnLogsListener(cpiSubId);
2990
- };
2991
- }
2992
3052
  };
2993
3053
 
2994
3054
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signet.js",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A TypeScript library for handling multi-chain transactions and signatures using Signet MPC",
5
5
  "type": "module",
6
6
  "exports": {
@@ -37,7 +37,9 @@
37
37
  "docs:dev": "vocs dev",
38
38
  "docs:build": "vocs build",
39
39
  "docs:preview": "vocs preview",
40
- "format": "prettier --write ."
40
+ "format": "prettier --write .",
41
+ "lint": "eslint 'src/**/*.ts'",
42
+ "typecheck": "tsc --noEmit"
41
43
  },
42
44
  "author": "Sig Network",
43
45
  "license": "MIT",