@gluwa/connect-kit 0.1.0-next.3 → 0.2.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/assets/creditwallet.png +0 -0
  3. package/assets/fonts/Inter-Medium.ttf +0 -0
  4. package/assets/fonts/Inter-Medium.woff2 +0 -0
  5. package/assets/fonts/Inter-Regular.ttf +0 -0
  6. package/assets/fonts/Inter-Regular.woff2 +0 -0
  7. package/assets/fonts/Inter-SemiBold.ttf +0 -0
  8. package/assets/fonts/Inter-SemiBold.woff2 +0 -0
  9. package/assets/graphic.png +0 -0
  10. package/assets/metamask.png +0 -0
  11. package/assets/wc-logo.svg +3 -0
  12. package/assets/wc-search.png +0 -0
  13. package/dist/chunk-EDNRW47A.js +401 -0
  14. package/dist/credit-connect.js +2 -394
  15. package/dist/index.css +2074 -0
  16. package/dist/index.d.ts +53 -33
  17. package/dist/index.js +1404 -1504
  18. package/dist/package.json +16 -10
  19. package/package.json +14 -11
  20. package/src/ConnectKitProvider.tsx +126 -75
  21. package/src/api/account.ts +21 -0
  22. package/src/api/balance.ts +10 -0
  23. package/src/api/chain.ts +17 -0
  24. package/src/api/contract.ts +33 -0
  25. package/src/api/transaction.ts +12 -0
  26. package/src/assets.d.ts +5 -0
  27. package/src/components/actionItem/index.tsx +50 -0
  28. package/src/components/actionItem/style.scss +47 -0
  29. package/src/components/copyLink/index.tsx +16 -0
  30. package/src/components/copyLink/style.scss +16 -0
  31. package/src/components/qrArea/index.tsx +70 -0
  32. package/src/components/qrArea/style.scss +130 -0
  33. package/src/components/searchBar/asset/Search.tsx +20 -0
  34. package/src/components/searchBar/asset/XMarkCircle.tsx +24 -0
  35. package/src/components/searchBar/index.tsx +44 -0
  36. package/src/components/searchBar/style.scss +58 -0
  37. package/src/components/snackbar/index.tsx +25 -0
  38. package/src/components/snackbar/style.scss +51 -0
  39. package/src/components/toggle/index.tsx +27 -0
  40. package/src/components/toggle/style.scss +53 -0
  41. package/src/components/walletGrid/index.tsx +64 -0
  42. package/src/components/walletGrid/style.scss +108 -0
  43. package/src/components/walletItem/index.tsx +49 -0
  44. package/src/components/walletItem/style.scss +70 -0
  45. package/src/connector-meta.ts +12 -7
  46. package/src/core/config.ts +102 -0
  47. package/src/creditConnectConnector.ts +192 -184
  48. package/src/events/account.ts +49 -0
  49. package/src/hooks/useWCState.ts +45 -13
  50. package/src/hooks/useWagmiConnect.ts +13 -9
  51. package/src/index.ts +35 -2
  52. package/src/layout/allWallets/index.tsx +52 -0
  53. package/src/layout/allWallets/style.scss +62 -0
  54. package/src/layout/connectDialog/asset/backIcon.tsx +24 -0
  55. package/src/layout/connectDialog/asset/checkIcon.tsx +28 -0
  56. package/src/layout/connectDialog/asset/closeIcon.tsx +21 -0
  57. package/src/layout/connectDialog/asset/copyIcon.tsx +28 -0
  58. package/src/layout/connectDialog/asset/linkIcon.tsx +24 -0
  59. package/src/layout/connectDialog/asset/spinner.tsx +21 -0
  60. package/src/layout/connectDialog/idle/index.tsx +32 -0
  61. package/src/layout/connectDialog/idle/style.scss +61 -0
  62. package/src/layout/connectDialog/index.tsx +286 -0
  63. package/src/layout/connectDialog/loading/index.tsx +13 -0
  64. package/src/layout/connectDialog/loading/style.scss +19 -0
  65. package/src/layout/connectDialog/metaMask/index.tsx +61 -0
  66. package/src/layout/connectDialog/metaMask/style.scss +65 -0
  67. package/src/layout/connectDialog/qr/index.tsx +33 -0
  68. package/src/layout/connectDialog/style.scss +95 -0
  69. package/src/layout/connectDialog/timeout/index.tsx +31 -0
  70. package/src/layout/connectDialog/timeout/style.scss +64 -0
  71. package/src/layout/connectDialog/walletConnect/index.tsx +45 -0
  72. package/src/layout/connectDialog/walletConnect/style.scss +48 -0
  73. package/src/layout/connectDialog/walletQR/index.tsx +37 -0
  74. package/src/layout/detailPanel/index.tsx +147 -0
  75. package/src/layout/detailPanel/style.scss +64 -0
  76. package/src/layout/selectorPanel/index.tsx +100 -0
  77. package/src/layout/selectorPanel/style.scss +113 -0
  78. package/src/style/color.scss +417 -0
  79. package/src/style/font.scss +28 -0
  80. package/src/style/index.scss +5 -0
  81. package/src/style/mixin.scss +217 -0
  82. package/src/style/reset.scss +110 -0
  83. package/src/style/variables.scss +13 -0
  84. package/src/types.ts +19 -7
  85. package/src/utils/platform.ts +7 -4
  86. package/tsup.config.ts +32 -2
  87. package/dist/chunk-DDA6FP6U.js +0 -7
  88. package/src/ConnectModal.scss +0 -665
  89. package/src/ConnectModal.tsx +0 -559
  90. package/src/components/QRFrame.tsx +0 -43
  91. package/src/views/CreditWalletView.tsx +0 -59
  92. package/src/views/IdleView.tsx +0 -10
  93. package/src/views/MetaMaskView.tsx +0 -64
  94. package/src/views/SwitchChainView.tsx +0 -67
  95. package/src/views/WalletConnectView.tsx +0 -206
