@leofcoin/chain 1.10.15 → 1.10.17

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.
Files changed (38) hide show
  1. package/exports/beacon-envelope.js +6 -6
  2. package/exports/beacon-epoch.js +1 -1
  3. package/exports/beacon-lifecycle.js +1 -1
  4. package/exports/beacon-round.js +1 -1
  5. package/exports/beacon.js +15 -14
  6. package/exports/browser/_commonjsHelpers-B85MJLTf.js +5 -0
  7. package/exports/browser/beacon-envelope.js +61 -150
  8. package/exports/browser/beacon-epoch.js +2 -2
  9. package/exports/browser/beacon-lifecycle.js +3 -3
  10. package/exports/browser/beacon-round.js +2 -2
  11. package/exports/browser/beacon-wire.js +3 -3
  12. package/exports/browser/beacon.js +2483 -1228
  13. package/exports/browser/{browser-DvU1xNFS-DmmNUdE-.js → browser-CCfR9KOM-CpStHgTj.js} +4 -4
  14. package/exports/browser/{browser-crypto-B9N9KlL4.js → browser-crypto-CH0ua22L.js} +284 -159
  15. package/exports/browser/chain.js +30 -16
  16. package/exports/browser/{client-DDC8C510-3uaB70KL.js → client-C_pBoi1g-_pfKzCvX.js} +256 -80
  17. package/exports/browser/{connection-monitor-GPztgSSJ.js → connection-monitor-D-krNiRH.js} +4 -4
  18. package/exports/browser/{hkdf-CnZvwpZ9.js → hkdf-BUHINPFj.js} +2 -94
  19. package/exports/browser/{index-Ccn5sExe.js → index-DmuI-29u.js} +55 -52
  20. package/exports/browser/{index-Bxr5Iztg-WbFIosyf.js → index-dTQVgkpf-BbdshWB0.js} +4 -4
  21. package/exports/browser/index.browser-CFfJpgKV.js +857 -0
  22. package/exports/browser/{messages-DH0tgNM9-C2CtyLsf.js → messages-BGHaihSi-eHZVl0y9.js} +10 -5
  23. package/exports/browser/{node-browser-BYi7D_oa.js → node-browser-BNe-en1l.js} +950 -482
  24. package/exports/browser/node-browser.js +4 -4
  25. package/exports/browser/{sha2-piimb6xL.js → sha2-BKcEJzgd.js} +144 -3
  26. package/exports/browser/weierstrass-BVfhtHbl.js +3596 -0
  27. package/exports/browser/workers/block-worker.js +5 -3
  28. package/exports/browser/workers/machine-worker.js +50 -22
  29. package/exports/{workers/worker-DMCj1e6z-CTYVa2yX.js → browser/workers/worker-BDTWOCzS-COrAszCt.js} +304 -168
  30. package/exports/chain.js +10 -5
  31. package/exports/{constants-CMYKv-Rt.js → constants-Dy9i9S_o.js} +1 -1
  32. package/exports/hash-format-state.js +17 -0
  33. package/exports/node.js +1 -1
  34. package/exports/workers/block-worker.js +5 -3
  35. package/exports/workers/machine-worker.js +50 -22
  36. package/exports/{browser/workers/worker-DMCj1e6z-CTYVa2yX.js → workers/worker-BDTWOCzS-COrAszCt.js} +304 -168
  37. package/package.json +14 -13
  38. package/exports/browser/weierstrass-Q0QND1ZT.js +0 -2156
@@ -1,9 +1,10 @@
1
- import { F as FormatInterface, p as proto, a as proto$1, b as proto$2, T as TransactionMessage, j as jsonStringifyBigInt, c as jsonParseBigInt, C as ContractMessage, d as Codec } from './browser-crypto-B9N9KlL4.js';
2
- import { B as BlockMessage, L as LastBlockMessage, P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO, C as ConnectionMonitor, a as BWMessage, S as StateMessage } from './connection-monitor-GPztgSSJ.js';
3
- import { M as MAX_TRANSACTION_BYTES, a as MAX_BLOCK_TRANSACTION_BYTES, b as MAX_BLOCK_TRANSACTIONS, c as MultiWallet, d as createTransactionHash, e as calculateFee, f as createContractMessage, s as signTransaction, r as randombytes, g as distributeTransactionFee, h as supportsTransactionFees, i as supportsMonetaryPolicy, v as validateBlockResourceLimits, j as calculateMonetaryPolicy, k as distributeAmount, l as aggregateValidatorFees, m as MONETARY_POLICY_AUTHORITY, n as contractFactoryMessage, o as nativeTokenMessage, p as validatorsMessage, q as nameServiceMessage, t as parseUnits, u as formatUnits } from './index-Ccn5sExe.js';
1
+ import { F as FormatInterface, p as proto, a as proto$1, b as proto$2, T as TransactionMessage, j as jsonStringifyBigInt, c as jsonParseBigInt, C as ContractMessage, d as Codec } from './browser-crypto-CH0ua22L.js';
2
+ import { B as BlockMessage, L as LastBlockMessage, P as PROTOCOL_VERSION, R as REACHED_ONE_ZERO_ZERO, C as ConnectionMonitor, a as BWMessage, S as StateMessage } from './connection-monitor-D-krNiRH.js';
3
+ import { M as MAX_TRANSACTION_BYTES, a as MAX_BLOCK_TRANSACTION_BYTES, b as MAX_BLOCK_TRANSACTIONS, c as MultiWallet, d as createTransactionHash, e as calculateFee, f as createContractMessage, s as signTransaction, r as randombytes, g as distributeTransactionFee, h as supportsTransactionFees, i as supportsMonetaryPolicy, v as validateBlockResourceLimits, j as calculateMonetaryPolicy, k as distributeAmount, l as aggregateValidatorFees, m as MONETARY_POLICY_AUTHORITY, n as contractFactoryMessage, o as nativeTokenMessage, p as validatorsMessage, q as nameServiceMessage, t as parseUnits, u as formatUnits } from './index-DmuI-29u.js';
4
4
  import { f as fromBase58 } from './index-CvKt4UDE.js';
