@yeying-community/web3-bs 1.0.8 → 1.0.11

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/README.md CHANGED
@@ -21,9 +21,14 @@ npm install @yeying-community/web3-bs
21
21
  ### 1) Provider 与账户
22
22
 
23
23
  - `getProvider` / `requireProvider`
24
- - `requestAccounts` / `getAccounts` / `getPreferredAccount` / `watchAccounts`
24
+ - `watchProvider`
25
+ - `requestAccounts` / `focusPendingApproval` / `getAccounts` / `getPreferredAccount` / `watchAccounts`
25
26
  - `getChainId` / `getBalance`
26
27
  - `onAccountsChanged` / `onChainChanged`
28
+ - `classifyWalletError` / `isUserRejectedWalletAction` / `isWalletReconnectError`
29
+
30
+ `requestAccounts` 默认会复用同一 provider 上尚未完成的连接请求,避免用户重复点击时触发多个钱包授权弹窗。
31
+ 当钱包已经存在待确认的连接、签名或解锁窗口时,可调用 `focusPendingApproval` 将该窗口重新拉到前台,而不是再发起一次新的请求。
27
32
 
28
33
  ### 2) SIWE + JWT
29
34
 
@@ -68,7 +73,17 @@ npm install @yeying-community/web3-bs
68
73
  ### 单后端登录(SIWE + JWT)
69
74
 