@@ -0,0 +1,49 @@
1
+ import { type FC } from 'react';
2
+ import './style.scss';
3
+ import { type ConnectorId } from '../../types';
4
+
5
+ interface WalletItemProps {
6
+ id: ConnectorId;
7
+ label: string;
8
+ logoUrl?: string;
9
+ isSelected: boolean;
10
+ isRecent?: boolean;
11
+ onSelect: (id: ConnectorId) => void;
12
+ }
13
+
14
+ export const WalletItem: FC<WalletItemProps> = ({
15
+ id,
16
+ label,
17
+ logoUrl,
18
+ isSelected,
19
+ isRecent = false,
20
+ onSelect,
21
+ }) => {
22
+ return (
23
+ <button
24
+ type="button"
25
+ className={`ck-wallet-item${isSelected ? ' is-selected' : ''}`}
26
+ onClick={() => onSelect(id)}
27
+ aria-pressed={isSelected}
28
+ >
29
+ {logoUrl ? (
30
+ <img
31
+ className={`ck-wallet-item-icon${id === 'METAMASK' ? ' has-bg' : ''}`}
32
+ src={logoUrl}
33
+ alt=""
34
+ aria-hidden="true"
35
+ />
36
+ ) : (
37
+ <span className="ck-wallet-item-icon" aria-hidden="true" />
38
+ )}
39
+ <span className="ck-wallet-item-txt">
40
+ <span className="ck-wallet-item-label">{label}</span>
41
+ {isRecent && (
42
+ <span className="ck-wallet-item-badge" aria-label="Recently used">
43
+ Recent
44
+ </span>
45
+ )}
46
+ </span>
47
+ </button>
48
+ );
49
+ };
@@ -0,0 +1,70 @@
1
+ @use '../../style/mixin.scss' as *;
2
+
3
+ .ck-root .ck-wallet-item {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: 12px;
7
+ width: 100%;
8
+ padding: 14px 20px;
9
+ border-radius: 12px;
10
+ background: var(--scale-gray-100);
11
+ cursor: pointer;
12
+ transition: background 0.15s ease;
13
+
14
+ &:hover {
15
+ background: var(--scale-gray-200);
16
+ }
17
+
18
+ &.is-selected {
19
+ background: var(--semantic-brand-trugi-primary);
20
+
21
+ .ck-wallet-item-label {
22
+ color: var(--static-white-main);
23
+ }
24
+
25
+ .ck-wallet-item-badge {
26
+ background: var(--static-white-alpha-200);
27
+ color: var(--static-white-main);
28
+ }
29
+ }
30
+ }
31
+
32
+ .ck-wallet-item-icon {
33
+ width: 36px;
34
+ height: 36px;
35
+ border-radius: 8px;
36
+ flex-shrink: 0;
37
+ overflow: hidden;
38
+ object-fit: cover;
39
+
40
+ &.has-bg {
41
+ background: var(--static-white-main);
42
+ padding: 2px;
43
+ }
44
+ }
45
+
46
+ .ck-wallet-item-txt {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 6px;
50
+ flex: 1;
51
+ min-width: 0;
52
+ }
53
+
54
+ .ck-wallet-item-label {
55
+ @include textToken('label3-bold');
56
+ color: var(--scale-gray-900);
57
+ overflow: hidden;
58
+ text-overflow: ellipsis;
59
+ white-space: nowrap;
60
+ flex-shrink: 1;
61
+ }
62
+
63
+ .ck-wallet-item-badge {
64
+ @include textToken('caption3-bold');
65
+ color: var(--semantic-brand-trugi-primary);
66
+ background: var(--scale-blue2-alpha-100);
67
+ padding: 4px 8px;
68
+ border-radius: 500px;
69
+ flex-shrink: 0;
70
+ }
@@ -17,13 +17,6 @@ export const CONNECTOR_META: Record<ConnectorId, ConnectorMeta> = {
17
17
  detailTitle: 'Scan with Credit Wallet',
18
18
  downloadUrl: 'https://creditwallet.onelink.me/Jv54',
19
19
  logoUrl: creditWalletIcon,
20
- // deepLinkBase: TODO — 앱 측 CC 딥링크 스킴 확정 후 추가
21
- },
22
- CREDIT_WALLET: {
23
- label: 'Credit Wallet',
24
- detailTitle: 'Scan with Credit Wallet',
25
- downloadUrl: 'https://creditwallet.onelink.me/Jv54',
26
- logoUrl: creditWalletIcon,
27
20
  deepLinkBase: 'creditwallet://',
28
21
  },
