@partylayer/provider 0.6.0 → 0.6.2

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.mjs CHANGED
@@ -219,6 +219,17 @@ function isRpcShaped(err) {
219
219
  return typeof e.message === "string" && typeof e.code === "number";
220
220
  }
221
221
 
222
+ // src/request-validation.ts
223
+ function validateRequestPayload(payload) {
224
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload) || !("method" in payload) || typeof payload.method !== "string") {
225
+ throw new ProviderRpcError("Request must be an object with a string method", JSON_RPC_ERRORS.INVALID_REQUEST);
226
+ }
227
+ const params = "params" in payload ? payload.params : void 0;
228
+ if (params !== void 0 && (params === null || typeof params !== "object")) {
229
+ throw new ProviderRpcError("Request params must be an object or array", JSON_RPC_ERRORS.INVALID_PARAMS);
230
+ }
231
+ }
232
+
222
233
  // src/method-router.ts
223
234
  var MethodRouter = class {
224
235
  handlers = /* @__PURE__ */ new Map();
@@ -240,6 +251,7 @@ var MethodRouter = class {
240
251
  }
241
252
  /** Route a request to the appropriate handler */
242
253
  async route(payload) {
254
+ validateRequestPayload(payload);
243
255
  const { method, params } = payload;
244
256
  const handler = this.handlers.get(method);
245
257
  if (!handler) {
@@ -513,7 +525,7 @@ var PartyLayerProvider = class {
513
525
  };
514
526
 
515
527
  // src/bridge.ts
516
- var PROVIDER_VERSION = "0.6.0" ;
528
+ var PROVIDER_VERSION = "0.6.2" ;
517
529
  function createProviderBridge(client) {
518
530
  const eventBus = new CIP0103EventBus();
519
531
  const pendingEmitted = /* @__PURE__ */ new Set();
@@ -538,6 +550,7 @@ function createProviderBridge(client) {
538
550
  return bridge;
539
551
  }
540
552
  async function handleRequest(client, args, eventBus, pendingEmitted) {
553
+ validateRequestPayload(args);
541
554
  const { method, params } = args;
542
555
  try {
543
556
  switch (method) {
@@ -578,9 +591,8 @@ async function handleRequest(client, args, eventBus, pendingEmitted) {
578
591
  }
579
592
  case "getActiveNetwork": {
580
593
  const session = await client.getActiveSession();
581
- return toCAIP2Network(
582
- session?.network ?? "devnet"
583
- );
594
+ if (!session) throw disconnected("No active session");
595
+ return toCAIP2Network(session.network);
584
596
  }
585
597
  case "listAccounts": {
586
598
  const session = await client.getActiveSession();
@@ -605,61 +617,42 @@ async function handleRequest(client, args, eventBus, pendingEmitted) {
605
617
  }
606
618
  case "prepareExecute": {
607
619
  const p = normalizeParams(params);
608
- const cmdId = `cmd_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
620
+ const session = await client.getActiveSession();
621
+ if (!session) throw disconnected("No active session");
622
+ const cmdId = nonempty(p.commandId) ? p.commandId : `cmd_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
623
+ if (pendingEmitted.has(cmdId)) throw resourceUnavailable("This commandId is already pending");
624
+ const payload = { ...p, commandId: cmdId };
609
625
  pendingEmitted.add(cmdId);
610
- eventBus.emit(CIP0103_EVENTS.TX_CHANGED, {
611
- status: "pending",
612
- commandId: cmdId
613
- });
626
+ eventBus.emit(CIP0103_EVENTS.TX_CHANGED, { status: "pending", commandId: cmdId });
627
+ let submitting = false;
614
628
  try {
615
- const signResult = await client.signTransaction({ tx: p });
616
- const session = await client.getActiveSession();
617
- const partyId = String(signResult.partyId ?? session?.partyId ?? "unknown");
618
- if (signResult.transactionHash !== void 0) {
619
- eventBus.emit(CIP0103_EVENTS.TX_CHANGED, {
620
- status: "signed",
621
- commandId: cmdId,
622
- payload: {
623
- signature: String(signResult.transactionHash),
624
- signedBy: partyId,
625
- party: partyId
626
- }
627
- });
629
+ let signedTx = payload;
630
+ if (!session.capabilitiesSnapshot || session.capabilitiesSnapshot.includes("signTransaction")) {
631
+ const signed = await client.signTransaction({ tx: payload });
632
+ if (signed.partyId !== void 0 && signed.partyId !== session.partyId) {
633
+ throw disconnected("Wallet account changed during signing");
634
+ }
635
+ signedTx = signed.signedTx;
636
+ if (signedTx === void 0) throw new Error("Wallet returned no signed transaction");
637
+ const event2 = signedEvent({ signature: signed.signature, signedBy: signed.signedBy, party: signed.partyId }, cmdId);
638
+ if (event2) eventBus.emit(CIP0103_EVENTS.TX_CHANGED, event2);
628
639
  }
629
- try {
630
- const receipt = await client.submitTransaction({
631
- signedTx: signResult.signedTx ?? signResult.transactionHash
632
- });
633
- pendingEmitted.delete(cmdId);
634
- eventBus.emit(CIP0103_EVENTS.TX_CHANGED, {
635
- status: "executed",
636
- commandId: cmdId,
637
- payload: {
638
- // All three are real identifiers — the receipt's update id, the
639
- // wallet's command id, or the bridge's own command id for this
640
- // operation. `String(receipt.transactionHash)` was the old tail
641
- // and would now stringify `undefined`.
642
- updateId: receipt.updateId ?? receipt.commandId ?? cmdId,
643
- completionOffset: 0
644
- }
645
- });
646
- } catch (submitErr) {
647
- pendingEmitted.delete(cmdId);
648
- eventBus.emit(CIP0103_EVENTS.TX_CHANGED, {
649
- status: "failed",
650
- commandId: cmdId
651
- });
652
- throw submitErr;
640
+ const current = await client.getActiveSession();
641
+ if (!current || current.sessionId !== session.sessionId || current.partyId !== session.partyId || current.network !== session.network) {
642
+ throw disconnected("Wallet session changed before submission");
653
643
  }
654
- } catch (signErr) {
655
- if (pendingEmitted.has(cmdId)) {
656
- pendingEmitted.delete(cmdId);
657
- eventBus.emit(CIP0103_EVENTS.TX_CHANGED, {
658
- status: "failed",
659
- commandId: cmdId
660
- });
644
+ submitting = true;
645
+ const receipt = await client.submitTransaction({ signedTx });
646
+ const event = executedEvent(receipt, cmdId);
647
+ if (event) eventBus.emit(CIP0103_EVENTS.TX_CHANGED, event);
648
+ } catch (reason) {
649
+ const code = reason?.code;
650
+ if (!submitting || code === 4001 || code === "USER_REJECTED" || code === -32003) {
651
+ eventBus.emit(CIP0103_EVENTS.TX_CHANGED, { status: "failed", commandId: cmdId });
661
652
  }
662
- throw signErr;
653
+ throw reason;
654
+ } finally {
655
+ pendingEmitted.delete(cmdId);
663
656
  }
664
657
  return void 0;
665
658
  }
@@ -734,59 +727,51 @@ function wireEvents(client, eventBus, pendingEmitted) {
734
727
  });
735
728
  });
736
729
  client.on("tx:status", (event) => {
737
- const e = event;
738
- if (!e || typeof e !== "object" || !("status" in e)) return;
739
- const statusMap = {
740
- pending: "pending",
741
- submitted: "pending",
742
- signed: "signed",
743
- committed: "executed",
744
- rejected: "failed",
745
- failed: "failed"
746
- };
747
- const mappedStatus = statusMap[e.status] ?? "pending";
748
- const commandId = String(e.txId);
749
- if (mappedStatus === "pending" && pendingEmitted.has(commandId)) {
750
- return;
751
- }
752
- if (mappedStatus === "executed" || mappedStatus === "failed") {
753
- pendingEmitted.delete(commandId);
754
- }
730
+ const e = record(event);
731
+ if (!e) return;
732
+ const raw = record(e.raw);
733
+ const tx = record(raw?.tx) ?? raw;
734
+ const commandId = nonempty(tx?.commandId) ? tx.commandId : e.txId;
735
+ if (!nonempty(commandId)) return;
755
736
  let txEvent;
756
- switch (mappedStatus) {
737
+ switch (e.status) {
738
+ case "pending":
739
+ case "submitted":
740
+ if (pendingEmitted.has(commandId)) return;
741
+ txEvent = { status: "pending", commandId };
742
+ break;
757
743
  case "signed":
758
- txEvent = {
759
- status: "signed",
760
- commandId,
761
- payload: {
762
- signature: "",
763
- signedBy: "",
764
- party: ""
765
- }
766
- };
744
+ txEvent = signedEvent(tx, commandId);
767
745
  break;
768
- case "executed": {
769
- const raw = e.raw;
770
- txEvent = {
771
- status: "executed",
772
- commandId,
773
- payload: {
774
- updateId: raw?.updateId ?? raw?.commandId ?? commandId,
775
- completionOffset: 0
776
- }
777
- };
746
+ case "committed":
747
+ txEvent = executedEvent(tx, commandId);
778
748
  break;
779
- }
749
+ case "rejected":
780
750
  case "failed":
781
751
  txEvent = { status: "failed", commandId };
782
752
  break;
783
753
  default:
784
- txEvent = { status: "pending", commandId };
785
- break;
754
+ return;
786
755
  }
787
- eventBus.emit(CIP0103_EVENTS.TX_CHANGED, txEvent);
756
+ if (txEvent) eventBus.emit(CIP0103_EVENTS.TX_CHANGED, txEvent);
788
757
  });
789
758
  }
759
+ function record(value) {
760
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
761
+ }
762
+ function nonempty(value) {
763
+ return typeof value === "string" && value.trim().length > 0;
764
+ }
765
+ function signedEvent(value, commandId) {
766
+ const tx = record(value);
767
+ const p = record(tx?.payload) ?? tx;
768
+ return nonempty(p?.signature) && nonempty(p?.signedBy) && nonempty(p?.party) ? { status: "signed", commandId, payload: { signature: p.signature, signedBy: p.signedBy, party: p.party } } : null;
769
+ }
770
+ function executedEvent(value, commandId) {
771
+ const tx = record(value);
772
+ const p = record(tx?.payload) ?? tx;
773
+ return nonempty(p?.updateId) && typeof p.completionOffset === "number" && Number.isSafeInteger(p.completionOffset) && p.completionOffset >= 0 ? { status: "executed", commandId, payload: { updateId: p.updateId, completionOffset: p.completionOffset } } : null;
774
+ }
790
775
  function normalizeParams(params) {
791
776
  if (!params) return {};
792
777
  if (Array.isArray(params)) return params[0] ?? {};
@@ -818,11 +803,33 @@ function createExtensionChannelProvider(options = {}) {
818
803
  const eventBus = new CIP0103EventBus();
819
804
  const channelId = `pl-${++channelSeq}-${target ?? "ext"}`;
820
805
  let requestSeq = 0;
806
+ let listening = false;
807
+ let destroyed = false;
808
+ const subscribedEvents = /* @__PURE__ */ new Set();
809
+ function listen() {
810
+ if (!listening && typeof window !== "undefined") {
811
+ window.addEventListener("message", onMessage);
812
+ listening = true;
813
+ }
814
+ }
815
+ function detachIfIdle() {
816
+ if (pending.size > 0 || [...subscribedEvents].some((name) => eventBus.listenerCount(name) > 0)) return;
817
+ if (listening && typeof window !== "undefined") window.removeEventListener("message", onMessage);
818
+ listening = false;
819
+ }
821
820
  const pending = /* @__PURE__ */ new Map();
822
821
  function onMessage(event) {
823
822
  if (event.source !== window) return;
824
823
  const selfOrigin = typeof window !== "undefined" ? window.location?.origin : void 0;
825
824
  if (selfOrigin && selfOrigin !== "null" && event.origin !== selfOrigin) return;
825
+ const notification = event.data;
826
+ if (notification?.type === SPLICE_WALLET_REQUEST && notification.request?.jsonrpc === "2.0" && notification.request.id == null && (!target || notification.target === target)) {
827
+ const method = notification.request.method;
828
+ if (method && ["statusChanged", "accountsChanged", "txChanged", "connected"].includes(method)) {
829
+ eventBus.emit(method, notification.request.params);
830
+ }
831
+ return;
832
+ }
826
833
  const data = event.data;
827
834
  if (!data || data.type !== SPLICE_WALLET_RESPONSE || !data.response) return;
828
835
  const { id, result, error } = data.response;
@@ -831,6 +838,7 @@ function createExtensionChannelProvider(options = {}) {
831
838
  if (!entry) return;
832
839
  pending.delete(String(id));
833
840
  clearTimeout(entry.timer);
841
+ detachIfIdle();
834
842
  if (error) {
835
843
  entry.reject(
836
844
  new ProviderRpcError(
@@ -843,24 +851,29 @@ function createExtensionChannelProvider(options = {}) {
843
851
  entry.resolve(result);
844
852
  }
845
853
  }
846
- if (typeof window !== "undefined") {
847
- window.addEventListener("message", onMessage);
848
- }
849
854
  function postTargetOrigin() {
850
855
  const origin = typeof window !== "undefined" ? window.location?.origin : void 0;
851
856
  return origin && origin !== "null" ? origin : "*";
852
857
  }
853
858
  const provider = {
854
859
  request(args) {
860
+ try {
861
+ validateRequestPayload(args);
862
+ } catch (error) {
863
+ return Promise.reject(error);
864
+ }
865
+ if (destroyed) return Promise.reject(new ProviderRpcError("Extension channel was destroyed", RPC_ERRORS.DISCONNECTED));
855
866
  if (typeof window === "undefined") {
856
867
  return Promise.reject(
857
868
  new ProviderRpcError("No window: extension channel unavailable", JSON_RPC_ERRORS.INTERNAL_ERROR)
858
869
  );
859
870
  }
871
+ listen();
860
872
  const id = `${channelId}-${++requestSeq}`;
861
873
  return new Promise((resolve, reject) => {
862
874
  const timer = setTimeout(() => {
863
875
  pending.delete(id);
876
+ detachIfIdle();
864
877
  reject(
865
878
  new ProviderRpcError(
866
879
  `Wallet request "${args.method}" timed out after ${timeoutMs}ms`,
@@ -879,11 +892,21 @@ function createExtensionChannelProvider(options = {}) {
879
892
  },
880
893
  ...target ? { target } : {}
881
894
  };
882
- window.postMessage(message, postTargetOrigin());
895
+ try {
896
+ window.postMessage(message, postTargetOrigin());
897
+ } catch (error) {
898
+ pending.delete(id);
899
+ clearTimeout(timer);
900
+ detachIfIdle();
901
+ reject(error);
902
+ }
883
903
  });
884
904
  },
885
905
  on(event, listener) {
906
+ if (destroyed) return provider;
886
907
  eventBus.on(event, listener);
908
+ subscribedEvents.add(event);
909
+ listen();
887
910
  return provider;
888
911
  },
889
912
  emit(event, ...args) {
@@ -891,7 +914,20 @@ function createExtensionChannelProvider(options = {}) {
891
914
  },
892
915
  removeListener(event, listener) {
893
916
  eventBus.removeListener(event, listener);
917
+ detachIfIdle();
894
918
  return provider;
919
+ },
920
+ destroy() {
921
+ if (destroyed) return;
922
+ destroyed = true;
923
+ eventBus.removeAllListeners();
924
+ subscribedEvents.clear();
925
+ for (const entry of pending.values()) {
926
+ clearTimeout(entry.timer);
927
+ entry.reject(new ProviderRpcError("Extension channel was destroyed", RPC_ERRORS.DISCONNECTED));
928
+ }
929
+ pending.clear();
930
+ detachIfIdle();
895
931
  }
896
932
  };
897
933
  eventBus.setOwner(provider);