@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.
- package/dist/services/walletconnect-client.d.ts +31 -1
- package/dist/services/walletconnect-client.d.ts.map +1 -1
- package/dist/services/walletconnect-client.js +56 -1
- package/dist/services/walletconnect-client.js.map +1 -1
- package/dist/wallet-connect-connector.d.ts +21 -2
- package/dist/wallet-connect-connector.d.ts.map +1 -1
- package/dist/wallet-connect-connector.js +161 -2
- package/dist/wallet-connect-connector.js.map +1 -1
- package/package.json +3 -2
- package/src/services/walletconnect-client.test.ts +120 -0
- package/src/services/walletconnect-client.ts +60 -1
- package/src/wallet-connect-connector.test.ts +201 -0
- package/src/wallet-connect-connector.ts +176 -3
|
@@ -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
|
-
|
|
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;
|
|
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
|
-
|
|
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":"AAAA,OAAO,UAAU,MAAM,4BAA4B,CAAA;
|
|
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,
|
|
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":"
|
|
1
|
+
{"version":3,"file":"wallet-connect-connector.js","sourceRoot":"","sources":["../src/wallet-connect-connector.ts"],"names":[],"mappings":"AAgBA,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,2EAA2E;AAC3E,2EAA2E;AAC3E,kEAAkE;AAElE,mDAAmD;AACnD,SAAS,eAAe,CAAC,KAAa;IACpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAC7B,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,KAAK,CAAA;IACpC,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,6DAA6D;AAC7D,SAAS,aAAa,CAAC,KAAa;IAClC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IAC7D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAC7B,MAAM,GAAG,GACP,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;QACvE,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAA;IACxB,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;GAGG;AACH,SAAS,cAAc,CAAC,KAAa;IACnC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACxC,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,GAAG,KAAK,GAAG,CAAC,CAAA;IAChD,IAAI,eAAe,GAAG,CAAC,IAAI,eAAe,GAAG,EAAE;QAAE,OAAO,KAAK,CAAA;IAC7D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;IACpE,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,MAAM,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAC7B,MAAM,EAAE,GACN,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;YACpB,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC;YACrB,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;YACpB,CAAC,KAAK,EAAE,IAAI,MAAM;YAClB,CAAC,KAAK,EAAE,CAAA,CAAC,MAAM;QACjB,IAAI,CAAC,EAAE;YAAE,OAAO,KAAK,CAAA;IACvB,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,aAAa,CAAC,IAAa,EAAE,KAAc;IAClD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,wEAAwE;QACxE,wEAAwE;QACxE,yEAAyE;QACzE,kEAAkE;QAClE,oDAAoD;QACpD,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACnD,IAAI,SAAS,KAAK,QAAQ,IAAI,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;YACzD,OAAO,IAAiB,CAAA;QAC1B,CAAC;QACD,OAAO,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;IACvC,CAAC;IACD,IAAI,OAA2B,CAAA;IAC/B,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,qEAAqE;QACrE,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QAC9B,CAAC;aAAM,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QAC9B,CAAC;IACH,CAAC;SAAM,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QAC3E,OAAO,GAAG,IAAI,CAAA;IAChB,CAAC;IACD,qEAAqE;IACrE,kEAAkE;IAClE,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAA;IAC7E,MAAM,SAAS,GACb,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC9C,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC,CAAC,QAAQ,CAAA;IACd,OAAO,GAAG,SAAS,IAAI,OAAO,EAAe,CAAA;AAC/C,CAAC;AAED,MAAM,OAAO,sBAAsB;IACzB,mBAAmB,CAAqB;IACxC,iBAAiB,CAAmB;IACpC,oBAAoB,CAAsB;IAC1C,gBAAgB,CAAkB;IAClC,kBAAkB,CAAoB;IACtC,aAAa,CAAe;IAEpC,YACE,MAA2B,EAC3B,gBAA+B,EAAE,EACjC,YAAuC;QAEvC,IAAI,CAAC,mBAAmB,GAAG,IAAI,mBAAmB,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;QACxE,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;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,gBAAgB,CACd,QAA+C;IAC/C,2EAA2E;IAC3E,yEAAyE;IACzE,qBAAqB;IACrB,QAAgC;QAEhC,MAAM,MAAM,GAAG,IAAI,CAAC,mBAAmB,CAAC,iBAAiB,EAAE,CAAA;QAC3D,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAA;QAE7B,MAAM,aAAa,GAAG,CAAC,KAAyB,EAAW,EAAE;YAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,EAAE,KAAK,CAAA;YACzD,OAAO,MAAM,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,CAAA;QACjD,CAAC,CAAA;QAED,MAAM,QAAQ,GAAG,CAAC,IAAuB,EAAE,EAAE;YAC3C,IAAI,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC;gBAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAA;QAClE,CAAC,CAAA;QACD,MAAM,QAAQ,GAAG,CAAC,IAAuB,EAAE,EAAE;YAC3C,IAAI,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC;gBAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAA;QACtE,CAAC,CAAA;QACD,MAAM,cAAc,GAAG,CAAC,IAGvB,EAAE,EAAE;YACH,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC;gBAAE,OAAM;YACvC,MAAM,IAAI,GAAG,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAA;YACtC,IAAI,IAAI,KAAK,iBAAiB,EAAE,CAAC;gBAC/B,uDAAuD;gBACvD,QAAQ,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAA;YACtC,CAAC;iBAAM,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;gBACnC,wEAAwE;gBACxE,MAAM,OAAO,GAAG,aAAa,CAC3B,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EACzB,IAAI,EAAE,MAAM,EAAE,OAAO,CACtB,CAAA;gBACD,QAAQ,CACN,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CACvE,CAAA;YACH,CAAC;QACH,CAAC,CAAA;QAED,MAAM,CAAC,EAAE,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAA;QACrC,MAAM,CAAC,EAAE,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAA;QACrC,MAAM,CAAC,EAAE,CAAC,eAAe,EAAE,cAAc,CAAC,CAAA;QAE1C,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAA;YACtC,MAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAA;YACtC,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAA;QAC7C,CAAC,CAAA;IACH,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meshconnect/uwc-wallet-connect-connector",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
29
|
-
|
|
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
|
}
|