29
22
  METAMASK: {
@@ -39,3 +32,15 @@ export const CONNECTOR_META: Record<ConnectorId, ConnectorMeta> = {
39
32
  logoUrl: wcIcon,
40
33
  },
41
34
  };
35
+
36
+ // wagmi connector id (예: 'credit-connect' · 'metaMaskSDK' · 'walletConnect') → kit ConnectorId.
37
+ // wagmi storage 의 recentConnectorId 복원 · account.connector 식별에 공용 사용.
38
+ export const resolveConnectorId = (wagmiConnectorId: string | undefined): ConnectorId | null => {
39
+ if (!wagmiConnectorId) return null;
40
+ if (wagmiConnectorId === 'credit-connect') return 'CREDIT_CONNECT';
41
+ if (wagmiConnectorId === 'walletConnect') return 'WALLET_CONNECT';
42
+ const lower = wagmiConnectorId.toLowerCase();
43
+ if (lower.includes('metamask') || wagmiConnectorId === 'injected') return 'METAMASK';
44
+ if (lower.includes('walletconnect')) return 'WALLET_CONNECT';
45
+ return null;
46
+ };
@@ -0,0 +1,102 @@
1
+ import { createConfig, createStorage } from 'wagmi';
2
+ import { metaMask, walletConnect, injected } from 'wagmi/connectors';
3
+ import type { Config } from 'wagmi';
4
+ import type { ConnectKitConfig } from '../types';
5
+ import { creditConnectConnector } from '../creditConnectConnector';
6
+ import { LocalStorageProvider } from '@gluwa/credit-connect-storage-local';
7
+ import { tweetNaclE2EE } from '@gluwa/credit-connect-e2ee-tweetnacl';
8
+ import { wsHubFactory } from '@gluwa/credit-connect-hub-node-ws';
9
+
10
+ type KitSlot = {
11
+ config: Config | null;
12
+ kitConfig: ConnectKitConfig | null;
13
+ };
14
+
15
+ const SLOT_KEY = Symbol.for('@gluwa/connect-kit:slot');
16
+
17
+ type GlobalWithSlot = typeof globalThis & {
18
+ [SLOT_KEY]?: KitSlot;
19
+ };
20
+
21
+ const getSlot = (): KitSlot => {
22
+ const g = globalThis as GlobalWithSlot;
23
+ if (!g[SLOT_KEY]) {
24
+ g[SLOT_KEY] = { config: null, kitConfig: null };
25
+ }
26
+ return g[SLOT_KEY];
27
+ };
28
+
29
+ export const initConfig = (kitConfig: ConnectKitConfig): Config => {
30
+ const slot = getSlot();
31
+ if (slot.config) {
32
+ if (slot.kitConfig !== kitConfig) {
33
+ // eslint-disable-next-line no-console
34
+ console.warn(
35
+ '[connect-kit] initConfig called again with a different kitConfig — new chains/transports ignored. Call resetConfig() first to re-init.',
36
+ );
37
+ }
38
+ return slot.config;
39
+ }
40
+
41
+ const connectors = [];
42
+
43
+ if (kitConfig.wcProjectId) {
44
+ connectors.push(walletConnect({ projectId: kitConfig.wcProjectId, showQrModal: false }));
45
+ }
46
+
47
+ connectors.push(
48
+ metaMask({ headless: true, storage: { enabled: true } }),
49
+ injected({ shimDisconnect: false }),
50
+ );
51
+
52
+ if (kitConfig.creditConnect) {
53
+ connectors.push(
54
+ creditConnectConnector({
55
+ projectKey: kitConfig.creditConnect.projectId,
56
+ serverUrl: kitConfig.creditConnect.serverUrl,
57
+ storage: new LocalStorageProvider(),
58
+ e2ee: tweetNaclE2EE,
59
+ createHub: wsHubFactory.createHubFromDapp,
60
+ }),
61
+ );
62
+ }
63
+
64
+ if (kitConfig.extraConnectors?.length) {
65
+ connectors.push(...kitConfig.extraConnectors);
66
+ }
67
+
68
+ const isBrowser = typeof window !== 'undefined';
69
+
70
+ slot.config = createConfig({
71
+ ssr: true,
72
+ chains: kitConfig.chains,
73
+ connectors,
74
+ transports: kitConfig.transports,
75
+ storage: isBrowser ? createStorage({ storage: window.localStorage }) : undefined,
76
+ });
77
+ slot.kitConfig = kitConfig;
78
+
79
+ return slot.config;
80
+ };
81
+
82
+ export const getConfig = (): Config => {
83
+ const slot = getSlot();
84
+ if (!slot.config) {
85
+ throw new Error('ConnectKit config not initialized. Make sure ConnectKitProvider is mounted.');
86
+ }
87
+ return slot.config;
88
+ };
89
+
90
+ export const getKitConfig = (): ConnectKitConfig => {
91
+ const slot = getSlot();
92
+ if (!slot.kitConfig) {
93
+ throw new Error('ConnectKit config not initialized. Make sure ConnectKitProvider is mounted.');
94
+ }
95
+ return slot.kitConfig;
96
+ };
97
+
98
+ export const resetConfig = (): void => {
99
+ const slot = getSlot();
100
+ slot.config = null;
101
+ slot.kitConfig = null;
102
+ };
@@ -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;
@@ -83,12 +84,23 @@ export const creditConnectConnector = (
83
84
  });