70
75
  ```ts
71
- import { loginWithChallenge, authFetch } from '@yeying-community/web3-bs';
76
+ import {
77
+ authFetch,
78
+ focusPendingApproval,
79
+ loginWithChallenge,
80
+ requestAccounts,
81
+ } from '@yeying-community/web3-bs';
82
+
83
+ const pending = await focusPendingApproval().catch(() => ({ focused: false }));
84
+ if (!pending.focused) {
85
+ await requestAccounts();
86
+ }
72
87
 
73
88
  await loginWithChallenge({
74
89
  baseUrl: 'http://localhost:3203/api/v1/public/auth',
@@ -1,8 +1,15 @@
1
- import { Eip1193Provider, ProviderDiscoveryOptions, ProviderInfo, RequestAccountsOptions, AccountSelection, PreferredAccountOptions, WatchAccountsOptions, AccountsChangedHandler } from './types';
1
+ import { Eip1193Provider, ProviderDiscoveryOptions, ProviderInfo, RequestAccountsOptions, AccountSelection, PreferredAccountOptions, WatchAccountsOptions, AccountsChangedHandler, WatchProviderOptions, ProviderChangedHandler, WalletErrorInfo, FocusPendingApprovalResult } from './types';
2
2
  export declare function isYeYingProvider(provider?: Eip1193Provider | null, info?: ProviderInfo): boolean;
3
3
  export declare function getProvider(options?: ProviderDiscoveryOptions): Promise<Eip1193Provider | null>;
4
+ export declare function watchProvider(handler: ProviderChangedHandler, options?: WatchProviderOptions): () => void;
5
+ export declare function getWalletErrorMessage(error: unknown): string;
6
+ export declare function getWalletErrorCode(error: unknown): number | null;
7
+ export declare function classifyWalletError(error: unknown): WalletErrorInfo;
8
+ export declare function isUserRejectedWalletAction(error: unknown): boolean;
9
+ export declare function isWalletReconnectError(error: unknown): boolean;
4
10
  export declare function requireProvider(options?: ProviderDiscoveryOptions): Promise<Eip1193Provider>;
5
11
  export declare function requestAccounts(options?: RequestAccountsOptions): Promise<string[]>;
12
+ export declare function focusPendingApproval(provider?: Eip1193Provider): Promise<FocusPendingApprovalResult>;
6
13
  export declare function getAccounts(provider?: Eip1193Provider): Promise<string[]>;
7
14
  export declare function getChainId(provider?: Eip1193Provider): Promise<string | null>;
8
15
  export declare function getPreferredAccount(options?: PreferredAccountOptions): Promise<AccountSelection>;
@@ -16,6 +16,8 @@ export interface Eip1193Provider {
16
16
  };
17
17
  isMetaMask?: boolean;
18
18
  isYeYing?: boolean;
19
+ providers?: Eip1193Provider[];
20
+ [key: string]: unknown;
19
21
  }
20
22
  export interface ProviderInfo {
21
23
  uuid?: string;
@@ -31,9 +33,31 @@ export interface ProviderDiscoveryOptions {
31
33
  timeoutMs?: number;
32
34
  preferYeYing?: boolean;
33
35
  }
36
+ export interface WatchProviderOptions extends ProviderDiscoveryOptions {
37
+ pollIntervalMs?: number;
38
+ maxPolls?: number;
39
+ }
40
+ export type ProviderChangedHandler = (payload: {
41
+ provider: Eip1193Provider | null;
42
+ present: boolean;
43
+ }) => void;
44
+ export type WalletErrorType = 'userRejected' | 'disconnected' | 'timeout' | 'notFound' | 'unknown';
45
+ export type WalletErrorInfo = {
46
+ type: WalletErrorType;
47
+ code: number | null;
48
+ message: string;
49
+ };
34
50
  export interface RequestAccountsOptions {
35
51
  provider?: Eip1193Provider;
52
+ dedupe?: boolean;
36
53
  }
54
+ export type FocusPendingApprovalResult = {
55
+ focused: boolean;
56
+ type: string | null;
57
+ requestId?: string | null;
58
+ origin?: string;
59
+ tabId?: number | null;
60
+ };
37
61
  export type AccountSelection = {
38
62
  account: string | null;
39
63
  accounts: string[];
@@ -1,10 +1,46 @@
1
1
  const YEYING_RDNS = 'io.github.yeying';
2
2
  const DEFAULT_TIMEOUT = 1000;
3
3
  const DEFAULT_ACCOUNT_STORAGE_KEY = 'yeying:last_account';
4
+ const DEFAULT_PROVIDER_POLL_INTERVAL = 100;
5
+ const DEFAULT_PROVIDER_MAX_POLLS = 20;
6
+ const requestAccountsInFlight = new WeakMap();
7
+ function isProvider(value) {
8
+ return !!value && typeof value.request === 'function';
9
+ }
4
10
  function getWindowEthereum() {
5
11
  if (typeof window === 'undefined')
6
12
  return null;
7
- return window.ethereum || null;
13
+ const ethereum = window.ethereum;
14
+ return isProvider(ethereum) ? ethereum : null;
15
+ }
16
+ function getWindowProviderCandidates() {
17
+ if (typeof window === 'undefined')
18
+ return [];
19
+ const source = window;
20
+ const candidates = [];
21
+ const addProvider = (provider) => {
22
+ if (isProvider(provider) && !candidates.includes(provider)) {
23
+ candidates.push(provider);
24
+ }
25
+ };
26
+ for (const name of [
27
+ 'ethereum',
28
+ 'yeeying',
29
+ 'yeying',
30
+ 'coinbaseWallet',
31
+ 'bitkeep',
32
+ 'tokenpocket',
33
+ '__YEYING_PROVIDER__',
34
+ ]) {
35
+ addProvider(source[name]);
36
+ }
37
+ const ethereum = getWindowEthereum();
38
+ if (Array.isArray(ethereum?.providers)) {
39
+ for (const provider of ethereum.providers) {
40
+ addProvider(provider);
41
+ }
42
+ }
43
+ return candidates;
8
44
  }
9
45
  function readStoredAccount(storageKey) {
10
46
  if (typeof localStorage === 'undefined')
@@ -48,7 +84,7 @@ function isYeYingProvider(provider, info) {
48
84
  }
49
85
  function selectBestProvider(candidates, preferYeYing) {
50
86
  if (candidates.length === 0)
51
- return null;
87
+ return selectBestWindowProvider(preferYeYing);
52
88
  if (preferYeYing) {
53
89
  const yeying = candidates.find(c => isYeYingProvider(c.provider, c.info));
54
90
  if (yeying)
@@ -56,10 +92,21 @@ function selectBestProvider(candidates, preferYeYing) {
56
92
  }
57
93
  return candidates[0].provider;
58
94
  }
95
+ function selectBestWindowProvider(preferYeYing) {
96
+ const candidates = getWindowProviderCandidates();
97
+ if (candidates.length === 0)
98
+ return null;
99
+ if (preferYeYing) {
100
+ const yeying = candidates.find(provider => isYeYingProvider(provider));
101
+ if (yeying)
102
+ return yeying;
103
+ }
104
+ return candidates[0];
105
+ }
59
106
  async function getProvider(options = {}) {
60
107
  const preferYeYing = options.preferYeYing !== false;
61
108
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
62
- const windowProvider = getWindowEthereum();
109
+ const windowProvider = selectBestWindowProvider(preferYeYing);
63
110
  if (preferYeYing && isYeYingProvider(windowProvider)) {
64
111
  return windowProvider;
65
112
  }
@@ -92,7 +139,7 @@ async function getProvider(options = {}) {
92
139
  }
93
140
  };
94
141
  const onEthereumInitialized = () => {
95
- const injected = getWindowEthereum();
142
+ const injected = selectBestWindowProvider(preferYeYing);
96
143
  if (preferYeYing && isYeYingProvider(injected)) {
97
144
  safeResolve(injected);
98
145
  }
@@ -104,7 +151,7 @@ async function getProvider(options = {}) {
104
151
  return;
105
152
  const best = selectBestProvider(discovered, preferYeYing) ||
106
153
  windowProvider ||
107
- getWindowEthereum();
154
+ selectBestWindowProvider(preferYeYing);
108
155
  safeResolve(best || null);
109
156
  }, timeoutMs);
110
157
  try {
@@ -118,6 +165,107 @@ async function getProvider(options = {}) {
118
165
  }
119
166
  });
120
167
  }
168
+ function watchProvider(handler, options = {}) {
169
+ if (typeof window === 'undefined') {
170
+ handler({ provider: null, present: false });
171
+ return () => { };
172
+ }
173
+ const preferYeYing = options.preferYeYing !== false;
174
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_PROVIDER_POLL_INTERVAL;
175
+ const maxPolls = options.maxPolls ?? DEFAULT_PROVIDER_MAX_POLLS;
176
+ let stopped = false;
177
+ let lastProvider;
178
+ let pollCount = 0;
179
+ let pollTimer = null;
180
+ const emit = () => {
181
+ if (stopped)
182
+ return;
183
+ const provider = selectBestWindowProvider(preferYeYing);
184
+ if (provider === lastProvider)
185
+ return;
186
+ lastProvider = provider;
187
+ handler({ provider, present: !!provider });
188
+ };
189
+ const poll = () => {
190
+ if (stopped)
191
+ return;
192
+ emit();
193
+ pollCount += 1;
194
+ if (lastProvider || pollCount >= maxPolls)
195
+ return;
196
+ pollTimer = setTimeout(poll, pollIntervalMs);
197
+ };
198
+ const handleProviderReady = () => {
199
+ emit();
200
+ };
201
+ window.addEventListener('ethereum#initialized', handleProviderReady);
202
+ window.addEventListener('eip6963:announceProvider', handleProviderReady);
203
+ try {
204
+ window.dispatchEvent(new Event('eip6963:requestProvider'));
205
+ }
206
+ catch {
207
+ // Ignore unsupported event dispatch environments.
208
+ }
209
+ poll();
210
+ return () => {
211
+ stopped = true;
212
+ if (pollTimer) {
213
+ clearTimeout(pollTimer);
214
+ }
215
+ window.removeEventListener('ethereum#initialized', handleProviderReady);
216
+ window.removeEventListener('eip6963:announceProvider', handleProviderReady);
217
+ };
218
+ }
219
+ function getWalletErrorMessage(error) {
220
+ if (!error)
221
+ return '';
222
+ if (typeof error === 'string')
223
+ return error;
224
+ if (error instanceof Error)
225
+ return error.message || String(error);
226
+ const message = error.message;
227
+ if (typeof message === 'string')
228
+ return message;
229
+ return String(error);
230
+ }
231
+ function getWalletErrorCode(error) {
232
+ const code = Number(error?.code);
233
+ if (!Number.isNaN(code))
234
+ return code;
235
+ const causeCode = Number(error?.cause?.code);
236
+ if (!Number.isNaN(causeCode))
237
+ return causeCode;
238
+ return null;
239
+ }
240
+ function classifyWalletError(error) {
241
+ const code = getWalletErrorCode(error);
242
+ const message = getWalletErrorMessage(error);
243
+ const lowerMessage = message.toLowerCase();
244
+ if (code === 4001 || lowerMessage.includes('user rejected')) {
245
+ return { type: 'userRejected', code, message };
246
+ }
247
+ if (code === 4900 ||
248
+ lowerMessage.includes('disconnected') ||
249
+ lowerMessage.includes('reconnect') ||
250
+ lowerMessage.includes('not connected')) {
251
+ return { type: 'disconnected', code, message };
252
+ }
253
+ if (lowerMessage.includes('timeout')) {
254
+ return { type: 'timeout', code, message };
255
+ }
256
+ if (lowerMessage.includes('no injected wallet provider') ||
257
+ lowerMessage.includes('未检测到钱包')) {
258
+ return { type: 'notFound', code, message };
259
+ }
260
+ return { type: 'unknown', code, message };
261
+ }
262
+ function isUserRejectedWalletAction(error) {
263
+ return classifyWalletError(error).type === 'userRejected';
264
+ }
265
+ function isWalletReconnectError(error) {
266
+ const type = classifyWalletError(error).type;
267
+ return type === 'disconnected' || type === 'timeout';
268
+ }
121
269
  async function requireProvider(options = {}) {
122
270
  const provider = await getProvider(options);
123
271
  if (!provider) {
@@ -127,10 +275,43 @@ async function requireProvider(options = {}) {
127
275
  }
128
276
  async function requestAccounts(options = {}) {
129
277
  const provider = options.provider || (await requireProvider());
130
- const accounts = (await provider.request({
278
+ const dedupe = options.dedupe !== false;
279
+ if (dedupe) {
280
+ const pending = requestAccountsInFlight.get(provider);
281
+ if (pending)
282
+ return pending;
283
+ }
284
+ const request = provider.request({
131
285
  method: 'eth_requestAccounts',
132
- }));
133
- return Array.isArray(accounts) ? accounts : [];
286
+ }).then(accounts => (Array.isArray(accounts) ? accounts : []));
287
+ if (!dedupe)
288
+ return request;
289
+ requestAccountsInFlight.set(provider, request);
290
+ try {
291
+ return await request;
292
+ }
293
+ finally {
294
+ requestAccountsInFlight.delete(provider);
295
+ }
296
+ }
297
+ async function focusPendingApproval(provider) {
298
+ const p = provider || (await requireProvider());
299
+ const result = await p.request({
300
+ method: 'wallet_focusPendingApproval',
301
+ });
302
+ if (!result || typeof result !== 'object') {
303
+ return { focused: false, type: null };
304
+ }
305
+ const payload = result;
306
+ return {
307
+ focused: Boolean(payload.focused),
308
+ type: typeof payload.type === 'string' ? payload.type : null,
309
+ requestId: typeof payload.requestId === 'string' ? payload.requestId : null,
310
+ origin: typeof payload.origin === 'string' ? payload.origin : '',
311
+ tabId: typeof payload.tabId === 'number' && Number.isFinite(payload.tabId)
312
+ ? payload.tabId
313
+ : null,
314
+ };
134
315
  }
135
316
  async function getAccounts(provider) {
136
317
  const p = provider || (await requireProvider());
@@ -2004,5 +2185,5 @@ async function initDappSession(options) {
2004
2185
  return result;
2005
2186
  }
2006
2187
 
2007
- export { WebDavClient, authCentralUcanFetch, authFetch, authUcanFetch, clearAccessToken, clearCentralSessionToken, clearUcanSession, createAndIssueCentralUcan, createCentralSession, createDelegationUcan, createInvocationUcan, createRootUcan, createUcanSession, createWebDavClient, deriveAppIdFromHost, deriveAppIdFromLocation, getAccessToken, getAccounts, getBalance, getCapabilityAction, getCapabilityResource, getCentralIssuerInfo, getCentralSessionToken, getChainId, getOrCreateUcanRoot, getPreferredAccount, getProvider, getStoredUcanRoot, getUcanSession, initDappSession, initWebDavStorage, isYeYingProvider, issueCentralUcan, loginWithChallenge, logout, normalizeAppHostnameForAppId, normalizeUcanCapabilities, normalizeUcanCapability, onAccountsChanged, onChainChanged, refreshAccessToken, requestAccounts, requireProvider, setAccessToken, setCentralSessionToken, signMessage, storeUcanRoot, watchAccounts };
2188
+ export { WebDavClient, authCentralUcanFetch, authFetch, authUcanFetch, classifyWalletError, clearAccessToken, clearCentralSessionToken, clearUcanSession, createAndIssueCentralUcan, createCentralSession, createDelegationUcan, createInvocationUcan, createRootUcan, createUcanSession, createWebDavClient, deriveAppIdFromHost, deriveAppIdFromLocation, focusPendingApproval, getAccessToken, getAccounts, getBalance, getCapabilityAction, getCapabilityResource, getCentralIssuerInfo, getCentralSessionToken, getChainId, getOrCreateUcanRoot, getPreferredAccount, getProvider, getStoredUcanRoot, getUcanSession, getWalletErrorCode, getWalletErrorMessage, initDappSession, initWebDavStorage, isUserRejectedWalletAction, isWalletReconnectError, isYeYingProvider, issueCentralUcan, loginWithChallenge, logout, normalizeAppHostnameForAppId, normalizeUcanCapabilities, normalizeUcanCapability, onAccountsChanged, onChainChanged, refreshAccessToken, requestAccounts, requireProvider, setAccessToken, setCentralSessionToken, signMessage, storeUcanRoot, watchAccounts, watchProvider };
2008
2189
  //# sourceMappingURL=web3-bs.esm.js.map