@gluwa/connect-kit 0.1.0-next.3 → 0.2.0-next.1
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/CHANGELOG.md +6 -0
- package/dist/credit-connect.js +148 -133
- package/dist/index.css +561 -0
- package/dist/index.d.ts +39 -25
- package/dist/index.js +315 -655
- package/dist/package.json +11 -6
- package/package.json +8 -5
- package/src/ConnectKitProvider.tsx +75 -49
- package/src/ConnectModal.tsx +39 -25
- package/src/api/account.ts +21 -0
- package/src/api/balance.ts +10 -0
- package/src/api/chain.ts +17 -0
- package/src/api/contract.ts +33 -0
- package/src/api/transaction.ts +12 -0
- package/src/components/QRFrame.tsx +2 -2
- package/src/core/config.ts +83 -0
- package/src/creditConnectConnector.ts +175 -159
- package/src/events/account.ts +49 -0
- package/src/hooks/useWCState.ts +24 -9
- package/src/hooks/useWagmiConnect.ts +13 -8
- package/src/index.ts +33 -0
- package/src/types.ts +9 -0
- package/src/utils/platform.ts +1 -4
- package/src/views/CreditWalletView.tsx +0 -1
- package/src/views/MetaMaskView.tsx +4 -2
- package/src/views/SwitchChainView.tsx +9 -4
- package/tsup.config.ts +1 -1
|
@@ -41,6 +41,7 @@ export const creditConnectConnector = (
|
|
|
41
41
|
let selectedChainId: number | null = null;
|
|
42
42
|
let connectPromise: Promise<boolean> | null = null;
|
|
43
43
|
let provider: CreditConnectProvider | null = null;
|
|
44
|
+
let providerPromise: Promise<CreditConnectProvider> | null = null;
|
|
44
45
|
let isLocalDisconnect = false;
|
|
45
46
|
|
|
46
47
|
let emitWagmiMessage: ((type: string, data: unknown) => void) | null = null;
|
|
@@ -128,6 +129,163 @@ export const creditConnectConnector = (
|
|
|
128
129
|
config.emitter.emit('disconnect');
|
|
129
130
|
};
|
|
130
131
|
|
|
132
|
+
const buildProvider = (): CreditConnectProviderWithEvents => {
|
|
133
|
+
const createdProvider = {
|
|
134
|
+
request: async (args) => {
|
|
135
|
+
const { method, params: rawParams } = args as {
|
|
136
|
+
method: string;
|
|
137
|
+
params?: unknown[] | object;
|
|
138
|
+
};
|
|
139
|
+
const params = Array.isArray(rawParams)
|
|
140
|
+
? [...rawParams]
|
|
141
|
+
: rawParams === undefined
|
|
142
|
+
? []
|
|
143
|
+
: [rawParams];
|
|
144
|
+
const session = getCurrentSession();
|
|
145
|
+
|
|
146
|
+
const getSelectedAddressInfo = (address?: string) => {
|
|
147
|
+
if (!session) throw new Error('No active session');
|
|
148
|
+
const evmInfo = getEvmAddressInfo(session);
|
|
149
|
+
if (evmInfo.length === 0) throw new Error('No EVM address found');
|
|
150
|
+
if (!address) return evmInfo[0];
|
|
151
|
+
const found = evmInfo.find(
|
|
152
|
+
(info) => info.address.toLowerCase() === address.toLowerCase(),
|
|
153
|
+
);
|
|
154
|
+
if (!found) throw new Error(`Address not in session: ${address}`);
|
|
155
|
+
return found;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (method === 'eth_accounts') {
|
|
159
|
+
if (!session) return [];
|
|
160
|
+
return getChecksummedAccounts(session);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (method === 'eth_chainId') {
|
|
164
|
+
if (!session) throw new Error('No active session');
|
|
165
|
+
if (selectedChainId !== null) return `0x${selectedChainId.toString(16)}`;
|
|
166
|
+
const evmInfo = getEvmAddressInfo(session);
|
|
167
|
+
if (evmInfo.length === 0) throw new Error('No EVM address found');
|
|
168
|
+
const chainId = Number.parseInt(evmInfo[0].networkIdentifier, 10);
|
|
169
|
+
if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
|
|
170
|
+
selectedChainId = chainId;
|
|
171
|
+
return `0x${chainId.toString(16)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (method === 'wallet_switchEthereumChain') {
|
|
175
|
+
const [{ chainId: rawChainId }] = (params ?? []) as [{ chainId?: string }];
|
|
176
|
+
if (!rawChainId) throw new Error('wallet_switchEthereumChain missing chainId param');
|
|
177
|
+
const chainId = Number.parseInt(rawChainId, 16);
|
|
178
|
+
if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${rawChainId}`);
|
|
179
|
+
if (!session) throw new Error('No active session');
|
|
180
|
+
|
|
181
|
+
const supported = getEvmAddressInfo(session).some(
|
|
182
|
+
(info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
|
|
183
|
+
);
|
|
184
|
+
if (!supported) {
|
|
185
|
+
const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
|
|
186
|
+
err.code = 4902;
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
selectedChainId = chainId;
|
|
190
|
+
config.emitter.emit('change', { chainId });
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (method === 'personal_sign') {
|
|
195
|
+
const [messageParam, addressParam] = (params ?? []) as [unknown, unknown];
|
|
196
|
+
const message =
|
|
197
|
+
typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
|
|
198
|
+
const address = typeof addressParam === 'string' ? addressParam : undefined;
|
|
199
|
+
const addressInfo = getSelectedAddressInfo(address);
|
|
200
|
+
const response = await session?.jsonRpc.request('signMessage', {
|
|
201
|
+
message,
|
|
202
|
+
addressInfo,
|
|
203
|
+
});
|
|
204
|
+
return response?.signature;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (method === 'eth_sign') {
|
|
208
|
+
const [addressParam, messageParam] = (params ?? []) as [unknown, unknown];
|
|
209
|
+
const address = typeof addressParam === 'string' ? addressParam : undefined;
|
|
210
|
+
const message =
|
|
211
|
+
typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
|
|
212
|
+
const addressInfo = getSelectedAddressInfo(address);
|
|
213
|
+
const response = await session?.jsonRpc.request('signMessage', {
|
|
214
|
+
message,
|
|
215
|
+
addressInfo,
|
|
216
|
+
});
|
|
217
|
+
return response?.signature;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
|
|
221
|
+
const [addressParam, typedDataParam] = (params ?? []) as [unknown, unknown];
|
|
222
|
+
const address = typeof addressParam === 'string' ? addressParam : undefined;
|
|
223
|
+
const typedData =
|
|
224
|
+
typeof typedDataParam === 'string'
|
|
225
|
+
? typedDataParam
|
|
226
|
+
: JSON.stringify(typedDataParam ?? '');
|
|
227
|
+
const addressInfo = getSelectedAddressInfo(address);
|
|
228
|
+
const response = await session?.jsonRpc.request('signTypedData', {
|
|
229
|
+
typedData,
|
|
230
|
+
addressInfo,
|
|
231
|
+
});
|
|
232
|
+
return response?.signature;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (method === 'eth_sendTransaction') {
|
|
236
|
+
const [txParams] = (params ?? []) as [Record<string, string | undefined>];
|
|
237
|
+
const addressInfo = getSelectedAddressInfo(txParams?.from);
|
|
238
|
+
const response = await session?.jsonRpc.request('sendTransaction', {
|
|
239
|
+
addressInfo,
|
|
240
|
+
to: txParams?.to,
|
|
241
|
+
value: txParams?.value,
|
|
242
|
+
data: txParams?.data,
|
|
243
|
+
gas: txParams?.gas,
|
|
244
|
+
gasPrice: txParams?.gasPrice,
|
|
245
|
+
maxFeePerGas: txParams?.maxFeePerGas,
|
|
246
|
+
maxPriorityFeePerGas: txParams?.maxPriorityFeePerGas,
|
|
247
|
+
nonce: txParams?.nonce,
|
|
248
|
+
chainId: txParams?.chainId,
|
|
249
|
+
});
|
|
250
|
+
return response?.txHash;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (method === 'credit_connect_send_message') {
|
|
254
|
+
if (!session) throw new Error('No active session');
|
|
255
|
+
const [messageParam] = (params ?? []) as [unknown];
|
|
256
|
+
const message =
|
|
257
|
+
typeof messageParam === 'string' ? messageParam : JSON.stringify(messageParam ?? '');
|
|
258
|
+
await session.msg.sendMessage(message);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (method === 'credit_connect_jsonrpc_ping') {
|
|
263
|
+
if (!session) throw new Error('No active session');
|
|
264
|
+
return await session.jsonRpc.request('pingPong', {});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
throw new Error(`Unsupported provider method: ${method}`);
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
on: (event: string, listener: ProviderListener) => {
|
|
271
|
+
let listeners = providerListeners.get(event);
|
|
272
|
+
if (!listeners) {
|
|
273
|
+
listeners = new Set();
|
|
274
|
+
providerListeners.set(event, listeners);
|
|
275
|
+
}
|
|
276
|
+
listeners.add(listener);
|
|
277
|
+
return createdProvider;
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
removeListener: (event: string, listener: ProviderListener) => {
|
|
281
|
+
providerListeners.get(event)?.delete(listener);
|
|
282
|
+
return createdProvider;
|
|
283
|
+
},
|
|
284
|
+
} as CreditConnectProviderWithEvents;
|
|
285
|
+
|
|
286
|
+
return createdProvider;
|
|
287
|
+
};
|
|
288
|
+
|
|
131
289
|
return {
|
|
132
290
|
id: CONNECTOR_ID,
|
|
133
291
|
name: CONNECTOR_NAME,
|
|
@@ -261,11 +419,13 @@ export const creditConnectConnector = (
|
|
|
261
419
|
throw err;
|
|
262
420
|
}
|
|
263
421
|
|
|
422
|
+
const chain = config.chains.find((c) => c.id === chainId);
|
|
423
|
+
if (!chain) throw new Error(`Chain ${chainId} not in config`);
|
|
424
|
+
|
|
264
425
|
selectedChainId = chainId;
|
|
265
426
|
config.emitter.emit('change', { chainId });
|
|
427
|
+
await Promise.resolve();
|
|
266
428
|
|
|
267
|
-
const chain = config.chains.find((c) => c.id === chainId);
|
|
268
|
-
if (!chain) throw new Error(`Chain ${chainId} not in config`);
|
|
269
429
|
return chain;
|
|
270
430
|
},
|
|
271
431
|
|
|
@@ -286,6 +446,7 @@ export const creditConnectConnector = (
|
|
|
286
446
|
isLocalDisconnect = false;
|
|
287
447
|
}
|
|
288
448
|
provider = null;
|
|
449
|
+
providerPromise = null;
|
|
289
450
|
resetState();
|
|
290
451
|
config.emitter.emit('disconnect');
|
|
291
452
|
},
|
|
@@ -314,165 +475,20 @@ export const creditConnectConnector = (
|
|
|
314
475
|
|
|
315
476
|
async getProvider() {
|
|
316
477
|
if (provider) return provider;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const createdProvider = {
|
|
320
|
-
request: async (args) => {
|
|
321
|
-
const { method, params: rawParams } = args as {
|
|
322
|
-
method: string;
|
|
323
|
-
params?: unknown[] | object;
|
|
324
|
-
};
|
|
325
|
-
const params = Array.isArray(rawParams)
|
|
326
|
-
? [...rawParams]
|
|
327
|
-
: rawParams === undefined
|
|
328
|
-
? []
|
|
329
|
-
: [rawParams];
|
|
330
|
-
const session = getCurrentSession();
|
|
331
|
-
|
|
332
|
-
const getSelectedAddressInfo = (address?: string) => {
|
|
333
|
-
if (!session) throw new Error('No active session');
|
|
334
|
-
const evmInfo = getEvmAddressInfo(session);
|
|
335
|
-
if (evmInfo.length === 0) throw new Error('No EVM address found');
|
|
336
|
-
if (!address) return evmInfo[0];
|
|
337
|
-
const found = evmInfo.find(
|
|
338
|
-
(info) => info.address.toLowerCase() === address.toLowerCase(),
|
|
339
|
-
);
|
|
340
|
-
if (!found) throw new Error(`Address not in session: ${address}`);
|
|
341
|
-
return found;
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
if (method === 'eth_accounts') {
|
|
345
|
-
if (!session) return [];
|
|
346
|
-
return getChecksummedAccounts(session);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (method === 'eth_chainId') {
|
|
350
|
-
if (!session) throw new Error('No active session');
|
|
351
|
-
if (selectedChainId !== null) return `0x${selectedChainId.toString(16)}`;
|
|
352
|
-
const evmInfo = getEvmAddressInfo(session);
|
|
353
|
-
if (evmInfo.length === 0) throw new Error('No EVM address found');
|
|
354
|
-
const chainId = Number.parseInt(evmInfo[0].networkIdentifier, 10);
|
|
355
|
-
if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
|
|
356
|
-
selectedChainId = chainId;
|
|
357
|
-
return `0x${chainId.toString(16)}`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (method === 'wallet_switchEthereumChain') {
|
|
361
|
-
const [{ chainId: rawChainId }] = (params ?? []) as [{ chainId?: string }];
|
|
362
|
-
if (!rawChainId) throw new Error('wallet_switchEthereumChain missing chainId param');
|
|
363
|
-
const chainId = Number.parseInt(rawChainId, 16);
|
|
364
|
-
if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${rawChainId}`);
|
|
365
|
-
if (!session) throw new Error('No active session');
|
|
366
|
-
|
|
367
|
-
const supported = getEvmAddressInfo(session).some(
|
|
368
|
-
(info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
|
|
369
|
-
);
|
|
370
|
-
if (!supported) {
|
|
371
|
-
const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
|
|
372
|
-
err.code = 4902;
|
|
373
|
-
throw err;
|
|
374
|
-
}
|
|
375
|
-
selectedChainId = chainId;
|
|
376
|
-
config.emitter.emit('change', { chainId });
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (method === 'personal_sign') {
|
|
381
|
-
const [messageParam, addressParam] = (params ?? []) as [unknown, unknown];
|
|
382
|
-
const message =
|
|
383
|
-
typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
|
|
384
|
-
const address = typeof addressParam === 'string' ? addressParam : undefined;
|
|
385
|
-
const addressInfo = getSelectedAddressInfo(address);
|
|
386
|
-
const response = await session?.jsonRpc.request('signMessage', {
|
|
387
|
-
message,
|
|
388
|
-
addressInfo,
|
|
389
|
-
});
|
|
390
|
-
return response?.signature;
|
|
391
|
-
}
|
|
478
|
+
if (providerPromise) return providerPromise;
|
|
392
479
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const response = await session?.jsonRpc.request('signMessage', {
|
|
400
|
-
message,
|
|
401
|
-
addressInfo,
|
|
402
|
-
});
|
|
403
|
-
return response?.signature;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
|
|
407
|
-
const [addressParam, typedDataParam] = (params ?? []) as [unknown, unknown];
|
|
408
|
-
const address = typeof addressParam === 'string' ? addressParam : undefined;
|
|
409
|
-
const typedData =
|
|
410
|
-
typeof typedDataParam === 'string'
|
|
411
|
-
? typedDataParam
|
|
412
|
-
: JSON.stringify(typedDataParam ?? '');
|
|
413
|
-
const addressInfo = getSelectedAddressInfo(address);
|
|
414
|
-
const response = await session?.jsonRpc.request('signTypedData', {
|
|
415
|
-
typedData,
|
|
416
|
-
addressInfo,
|
|
417
|
-
});
|
|
418
|
-
return response?.signature;
|
|
419
|
-
}
|
|
480
|
+
providerPromise = (async (): Promise<CreditConnectProvider> => {
|
|
481
|
+
await ensureManagerInitialized();
|
|
482
|
+
const createdProvider = buildProvider();
|
|
483
|
+
provider = createdProvider;
|
|
484
|
+
return createdProvider;
|
|
485
|
+
})();
|
|
420
486
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
to: txParams?.to,
|
|
427
|
-
value: txParams?.value,
|
|
428
|
-
data: txParams?.data,
|
|
429
|
-
gas: txParams?.gas,
|
|
430
|
-
gasPrice: txParams?.gasPrice,
|
|
431
|
-
maxFeePerGas: txParams?.maxFeePerGas,
|
|
432
|
-
maxPriorityFeePerGas: txParams?.maxPriorityFeePerGas,
|
|
433
|
-
nonce: txParams?.nonce,
|
|
434
|
-
chainId: txParams?.chainId,
|
|
435
|
-
});
|
|
436
|
-
return response?.txHash;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (method === 'credit_connect_send_message') {
|
|
440
|
-
if (!session) throw new Error('No active session');
|
|
441
|
-
const [messageParam] = (params ?? []) as [unknown];
|
|
442
|
-
const message =
|
|
443
|
-
typeof messageParam === 'string'
|
|
444
|
-
? messageParam
|
|
445
|
-
: JSON.stringify(messageParam ?? '');
|
|
446
|
-
await session.msg.sendMessage(message);
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (method === 'credit_connect_jsonrpc_ping') {
|
|
451
|
-
if (!session) throw new Error('No active session');
|
|
452
|
-
return await session.jsonRpc.request('pingPong', {});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
throw new Error(`Unsupported provider method: ${method}`);
|
|
456
|
-
},
|
|
457
|
-
|
|
458
|
-
on: (event: string, listener: ProviderListener) => {
|
|
459
|
-
let listeners = providerListeners.get(event);
|
|
460
|
-
if (!listeners) {
|
|
461
|
-
listeners = new Set();
|
|
462
|
-
providerListeners.set(event, listeners);
|
|
463
|
-
}
|
|
464
|
-
listeners.add(listener);
|
|
465
|
-
return createdProvider;
|
|
466
|
-
},
|
|
467
|
-
|
|
468
|
-
removeListener: (event: string, listener: ProviderListener) => {
|
|
469
|
-
providerListeners.get(event)?.delete(listener);
|
|
470
|
-
return createdProvider;
|
|
471
|
-
},
|
|
472
|
-
} as CreditConnectProviderWithEvents;
|
|
473
|
-
|
|
474
|
-
provider = createdProvider;
|
|
475
|
-
return provider;
|
|
487
|
+
try {
|
|
488
|
+
return await providerPromise;
|
|
489
|
+
} finally {
|
|
490
|
+
providerPromise = null;
|
|
491
|
+
}
|
|
476
492
|
},
|
|
477
493
|
|
|
478
494
|
onAccountsChanged(accounts) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
watchAccount as wagmiWatchAccount,
|
|
3
|
+
watchConnections as wagmiWatchConnections,
|
|
4
|
+
reconnect as wagmiReconnect,
|
|
5
|
+
disconnect as wagmiDisconnect,
|
|
6
|
+
type WatchAccountParameters,
|
|
7
|
+
type WatchConnectionsParameters,
|
|
8
|
+
type ReconnectReturnType,
|
|
9
|
+
} from '@wagmi/core';
|
|
10
|
+
import { getConfig } from '../core/config';
|
|
11
|
+
|
|
12
|
+
export const watchAccount = (params: WatchAccountParameters): (() => void) => {
|
|
13
|
+
return wagmiWatchAccount(getConfig(), params);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const watchConnections = (params: WatchConnectionsParameters): (() => void) => {
|
|
17
|
+
return wagmiWatchConnections(getConfig(), params);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const reconnect = (): Promise<ReconnectReturnType> => {
|
|
21
|
+
return wagmiReconnect(getConfig());
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const disconnectAll = async (
|
|
25
|
+
onLog?: (message: string, error?: Error) => void,
|
|
26
|
+
): Promise<void> => {
|
|
27
|
+
const config = getConfig();
|
|
28
|
+
const results = await Promise.allSettled(
|
|
29
|
+
config.connectors.map((connector) =>
|
|
30
|
+
wagmiDisconnect(config, { connector }).then(() => connector.id),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const errors: Array<{ connectorId: string; error: Error }> = [];
|
|
35
|
+
results.forEach((result, idx) => {
|
|
36
|
+
if (result.status === 'rejected') {
|
|
37
|
+
const error =
|
|
38
|
+
result.reason instanceof Error ? result.reason : new Error(String(result.reason));
|
|
39
|
+
const connectorId = config.connectors[idx].id;
|
|
40
|
+
errors.push({ connectorId, error });
|
|
41
|
+
onLog?.(`[connect-kit] disconnect failed for "${connectorId}": ${error.message}`, error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (errors.length === config.connectors.length && errors.length > 0) {
|
|
46
|
+
const messages = errors.map((e) => `${e.connectorId}: ${e.error.message}`).join('; ');
|
|
47
|
+
throw new Error(`All disconnects failed — ${messages}`);
|
|
48
|
+
}
|
|
49
|
+
};
|
package/src/hooks/useWCState.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { useState, useRef, useCallback } from 'react';
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import type { WCWallet, WCSubView } from '../types';
|
|
3
3
|
|
|
4
|
-
const fetchWCWalletList = async (projectId: string): Promise<WCWallet[]> => {
|
|
4
|
+
const fetchWCWalletList = async (projectId: string, signal?: AbortSignal): Promise<WCWallet[]> => {
|
|
5
5
|
const all: WCWallet[] = [];
|
|
6
6
|
let page = 1;
|
|
7
7
|
const entries = 100;
|
|
8
8
|
|
|
9
9
|
while (page <= 10) {
|
|
10
|
+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
|
|
10
11
|
const url = new URL('https://explorer-api.walletconnect.com/v3/wallets');
|
|
11
12
|
url.searchParams.set('projectId', projectId);
|
|
12
13
|
url.searchParams.set('entries', String(entries));
|
|
13
14
|
url.searchParams.set('page', String(page));
|
|
14
15
|
|
|
15
|
-
const res = await fetch(url.toString());
|
|
16
|
+
const res = await fetch(url.toString(), { signal });
|
|
16
17
|
if (!res.ok) throw new Error(`Failed to fetch wallet list: ${res.status}`);
|
|
17
18
|
|
|
18
19
|
const json = (await res.json()) as {
|
|
@@ -66,7 +67,6 @@ export interface WCState {
|
|
|
66
67
|
onSelectWallet: (wallet: WCWallet) => void;
|
|
67
68
|
onSearchChange: (q: string) => void;
|
|
68
69
|
onFilterToggle: () => void;
|
|
69
|
-
// Returns true if WC handled the back navigation, false if the parent should handle it
|
|
70
70
|
handleBack: () => boolean;
|
|
71
71
|
resetView: () => void;
|
|
72
72
|
reset: () => void;
|
|
@@ -80,18 +80,33 @@ export const useWCState = (): WCState => {
|
|
|
80
80
|
const [filterActive, setFilterActive] = useState(false);
|
|
81
81
|
const [selectedWallet, setSelectedWallet] = useState<WCWallet | null>(null);
|
|
82
82
|
const loadedRef = useRef(false);
|
|
83
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
84
|
+
const isMountedRef = useRef(true);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
isMountedRef.current = true;
|
|
88
|
+
return () => {
|
|
89
|
+
isMountedRef.current = false;
|
|
90
|
+
abortControllerRef.current?.abort();
|
|
91
|
+
abortControllerRef.current = null;
|
|
92
|
+
};
|
|
93
|
+
}, []);
|
|
83
94
|
|
|
84
95
|
const loadWalletList = useCallback(async (projectId: string): Promise<void> => {
|
|
85
96
|
if (loadedRef.current) return;
|
|
86
97
|
loadedRef.current = true;
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
abortControllerRef.current = controller;
|
|
87
100
|
setWalletListLoading(true);
|
|
88
101
|
try {
|
|
89
|
-
const list = await fetchWCWalletList(projectId);
|
|
90
|
-
setWalletList(list);
|
|
91
|
-
} catch {
|
|
92
|
-
|
|
102
|
+
const list = await fetchWCWalletList(projectId, controller.signal);
|
|
103
|
+
if (isMountedRef.current) setWalletList(list);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const isAbort = err instanceof DOMException && err.name === 'AbortError';
|
|
106
|
+
if (!isAbort) loadedRef.current = false;
|
|
93
107
|
} finally {
|
|
94
|
-
setWalletListLoading(false);
|
|
108
|
+
if (isMountedRef.current) setWalletListLoading(false);
|
|
109
|
+
if (abortControllerRef.current === controller) abortControllerRef.current = null;
|
|
95
110
|
}
|
|
96
111
|
}, []);
|
|
97
112
|
|
|
@@ -14,7 +14,14 @@ const hasWalletConnectKeyword = (value: string): boolean =>
|
|
|
14
14
|
value.toLowerCase().includes('walletconnect');
|
|
15
15
|
|
|
16
16
|
const isMetaMaskConnector = (connector: WagmiConnectorLike): boolean => {
|
|
17
|
-
if (connector.id === '
|
|
17
|
+
if (connector.id === 'metaMaskSDK') return true;
|
|
18
|
+
|
|
19
|
+
if (connector.id === 'injected') {
|
|
20
|
+
if (typeof window === 'undefined') return false;
|
|
21
|
+
const eth = (window as Window & { ethereum?: { isMetaMask?: boolean } }).ethereum;
|
|
22
|
+
return eth?.isMetaMask === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
if (hasMetaMaskKeyword(connector.id) || hasMetaMaskKeyword(connector.name)) return true;
|
|
19
26
|
|
|
20
27
|
if (connector.rdns) {
|
|
@@ -70,8 +77,8 @@ export const useWagmiConnect = ({
|
|
|
70
77
|
}: Options): UseWagmiConnectResult => {
|
|
71
78
|
const config = useConfig();
|
|
72
79
|
const pendingConnectorId = useRef<ConnectorId | null>(null);
|
|
80
|
+
const attemptCounter = useRef(0);
|
|
73
81
|
|
|
74
|
-
// 콜백 참조
|
|
75
82
|
const onConnectRef = useRef(onConnect);
|
|
76
83
|
const onErrorRef = useRef(onError);
|
|
77
84
|
const onQrUriRef = useRef(onQrUri);
|
|
@@ -81,10 +88,8 @@ export const useWagmiConnect = ({
|
|
|
81
88
|
onQrUriRef.current = onQrUri;
|
|
82
89
|
}, [onConnect, onError, onQrUri]);
|
|
83
90
|
|
|
84
|
-
// 연결 시도
|
|
85
91
|
const { connectAsync, reset, isPending } = useConnect();
|
|
86
92
|
|
|
87
|
-
// QR 코드 표시
|
|
88
93
|
useEffect(() => {
|
|
89
94
|
const handler = (data: unknown): void => {
|
|
90
95
|
const msg = data as { type?: string; data?: unknown };
|
|
@@ -107,7 +112,6 @@ export const useWagmiConnect = ({
|
|
|
107
112
|
};
|
|
108
113
|
}, [config.connectors]);
|
|
109
114
|
|
|
110
|
-
// 연결 트리거
|
|
111
115
|
const triggerConnect = useCallback(
|
|
112
116
|
(connectorId: ConnectorId): void => {
|
|
113
117
|
const connector = resolveWagmiConnector(config.connectors, connectorId);
|
|
@@ -117,10 +121,11 @@ export const useWagmiConnect = ({
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
onQrUriRef.current(null);
|
|
124
|
+
const attemptId = ++attemptCounter.current;
|
|
120
125
|
pendingConnectorId.current = connectorId;
|
|
121
126
|
connectAsync({ connector })
|
|
122
127
|
.then((result) => {
|
|
123
|
-
if (
|
|
128
|
+
if (attemptCounter.current !== attemptId) return;
|
|
124
129
|
const address = result.accounts?.[0];
|
|
125
130
|
if (!address) {
|
|
126
131
|
throw new Error('Connected but no account returned');
|
|
@@ -129,7 +134,7 @@ export const useWagmiConnect = ({
|
|
|
129
134
|
onConnectRef.current({ address, connectorId });
|
|
130
135
|
})
|
|
131
136
|
.catch((error) => {
|
|
132
|
-
if (
|
|
137
|
+
if (attemptCounter.current !== attemptId) return;
|
|
133
138
|
pendingConnectorId.current = null;
|
|
134
139
|
onErrorRef.current(error as Error, connectorId);
|
|
135
140
|
});
|
|
@@ -137,8 +142,8 @@ export const useWagmiConnect = ({
|
|
|
137
142
|
[config.connectors, connectAsync],
|
|
138
143
|
);
|
|
139
144
|
|
|
140
|
-
// 연결 취소
|
|
141
145
|
const cancelConnect = useCallback((): void => {
|
|
146
|
+
attemptCounter.current += 1;
|
|
142
147
|
pendingConnectorId.current = null;
|
|
143
148
|
onQrUriRef.current(null);
|
|
144
149
|
reset();
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
import { readContract, writeContract } from './api/contract';
|
|
2
|
+
import { getBalance } from './api/balance';
|
|
3
|
+
import { switchChain, getPublicClient } from './api/chain';
|
|
4
|
+
import { waitForTransactionReceipt } from './api/transaction';
|
|
5
|
+
import { getAccount, signMessage, signTypedData } from './api/account';
|
|
6
|
+
import { watchAccount, watchConnections, reconnect, disconnectAll } from './events/account';
|
|
7
|
+
|
|
1
8
|
export { ConnectModal } from './ConnectModal';
|
|
2
9
|
export { ConnectKitProvider, useConnectKit } from './ConnectKitProvider';
|
|
3
10
|
export type { ConnectKitContextValue, ConnectKitProviderProps } from './ConnectKitProvider';
|
|
11
|
+
|
|
12
|
+
export { initConfig } from './core/config';
|
|
13
|
+
|
|
4
14
|
export type {
|
|
5
15
|
ConnectErrorContext,
|
|
6
16
|
ConnectErrorReason,
|
|
7
17
|
ConnectModalProps,
|
|
18
|
+
ConnectKitConfig,
|
|
8
19
|
Connectors,
|
|
9
20
|
ConnectResult,
|
|
10
21
|
ConnectorId,
|
|
@@ -12,3 +23,25 @@ export type {
|
|
|
12
23
|
WCSubView,
|
|
13
24
|
WCWallet,
|
|
14
25
|
} from './types';
|
|
26
|
+
|
|
27
|
+
export const connectKit = {
|
|
28
|
+
api: {
|
|
29
|
+
readContract,
|
|
30
|
+
writeContract,
|
|
31
|
+
getBalance,
|
|
32
|
+
switchChain,
|
|
33
|
+
getPublicClient,
|
|
34
|
+
waitForTransactionReceipt,
|
|
35
|
+
getAccount,
|
|
36
|
+
signMessage,
|
|
37
|
+
signTypedData,
|
|
38
|
+
},
|
|
39
|
+
events: {
|
|
40
|
+
watchAccount,
|
|
41
|
+
watchConnections,
|
|
42
|
+
reconnect,
|
|
43
|
+
disconnectAll,
|
|
44
|
+
},
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
export type * from '@wagmi/core';
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Chain, Transport } from 'viem';
|
|
3
|
+
import type { CreateConnectorFn } from 'wagmi';
|
|
2
4
|
|
|
3
5
|
export type ConnectorId = 'CREDIT_WALLET' | 'CREDIT_CONNECT' | 'METAMASK' | 'WALLET_CONNECT';
|
|
4
6
|
|
|
7
|
+
export interface ConnectKitConfig {
|
|
8
|
+
chains: [Chain, ...Chain[]];
|
|
9
|
+
transports: Record<number, Transport>;
|
|
10
|
+
wcProjectId?: string;
|
|
11
|
+
extraConnectors?: CreateConnectorFn[];
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
export type CreditWalletStrategy = 'walletConnect' | 'creditConnect';
|
|
6
15
|
|
|
7
16
|
export interface Connectors {
|
package/src/utils/platform.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
// 모바일 기기 감지
|
|
2
1
|
export const isMobileDevice = (): boolean => {
|
|
3
2
|
if (typeof window === 'undefined') return false;
|
|
4
3
|
return /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
|
5
4
|
};
|
|
6
5
|
|
|
7
|
-
// 딥링크 열기 (walletConnect가 지원하는 지갑앱)
|
|
8
6
|
export const tryOpenDeepLink = (uri: string, deepLinkBase: string): void => {
|
|
9
7
|
const sep = deepLinkBase.endsWith('/') ? '' : '/';
|
|
10
|
-
window.location.
|
|
8
|
+
window.location.replace(`${deepLinkBase}${sep}wc?uri=${encodeURIComponent(uri)}`);
|
|
11
9
|
};
|
|
12
10
|
|
|
13
|
-
// metaMask extension 감지
|
|
14
11
|
export const detectMetaMaskExtension = (): boolean =>
|
|
15
12
|
typeof window !== 'undefined' &&
|
|
16
13
|
Boolean((window as Window & { ethereum?: { isMetaMask?: boolean } }).ethereum?.isMetaMask);
|
|
@@ -12,7 +12,6 @@ interface CreditWalletViewProps {
|
|
|
12
12
|
export const CreditWalletView: FC<CreditWalletViewProps> = ({ connectorId, qrUri }) => {
|
|
13
13
|
const { downloadUrl, logoUrl, deepLinkBase } = CONNECTOR_META[connectorId];
|
|
14
14
|
const isMobile = useMemo(() => isMobileDevice(), []);
|
|
15
|
-
// WC 세션 만료로 qrUri가 재발급되더라도 딥링크는 최초 1회만 실행
|
|
16
15
|
const hasRedirectedRef = useRef(false);
|
|
17
16
|
|
|
18
17
|
useEffect(() => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FC, useEffect, useMemo } from 'react';
|
|
1
|
+
import { type FC, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { isMobileDevice, tryOpenDeepLink } from '../utils/platform';
|
|
3
3
|
import { CONNECTOR_META } from '../connector-meta';
|
|
4
4
|
import { QRFrame, CopyLinkButton } from '../components/QRFrame';
|
|
@@ -13,9 +13,11 @@ interface MetaMaskViewProps {
|
|
|
13
13
|
|
|
14
14
|
export const MetaMaskView: FC<MetaMaskViewProps> = ({ qrUri, hasExtension, logoUrl }) => {
|
|
15
15
|
const isMobile = useMemo(() => isMobileDevice(), []);
|
|
16
|
+
const hasRedirectedRef = useRef(false);
|
|
16
17
|
|
|
17
18
|
useEffect(() => {
|
|
18
|
-
if (isMobile && qrUri && deepLinkBase) {
|
|
19
|
+
if (isMobile && qrUri && deepLinkBase && !hasRedirectedRef.current) {
|
|
20
|
+
hasRedirectedRef.current = true;
|
|
19
21
|
tryOpenDeepLink(qrUri, deepLinkBase);
|
|
20
22
|
}
|
|
21
23
|
}, [isMobile, qrUri]);
|