5
+ import { g as getDefaultExportFromCjs } from './_commonjsHelpers-B85MJLTf.js';
5
6
  import { q as quorumThreshold } from './quorum-S_qdgiAY.js';
6
- import './sha2-piimb6xL.js';
7
+ import './sha2-BKcEJzgd.js';
7
8
 
8
9
  const getTargets = () => Array.from([]);
9
10
  const isEnabled = (target) => {
@@ -77,10 +78,10 @@ class ProposalMessage extends FormatInterface {
77
78
  }
78
79
  }
79
80
 
80
- var contractFactory$1 = "IHNY2GQG2XOULCWHDBW6MDLOPW35MCZC6KT6B6IBHIVWA3QJR56UFM2EVDG";
81
- var nativeToken$1 = "IHNY2GQG5V74HIRRP6YPMZYDZ7YM3SR3MQX63XSYEZIZ7TWHBPDWRWVA2XA";
82
- var nameService$1 = "IHNY2GQGQQKXPAEB2OKLN6BQ2JA2DJBV7BF3BHFKPBFJ6DCEJDDKSPKOZE6";
83
- var validators$1 = "IHNY2GQH432JU2JPBPU5C6QXGLMDFLXBRNL4QA34I5BFJNSO6Y42B3POXJD";
81
+ var contractFactory$1 = "B3OGRUBSBGSASFXWDZUI6AYITY76PYQZS4O5UDWVLSW56X5CSWTQGTGQHUGU";
82
+ var nativeToken$1 = "B3OGRUBSA5V74HIRRP6YPMZYDZ7YM3SR3MQX63XSYEZIZ7TWHBPDWRWVA2XA";
83
+ var nameService$1 = "B3OGRUBSBXX7EPFWHGZQGGSWBMMK7ATOSDICOW5CCFP25OMZBTRMHYGM4KFF";
84
+ var validators$1 = "B3OGRUBSBXLCYRTXG5SRL6XNINEJNAI7CLJBKSRCXY24GMFU5ZY7TXHAGJGT";
84
85
  var addresses$1 = {
85
86
  contractFactory: contractFactory$1,
86
87
  nativeToken: nativeToken$1,
@@ -98,10 +99,6 @@ var addresses = {
98
99
  validators
99
100
  };
100
101
 
101
- function getDefaultExportFromCjs (x) {
102
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
103
- }
104
-
105
102
  var re = {exports: {}};
106
103
 
107
104
  var constants;
@@ -5176,6 +5173,19 @@ const pruneCanonicalTransactions = async (transactions, latestTransactionHashes,
5176
5173
  return pending;
5177
5174
  };
5178
5175
 
5176
+ const assertHashFormatState = async (chainStore, stateStore) => {
5177
+ if (await chainStore.has("version")) {
5178
+ const version = new TextDecoder().decode(await chainStore.get("version"));
5179
+ const parsed = /^(\d+)\.(\d+)\.\d+$/.exec(version);
5180
+ const current = PROTOCOL_VERSION.split(".");
5181
+ if (parsed && parsed[1] === current[0] && parsed[2] === current[1]) return;
5182
+ throw new Error(`stored protocol ${version} is incompatible with canonical hash protocol ${PROTOCOL_VERSION}; back up existing data and use a separate migrated data root`);
5183
+ }
5184
+ if (await chainStore.has("lastBlock") || await stateStore?.has("lastBlock")) {
5185
+ throw new Error("existing chainstate has no protocol version; explicit canonical hash migration required");
5186
+ }
5187
+ };
5188
+
5179
5189
  const debug = createDebugger("leofcoin/chain");
5180
5190
  class Chain extends VersionControl {
5181
5191
  constructor(config) {
@@ -5223,8 +5233,9 @@ class Chain extends VersionControl {
5223
5233
  this.#committingHeights = /* @__PURE__ */ new Set();
5224
5234
  /** Prevents casting duplicate prevote/precommit per height:round */
5225
5235
  this.#castedVotes = /* @__PURE__ */ new Set();
5226
- this.ready = new Promise((resolve) => {
5236
+ this.ready = new Promise((resolve, reject) => {
5227
5237
  this.readyResolve = resolve;
5238
+ this.readyReject = reject;
5228
5239
  });
5229
5240
  // ── Tendermint consensus handlers ────────────────────────────────────────
5230
5241
  /**
@@ -5408,7 +5419,7 @@ class Chain extends VersionControl {
5408
5419
  const hash = await this.#sendTransaction(message);
5409
5420
  debug(`added ${hash}`);
5410
5421
  };
5411
- this.#init();
5422
+ this.#init().catch((error) => this.readyReject(error));
5412
5423
  }
5413
5424
  #state;
5414
5425
  #slotTime;
@@ -5704,15 +5715,18 @@ class Chain extends VersionControl {
5704
5715
  message: nameServiceMessage
5705
5716
  }
5706
5717
  ];
5707
- await Promise.all(
5718
+ const prepared = await Promise.all(
5708
5719
  contracts.map(async ({ address, message }) => {
5709
5720
  message = await new ContractMessage(Uint8Array.from(message.split(",").map((string) => Number(string))));
5710
- await globalThis.contractStore.put(address, message.encoded);
5721
+ if (await message.hash() !== address) throw new Error(`genesis contract hash mismatch for ${address}`);
5722
+ return { address, encoded: message.encoded };
5711
5723
  })
5712
5724
  );
5725
+ await Promise.all(prepared.map(({ address, encoded }) => globalThis.contractStore.put(address, encoded)));
5713
5726
  console.log("handle native contracts");
5714
5727
  }
5715
5728
  async #init() {
5729
+ await assertHashFormatState(globalThis.chainStore, globalThis.stateStore);
5716
5730
  this.#participants = [];
5717
5731
  this.#participating = false;
5718
5732
  this.#connectionMonitor = new ConnectionMonitor();
@@ -1,8 +1,8 @@
1
- import { i as inflate_1, c as createDebugger, d as deflate_1, L as LittlePubSub } from './node-browser-BYi7D_oa.js';
2
- import './browser-crypto-B9N9KlL4.js';
3
- import './connection-monitor-GPztgSSJ.js';
1
+ import { i as inflate_1, c as createDebugger, d as deflate_1, L as LittlePubSub } from './node-browser-BNe-en1l.js';
2
+ import './browser-crypto-CH0ua22L.js';
3
+ import './connection-monitor-D-krNiRH.js';
4
4
  import './index-CvKt4UDE.js';
5
- import './sha2-piimb6xL.js';
5
+ import './sha2-BKcEJzgd.js';
6
6
 
7
7
  class Api {
8
8
  _pubsub;
@@ -233,7 +233,7 @@ class SocketRequestClient {
233
233
  const init = async () => {
234
234
  // @ts-ignore
235
235
  if (!globalThis.WebSocket && !this.#experimentalWebsocket)
236
- globalThis.WebSocket = (await import('./browser-DvU1xNFS-DmmNUdE-.js').then(function (n) { return n.b; })).default.w3cwebsocket;
236
+ globalThis.WebSocket = (await import('./browser-CCfR9KOM-CpStHgTj.js').then(function (n) { return n.b; })).default.w3cwebsocket;
237
237
  const client = new WebSocket(this.#url, this.#protocol);
238
238
  if (this.#experimentalWebsocket) {
239
239
  client.addEventListener('error', this.onerror);
@@ -820,8 +820,61 @@ class WebRTCPeer {
820
820
  }
821
821
  }
822
822
 
823
- const getWebTransportCtor = () => {
823
+ // WebTransport streams carry bytes, not application message boundaries.
824
+ const MAX_FRAME_BYTES = 64 * 1024;
825
+ const encodeFrame = (payload) => {
826
+ if (!(payload instanceof Uint8Array) || !payload.length || payload.length > MAX_FRAME_BYTES) {
827
+ throw new Error('invalid transport frame length');
828
+ }
829
+ const frame = new Uint8Array(payload.length + 4);
830
+ new DataView(frame.buffer).setUint32(0, payload.length);
831
+ frame.set(payload, 4);
832
+ return frame;
833
+ };
834
+ class FrameReader {
835
+ constructor(reader) { this.reader = reader; this.chunk = new Uint8Array(); this.offset = 0; }
836
+ async exact(length, allowEnd = false) {
837
+ const result = new Uint8Array(length);
838
+ let written = 0;
839
+ while (written < length) {
840
+ if (this.offset === this.chunk.length) {
841
+ const { done, value } = await this.reader.read();
842
+ if (done) {
843
+ if (allowEnd && written === 0)
844
+ return null;
845
+ throw new Error('truncated transport frame');
846
+ }
847
+ this.chunk = value;
848
+ this.offset = 0;
849
+ if (!value?.length)
850
+ continue;
851
+ }
852
+ const count = Math.min(length - written, this.chunk.length - this.offset);
853
+ result.set(this.chunk.subarray(this.offset, this.offset + count), written);
854
+ this.offset += count;
855
+ written += count;
856
+ }
857
+ return result;
858
+ }
859
+ async read() {
860
+ const header = await this.exact(4, true);
861
+ if (!header)
862
+ return null;
863
+ const length = new DataView(header.buffer).getUint32(0);
864
+ if (!length || length > MAX_FRAME_BYTES)
865
+ throw new Error('invalid transport frame length');
866
+ return this.exact(length);
867
+ }
868
+ }
869
+ const jsonBytes = (value) => new TextEncoder().encode(JSON.stringify(value));
870
+ const parseJson = (bytes) => JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
871
+
872
+ const getWebTransportCtor = async () => {
824
873
  const candidate = globalThis;
874
+ if (!candidate.WebTransport && candidate.process?.versions?.node) {
875
+ const { WebTransport } = await import('./index.browser-CFfJpgKV.js');
876
+ return WebTransport;
877
+ }
825
878
  if (!candidate.WebTransport) {
826
879
  throw new Error('WebTransport is not available in this runtime');
827
880
  }
@@ -855,7 +908,12 @@ class WebTransportPeer {
855
908
  kind: 'webtransport-bidi',
856
909
  isConnected: () => this.connected,
857
910
  sendRaw: (payload) => {
858
- void this.#sendRaw(payload);
911
+ void this.#sendRaw(payload).catch(error => {
912
+ if (!this.#destroyed) {
913
+ this.#emit('error', error);
914
+ this.destroy();
915
+ }
916
+ });
859
917
  }
860
918
  }, {
861
919
  maxMessageSize: MAX_MESSAGE_SIZE,
@@ -866,7 +924,7 @@ class WebTransportPeer {
866
924
  }
867
925
  });
868
926
  queueMicrotask(() => {
869
- void this.#open(options.url);
927
+ void this.#open(options);
870
928
  });
871
929
  }
872
930
  #createMessageId() {
@@ -893,41 +951,55 @@ class WebTransportPeer {
893
951
  for (const handler of listeners)
894
952
  handler(...args);
895
953
  }
896
- async #open(url) {
954
+ async #open(options) {
897
955
  try {
898
- const WebTransportCtor = getWebTransportCtor();
899
- const session = new WebTransportCtor(url);
956
+ const ticket = await options.getTicket();
957
+ if (this.#destroyed)
958
+ return;
959
+ const WebTransportCtor = await getWebTransportCtor();
960
+ const session = new WebTransportCtor(options.url);
900
961
  this.#session = session;
962
+ void session.closed.then(() => this.destroy(), error => {
963
+ if (!this.#destroyed) {
964
+ this.#emit('error', error);
965
+ this.destroy();
966
+ }
967
+ });
901
968
  await session.ready;
969
+ if (this.#destroyed)
970
+ return;
902
971
  const stream = (await session.createBidirectionalStream());
972
+ if (this.#destroyed)
973
+ return;
903
974
  this.#reader = stream.readable.getReader();
904
975
  this.#writer = stream.writable.getWriter();
976
+ const frames = new FrameReader(this.#reader);
977
+ await this.#writer.write(encodeFrame(jsonBytes({ protocol: 'swarm-relay/1', ticket })));
978
+ const ready = parseJson(await frames.read());
979
+ if (ready.protocol !== 'swarm-relay/1' || ready.peerId !== this.peerId || String(ready.version) !== this.version) {
980
+ throw new Error('invalid WebTransport relay handshake');
981
+ }
982
+ if (this.#destroyed)
983
+ return;
905
984
  this.#connected = true;
906
985
  this.#emit('connect');
907
- void this.#readLoop();
908
- void session.closed.catch((error) => {
909
- if (this.#destroyed)
910
- return;
911
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
912
- });
986
+ void this.#readLoop(frames);
913
987
  }
914
988
  catch (error) {
915
989
  this.#emit('error', error instanceof Error ? error : new Error(String(error)));
916
990
  this.destroy();
917
991
  }
918
992
  }
919
- async #readLoop() {
993
+ async #readLoop(frames) {
920
994
  const reader = this.#reader;
921
995
  if (!reader)
922
996
  return;
923
997
  try {
924
998
  while (!this.#destroyed) {
925
999
  // eslint-disable-next-line no-await-in-loop
926
- const { done, value } = await reader.read();
927
- if (done)
928
- break;
1000
+ const value = await frames.read();
929
1001
  if (!value)
930
- continue;
1002
+ break;
931
1003
  this.bw.down += value.length;
932
1004
  this.#emit('data', value);
933
1005
  }
@@ -943,7 +1015,7 @@ class WebTransportPeer {
943
1015
  async #sendRaw(payload) {
944
1016
  if (!this.#writer)
945
1017
  throw new Error('WebTransport writer is not ready');
946
- await this.#writer.write(payload);
1018
+ await this.#writer.write(encodeFrame(payload));
947
1019
  }
948
1020
  on(event, handler) {
949
1021
  const listeners = this.#listeners.get(event) ?? new Set();
@@ -973,13 +1045,13 @@ class WebTransportPeer {
973
1045
  this.#destroyed = true;
974
1046
  this.#connected = false;
975
1047
  try {
976
- void this.#reader?.cancel?.();
1048
+ void this.#reader?.cancel?.().catch(() => { });
977
1049
  }
978
1050
  catch (e) { }
979
1051
  this.#reader?.releaseLock?.();
980
1052
  this.#reader = null;
981
1053
  try {
982
- void this.#writer?.close?.();
1054
+ void this.#writer?.close?.().catch(() => { });
983
1055
  }
984
1056
  catch (e) { }
985
1057
  this.#writer?.releaseLock?.();
@@ -1007,6 +1079,7 @@ class CircuitPeer {
1007
1079
  #destroyed = false;
1008
1080
  #connected = false;
1009
1081
  #sendViaCircuit;
1082
+ #core;
1010
1083
  initiator = true;
1011
1084
  peerId;
1012
1085
  channelName;
@@ -1020,12 +1093,40 @@ class CircuitPeer {
1020
1093
  this.channelName = `circuit:${options.from}:${options.to}`;
1021
1094
  this.version = String(options.version);
1022
1095
  this.#sendViaCircuit = options.sendViaCircuit;
1023
- setTimeout(() => {
1024
- if (this.#destroyed)
1025
- return;
1026
- this.#connected = true;
1027
- this.#emit('connect');
1028
- }, 0);
1096
+ this.#core = new PeerCore({
1097
+ kind: 'circuit',
1098
+ isConnected: () => this.connected,
1099
+ sendRaw: data => {
1100
+ void this.#sendViaCircuit({ id: this.#createMessageId(), data: Array.from(data) }).catch(error => {
1101
+ if (this.#destroyed)
1102
+ return;
1103
+ this.#emit('error', error);
1104
+ this.destroy();
1105
+ });
1106
+ }
1107
+ }, {
1108
+ // JSON byte arrays expand up to four characters per byte. Keep the whole
1109
+ // WebSocket frame below its default 64 KiB limit, including wrapper/ID.
1110
+ maxMessageSize: 14_000,
1111
+ compressionThreshold: 0.98,
1112
+ getPubSub,
1113
+ onPayloadSend: bytes => { this.bw.up += bytes; }
1114
+ });
1115
+ queueMicrotask(async () => {
1116
+ try {
1117
+ await options.handshake();
1118
+ if (this.#destroyed)
1119
+ return;
1120
+ this.#connected = true;
1121
+ this.#emit('connect');
1122
+ }
1123
+ catch (error) {
1124
+ if (this.#destroyed)
1125
+ return;
1126
+ this.#emit('error', error);
1127
+ this.destroy();
1128
+ }
1129
+ });
1029
1130
  }
1030
1131
  #createMessageId() {
1031
1132
  const randomUUID = globalThis.crypto?.randomUUID;
@@ -1055,43 +1156,21 @@ class CircuitPeer {
1055
1156
  // Circuit transport does not use SDP/ICE signaling.
1056
1157
  }
1057
1158
  send(data, id = this.#createMessageId()) {
1159
+ if (new TextEncoder().encode(id).length > 1024) {
1160
+ this.#emit('error', new Error('circuit message id exceeds 1024 bytes'));
1161
+ return;
1162
+ }
1058
1163
  if (this.#destroyed) {
1059
1164
  this.#emit('error', new Error('circuit peer is destroyed'));
1060
1165
  return;
1061
1166
  }
1062
- this.bw.up += data.length;
1063
- void this.#sendViaCircuit({ id, data: Array.from(data) }).catch((error) => {
1064
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
1065
- });
1167
+ this.#core.send(data, id);
1066
1168
  }
1067
1169
  request(data, id = this.#createMessageId()) {
1068
- const pubsub = getPubSub();
1069
- if (!pubsub) {
1070
- return Promise.reject(new Error('globalThis.pubsub is not available'));
1170
+ if (new TextEncoder().encode(id).length > 1024) {
1171
+ return Promise.reject(new Error('circuit message id exceeds 1024 bytes'));
1071
1172
  }
1072
- return new Promise((resolve, reject) => {
1073
- let timeout;
1074
- let settled = false;
1075
- const finish = (fn) => {
1076
- if (settled)
1077
- return;
1078
- settled = true;
1079
- clearTimeout(timeout);
1080
- try {
1081
- pubsub.unsubscribe(id, onResponse);
1082
- }
1083
- catch (e) { }
1084
- fn();
1085
- };
1086
- const onResponse = ({ data: response }) => {
1087
- finish(() => resolve(response));
1088
- };
1089
- timeout = setTimeout(() => {
1090
- finish(() => reject(new Error(`request for ${id} timed out`)));
1091
- }, 30_000);
1092
- pubsub.subscribe(id, onResponse);
1093
- this.send(data, id);
1094
- });
1173
+ return this.#core.request(data, id);
1095
1174
  }
1096
1175
  async getNetworkStats() {
1097
1176
  return null;
@@ -1164,6 +1243,8 @@ class Client {
1164
1243
  #fallbackCircuitMethod = 'swarm:fallback:send';
1165
1244
  #testHooks;
1166
1245
  #webTransportUrlTemplate;
1246
+ #discoverWebTransport = true;
1247
+ #relayUrls = new WeakMap();
1167
1248
  #circuitHandlers = new Map();
1168
1249
  #pendingCircuitRequests = new Map();
1169
1250
  #circuitRequestTimeoutMs = 30_000;
@@ -1202,7 +1283,9 @@ class Client {
1202
1283
  ? options.transport.kinds
1203
1284
  : options.transport?.kind
1204
1285
  ? [options.transport.kind]
1205
- : ['webrtc', 'webtransport'];
1286
+ : (options.transport?.webtransport?.urlTemplate
1287
+ ? ['webrtc', 'webtransport']
1288
+ : ['webrtc']);
1206
1289
  this.#supportedTransports = new Set(this.#normalizeTransportKinds(configuredKinds));
1207
1290
  if (!this.#supportedTransports.size)
1208
1291
  this.#supportedTransports.add('webrtc');
@@ -1213,6 +1296,11 @@ class Client {
1213
1296
  ? 'webrtc'
1214
1297
  : 'webtransport';
1215
1298
  this.#webTransportUrlTemplate = options.transport?.webtransport?.urlTemplate;
1299
+ this.#discoverWebTransport = !options.transport?.kind && !options.transport?.kinds ||
1300
+ !!options.transport?.kinds?.includes('webtransport') || options.transport?.kind === 'webtransport';
1301
+ if (this.#discoverWebTransport && (typeof globalThis.WebTransport === 'function' || globalThis.process?.versions?.node)) {
1302
+ this.#supportedTransports.add('webtransport');
1303
+ }
1216
1304
  const wrtcBackend = options.transport?.webrtc?.backend ||
1217
1305
  globalThis.process?.env?.NETPEER_SWARM_WRTC_BACKEND ||
1218
1306
  'vandeurenglenn';
@@ -1230,6 +1318,9 @@ class Client {
1230
1318
  if (options?.retry)
1231
1319
  this.#retryOptions = { ...this.#retryOptions, ...options.retry };
1232
1320
  this.#circuitHandlers.set(this.#fallbackCircuitMethod, (payload, context) => this.#handleFallbackCircuitPayload(payload, context));
1321
+ this.#circuitHandlers.set('swarm:fallback:hello', () => ({
1322
+ peerId: this.peerId, version: this.version, encoding: 'peer-core/1'
1323
+ }));
1233
1324
  if (!this.#testHooks?.skipInit) {
1234
1325
  this._init();
1235
1326
  }
@@ -1368,6 +1459,30 @@ class Client {
1368
1459
  const onServerData = (message) => {
1369
1460
  globalThis.pubsub.publish('server:data', message);
1370
1461
  };
1462
+ const onRelayReady = (message) => {
1463
+ if (!this.#discoverWebTransport || message?.protocol !== 'swarm-relay/1' || typeof message.url !== 'string')
1464
+ return;
1465
+ let url;
1466
+ try {
1467
+ url = new URL(message.url);
1468
+ }
1469
+ catch {
1470
+ return;
1471
+ }
1472
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash)
1473
+ return;
1474
+ if (this.#relayUrls.get(starConnection) === url.href)
1475
+ return;
1476
+ this.#relayUrls.set(starConnection, url.href);
1477
+ this.#supportedTransports.add('webtransport');
1478
+ // Update capabilities on this same authenticated star connection. The
1479
+ // server sends relay metadata before its peer snapshot, so fallback can
1480
+ // resolve a real endpoint without users configuring transport URLs.
1481
+ starConnection.send({ url: 'join', params: {
1482
+ version: this.version, peerId: this.peerId,
1483
+ transport: { kind: this.#preferredTransportKind, kinds: [...this.#supportedTransports] }
1484
+ } });
1485
+ };
1371
1486
  starConnection.pubsub.subscribe('peer:joined', onPeerJoined);
1372
1487
  starConnection.pubsub.subscribe('peer:left', onPeerLeft);
1373
1488
  starConnection.pubsub.subscribe('star:joined', onStarJoined);
@@ -1375,6 +1490,7 @@ class Client {
1375
1490
  starConnection.pubsub.subscribe('signal', onSignal);
1376
1491
  starConnection.pubsub.subscribe('circuit', onCircuit);
1377
1492
  starConnection.pubsub.subscribe('server:data', onServerData);
1493
+ starConnection.pubsub.subscribe('relay:ready', onRelayReady);
1378
1494
  this.#starListeners[starId] = [
1379
1495
  { topic: 'peer:joined', handler: onPeerJoined },
1380
1496
  { topic: 'peer:left', handler: onPeerLeft },
@@ -1382,7 +1498,8 @@ class Client {
1382
1498
  { topic: 'star:left', handler: onStarLeft },
1383
1499
  { topic: 'signal', handler: onSignal },
1384
1500
  { topic: 'circuit', handler: onCircuit },
1385
- { topic: 'server:data', handler: onServerData }
1501
+ { topic: 'server:data', handler: onServerData },
1502
+ { topic: 'relay:ready', handler: onRelayReady }
1386
1503
  ];
1387
1504
  }
1388
1505
  #starJoined = (id) => {
@@ -1588,12 +1705,12 @@ class Client {
1588
1705
  peer = this.#createRTCPeerConnection(peerId, attempt.star, attempt.version, attempt.initiator);
1589
1706
  }
1590
1707
  else if (transportKind === 'webtransport') {
1591
- const url = this.#resolveWebTransportUrl(peerId);
1708
+ const url = this.#resolveWebTransportUrl(peerId, attempt.star);
1592
1709
  if (!url) {
1593
1710
  this.#advanceTransportAttempt(peerId);
1594
1711
  return;
1595
1712
  }
1596
- peer = this.#createWebTransportConnection(peerId, url, attempt.version);
1713
+ peer = this.#createWebTransportConnection(peerId, url, attempt.version, attempt.star);
1597
1714
  }
1598
1715
  else {
1599
1716
  peer = this.#createCircuitConnection(peerId, attempt.version);
@@ -1640,7 +1757,25 @@ class Client {
1640
1757
  if (!peer) {
1641
1758
  peer = this.#createCircuitConnection(context.from, this.version);
1642
1759
  }
1643
- this.#noticeMessage(decoded.data, decoded.id, context.from, peer);
1760
+ if (!peer)
1761
+ throw new Error('circuit peer unavailable');
1762
+ if (!peer.connected) {
1763
+ const waitingPeer = peer;
1764
+ await new Promise((resolve, reject) => {
1765
+ const finish = (error) => {
1766
+ clearTimeout(timer);
1767
+ waitingPeer.off('connect', onConnect);
1768
+ waitingPeer.off('close', onClose);
1769
+ error ? reject(error) : resolve();
1770
+ };
1771
+ const onConnect = () => finish();
1772
+ const onClose = () => finish(new Error('circuit peer closed during handshake'));
1773
+ const timer = setTimeout(() => finish(new Error('circuit handshake timed out')), this.#transportConnectTimeoutMs);
1774
+ waitingPeer.on('connect', onConnect);
1775
+ waitingPeer.on('close', onClose);
1776
+ });
1777
+ }
1778
+ this.#peerData(peer, decoded.data);
1644
1779
  return { ok: true };
1645
1780
  }
1646
1781
  #createCircuitId() {
@@ -1698,15 +1833,22 @@ class Client {
1698
1833
  this.#pendingCircuitRequests.delete(id);
1699
1834
  reject(new Error(`circuit request for ${id} timed out`));
1700
1835
  }, timeoutMs);
1701
- this.#pendingCircuitRequests.set(id, { resolve, reject, timeout });
1702
- this.#sendCircuit(starConnection, {
1703
- to: peerId,
1704
- from: this.peerId,
1705
- id,
1706
- phase: 'request',
1707
- method,
1708
- data: this.#encodeCircuitData(data)
1709
- });
1836
+ this.#pendingCircuitRequests.set(id, { resolve, reject, timeout, peerId, starConnection });
1837
+ try {
1838
+ this.#sendCircuit(starConnection, {
1839
+ to: peerId,
1840
+ from: this.peerId,
1841
+ id,
1842
+ phase: 'request',
1843
+ method,
1844
+ data: this.#encodeCircuitData(data)
1845
+ });
1846
+ }
1847
+ catch (error) {
1848
+ clearTimeout(timeout);
1849
+ this.#pendingCircuitRequests.delete(id);
1850
+ reject(error);
1851
+ }
1710
1852
  });
1711
1853
  }
1712
1854
  #inComingCircuit = async ({ from, to, id, phase, method, data, error }, starConnection) => {
@@ -1716,6 +1858,10 @@ class Client {
1716
1858
  const pending = this.#pendingCircuitRequests.get(id);
1717
1859
  if (!pending)
1718
1860
  return;
1861
+ if (pending.starConnection !== starConnection)
1862
+ return;
1863
+ if (from !== pending.peerId && !(from === 'server' && error))
1864
+ return;
1719
1865
  clearTimeout(pending.timeout);
1720
1866
  this.#pendingCircuitRequests.delete(id);
1721
1867
  if (error) {
@@ -1789,17 +1935,16 @@ class Client {
1789
1935
  connectWebTransport(peerId, url) {
1790
1936
  this.#createWebTransportConnection(peerId, url, this.version);
1791
1937
  }
1792
- #resolveWebTransportUrl(peerId) {
1793
- const urlTemplate = this.#webTransportUrlTemplate;
1938
+ #resolveWebTransportUrl(peerId, starConnection) {
1939
+ const urlTemplate = this.#webTransportUrlTemplate || (starConnection && this.#relayUrls.get(starConnection));
1794
1940
  if (!urlTemplate) {
1795
- console.warn('WebTransport requires transport.webtransport.urlTemplate in client options');
1796
1941
  return null;
1797
1942
  }
1798
1943
  return urlTemplate
1799
1944
  .replace('{peerId}', encodeURIComponent(peerId))
1800
1945
  .replace('{selfId}', encodeURIComponent(this.peerId));
1801
1946
  }
1802
- #createWebTransportConnection(peerId, url, version) {
1947
+ #createWebTransportConnection(peerId, url, version, starConnection) {
1803
1948
  if (this.#connections[peerId]) {
1804
1949
  debug(`peer ${peerId} already connected`);
1805
1950
  return null;
@@ -1824,7 +1969,27 @@ class Client {
1824
1969
  from: this.peerId,
1825
1970
  to: peerId,
1826
1971
  version,
1827
- url
1972
+ url,
1973
+ getTicket: async () => {
1974
+ const star = starConnection || this.#pickStarConnection();
1975
+ if (!star)
1976
+ throw new Error('no connected star available for WebTransport ticket');
1977
+ let timer;
1978
+ try {
1979
+ const response = await Promise.race([
1980
+ star.request({ url: 'webtransport-ticket', params: { from: this.peerId, to: peerId } }),
1981
+ new Promise((_, reject) => {
1982
+ timer = setTimeout(() => reject(new Error('WebTransport ticket request timed out')), this.#transportConnectTimeoutMs);
1983
+ })
1984
+ ]);
1985
+ if (typeof response?.ticket !== 'string')
1986
+ throw new Error('star did not authorize WebTransport relay');
1987
+ return response.ticket;
1988
+ }
1989
+ finally {
1990
+ clearTimeout(timer);
1991
+ }
1992
+ }
1828
1993
  });
1829
1994
  peer.on('connect', () => this.#peerConnect(peer));
1830
1995
  peer.on('close', () => this.#peerClose(peer));
@@ -1858,6 +2023,12 @@ class Client {
1858
2023
  from: this.peerId,
1859
2024
  to: peerId,
1860
2025
  version,
2026
+ handshake: async () => {
2027
+ const response = await this.circuitRequest(peerId, 'swarm:fallback:hello', undefined, this.#transportConnectTimeoutMs);
2028
+ if (response?.peerId !== peerId || String(response?.version) !== String(version) || response.encoding !== 'peer-core/1') {
2029
+ throw new Error('invalid circuit handshake response');
2030
+ }
2031
+ },
1861
2032
  sendViaCircuit: (payload) => this.circuitRequest(peerId, this.#fallbackCircuitMethod, payload)
1862
2033
  });
1863
2034
  peer.on('connect', () => this.#peerConnect(peer));
@@ -2174,6 +2345,11 @@ class Client {
2174
2345
  peer.destroy();
2175
2346
  };
2176
2347
  async close() {
2348
+ for (const pending of this.#pendingCircuitRequests.values()) {
2349
+ clearTimeout(pending.timeout);
2350
+ pending.reject(new Error('client closed'));
2351
+ }
2352
+ this.#pendingCircuitRequests.clear();
2177
2353
  for (const peerId of this.#peerTransportAttempts.keys()) {
2178
2354
  this.#clearTransportAttempt(peerId);
2179
2355
  }