84
85
 
85
86
  manager.on('changeState', (state) => {
87
+ // eslint-disable-next-line no-console
88
+ console.log('[CC connector] manager state →', state.status, {
89
+ hasConnectingPayload: Boolean(state.connectingPayload),
90
+ hasSession: 'session' in state && Boolean((state as { session?: unknown }).session),
91
+ });
86
92
  if (state.status === 'CONNECTING' && state.connectingPayload) {
93
+ // eslint-disable-next-line no-console
94
+ console.log('[CC connector] emit display_uri', {
95
+ payloadPrefix: state.connectingPayload.slice(0, 80),
96
+ });
87
97
  options.onDisplayUri?.(state.connectingPayload);
88
98
  emitWagmiMessage?.('display_uri', state.connectingPayload);
89
99
  }
90
100
 
91
101
  if (state.status === 'DISCONNECTED' && !isLocalDisconnect) {
102
+ // eslint-disable-next-line no-console
103
+ console.log('[CC connector] DISCONNECTED (non-local) → resetState + emit');
92
104
  resetState();
93
105
  emitProviderEvent('disconnect');
94
106
  emitWagmiDisconnect?.();
@@ -128,6 +140,163 @@ export const creditConnectConnector = (
128
140
  config.emitter.emit('disconnect');
129
141
  };
130
142
 
143
+ const buildProvider = (): CreditConnectProviderWithEvents => {
144
+ const createdProvider = {
145
+ request: async (args) => {
146
+ const { method, params: rawParams } = args as {
147
+ method: string;
148
+ params?: unknown[] | object;
149
+ };
150
+ const params = Array.isArray(rawParams)
151
+ ? [...rawParams]
152
+ : rawParams === undefined
153
+ ? []
154
+ : [rawParams];
155
+ const session = getCurrentSession();
156
+
157
+ const getSelectedAddressInfo = (address?: string) => {
158
+ if (!session) throw new Error('No active session');
159
+ const evmInfo = getEvmAddressInfo(session);
160
+ if (evmInfo.length === 0) throw new Error('No EVM address found');
161
+ if (!address) return evmInfo[0];
162
+ const found = evmInfo.find(
163
+ (info) => info.address.toLowerCase() === address.toLowerCase(),
164
+ );
165
+ if (!found) throw new Error(`Address not in session: ${address}`);
166
+ return found;
167
+ };
168
+
169
+ if (method === 'eth_accounts') {
170
+ if (!session) return [];
171
+ return getChecksummedAccounts(session);
172
+ }
173
+
174
+ if (method === 'eth_chainId') {
175
+ if (!session) throw new Error('No active session');
176
+ if (selectedChainId !== null) return `0x${selectedChainId.toString(16)}`;
177
+ const evmInfo = getEvmAddressInfo(session);
178
+ if (evmInfo.length === 0) throw new Error('No EVM address found');
179
+ const chainId = Number.parseInt(evmInfo[0].networkIdentifier, 10);
180
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
181
+ selectedChainId = chainId;
182
+ return `0x${chainId.toString(16)}`;
183
+ }
184
+
185
+ if (method === 'wallet_switchEthereumChain') {
186
+ const [{ chainId: rawChainId }] = (params ?? []) as [{ chainId?: string }];
187
+ if (!rawChainId) throw new Error('wallet_switchEthereumChain missing chainId param');
188
+ const chainId = Number.parseInt(rawChainId, 16);
189
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${rawChainId}`);
190
+ if (!session) throw new Error('No active session');
191
+
192
+ const supported = getEvmAddressInfo(session).some(
193
+ (info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
194
+ );
195
+ if (!supported) {
196
+ const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
197
+ err.code = 4902;
198
+ throw err;
199
+ }
200
+ selectedChainId = chainId;
201
+ config.emitter.emit('change', { chainId });
202
+ return null;
203
+ }
204
+
205
+ if (method === 'personal_sign') {
206
+ const [messageParam, addressParam] = (params ?? []) as [unknown, unknown];
207
+ const message =
208
+ typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
209
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
210
+ const addressInfo = getSelectedAddressInfo(address);
211
+ const response = await session?.jsonRpc.request('signMessage', {
212
+ message,
213
+ addressInfo,
214
+ });
215
+ return response?.signature;
216
+ }
217
+
218
+ if (method === 'eth_sign') {
219
+ const [addressParam, messageParam] = (params ?? []) as [unknown, unknown];
220
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
221
+ const message =
222
+ typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
223
+ const addressInfo = getSelectedAddressInfo(address);
224
+ const response = await session?.jsonRpc.request('signMessage', {
225
+ message,
226
+ addressInfo,
227
+ });
228
+ return response?.signature;
229
+ }
230
+
231
+ if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
232
+ const [addressParam, typedDataParam] = (params ?? []) as [unknown, unknown];
233
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
234
+ const typedData =
235
+ typeof typedDataParam === 'string'
236
+ ? typedDataParam
237
+ : JSON.stringify(typedDataParam ?? '');
238
+ const addressInfo = getSelectedAddressInfo(address);
239
+ const response = await session?.jsonRpc.request('signTypedData', {
240
+ typedData,
241
+ addressInfo,
242
+ });
243
+ return response?.signature;
244
+ }
245
+
246
+ if (method === 'eth_sendTransaction') {
247
+ const [txParams] = (params ?? []) as [Record<string, string | undefined>];
248
+ const addressInfo = getSelectedAddressInfo(txParams?.from);
249
+ const response = await session?.jsonRpc.request('sendTransaction', {
250
+ addressInfo,
251
+ to: txParams?.to,
252
+ value: txParams?.value,
253
+ data: txParams?.data,
254
+ gas: txParams?.gas,
255
+ gasPrice: txParams?.gasPrice,
256
+ maxFeePerGas: txParams?.maxFeePerGas,
257
+ maxPriorityFeePerGas: txParams?.maxPriorityFeePerGas,
258
+ nonce: txParams?.nonce,
259
+ chainId: txParams?.chainId,
260
+ });
261
+ return response?.txHash;
262
+ }
263
+
264
+ if (method === 'credit_connect_send_message') {
265
+ if (!session) throw new Error('No active session');
266
+ const [messageParam] = (params ?? []) as [unknown];
267
+ const message =
268
+ typeof messageParam === 'string' ? messageParam : JSON.stringify(messageParam ?? '');
269
+ await session.msg.sendMessage(message);
270
+ return true;
271
+ }
272
+
273
+ if (method === 'credit_connect_jsonrpc_ping') {
274
+ if (!session) throw new Error('No active session');
275
+ return await session.jsonRpc.request('pingPong', {});
276
+ }
277
+
278
+ throw new Error(`Unsupported provider method: ${method}`);
279
+ },
280
+
281
+ on: (event: string, listener: ProviderListener) => {
282
+ let listeners = providerListeners.get(event);
283
+ if (!listeners) {
284
+ listeners = new Set();
285
+ providerListeners.set(event, listeners);
286
+ }
287
+ listeners.add(listener);
288
+ return createdProvider;
289
+ },
290
+
291
+ removeListener: (event: string, listener: ProviderListener) => {
292
+ providerListeners.get(event)?.delete(listener);
293
+ return createdProvider;
294
+ },
295
+ } as CreditConnectProviderWithEvents;
296
+
297
+ return createdProvider;
298
+ };
299
+
131
300
  return {
132
301
  id: CONNECTOR_ID,
133
302
  name: CONNECTOR_NAME,
@@ -185,32 +354,13 @@ export const creditConnectConnector = (
185
354
  });
186
355
  }
187
356
 
188
- if (m.state.status === 'CONNECTING' && m.state.connectingPayload) {
189
- options.onDisplayUri?.(m.state.connectingPayload);
190
-
191
- if (!connectPromise) {
192
- connectPromise = new Promise<boolean>((resolve) => {
193
- const unsub = m.on('changeState', (state) => {
194
- if (state.status === 'CONNECTED') {
195
- unsub();
196
- resolve(true);
197
- } else if (state.status === 'DISCONNECTED') {
198
- unsub();
199
- resolve(false);
200
- }
201
- });
202
- });
203
- }
204
- }
205
-
357
+ // connect 시도 stale state (CONNECTING/CONNECTING_WAITING_PAYLOAD)
358
+ // stale connectPromise 는 SDK 의 self-heal (cleanupHub + attempt-id) 에 위임.
359
+ // connector 는 단순히 새 connectNewSession() 을 호출 — 이전 attempt 는 자연 정리됨.
206
360
  if (m.state.status !== 'CONNECTED') {
207
- if (!connectPromise) {
208
- if (m.state.status !== 'DISCONNECTED') {
209
- throw new Error(`Cannot connect from ${m.state.status} state`);
210
- }
211
- connectPromise = m.connectNewSession();
212
- }
213
-
361
+ // 이전 attempt 의 await 가 살아있어도, SDK 의 attempt-id 패턴이
362
+ // 결과를 무시하도록 보장한다. connectPromise 는 무조건 새 promise 로.
363
+ connectPromise = m.connectNewSession();
214
364
  const connected = await connectPromise;
215
365
  connectPromise = null;
216
366
 
@@ -261,11 +411,13 @@ export const creditConnectConnector = (
261
411
  throw err;
262
412
  }
263
413
 
414
+ const chain = config.chains.find((c) => c.id === chainId);
415
+ if (!chain) throw new Error(`Chain ${chainId} not in config`);
416
+
264
417
  selectedChainId = chainId;
265
418
  config.emitter.emit('change', { chainId });
419
+ await Promise.resolve();
266
420
 
267
- const chain = config.chains.find((c) => c.id === chainId);
268
- if (!chain) throw new Error(`Chain ${chainId} not in config`);
269
421
  return chain;
270
422
  },
271
423
 
@@ -286,6 +438,7 @@ export const creditConnectConnector = (
286
438
  isLocalDisconnect = false;
287
439
  }
288
440
  provider = null;
441
+ providerPromise = null;
289
442
  resetState();
290
443
  config.emitter.emit('disconnect');
291
444
  },
@@ -314,165 +467,20 @@ export const creditConnectConnector = (
314
467
 
315
468
  async getProvider() {
316
469
  if (provider) return provider;
317
- await ensureManagerInitialized();
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
- }
392
-
393
- if (method === 'eth_sign') {
394
- const [addressParam, messageParam] = (params ?? []) as [unknown, unknown];
395
- const address = typeof addressParam === 'string' ? addressParam : undefined;
396
- const message =
397
- typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
398
- const addressInfo = getSelectedAddressInfo(address);
399
- const response = await session?.jsonRpc.request('signMessage', {
400
- message,
401
- addressInfo,
402
- });
403
- return response?.signature;
404
- }
470
+ if (providerPromise) return providerPromise;
405
471
 
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
- }
420
-
421
- if (method === 'eth_sendTransaction') {
422
- const [txParams] = (params ?? []) as [Record<string, string | undefined>];
423
- const addressInfo = getSelectedAddressInfo(txParams?.from);
424
- const response = await session?.jsonRpc.request('sendTransaction', {
425
- addressInfo,
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
- }
472
+ providerPromise = (async (): Promise<CreditConnectProvider> => {
473
+ await ensureManagerInitialized();
474
+ const createdProvider = buildProvider();
475
+ provider = createdProvider;
476
+ return createdProvider;
477
+ })();
438
478
 
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;
479
+ try {
480
+ return await providerPromise;
481
+ } finally {
482
+ providerPromise = null;
483
+ }
476
484
  },
477
485
 
478
486
  onAccountsChanged(accounts) {