@meshconnect/uwc-wallet-connect-connector 0.6.4 → 0.7.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.
@@ -1,13 +1,26 @@
1
1
  import SignClient from '@walletconnect/sign-client';
2
2
  import type { WalletConnectConfig } from '@meshconnect/uwc-types';
3
+ /**
4
+ * Cap on distinct relay-error messages remembered for dedup. Dedup is a storm
5
+ * guard, not a permanent ledger — bounding it keeps memory flat on a long-lived
6
+ * session even if messages are high-cardinality (timestamps / urls).
7
+ */
8
+ export declare const MAX_REPORTED_RELAY_ERRORS = 100;
3
9
  /**
4
10
  * Service for managing WalletConnect client instance
5
11
  */
6
12
  export declare class WalletConnectClient {
7
13
  private config;
14
+ private onRelayError?;
8
15
  private client;
9
16
  private initPromise;
10
- constructor(config: WalletConnectConfig);
17
+ /** Dedupe relay-error reports so a reconnect storm surfaces once, not per tick. */
18
+ private readonly relayErrorDedup;
19
+ /**
20
+ * @param onRelayError - optional telemetry sink for relay-socket failures (see
21
+ * `attachRelayErrorListener`). Opt-in: with nothing wired, behaviour is unchanged.
22
+ */
23
+ constructor(config: WalletConnectConfig, onRelayError?: ((error: unknown) => void) | undefined);
11
24
  /**
12
25
  * Initialize or get the WalletConnect client
13
26
  */
@@ -16,6 +29,23 @@ export declare class WalletConnectClient {
16
29
  * Initialize the WalletConnect client
17
30
  */
18
31
  private initClient;
32
+ /**
33
+ * Surface relay-socket failures as telemetry. The relay JsonRpcProvider re-emits
34
+ * an abnormal WebSocket close under `RELAYER_EVENTS.error` — e.g. close code 3000
35
+ * "JWT Token is not yet valid" when the device clock is skewed past the relay's
36
+ * 120s leeway. That error is otherwise detached from the connect() promise, so it
37
+ * escapes the connect handoff and lands as an unhandled error. Forward it (deduped
38
+ * per message) so it becomes a classified signal instead. `error` only —
39
+ * `connection_stalled` / `transport_closed` fire on transient blips and would add
40
+ * noise. Telemetry-only; it never changes connection behaviour.
41
+ */
42
+ private attachRelayErrorListener;
43
+ /**
44
+ * The initialized client if `connect()` already ran, else null. Synchronous —
45
+ * lets callers attach lifecycle listeners post-connect without forcing a
46
+ * re-init or awaiting.
47
+ */
48
+ getExistingClient(): SignClient | null;
19
49
  /**
20
50
  * Disconnect all sessions
21
51
  */
@@ -1 +1 @@
1
- {"version":3,"file":"walletconnect-client.d.ts","sourceRoot":"","sources":["../../src/services/walletconnect-client.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,4BAA4B,CAAA;AACnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAEjE;;GAEG;AACH,qBAAa,mBAAmB;IAIlB,OAAO,CAAC,MAAM;IAH1B,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,WAAW,CAAmC;gBAElC,MAAM,EAAE,mBAAmB;IAE/C;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;IAmBtC;;OAEG;YACW,UAAU;IA6BxB;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBpC;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAW/C"}
1
+ {"version":3,"file":"walletconnect-client.d.ts","sourceRoot":"","sources":["../../src/services/walletconnect-client.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,4BAA4B,CAAA;AAGnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAEjE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAA;AAE5C;;GAEG;AACH,qBAAa,mBAAmB;IAa5B,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY,CAAC;IAbvB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,WAAW,CAAmC;IACtD,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAE/B;IAED;;;OAGG;gBAEO,MAAM,EAAE,mBAAmB,EAC3B,YAAY,CAAC,GAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,aAAA;IAGjD;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;IAmBtC;;OAEG;YACW,UAAU;IA+BxB;;;;;;;;;OASG;IACH,OAAO,CAAC,wBAAwB;IAkBhC;;;;OAIG;IACH,iBAAiB,IAAI,UAAU,GAAG,IAAI;IAItC;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBpC;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAW/C"}
@@ -1,13 +1,29 @@
1
1
  import SignClient from '@walletconnect/sign-client';
2
+ import { RELAYER_EVENTS } from '@walletconnect/core';
3
+ import { createBoundedDedup, safeErrorMessage } from '@meshconnect/uwc-types';
4
+ /**
5
+ * Cap on distinct relay-error messages remembered for dedup. Dedup is a storm
6
+ * guard, not a permanent ledger — bounding it keeps memory flat on a long-lived
7
+ * session even if messages are high-cardinality (timestamps / urls).
8
+ */
9
+ export const MAX_REPORTED_RELAY_ERRORS = 100;
2
10
  /**
3
11
  * Service for managing WalletConnect client instance
4
12
  */
5
13
  export class WalletConnectClient {
6
14
  config;
15
+ onRelayError;
7
16
  client = null;
8
17
  initPromise = null;
9
- constructor(config) {
18
+ /** Dedupe relay-error reports so a reconnect storm surfaces once, not per tick. */
19
+ relayErrorDedup = createBoundedDedup(MAX_REPORTED_RELAY_ERRORS);
20
+ /**
21
+ * @param onRelayError - optional telemetry sink for relay-socket failures (see
22
+ * `attachRelayErrorListener`). Opt-in: with nothing wired, behaviour is unchanged.
23
+ */
24
+ constructor(config, onRelayError) {
10
25
  this.config = config;
26
+ this.onRelayError = onRelayError;
11
27
  }
12
28
  /**
13
29
  * Initialize or get the WalletConnect client
@@ -52,8 +68,47 @@ export class WalletConnectClient {
52
68
  projectId: this.config.projectId,
53
69
  metadata
54
70
  });
71
+ this.attachRelayErrorListener(client);
55
72
  return client;
56
73
  }
74
+ /**
75
+ * Surface relay-socket failures as telemetry. The relay JsonRpcProvider re-emits
76
+ * an abnormal WebSocket close under `RELAYER_EVENTS.error` — e.g. close code 3000
77
+ * "JWT Token is not yet valid" when the device clock is skewed past the relay's
78
+ * 120s leeway. That error is otherwise detached from the connect() promise, so it
79
+ * escapes the connect handoff and lands as an unhandled error. Forward it (deduped
80
+ * per message) so it becomes a classified signal instead. `error` only —
81
+ * `connection_stalled` / `transport_closed` fire on transient blips and would add
82
+ * noise. Telemetry-only; it never changes connection behaviour.
83
+ */
84
+ attachRelayErrorListener(client) {
85
+ if (!this.onRelayError)
86
+ return;
87
+ client.core?.relayer?.on(RELAYER_EVENTS.error, (error) => {
88
+ // safeErrorMessage: this runs INSIDE the SDK's relayer dispatch, before
89
+ // the sink guard below — a throwing toString() here would break the
90
+ // emitter just like a throwing sink; the dedup key stays bounded too.
91
+ const message = safeErrorMessage(error);
92
+ if (!this.relayErrorDedup.firstSeen(message))
93
+ return;
94
+ try {
95
+ this.onRelayError?.(error);
96
+ }
97
+ catch {
98
+ // A throwing sink would propagate synchronously out of the WC SDK's
99
+ // relayer emitter and prevent its own error handling (transportClose)
100
+ // from running. Telemetry must never break the relay.
101
+ }
102
+ });
103
+ }
104
+ /**
105
+ * The initialized client if `connect()` already ran, else null. Synchronous —
106
+ * lets callers attach lifecycle listeners post-connect without forcing a
107
+ * re-init or awaiting.
108
+ */
109
+ getExistingClient() {
110
+ return this.client;
111
+ }
57
112
  /**
58
113
  * Disconnect all sessions
59
114
  */
@@ -1 +1 @@
1
- {"version":3,"file":"walletconnect-client.js","sourceRoot":"","sources":["../../src/services/walletconnect-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"walletconnect-client.js","sourceRoot":"","sources":["../../src/services/walletconnect-client.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { Network, Connector, ConnectorResult, SwitchNetworkResult, WalletConnectConfig, WalletConnectProvider, TransactionRequest, TransactionResult, NetworkRpcMap, SignatureType, EIP712TypedData } from '@meshconnect/uwc-types';
1
+ import type { Network, Connector, ConnectorResult, SwitchNetworkResult, WalletConnectConfig, WalletConnectProvider, TransactionRequest, TransactionResult, NetworkRpcMap, SignatureType, EIP712TypedData, WalletLifecycleEvent, WalletLifecycleContext } from '@meshconnect/uwc-types';
2
2
  export declare class WalletConnectConnector implements Connector {
3
3
  private walletConnectClient;
4
4
  private connectionService;
@@ -6,7 +6,7 @@ export declare class WalletConnectConnector implements Connector {
6
6
  private signatureService;
7
7
  private transactionService;
8
8
  private networkRpcMap;
9
- constructor(config: WalletConnectConfig, networkRpcMap?: NetworkRpcMap);
9
+ constructor(config: WalletConnectConfig, networkRpcMap?: NetworkRpcMap, onRelayError?: (error: unknown) => void);
10
10
  connect(network: Network, provider?: WalletConnectProvider): Promise<ConnectorResult>;
11
11
  getConnectionURI(): string;
12
12
  disconnect(): Promise<void>;
@@ -22,5 +22,24 @@ export declare class WalletConnectConnector implements Connector {
22
22
  * Used by fee-payer relay flows (Mesh BE owns broadcast); never broadcasts via the wallet.
23
23
  */
24
24
  signSolanaTransactionBytes(serializedTx: Uint8Array): Promise<Uint8Array>;
25
+ /**
26
+ * Observe post-connect lifecycle from the WalletConnect SignClient. Unlike the
27
+ * injected path, the SignClient lives IN this context (its own relay, not the
28
+ * iframe bridge), so this works in the production iframe too — no bridge needed.
29
+ *
30
+ * Maps the verified SignClient events (`@walletconnect/types`): `session_delete`
31
+ * → disconnect, `session_expire` → sessionExpired, and `session_event` (CAIP
32
+ * `accountsChanged` / `chainChanged`, the latter normalized to CAIP-2 from
33
+ * `event.data` — see `toCaipChainId`). The account address in the event
34
+ * payload is never forwarded. Returns undefined before a session exists so
35
+ * the core reports lifecycle unavailable.
36
+ *
37
+ * TOPIC FILTER: SignClient events are client-global and WC v2 persists old
38
+ * sessions in storage, so a stale session expiring would otherwise surface as a
39
+ * phantom disconnect/expiry of the live one. Every payload carries a `topic`
40
+ * (`@walletconnect/types` BaseEventArgs); only the ACTIVE session's topic is
41
+ * forwarded, resolved per event so a restore/re-connect is honoured live.
42
+ */
43
+ onLifecycleEvent(listener: (event: WalletLifecycleEvent) => void, _context: WalletLifecycleContext): (() => void) | undefined;
25
44
  }
26
45
  //# sourceMappingURL=wallet-connect-connector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-connect-connector.d.ts","sourceRoot":"","sources":["../src/wallet-connect-connector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,eAAe,EAChB,MAAM,wBAAwB,CAAA;AAO/B,qBAAa,sBAAuB,YAAW,SAAS;IACtD,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,iBAAiB,CAAmB;IAC5C,OAAO,CAAC,oBAAoB,CAAsB;IAClD,OAAO,CAAC,gBAAgB,CAAkB;IAC1C,OAAO,CAAC,kBAAkB,CAAoB;IAC9C,OAAO,CAAC,aAAa,CAAe;gBAExB,MAAM,EAAE,mBAAmB,EAAE,aAAa,GAAE,aAAkB;IAmBpE,OAAO,CACX,OAAO,EAAE,OAAO,EAChB,QAAQ,CAAC,EAAE,qBAAqB,GAC/B,OAAO,CAAC,eAAe,CAAC;IAU3B,gBAAgB,IAAI,MAAM;IAQpB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAI7D,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAIpD,aAAa,CAAC,SAAS,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAI1D,eAAe,CACnB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC;IAI7B;;;;;;OAMG;IACG,0BAA0B,CAC9B,YAAY,EAAE,UAAU,GACvB,OAAO,CAAC,UAAU,CAAC;CAGvB"}
1
+ {"version":3,"file":"wallet-connect-connector.d.ts","sourceRoot":"","sources":["../src/wallet-connect-connector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAEP,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,wBAAwB,CAAA;AAsG/B,qBAAa,sBAAuB,YAAW,SAAS;IACtD,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,iBAAiB,CAAmB;IAC5C,OAAO,CAAC,oBAAoB,CAAsB;IAClD,OAAO,CAAC,gBAAgB,CAAkB;IAC1C,OAAO,CAAC,kBAAkB,CAAoB;IAC9C,OAAO,CAAC,aAAa,CAAe;gBAGlC,MAAM,EAAE,mBAAmB,EAC3B,aAAa,GAAE,aAAkB,EACjC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI;IAoBnC,OAAO,CACX,OAAO,EAAE,OAAO,EAChB,QAAQ,CAAC,EAAE,qBAAqB,GAC/B,OAAO,CAAC,eAAe,CAAC;IAU3B,gBAAgB,IAAI,MAAM;IAQpB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAI7D,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAIpD,aAAa,CAAC,SAAS,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAI1D,eAAe,CACnB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC;IAI7B;;;;;;OAMG;IACG,0BAA0B,CAC9B,YAAY,EAAE,UAAU,GACvB,OAAO,CAAC,UAAU,CAAC;IAItB;;;;;;;;;;;;;;;;;OAiBG;IACH,gBAAgB,CACd,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,EAI/C,QAAQ,EAAE,sBAAsB,GAC/B,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;CA8C5B"}
@@ -3,6 +3,104 @@ import { ConnectionService } from './services/connection-service';
3
3
  import { NetworkSwitchService } from './services/network-switch-service';
4
4
  import { SignatureService } from './services/signature-service';
5
5
  import { TransactionService } from './services/transaction-service';
6
+ // The shape checks below are plain char loops, not regexes: the payload is
7
+ // wallet-supplied, and the SAST gate fails the pipeline for ANY regex over
8
+ // untrusted input (WS-I002-JAVASCRIPT-00082), linear-time or not.
9
+ /** Non-empty and every char is a decimal digit. */
10
+ function isDecimalString(value) {
11
+ if (value.length === 0)
12
+ return false;
13
+ for (let i = 0; i < value.length; i++) {
14
+ const c = value.charCodeAt(i);
15
+ if (c < 48 || c > 57)
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+ /** `0x` followed by at least one hex digit, nothing else. */
21
+ function isHexQuantity(value) {
22
+ if (value.length < 3 || !value.startsWith('0x'))
23
+ return false;
24
+ for (let i = 2; i < value.length; i++) {
25
+ const c = value.charCodeAt(i);
26
+ const hex = (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70);
27
+ if (!hex)
28
+ return false;
29
+ }
30
+ return true;
31
+ }
32
+ /**
33
+ * CAIP-2 shape: `[a-z0-9]{3,8}` namespace, one colon, `[-_a-zA-Z0-9]{1,32}`
34
+ * reference. The reference charset excludes `:`, so a second colon fails.
35
+ */
36
+ function isCaip2ChainId(value) {
37
+ const colon = value.indexOf(':');
38
+ if (colon < 3 || colon > 8)
39
+ return false;
40
+ const referenceLength = value.length - colon - 1;
41
+ if (referenceLength < 1 || referenceLength > 32)
42
+ return false;
43
+ for (let i = 0; i < colon; i++) {
44
+ const c = value.charCodeAt(i);
45
+ if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 122)))
46
+ return false;
47
+ }
48
+ for (let i = colon + 1; i < value.length; i++) {
49
+ const c = value.charCodeAt(i);
50
+ const ok = (c >= 48 && c <= 57) ||
51
+ (c >= 97 && c <= 122) ||
52
+ (c >= 65 && c <= 90) ||
53
+ c === 45 || // '-'
54
+ c === 95; // '_'
55
+ if (!ok)
56
+ return false;
57
+ }
58
+ return true;
59
+ }
60
+ /**
61
+ * Normalize a WC `session_event` chainChanged payload to CAIP-2. The NEW chain
62
+ * travels in `event.data` (hex / decimal string / number per the EIP-1193
63
+ * convention — WalletConnect's own EthereumProvider reads `event.data`); the
64
+ * sibling `params.chainId` is only the CAIP-2 scope the event was emitted
65
+ * under, i.e. potentially the PRE-switch chain, so it donates its namespace
66
+ * and nothing else. Unparseable data yields undefined — a content-free
67
+ * `chainChanged` beats a wrong chain id.
68
+ */
69
+ function toCaipChainId(data, scope) {
70
+ if (typeof data === 'string' && isCaip2ChainId(data)) {
71
+ // Some wallets already send CAIP-2. An eip155 reference must be DECIMAL
72
+ // per CAIP-2, so a non-decimal ref (`eip155:0x1`) re-enters the numeric
73
+ // normalization below — the namespace survives via the `scope` argument.
74
+ // The shape check guarantees one colon with a non-empty tail; the
75
+ // fallback only satisfies noUncheckedIndexedAccess.
76
+ const [namespace, reference = ''] = data.split(':');
77
+ if (namespace !== 'eip155' || isDecimalString(reference)) {
78
+ return data;
79
+ }
80
+ return toCaipChainId(reference, data);
81
+ }
82
+ let decimal;
83
+ if (typeof data === 'string') {
84
+ // Full-shape match before converting: parseInt('0x1junk', 16) === 1.
85
+ if (isHexQuantity(data)) {
86
+ decimal = parseInt(data, 16);
87
+ }
88
+ else if (isDecimalString(data)) {
89
+ decimal = parseInt(data, 10);
90
+ }
91
+ }
92
+ else if (typeof data === 'number' && Number.isInteger(data) && data >= 0) {
93
+ decimal = data;
94
+ }
95
+ // isSafeInteger: beyond 2^53-1 parseInt has already lost precision —
96
+ // emitting a silently-wrong chain id is worse than emitting none.
97
+ if (decimal === undefined || !Number.isSafeInteger(decimal))
98
+ return undefined;
99
+ const namespace = typeof scope === 'string' && scope.includes(':')
100
+ ? scope.split(':')[0]
101
+ : 'eip155';
102
+ return `${namespace}:${decimal}`;
103
+ }
6
104
  export class WalletConnectConnector {
7
105
  walletConnectClient;
8
106
  connectionService;
@@ -10,8 +108,8 @@ export class WalletConnectConnector {
10
108
  signatureService;
11
109
  transactionService;
12
110
  networkRpcMap;
13
- constructor(config, networkRpcMap = {}) {
14
- this.walletConnectClient = new WalletConnectClient(config);
111
+ constructor(config, networkRpcMap = {}, onRelayError) {
112
+ this.walletConnectClient = new WalletConnectClient(config, onRelayError);
15
113
  this.networkRpcMap = networkRpcMap;
16
114
  this.connectionService = new ConnectionService(this.walletConnectClient);
17
115
  this.networkSwitchService = new NetworkSwitchService(this.walletConnectClient, this.connectionService);
@@ -56,5 +154,66 @@ export class WalletConnectConnector {
56
154
  async signSolanaTransactionBytes(serializedTx) {
57
155
  return this.transactionService.signSolanaTransactionBytes(serializedTx);
58
156
  }
157
+ /**
158
+ * Observe post-connect lifecycle from the WalletConnect SignClient. Unlike the
159
+ * injected path, the SignClient lives IN this context (its own relay, not the
160
+ * iframe bridge), so this works in the production iframe too — no bridge needed.
161
+ *
162
+ * Maps the verified SignClient events (`@walletconnect/types`): `session_delete`
163
+ * → disconnect, `session_expire` → sessionExpired, and `session_event` (CAIP
164
+ * `accountsChanged` / `chainChanged`, the latter normalized to CAIP-2 from
165
+ * `event.data` — see `toCaipChainId`). The account address in the event
166
+ * payload is never forwarded. Returns undefined before a session exists so
167
+ * the core reports lifecycle unavailable.
168
+ *
169
+ * TOPIC FILTER: SignClient events are client-global and WC v2 persists old
170
+ * sessions in storage, so a stale session expiring would otherwise surface as a
171
+ * phantom disconnect/expiry of the live one. Every payload carries a `topic`
172
+ * (`@walletconnect/types` BaseEventArgs); only the ACTIVE session's topic is
173
+ * forwarded, resolved per event so a restore/re-connect is honoured live.
174
+ */
175
+ onLifecycleEvent(listener,
176
+ // The active-session TOPIC (below) is the discriminator here — the context
177
+ // names the session but carries no topic, so it is accepted for contract
178
+ // parity and unused.
179
+ _context) {
180
+ const client = this.walletConnectClient.getExistingClient();
181
+ if (!client)
182
+ return undefined;
183
+ const isActiveTopic = (topic) => {
184
+ const active = this.connectionService.getSession()?.topic;
185
+ return active !== undefined && topic === active;
186
+ };
187
+ const onDelete = (args) => {
188
+ if (isActiveTopic(args?.topic))
189
+ listener({ type: 'disconnect' });
190
+ };
191
+ const onExpire = (args) => {
192
+ if (isActiveTopic(args?.topic))
193
+ listener({ type: 'sessionExpired' });
194
+ };
195
+ const onSessionEvent = (args) => {
196
+ if (!isActiveTopic(args?.topic))
197
+ return;
198
+ const name = args?.params?.event?.name;
199
+ if (name === 'accountsChanged') {
200
+ // Address dropped — signal THAT it changed, not which.
201
+ listener({ type: 'accountChanged' });
202
+ }
203
+ else if (name === 'chainChanged') {
204
+ // The new chain is in `event.data`; `params.chainId` is only the scope.
205
+ const chainId = toCaipChainId(args?.params?.event?.data, args?.params?.chainId);
206
+ listener(chainId ? { type: 'chainChanged', chainId } : { type: 'chainChanged' });
207
+ }
208
+ };
209
+ client.on('session_delete', onDelete);
210
+ client.on('session_expire', onExpire);
211
+ client.on('session_event', onSessionEvent);
212
+ return () => {
213
+ client.off('session_delete', onDelete);
214
+ client.off('session_expire', onExpire);
215
+ client.off('session_event', onSessionEvent);
216
+ };
217
+ }
59
218
  }
60
219
  //# sourceMappingURL=wallet-connect-connector.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wallet-connect-connector.js","sourceRoot":"","sources":["../src/wallet-connect-connector.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAA;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AAEnE,MAAM,OAAO,sBAAsB;IACzB,mBAAmB,CAAqB;IACxC,iBAAiB,CAAmB;IACpC,oBAAoB,CAAsB;IAC1C,gBAAgB,CAAkB;IAClC,kBAAkB,CAAoB;IACtC,aAAa,CAAe;IAEpC,YAAY,MAA2B,EAAE,gBAA+B,EAAE;QACxE,IAAI,CAAC,mBAAmB,GAAG,IAAI,mBAAmB,CAAC,MAAM,CAAC,CAAA;QAC1D,IAAI,CAAC,aAAa,GAAG,aAAa,CAAA;QAClC,IAAI,CAAC,iBAAiB,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACxE,IAAI,CAAC,oBAAoB,GAAG,IAAI,oBAAoB,CAClD,IAAI,CAAC,mBAAmB,EACxB,IAAI,CAAC,iBAAiB,CACvB,CAAA;QACD,IAAI,CAAC,gBAAgB,GAAG,IAAI,gBAAgB,CAC1C,IAAI,CAAC,mBAAmB,EACxB,IAAI,CAAC,iBAAiB,CACvB,CAAA;QACD,IAAI,CAAC,kBAAkB,GAAG,IAAI,kBAAkB,CAC9C,IAAI,CAAC,mBAAmB,EACxB,IAAI,CAAC,iBAAiB,EACtB,IAAI,CAAC,aAAa,CACnB,CAAA;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CACX,OAAgB,EAChB,QAAgC;QAEhC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,gEAAgE,CACjE,CAAA;QACH,CAAC;QAED,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAC1D,CAAC;IAED,gBAAgB;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,CAAA;QACrD,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAA;QACvE,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,UAAU;QACd,OAAO,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,CAAA;IAC5C,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAgB;QAClC,OAAO,IAAI,CAAC,oBAAoB,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;IACzD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAe;QAC/B,OAAO,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;IACnD,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,SAA0B;QAC5C,OAAO,IAAI,CAAC,gBAAgB,CAAC,aAAa,CAAC,SAAS,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,OAA2B;QAE3B,OAAO,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAA;IACzD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,0BAA0B,CAC9B,YAAwB;QAExB,OAAO,IAAI,CAAC,kBAAkB,CAAC,0BAA0B,CAAC,YAAY,CAAC,CAAA;IACzE,CAAC;CACF"}
1
+ {"version":3,"file":"wallet-connect-connector.js","sourceRoot":"","sources":["../src/wallet-connect-connector.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshconnect/uwc-wallet-connect-connector",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "description": "WalletConnect connector for Universal Wallet Connector",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,13 +18,14 @@
18
18
  "dependencies": {
19
19
  "@solana/spl-token": "^0.4.14",
20
20
  "@solana/web3.js": "^1.98.4",
21
+ "@walletconnect/core": "^2.23.4",
21
22
  "@walletconnect/sign-client": "^2.23.4",
22
23
  "@walletconnect/types": "^2.23.4",
23
24
  "@walletconnect/utils": "^2.23.4",
24
25
  "bs58": "6.0.0",
25
26
  "buffer": "^6.0.3",
26
27
  "viem": "^2.44.4",
27
- "@meshconnect/uwc-types": "0.16.1"
28
+ "@meshconnect/uwc-types": "0.17.0"
28
29
  },
29
30
  "devDependencies": {
30
31
  "typescript": "^5.9.3"
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import type { WalletConnectConfig } from '@meshconnect/uwc-types'
3
+ import { RELAYER_EVENTS } from '@walletconnect/core'
4
+
5
+ // Relayer error handlers registered by the client under test, keyed by event name.
6
+ const relayerHandlers: Record<string, (arg?: unknown) => void> = {}
7
+ const fakeClient = {
8
+ core: {
9
+ relayer: {
10
+ on: (event: string, handler: (arg?: unknown) => void) => {
11
+ relayerHandlers[event] = handler
12
+ }
13
+ }
14
+ }
15
+ }
16
+
17
+ vi.mock('@walletconnect/sign-client', () => ({
18
+ default: { init: vi.fn(async () => fakeClient) }
19
+ }))
20
+
21
+ import {
22
+ WalletConnectClient,
23
+ MAX_REPORTED_RELAY_ERRORS
24
+ } from './walletconnect-client'
25
+
26
+ const CONFIG = { projectId: 'p' } as WalletConnectConfig
27
+
28
+ describe('WalletConnectClient relay-error surfacing', () => {
29
+ beforeEach(() => {
30
+ for (const key of Object.keys(relayerHandlers)) delete relayerHandlers[key]
31
+ })
32
+
33
+ it('forwards a relayer error to onRelayError', async () => {
34
+ const errors: unknown[] = []
35
+ const client = new WalletConnectClient(CONFIG, e => errors.push(e))
36
+ await client.getClient()
37
+
38
+ relayerHandlers[RELAYER_EVENTS.error]?.(
39
+ new Error(
40
+ 'WebSocket connection closed abnormally with code: 3000 (JWT validation error: JWT Token is not yet valid)'
41
+ )
42
+ )
43
+
44
+ expect(errors).toHaveLength(1)
45
+ expect((errors[0] as Error).message).toContain('3000')
46
+ })
47
+
48
+ it('dedupes repeated identical relay errors (reconnect storm → one report)', async () => {
49
+ const errors: unknown[] = []
50
+ const client = new WalletConnectClient(CONFIG, e => errors.push(e))
51
+ await client.getClient()
52
+
53
+ const fire = () =>
54
+ relayerHandlers[RELAYER_EVENTS.error]?.(new Error('same message'))
55
+ fire()
56
+ fire()
57
+ fire()
58
+
59
+ expect(errors).toHaveLength(1)
60
+ })
61
+
62
+ it('swallows a throwing onRelayError sink — must not break the relayer emitter', async () => {
63
+ const client = new WalletConnectClient(CONFIG, () => {
64
+ throw new Error('sink exploded')
65
+ })
66
+ await client.getClient()
67
+
68
+ // The handler runs inside the WC SDK's own relayer error dispatch — a
69
+ // propagating throw would prevent the SDK's transportClose from running.
70
+ expect(() =>
71
+ relayerHandlers[RELAYER_EVENTS.error]?.(new Error('relay down'))
72
+ ).not.toThrow()
73
+ })
74
+
75
+ it('never throws when the relayer emits a value with a throwing toString()', async () => {
76
+ const errors: unknown[] = []
77
+ const client = new WalletConnectClient(CONFIG, e => errors.push(e))
78
+ await client.getClient()
79
+
80
+ const evil = {
81
+ toString(): string {
82
+ throw new Error('poisoned toString')
83
+ }
84
+ }
85
+ // Message derivation runs inside the SDK's relayer dispatch, before the
86
+ // sink guard — it must be throw-proof too.
87
+ expect(() => relayerHandlers[RELAYER_EVENTS.error]?.(evil)).not.toThrow()
88
+ expect(errors).toHaveLength(1)
89
+ })
90
+
91
+ it('does not attach a relayer listener when no onRelayError is wired', async () => {
92
+ const client = new WalletConnectClient(CONFIG)
93
+ await expect(client.getClient()).resolves.toBeDefined()
94
+ expect(relayerHandlers[RELAYER_EVENTS.error]).toBeUndefined()
95
+ })
96
+
97
+ it('bounds the dedup set — an evicted message can report again past the cap', async () => {
98
+ const errors: unknown[] = []
99
+ const client = new WalletConnectClient(CONFIG, e => errors.push(e))
100
+ await client.getClient()
101
+ const fire = (m: string) =>
102
+ relayerHandlers[RELAYER_EVENTS.error]?.(new Error(m))
103
+
104
+ // Fill the set to the cap with distinct messages — each reports once.
105
+ for (let i = 0; i < MAX_REPORTED_RELAY_ERRORS; i++) fire(`err-${i}`)
106
+ expect(errors).toHaveLength(MAX_REPORTED_RELAY_ERRORS)
107
+
108
+ // Still deduped while in the set.
109
+ fire('err-0')
110
+ expect(errors).toHaveLength(MAX_REPORTED_RELAY_ERRORS)
111
+
112
+ // One more distinct message trips the cap → the set is evicted → it reports.
113
+ fire('over-cap')
114
+ expect(errors).toHaveLength(MAX_REPORTED_RELAY_ERRORS + 1)
115
+
116
+ // err-0 was evicted, so it reports again — proving the set stays bounded.
117
+ fire('err-0')
118
+ expect(errors).toHaveLength(MAX_REPORTED_RELAY_ERRORS + 2)
119
+ })
120
+ })
@@ -1,14 +1,34 @@
1
1
  import SignClient from '@walletconnect/sign-client'
2
+ import { RELAYER_EVENTS } from '@walletconnect/core'
3
+ import { createBoundedDedup, safeErrorMessage } from '@meshconnect/uwc-types'
2
4
  import type { WalletConnectConfig } from '@meshconnect/uwc-types'
3
5
 
6
+ /**
7
+ * Cap on distinct relay-error messages remembered for dedup. Dedup is a storm
8
+ * guard, not a permanent ledger — bounding it keeps memory flat on a long-lived
9
+ * session even if messages are high-cardinality (timestamps / urls).
10
+ */
11
+ export const MAX_REPORTED_RELAY_ERRORS = 100
12
+
4
13
  /**
5
14
  * Service for managing WalletConnect client instance
6
15
  */
7
16
  export class WalletConnectClient {
8
17
  private client: SignClient | null = null
9
18
  private initPromise: Promise<SignClient> | null = null
19
+ /** Dedupe relay-error reports so a reconnect storm surfaces once, not per tick. */
20
+ private readonly relayErrorDedup = createBoundedDedup(
21
+ MAX_REPORTED_RELAY_ERRORS
22
+ )
10
23
 
11
- constructor(private config: WalletConnectConfig) {}
24
+ /**
25
+ * @param onRelayError - optional telemetry sink for relay-socket failures (see
26
+ * `attachRelayErrorListener`). Opt-in: with nothing wired, behaviour is unchanged.
27
+ */
28
+ constructor(
29
+ private config: WalletConnectConfig,
30
+ private onRelayError?: (error: unknown) => void
31
+ ) {}
12
32
 
13
33
  /**
14
34
  * Initialize or get the WalletConnect client
@@ -61,9 +81,48 @@ export class WalletConnectClient {
61
81
  metadata
62
82
  })
63
83
 
84
+ this.attachRelayErrorListener(client)
85
+
64
86
  return client
65
87
  }
66
88
 
89
+ /**
90
+ * Surface relay-socket failures as telemetry. The relay JsonRpcProvider re-emits
91
+ * an abnormal WebSocket close under `RELAYER_EVENTS.error` — e.g. close code 3000
92
+ * "JWT Token is not yet valid" when the device clock is skewed past the relay's
93
+ * 120s leeway. That error is otherwise detached from the connect() promise, so it
94
+ * escapes the connect handoff and lands as an unhandled error. Forward it (deduped
95
+ * per message) so it becomes a classified signal instead. `error` only —
96
+ * `connection_stalled` / `transport_closed` fire on transient blips and would add
97
+ * noise. Telemetry-only; it never changes connection behaviour.
98
+ */
99
+ private attachRelayErrorListener(client: SignClient): void {
100
+ if (!this.onRelayError) return
101
+ client.core?.relayer?.on(RELAYER_EVENTS.error, (error: unknown) => {
102
+ // safeErrorMessage: this runs INSIDE the SDK's relayer dispatch, before
103
+ // the sink guard below — a throwing toString() here would break the
104
+ // emitter just like a throwing sink; the dedup key stays bounded too.
105
+ const message = safeErrorMessage(error)
106
+ if (!this.relayErrorDedup.firstSeen(message)) return
107
+ try {
108
+ this.onRelayError?.(error)
109
+ } catch {
110
+ // A throwing sink would propagate synchronously out of the WC SDK's
111
+ // relayer emitter and prevent its own error handling (transportClose)
112
+ // from running. Telemetry must never break the relay.
113
+ }
114
+ })
115
+ }
116
+
117
+ /**
118
+ * The initialized client if `connect()` already ran, else null. Synchronous —
119
+ * lets callers attach lifecycle listeners post-connect without forcing a
120
+ * re-init or awaiting.
121
+ */
122
+ getExistingClient(): SignClient | null {
123
+ return this.client
124
+ }
125
+
67
126
  /**
68
127
  * Disconnect all sessions
69
128
  */
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import type {
3
+ WalletConnectConfig,
4
+ WalletLifecycleEvent
5
+ } from '@meshconnect/uwc-types'
6
+ import { WalletConnectConnector } from './wallet-connect-connector'
7
+
8
+ const WC_CONTEXT = { connectionMode: 'walletConnect' as const }
9
+
10
+ function mockSignClient() {
11
+ const handlers: Record<string, (a?: unknown) => void> = {}
12
+ return {
13
+ on: (e: string, h: (a?: unknown) => void) => {
14
+ handlers[e] = h
15
+ },
16
+ off: vi.fn((e: string) => {
17
+ delete handlers[e]
18
+ }),
19
+ fire: (e: string, arg?: unknown) => handlers[e]?.(arg)
20
+ }
21
+ }
22
+
23
+ function makeConnector(client: unknown) {
24
+ const connector = new WalletConnectConnector({
25
+ projectId: 'test'
26
+ } as WalletConnectConfig)
27
+ // Stub the post-connect client lookup (no real relay in a unit test).
28
+ ;(
29
+ connector as unknown as {
30
+ walletConnectClient: { getExistingClient: () => unknown }
31
+ }
32
+ ).walletConnectClient.getExistingClient = () => client
33
+ return connector
34
+ }
35
+
36
+ // The SignClient is client-global: its events fire for EVERY stored session
37
+ // (WC v2 persists old sessions across page loads), so the lifecycle handlers
38
+ // must filter by the ACTIVE session's topic. This helper plants the active
39
+ // session the way `connect()` would.
40
+ function setActiveTopic(connector: WalletConnectConnector, topic: string) {
41
+ ;(
42
+ connector as unknown as {
43
+ connectionService: { setSession: (s: unknown) => void }
44
+ }
45
+ ).connectionService.setSession({ topic })
46
+ }
47
+
48
+ describe('WalletConnectConnector.onLifecycleEvent', () => {
49
+ it('returns undefined before a session exists', () => {
50
+ expect(
51
+ makeConnector(null).onLifecycleEvent(() => {}, WC_CONTEXT)
52
+ ).toBeUndefined()
53
+ })
54
+
55
+ it('maps session_delete / session_expire / session_event for the ACTIVE topic and drops the address', () => {
56
+ const client = mockSignClient()
57
+ const events: WalletLifecycleEvent[] = []
58
+ const connector = makeConnector(client)
59
+ setActiveTopic(connector, 't-active')
60
+ const stop = connector.onLifecycleEvent(e => events.push(e), WC_CONTEXT)
61
+ expect(typeof stop).toBe('function')
62
+
63
+ client.fire('session_delete', { id: 1, topic: 't-active' })
64
+ client.fire('session_expire', { topic: 't-active' })
65
+ // CAIP session_event: accountsChanged carries an address we must NOT forward.
66
+ client.fire('session_event', {
67
+ id: 2,
68
+ topic: 't-active',
69
+ params: {
70
+ event: { name: 'accountsChanged', data: ['eip155:1:0xDEADBEEF'] },
71
+ chainId: 'eip155:1'
72
+ }
73
+ })
74
+ // Real WC payload shape: the NEW chain rides in `event.data` (EIP-1193
75
+ // convention); `params.chainId` is only the scope — here the PRE-switch
76
+ // chain, which must NOT be reported.
77
+ client.fire('session_event', {
78
+ id: 3,
79
+ topic: 't-active',
80
+ params: {
81
+ event: { name: 'chainChanged', data: '0x2105' },
82
+ chainId: 'eip155:1'
83
+ }
84
+ })
85
+
86
+ expect(events).toEqual([
87
+ { type: 'disconnect' },
88
+ { type: 'sessionExpired' },
89
+ { type: 'accountChanged' },
90
+ { type: 'chainChanged', chainId: 'eip155:8453' }
91
+ ])
92
+ expect(JSON.stringify(events)).not.toContain('0xDEADBEEF')
93
+
94
+ stop!()
95
+ expect(client.off).toHaveBeenCalledTimes(3)
96
+ })
97
+
98
+ it('drops lifecycle events from a NON-active topic (stale persisted session)', () => {
99
+ // A stale session left in WC storage from a previous pairing expiring must
100
+ // not surface as a phantom disconnect/expiry of the live session.
101
+ const client = mockSignClient()
102
+ const events: WalletLifecycleEvent[] = []
103
+ const connector = makeConnector(client)
104
+ setActiveTopic(connector, 't-active')
105
+ connector.onLifecycleEvent(e => events.push(e), WC_CONTEXT)
106
+
107
+ client.fire('session_delete', { id: 1, topic: 't-stale' })
108
+ client.fire('session_expire', { topic: 't-stale' })
109
+ client.fire('session_event', {
110
+ id: 2,
111
+ topic: 't-stale',
112
+ params: { event: { name: 'chainChanged' }, chainId: 'eip155:10' }
113
+ })
114
+
115
+ expect(events).toEqual([])
116
+ })
117
+
118
+ it('drops all lifecycle events when no session is active', () => {
119
+ // Post-disconnect (session cleared) the client can still emit for stored
120
+ // sessions; with no active topic nothing may be forwarded.
121
+ const client = mockSignClient()
122
+ const events: WalletLifecycleEvent[] = []
123
+ makeConnector(client).onLifecycleEvent(e => events.push(e), WC_CONTEXT)
124
+
125
+ client.fire('session_delete', { id: 1, topic: 't-any' })
126
+ client.fire('session_expire', { topic: 't-any' })
127
+
128
+ expect(events).toEqual([])
129
+ })
130
+
131
+ it('reads the active topic per event, so a re-connect is honoured live', () => {
132
+ const client = mockSignClient()
133
+ const events: WalletLifecycleEvent[] = []
134
+ const connector = makeConnector(client)
135
+ setActiveTopic(connector, 't-1')
136
+ connector.onLifecycleEvent(e => events.push(e), WC_CONTEXT)
137
+
138
+ client.fire('session_expire', { topic: 't-2' })
139
+ expect(events).toEqual([])
140
+
141
+ // New session replaces the old one (restore / re-connect) — events for the
142
+ // new topic must flow without re-subscribing.
143
+ setActiveTopic(connector, 't-2')
144
+ client.fire('session_expire', { topic: 't-2' })
145
+ expect(events).toEqual([{ type: 'sessionExpired' }])
146
+ })
147
+
148
+ it('normalizes chainChanged event.data variants to CAIP-2 and never falls back to the scope chain', () => {
149
+ const client = mockSignClient()
150
+ const events: WalletLifecycleEvent[] = []
151
+ const connector = makeConnector(client)
152
+ setActiveTopic(connector, 't-active')
153
+ connector.onLifecycleEvent(e => events.push(e), WC_CONTEXT)
154
+
155
+ const fire = (data: unknown) =>
156
+ client.fire('session_event', {
157
+ id: 1,
158
+ topic: 't-active',
159
+ params: { event: { name: 'chainChanged', data }, chainId: 'eip155:1' }
160
+ })
161
+
162
+ fire('0x89') // hex string
163
+ fire('137') // decimal string
164
+ fire(8453) // number
165
+ fire('eip155:10') // already CAIP-2
166
+ fire('eip155:0x2105') // CAIP-2 shape, but eip155 refs must be decimal
167
+ fire('tvm:-239') // non-eip155 namespace: reference is opaque, verbatim
168
+ // Malformed / missing / precision-lost data must yield a content-free
169
+ // event, NOT the scope chain (`eip155:1` here is the PRE-switch chain).
170
+ fire('0x1junk')
171
+ fire('eip155:junk') // CAIP-2 shape, eip155 ref neither decimal nor hex
172
+ fire(undefined)
173
+ fire('0x20000000000001') // 2^53+1: parseInt lost precision
174
+ // CAIP-2 shape boundaries — lock the validator's exact contract:
175
+ fire(`solana:${'a'.repeat(32)}`) // reference at the 32-char cap: valid
176
+ fire('ab:1') // namespace under 3 chars
177
+ fire('abcdefghi:1') // namespace over 8 chars
178
+ fire(`solana:${'a'.repeat(33)}`) // reference over the 32-char cap
179
+ fire('EIP155:1') // namespace must be lowercase
180
+ fire('tvm:-239:extra') // a second colon is not a CAIP-2 reference
181
+
182
+ expect(events).toEqual([
183
+ { type: 'chainChanged', chainId: 'eip155:137' },
184
+ { type: 'chainChanged', chainId: 'eip155:137' },
185
+ { type: 'chainChanged', chainId: 'eip155:8453' },
186
+ { type: 'chainChanged', chainId: 'eip155:10' },
187
+ { type: 'chainChanged', chainId: 'eip155:8453' },
188
+ { type: 'chainChanged', chainId: 'tvm:-239' },
189
+ { type: 'chainChanged' },
190
+ { type: 'chainChanged' },
191
+ { type: 'chainChanged' },
192
+ { type: 'chainChanged' },
193
+ { type: 'chainChanged', chainId: `solana:${'a'.repeat(32)}` },
194
+ { type: 'chainChanged' },
195
+ { type: 'chainChanged' },
196
+ { type: 'chainChanged' },
197
+ { type: 'chainChanged' },
198
+ { type: 'chainChanged' }
199
+ ])
200
+ })
201
+ })
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  Network,
3
+ NetworkId,
3
4
  Connector,
4
5
  ConnectorResult,
5
6
  SwitchNetworkResult,
@@ -9,7 +10,9 @@ import type {
9
10
  TransactionResult,
10
11
  NetworkRpcMap,
11
12
  SignatureType,
12
- EIP712TypedData
13
+ EIP712TypedData,
14
+ WalletLifecycleEvent,
15
+ WalletLifecycleContext
13
16
  } from '@meshconnect/uwc-types'
14
17
  import { WalletConnectClient } from './services/walletconnect-client'
15
18
  import { ConnectionService } from './services/connection-service'
@@ -17,6 +20,101 @@ import { NetworkSwitchService } from './services/network-switch-service'
17
20
  import { SignatureService } from './services/signature-service'
18
21
  import { TransactionService } from './services/transaction-service'
19
22
 
23
+ // The shape checks below are plain char loops, not regexes: the payload is
24
+ // wallet-supplied, and the SAST gate fails the pipeline for ANY regex over
25
+ // untrusted input (WS-I002-JAVASCRIPT-00082), linear-time or not.
26
+
27
+ /** Non-empty and every char is a decimal digit. */
28
+ function isDecimalString(value: string): boolean {
29
+ if (value.length === 0) return false
30
+ for (let i = 0; i < value.length; i++) {
31
+ const c = value.charCodeAt(i)
32
+ if (c < 48 || c > 57) return false
33
+ }
34
+ return true
35
+ }
36
+
37
+ /** `0x` followed by at least one hex digit, nothing else. */
38
+ function isHexQuantity(value: string): boolean {
39
+ if (value.length < 3 || !value.startsWith('0x')) return false
40
+ for (let i = 2; i < value.length; i++) {
41
+ const c = value.charCodeAt(i)
42
+ const hex =
43
+ (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70)
44
+ if (!hex) return false
45
+ }
46
+ return true
47
+ }
48
+
49
+ /**
50
+ * CAIP-2 shape: `[a-z0-9]{3,8}` namespace, one colon, `[-_a-zA-Z0-9]{1,32}`
51
+ * reference. The reference charset excludes `:`, so a second colon fails.
52
+ */
53
+ function isCaip2ChainId(value: string): boolean {
54
+ const colon = value.indexOf(':')
55
+ if (colon < 3 || colon > 8) return false
56
+ const referenceLength = value.length - colon - 1
57
+ if (referenceLength < 1 || referenceLength > 32) return false
58
+ for (let i = 0; i < colon; i++) {
59
+ const c = value.charCodeAt(i)
60
+ if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 122))) return false
61
+ }
62
+ for (let i = colon + 1; i < value.length; i++) {
63
+ const c = value.charCodeAt(i)
64
+ const ok =
65
+ (c >= 48 && c <= 57) ||
66
+ (c >= 97 && c <= 122) ||
67
+ (c >= 65 && c <= 90) ||
68
+ c === 45 || // '-'
69
+ c === 95 // '_'
70
+ if (!ok) return false
71
+ }
72
+ return true
73
+ }
74
+
75
+ /**
76
+ * Normalize a WC `session_event` chainChanged payload to CAIP-2. The NEW chain
77
+ * travels in `event.data` (hex / decimal string / number per the EIP-1193
78
+ * convention — WalletConnect's own EthereumProvider reads `event.data`); the
79
+ * sibling `params.chainId` is only the CAIP-2 scope the event was emitted
80
+ * under, i.e. potentially the PRE-switch chain, so it donates its namespace
81
+ * and nothing else. Unparseable data yields undefined — a content-free
82
+ * `chainChanged` beats a wrong chain id.
83
+ */
84
+ function toCaipChainId(data: unknown, scope: unknown): NetworkId | undefined {
85
+ if (typeof data === 'string' && isCaip2ChainId(data)) {
86
+ // Some wallets already send CAIP-2. An eip155 reference must be DECIMAL
87
+ // per CAIP-2, so a non-decimal ref (`eip155:0x1`) re-enters the numeric
88
+ // normalization below — the namespace survives via the `scope` argument.
89
+ // The shape check guarantees one colon with a non-empty tail; the
90
+ // fallback only satisfies noUncheckedIndexedAccess.
91
+ const [namespace, reference = ''] = data.split(':')
92
+ if (namespace !== 'eip155' || isDecimalString(reference)) {
93
+ return data as NetworkId
94
+ }
95
+ return toCaipChainId(reference, data)
96
+ }
97
+ let decimal: number | undefined
98
+ if (typeof data === 'string') {
99
+ // Full-shape match before converting: parseInt('0x1junk', 16) === 1.
100
+ if (isHexQuantity(data)) {
101
+ decimal = parseInt(data, 16)
102
+ } else if (isDecimalString(data)) {
103
+ decimal = parseInt(data, 10)
104
+ }
105
+ } else if (typeof data === 'number' && Number.isInteger(data) && data >= 0) {
106
+ decimal = data
107
+ }
108
+ // isSafeInteger: beyond 2^53-1 parseInt has already lost precision —
109
+ // emitting a silently-wrong chain id is worse than emitting none.
110
+ if (decimal === undefined || !Number.isSafeInteger(decimal)) return undefined
111
+ const namespace =
112
+ typeof scope === 'string' && scope.includes(':')
113
+ ? scope.split(':')[0]
114
+ : 'eip155'
115
+ return `${namespace}:${decimal}` as NetworkId
116
+ }
117
+
20
118
  export class WalletConnectConnector implements Connector {
21
119
  private walletConnectClient: WalletConnectClient
22
120
  private connectionService: ConnectionService
@@ -25,8 +123,12 @@ export class WalletConnectConnector implements Connector {
25
123
  private transactionService: TransactionService
26
124
  private networkRpcMap: NetworkRpcMap
27
125
 
28
- constructor(config: WalletConnectConfig, networkRpcMap: NetworkRpcMap = {}) {
29
- this.walletConnectClient = new WalletConnectClient(config)
126
+ constructor(
127
+ config: WalletConnectConfig,
128
+ networkRpcMap: NetworkRpcMap = {},
129
+ onRelayError?: (error: unknown) => void
130
+ ) {
131
+ this.walletConnectClient = new WalletConnectClient(config, onRelayError)
30
132
  this.networkRpcMap = networkRpcMap
31
133
  this.connectionService = new ConnectionService(this.walletConnectClient)
32
134
  this.networkSwitchService = new NetworkSwitchService(
@@ -99,4 +201,75 @@ export class WalletConnectConnector implements Connector {
99
201
  ): Promise<Uint8Array> {
100
202
  return this.transactionService.signSolanaTransactionBytes(serializedTx)
101
203
  }
204
+
205
+ /**
206
+ * Observe post-connect lifecycle from the WalletConnect SignClient. Unlike the
207
+ * injected path, the SignClient lives IN this context (its own relay, not the
208
+ * iframe bridge), so this works in the production iframe too — no bridge needed.
209
+ *
210
+ * Maps the verified SignClient events (`@walletconnect/types`): `session_delete`
211
+ * → disconnect, `session_expire` → sessionExpired, and `session_event` (CAIP
212
+ * `accountsChanged` / `chainChanged`, the latter normalized to CAIP-2 from
213
+ * `event.data` — see `toCaipChainId`). The account address in the event
214
+ * payload is never forwarded. Returns undefined before a session exists so
215
+ * the core reports lifecycle unavailable.
216
+ *
217
+ * TOPIC FILTER: SignClient events are client-global and WC v2 persists old
218
+ * sessions in storage, so a stale session expiring would otherwise surface as a
219
+ * phantom disconnect/expiry of the live one. Every payload carries a `topic`
220
+ * (`@walletconnect/types` BaseEventArgs); only the ACTIVE session's topic is
221
+ * forwarded, resolved per event so a restore/re-connect is honoured live.
222
+ */
223
+ onLifecycleEvent(
224
+ listener: (event: WalletLifecycleEvent) => void,
225
+ // The active-session TOPIC (below) is the discriminator here — the context
226
+ // names the session but carries no topic, so it is accepted for contract
227
+ // parity and unused.
228
+ _context: WalletLifecycleContext
229
+ ): (() => void) | undefined {
230
+ const client = this.walletConnectClient.getExistingClient()
231
+ if (!client) return undefined
232
+
233
+ const isActiveTopic = (topic: string | undefined): boolean => {
234
+ const active = this.connectionService.getSession()?.topic
235
+ return active !== undefined && topic === active
236
+ }
237
+
238
+ const onDelete = (args: { topic: string }) => {
239
+ if (isActiveTopic(args?.topic)) listener({ type: 'disconnect' })
240
+ }
241
+ const onExpire = (args: { topic: string }) => {
242
+ if (isActiveTopic(args?.topic)) listener({ type: 'sessionExpired' })
243
+ }
244
+ const onSessionEvent = (args: {
245
+ topic: string
246
+ params: { event: { name: string; data?: unknown }; chainId: string }
247
+ }) => {
248
+ if (!isActiveTopic(args?.topic)) return
249
+ const name = args?.params?.event?.name
250
+ if (name === 'accountsChanged') {
251
+ // Address dropped — signal THAT it changed, not which.
252
+ listener({ type: 'accountChanged' })
253
+ } else if (name === 'chainChanged') {
254
+ // The new chain is in `event.data`; `params.chainId` is only the scope.
255
+ const chainId = toCaipChainId(
256
+ args?.params?.event?.data,
257
+ args?.params?.chainId
258
+ )
259
+ listener(
260
+ chainId ? { type: 'chainChanged', chainId } : { type: 'chainChanged' }
261
+ )
262
+ }
263
+ }
264
+
265
+ client.on('session_delete', onDelete)
266
+ client.on('session_expire', onExpire)
267
+ client.on('session_event', onSessionEvent)
268
+
269
+ return () => {
270
+ client.off('session_delete', onDelete)
271
+ client.off('session_expire', onExpire)
272
+ client.off('session_event', onSessionEvent)
273
+ }
274
+ }
102
275
  }