@usebutr/wallet-standard-shared 1.0.0 → 2.0.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 +38 -0
- package/README.md +24 -7
- package/dist/index.d.ts +55 -84
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +90 -145
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @usebutr/wallet-standard-shared
|
|
2
2
|
|
|
3
|
+
## 2.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`488fe13`](https://github.com/pedroapfilho/usebutr/commit/488fe13fe9cddb99ff6c5d60f798140e6ac4076f)]:
|
|
8
|
+
- @usebutr/core@3.0.1
|
|
9
|
+
|
|
10
|
+
## 2.0.0
|
|
11
|
+
|
|
12
|
+
### Major Changes
|
|
13
|
+
|
|
14
|
+
- [#203](https://github.com/pedroapfilho/usebutr/pull/203) [`1263f86`](https://github.com/pedroapfilho/usebutr/commit/1263f8666afd43b4f7dbf8df1763709c3edf09d9) Thanks [@pedroapfilho](https://github.com/pedroapfilho)! - Adapters now say what they can do by having the method: `capabilities` is gone, and an optional method (`sendTx`, `signMessage`, `getBalance`, `switchChain`, …) exists only when it works for that wallet. No adapter returns a placeholder balance or a receipt that stays pending anymore.
|
|
15
|
+
|
|
16
|
+
- `sendTx(tx, { account, chain })` replaces `sendTx` and `sendTxToChain`, and `signMessage`, `signTransaction` and `getBalance` take options objects. `chain` is a `ChainBase`. An adapter either routes the call to that chain, switches the wallet to it, or rejects; none ignores it.
|
|
17
|
+
- An `account` the wallet does not expose now rejects instead of signing with the first account. `chain.name` is the chain's name, never the wallet's.
|
|
18
|
+
- `getAccounts()` resolves the active account first. `getAccount` and `switchAccount` are removed.
|
|
19
|
+
- `getSigner()` resolves a tagged `WalletSigner` (`eip1193`, `wallet-standard`, `walletconnect`, `ledger-*`, …), so it can be narrowed with `switch (signer.kind)` instead of cast.
|
|
20
|
+
- `createWalletManager(config, { initialState })` owns discovery, hydration and persistence, and works without React. `config.sources` replaces the `discovery`, `connectors` and `createConnector` props. `fromAdapters` registers WalletConnect, Ledger and hand-built adapters, which now also appear in `useDiscoveredWallets()`.
|
|
21
|
+
- `WalletManagerProvider` takes `config` and `initialState`. `useSigner(wallet)` and `useBalance(wallet, { token, account })` take the wallet entry itself, and id-taking hooks read the active wallet only when the id is omitted (`null` reads none). The hooks are `useWalletManager`, `useWalletState`, `useConnect`, `useWallet`, `useSelectedWallet` (now typed per platform), `useAccounts`, `useConnectionStatus`, `useIsHydrated`, `useIsReconnecting`, `useDiscoveredWallets`, `useConnectedWallets`, the `…ByPlatform` groupings, `useBalance` and `useSigner`.
|
|
22
|
+
- `manager.connect()` and `useConnect().connectAsync()` resolve the `ConnectedWallet` and reject with a `ConnectionError`, now an `Error` subclass with a `kind` and a new `WalletNotFound` kind. `useConnect().connect()` never rejects: the failure lands in its `error`.
|
|
23
|
+
- `WalletPersistence` is `load()` / `save(state)`, and `createWalletStorage` replaces `new WalletStorage`. Storage keys are unchanged.
|
|
24
|
+
- Chain registries (`EVM_CHAINS`, `SVM_CHAINS`, …, `CHAINS_BY_PLATFORM`) move to `@usebutr/core`, and `BITCOIN_CHAINS` gains `testnet4`. `@usebutr/wallets` loads every platform's signer kinds, so an app that imports only it can narrow `signer.kind`.
|
|
25
|
+
- `@usebutr/react` requires React 19, which its `use()` call already needed.
|
|
26
|
+
- WalletConnect requests on Solana, Sui and Bitcoin now carry their chain, so they no longer fall through to the first namespace in a multi-namespace session. Solana takes `SVM_CHAINS` chains and maps them to the genesis-hash ids WalletConnect sessions use.
|
|
27
|
+
- Ledger's Solana `signTransaction` returns the full signed transaction, Sui signs the intent message, and Bitcoin `signMessage` returns a BIP-137 signature.
|
|
28
|
+
- `@usebutr/testing`'s fake adapter resolves a `signer` you pass instead of registering a test-only signer kind.
|
|
29
|
+
- `setAccount` selects an exposed account without changing any account's chain. Unknown accounts are ignored; chain changes come from the adapter.
|
|
30
|
+
- WalletConnect EVM events use the namespace in `session_event`, including empty account lists that end the connection. Local chain switches still update the manager.
|
|
31
|
+
- Failed persistence saves wait for every key write to finish before reporting failure, so delayed writes cannot overwrite a later disconnect.
|
|
32
|
+
- `@usebutr/svm/transaction` provides the shared legacy/v0 signing codec used by Ledger and WalletConnect, with consistent layout and signature validation.
|
|
33
|
+
|
|
34
|
+
See the migration guide at https://docs.usebutr.com/migration.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Updated dependencies [[`1263f86`](https://github.com/pedroapfilho/usebutr/commit/1263f8666afd43b4f7dbf8df1763709c3edf09d9), [`5d1f234`](https://github.com/pedroapfilho/usebutr/commit/5d1f23403263f7a1f8a48be061727972fe749b5f)]:
|
|
39
|
+
- @usebutr/core@3.0.0
|
|
40
|
+
|
|
3
41
|
## 1.0.0
|
|
4
42
|
|
|
5
43
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -11,16 +11,33 @@ connection-state library. Your application owns the picker UI and chain client.
|
|
|
11
11
|
npm install @usebutr/wallet-standard-shared @wallet-standard/app zustand
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
For adapter authors. The package supplies discovery,
|
|
14
|
+
For adapter authors. The package supplies discovery, the session plumbing every Wallet Standard adapter shares, and `getFeature`. The default registry loader requires `@wallet-standard/app`; tests can inject a loader. Importing it registers the `wallet-standard` signer kind, so every adapter built on it resolves `getSigner()` to `{ kind: "wallet-standard", wallet }`.
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
17
17
|
|
|
18
|
-
```
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
```ts
|
|
19
|
+
import type { SvmAdapter } from "@usebutr/core";
|
|
20
|
+
import { SVM_CHAINS_LIST } from "@usebutr/core";
|
|
21
|
+
import type { WalletStandardWallet } from "@usebutr/wallet-standard-shared";
|
|
22
|
+
import { createWalletStandardCore, slugify } from "@usebutr/wallet-standard-shared";
|
|
23
|
+
|
|
24
|
+
export const buildAdapter = (wallet: WalletStandardWallet): SvmAdapter | null => {
|
|
25
|
+
const core = createWalletStandardCore({
|
|
26
|
+
chains: SVM_CHAINS_LIST,
|
|
27
|
+
// Namespace the id so a multi-chain wallet has one adapter per platform.
|
|
28
|
+
id: slugify("svm", wallet.name),
|
|
29
|
+
label: "Solana",
|
|
30
|
+
namespace: "solana",
|
|
31
|
+
preferredChainIds: ["solana:mainnet"],
|
|
32
|
+
trackChainChanges: true,
|
|
33
|
+
wallet,
|
|
34
|
+
});
|
|
35
|
+
// `base` defines `disconnect`, `subscribe` and `switchChain` only when the
|
|
36
|
+
// wallet supports them. Add platform methods the same way: only when the
|
|
37
|
+
// wallet advertises the feature, routing `options.account` through
|
|
38
|
+
// `core.resolveAccount` and `options.chain` through `core.resolveChainId`.
|
|
39
|
+
return core === null ? null : { ...core.base, chainPlatform: "svm" };
|
|
40
|
+
};
|
|
24
41
|
```
|
|
25
42
|
|
|
26
43
|
## Documentation
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Account, ChainBase,
|
|
1
|
+
import { Account, ChainBase, Connector, WalletAdapter, WalletSigner } from "@usebutr/core";
|
|
2
2
|
import "@wallet-standard/app";
|
|
3
3
|
//#region src/types.d.ts
|
|
4
4
|
/**
|
|
@@ -15,13 +15,16 @@ type WalletStandardWalletAccount = {
|
|
|
15
15
|
readonly [index: number]: number;
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
|
+
/** Reverse-DNS-keyed and open-ended by spec; `getFeature` is the one place
|
|
19
|
+
* that reads a member off it. */
|
|
18
20
|
type WalletStandardFeature = {
|
|
21
|
+
readonly [member: string]: unknown;
|
|
19
22
|
version?: string;
|
|
20
23
|
};
|
|
21
24
|
type WalletStandardWallet = {
|
|
22
25
|
accounts: ReadonlyArray<WalletStandardWalletAccount>;
|
|
23
26
|
chains: ReadonlyArray<string>;
|
|
24
|
-
features: Readonly<Record<string, WalletStandardFeature
|
|
27
|
+
features: Readonly<Partial<Record<string, WalletStandardFeature>>>;
|
|
25
28
|
icon: string;
|
|
26
29
|
name: string;
|
|
27
30
|
version: string;
|
|
@@ -63,106 +66,63 @@ type StandardEventsFeature = {
|
|
|
63
66
|
*/
|
|
64
67
|
declare const slugify: (platformPrefix: string, name: string) => string;
|
|
65
68
|
/**
|
|
66
|
-
* The spec
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
+
* The spec keys `features` by reverse-DNS name with no fixed shape, so the
|
|
70
|
+
* caller names the feature type and the method that proves it:
|
|
71
|
+
* `getFeature<SolanaSignMessageFeature>(wallet, "solana:signMessage", "signMessage")`.
|
|
69
72
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
declare const
|
|
74
|
-
/**
|
|
75
|
-
* Per-call routing for `sendTx` / `signMessage`: callers pass an `account`
|
|
76
|
-
* from `ConnectedWallet.accounts` to sign with a non-active address, but
|
|
77
|
-
* the wallet's own `accounts[]` stays the source of truth.
|
|
78
|
-
*/
|
|
79
|
-
declare const pickAccountByAddress: (accounts: ReadonlyArray<WalletStandardWalletAccount>, address: string) => WalletStandardWalletAccount | undefined;
|
|
73
|
+
declare const getFeature: <Feature extends object>(wallet: WalletStandardWallet, name: string, method: keyof Feature & string) => (WalletStandardFeature & Feature) | undefined;
|
|
74
|
+
/** Wallet Standard account for a butr address. `undefined` for an address
|
|
75
|
+
* the wallet does not expose: never another account in its place. */
|
|
76
|
+
declare const findAccount: (accounts: ReadonlyArray<WalletStandardWalletAccount>, address: string) => WalletStandardWalletAccount | undefined;
|
|
80
77
|
//#endregion
|
|
81
78
|
//#region src/adapter-core.d.ts
|
|
82
79
|
type WalletStandardCoreInput = {
|
|
83
|
-
/**
|
|
84
|
-
|
|
80
|
+
/** The platform's chain registry, for chain names. A chain outside it is
|
|
81
|
+
* named by its CAIP-2 id. */
|
|
82
|
+
chains: ReadonlyArray<ChainBase>;
|
|
85
83
|
/** Stable adapter id. Each platform package owns its own slug prefix. */
|
|
86
84
|
id: string;
|
|
87
|
-
/** How
|
|
85
|
+
/** How the platform reads in errors: `Solana`, `Sui`, `Bitcoin`. */
|
|
88
86
|
label: string;
|
|
89
|
-
/** CAIP-2 namespace without its colon
|
|
87
|
+
/** CAIP-2 namespace without its colon: `solana`, `sui`, `bip122`. */
|
|
90
88
|
namespace: string;
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
/** Chain ids worth preferring over a bare prefix match. Matched against
|
|
94
|
-
* the wallet's own ordering, so a wallet listing `mainnet-beta` before
|
|
89
|
+
/** Chain ids worth preferring over the first advertised one. Matched in
|
|
90
|
+
* the wallet's own order, so a wallet listing `mainnet-beta` before
|
|
95
91
|
* `mainnet` keeps the chain it advertised first. */
|
|
96
92
|
preferredChainIds: ReadonlyArray<string>;
|
|
97
|
-
/** Called with a function that pushes a synthetic `disconnected` event
|
|
98
|
-
*
|
|
99
|
-
* Standard `unregister`. */
|
|
93
|
+
/** Called with a function that pushes a synthetic `disconnected` event to
|
|
94
|
+
* all current subscribers. Discovery invokes it on `unregister`. */
|
|
100
95
|
registerDisconnector?: (emit: () => void) => void;
|
|
101
96
|
/** Re-point the active chain when the wallet reports a `chains` change.
|
|
102
|
-
* Polkadot opts out: its wallets advertise a single relay chain
|
|
103
|
-
* re-point would only ever move the adapter off the chain it resolved. */
|
|
97
|
+
* Polkadot opts out: its wallets advertise a single relay chain. */
|
|
104
98
|
trackChainChanges: boolean;
|
|
105
99
|
wallet: WalletStandardWallet;
|
|
106
100
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
silent?: boolean;
|
|
112
|
-
}) => Promise<void>;
|
|
113
|
-
/** Active CAIP-2 chain id, for features that take a per-call `chain`. */
|
|
114
|
-
currentChainId: () => string;
|
|
115
|
-
disconnect: () => Promise<void>;
|
|
116
|
-
getAccount: () => Promise<Account | null>;
|
|
117
|
-
getAccounts: () => Promise<Array<Account>>;
|
|
118
|
-
getSigner: () => Promise<WalletStandardWallet>;
|
|
119
|
-
/** Whether the wallet advertises `standard:events`; feeds capability gating. */
|
|
120
|
-
hasEvents: boolean;
|
|
121
|
-
icon: string | undefined;
|
|
122
|
-
id: string;
|
|
123
|
-
name: string;
|
|
124
|
-
/** Wallet Standard account a call should route through: the one matching
|
|
125
|
-
* `account`, else the wallet's first exposed account. */
|
|
126
|
-
resolveAccount: (account?: {
|
|
127
|
-
walletAddress: string;
|
|
128
|
-
}) => WalletStandardWalletAccount;
|
|
129
|
-
subscribe: (listener: (event: ConnectorEvent) => void) => () => void;
|
|
130
|
-
switchChain: (chain: ChainBase) => Promise<void>;
|
|
131
|
-
/** butr chain for the active chain id, for responses that mint their own
|
|
132
|
-
* `Account` (Sign-In-With-Solana) rather than going through `getAccounts`. */
|
|
133
|
-
toChain: () => ChainBase;
|
|
101
|
+
/** The members every Wallet Standard adapter shares, ready to spread. */
|
|
102
|
+
type WalletStandardConnector = Pick<Connector, "connect" | "disconnect" | "getAccounts" | "icon" | "id" | "name" | "subscribe"> & {
|
|
103
|
+
getSigner: () => Promise<WalletSigner>;
|
|
104
|
+
switchChain?: (chain: ChainBase) => Promise<void>;
|
|
134
105
|
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
*/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
/** Wallet advertises a sign-and-broadcast feature:
|
|
149
|
-
* `solana:signAndSendTransaction`, `sui:signAndExecuteTransaction`,
|
|
150
|
-
* `bitcoin:sendTransfer`. */
|
|
151
|
-
sendTransaction: boolean;
|
|
152
|
-
/** Wallet advertises `solana:signIn`. No other namespace has an
|
|
153
|
-
* equivalent, so every non-SVM caller passes `false`. */
|
|
154
|
-
signIn: boolean;
|
|
155
|
-
signMessage: boolean;
|
|
156
|
-
/** Wallet advertises a sign-only feature: `solana:signTransaction`,
|
|
157
|
-
* `sui:signTransaction`, `bitcoin:signPsbt`. */
|
|
158
|
-
signTransaction: boolean;
|
|
106
|
+
type WalletStandardCore = {
|
|
107
|
+
/** Spread into the adapter. `disconnect`, `subscribe` and `switchChain` are
|
|
108
|
+
* present only when the wallet supports them. */
|
|
109
|
+
base: WalletStandardConnector;
|
|
110
|
+
/** The active chain, for results that mint their own `Account`. */
|
|
111
|
+
currentChain: () => ChainBase;
|
|
112
|
+
/** The Wallet Standard account a call signs with: the one matching
|
|
113
|
+
* `account`, else the active one. Throws for an account the wallet does
|
|
114
|
+
* not expose. */
|
|
115
|
+
resolveAccount: (account?: Account) => WalletStandardWalletAccount;
|
|
116
|
+
/** The chain id a call targets: `chain` when given, else the current one.
|
|
117
|
+
* Throws for a chain outside the namespace or not advertised. */
|
|
118
|
+
resolveChainId: (chain?: ChainBase) => string;
|
|
159
119
|
};
|
|
160
120
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
121
|
+
* `null` when the wallet advertises no chain in `namespace` or lacks
|
|
122
|
+
* `standard:connect`, so a multi-chain wallet yields one adapter per
|
|
123
|
+
* platform it actually speaks.
|
|
164
124
|
*/
|
|
165
|
-
declare const
|
|
125
|
+
declare const createWalletStandardCore: ({ chains, id, label, namespace, preferredChainIds, registerDisconnector, trackChainChanges, wallet }: WalletStandardCoreInput) => WalletStandardCore | null;
|
|
166
126
|
//#endregion
|
|
167
127
|
//#region src/discovery.d.ts
|
|
168
128
|
/**
|
|
@@ -178,5 +138,16 @@ type WalletStandardAdapterBuilder = (wallet: WalletStandardWallet, registerDisco
|
|
|
178
138
|
*/
|
|
179
139
|
declare const discoverWalletStandard: (onAdapter: (adapter: WalletAdapter) => void, build: WalletStandardAdapterBuilder, loadModule?: WalletStandardModuleLoader) => (() => void);
|
|
180
140
|
//#endregion
|
|
181
|
-
|
|
141
|
+
//#region src/signer-augmentation.d.ts
|
|
142
|
+
/** Every Wallet Standard adapter, on any platform, hands back the raw wallet;
|
|
143
|
+
* reach its features with `getFeature`. */
|
|
144
|
+
declare module "@usebutr/core" {
|
|
145
|
+
interface WalletSignerRegistry {
|
|
146
|
+
"wallet-standard": {
|
|
147
|
+
wallet: WalletStandardWallet;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { type StandardConnectFeature, type StandardDisconnectFeature, type StandardEventsFeature, type StandardEventsListener, type WalletStandardAdapterBuilder, type WalletStandardAppModule, type WalletStandardCore, type WalletStandardCoreInput, type WalletStandardFeature, type WalletStandardModuleLoader, type WalletStandardWallet, type WalletStandardWalletAccount, type WalletsApp, createWalletStandardCore, discoverWalletStandard, findAccount, getFeature, slugify };
|
|
182
153
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/primitives.ts","../src/adapter-core.ts","../src/
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/primitives.ts","../src/adapter-core.ts","../src/discovery.ts","../src/signer-augmentation.ts"],"mappings":";;;;;;;;KAMK;EACH;EACA,QAAQ;EACR,UAAU;EACV;aAAuB;cAA0B;;;;;KAM9C;YAAoC;EAA0B;;KAE9D;EACH,UAAU,cAAc;EACxB,QAAQ;EAER,UAAU,SAAS,QAAQ,eAAe;EAC1C;EACA;EACA;;KAGG;EACH,WAAW,cAAc;EACzB,KACE,kCACA,cAAc,SAAS,cAAc;;KAIpC;EACH,kBAAkB;;KAGf,mCAAmC,QAAQ;KAE3C;EACH,UAAU;IAAU;QAAuB;IACzC,UAAU,cAAc;;EAE1B;;KAGG;EACH,kBAAkB;EAClB;;KAGG,0BAA0B;EAC7B,WAAW,cAAc;EACzB,SAAS;EACT,WAAW;;KAGR;EACH,KAAK,iBAAiB,UAAU;EAChC;;;;;;;;cCpDI,UAAO,wBAA0B;;;;;;cAkBjC,aAAc,wBAAsB,QAChC,sBAAoB,cAChB,cACE,sBACZ,wBAAwB;;;cAOtB,cAAW,UACL,cAAc,8BAA4B,oBAEnD;;;KC9BE;;;EAGH,QAAQ,cAAc;;EAEtB;;EAEA;;EAEA;;;;EAIA,mBAAmB;;;EAGnB,wBAAwB;;;EAGxB;EACA,QAAQ;;;KAIL,0BAA0B,KAC7B;EAGA,iBAAiB,QAAQ;EACzB,eAAe,OAAO,cAAc;;KAGjC;;;EAGH,MAAM;;EAEN,oBAAoB;;;;EAIpB,iBAAiB,UAAU,YAAY;;;EAGvC,iBAAiB,QAAQ;;;;;;;cAQrB,6BAAwB,QAAA,IAAA,OAAA,WAAA,mBAAA,sBAAA,mBAAA,UAS3B,4BAA0B;;;;;;;;KCRxB,gCACH,QAAQ,sBACR,uBAAuB,8BACpB;;;;;;cAwBC,yBAAsB,YACd,SAAS,wBAAsB,OACpC,8BAA4B,aACvB;;;;;;YCzFF;IACR;MAAqB,QAAQ"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildAccount,
|
|
1
|
+
import { buildAccount, logWarn, resolveChain, sanitizeIcon } from "@usebutr/core";
|
|
2
2
|
|
|
3
3
|
//#region src/primitives.ts
|
|
4
4
|
/**
|
|
@@ -8,177 +8,122 @@ import { buildAccount, buildAccount as buildAccount$1, logWarn, sanitizeIcon } f
|
|
|
8
8
|
const slugify = (platformPrefix, name) => {
|
|
9
9
|
return `wallet-standard:${platformPrefix}-${name.trim().toLowerCase().replaceAll(new RegExp("[^a-z0-9]+", "gv"), "-")}`;
|
|
10
10
|
};
|
|
11
|
-
const
|
|
12
|
-
const feature = wallet.features[name];
|
|
13
|
-
if (feature === void 0 || !isFeature(feature)) return;
|
|
14
|
-
return feature;
|
|
15
|
-
};
|
|
16
|
-
const isWalletStandardWallet = (value) => "accounts" in value && Array.isArray(value.accounts) && "chains" in value && Array.isArray(value.chains) && "features" in value && typeof value.features === "object" && value.features !== null && "icon" in value && typeof value.icon === "string" && "name" in value && typeof value.name === "string" && "version" in value && typeof value.version === "string";
|
|
17
|
-
const pickFirstAddress = (accounts) => {
|
|
18
|
-
const first = accounts[0];
|
|
19
|
-
return first === void 0 ? null : first.address;
|
|
20
|
-
};
|
|
11
|
+
const hasMethod = (feature, method) => typeof feature[method] === "function";
|
|
21
12
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
13
|
+
* The spec keys `features` by reverse-DNS name with no fixed shape, so the
|
|
14
|
+
* caller names the feature type and the method that proves it:
|
|
15
|
+
* `getFeature<SolanaSignMessageFeature>(wallet, "solana:signMessage", "signMessage")`.
|
|
25
16
|
*/
|
|
26
|
-
const
|
|
17
|
+
const getFeature = (wallet, name, method) => {
|
|
18
|
+
const feature = wallet.features[name];
|
|
19
|
+
return feature !== void 0 && hasMethod(feature, method) ? feature : void 0;
|
|
20
|
+
};
|
|
21
|
+
/** Wallet Standard account for a butr address. `undefined` for an address
|
|
22
|
+
* the wallet does not expose: never another account in its place. */
|
|
23
|
+
const findAccount = (accounts, address) => accounts.find((a) => a.address === address);
|
|
27
24
|
|
|
28
25
|
//#endregion
|
|
29
26
|
//#region src/adapter-core.ts
|
|
30
|
-
const isStandardConnectFeature = (feature) => "connect" in feature && typeof feature.connect === "function";
|
|
31
|
-
const isStandardDisconnectFeature = (feature) => "disconnect" in feature && typeof feature.disconnect === "function";
|
|
32
|
-
const isStandardEventsFeature = (feature) => "on" in feature && typeof feature.on === "function";
|
|
33
27
|
/**
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
28
|
+
* `null` when the wallet advertises no chain in `namespace` or lacks
|
|
29
|
+
* `standard:connect`, so a multi-chain wallet yields one adapter per
|
|
30
|
+
* platform it actually speaks.
|
|
37
31
|
*/
|
|
38
|
-
const createWalletStandardCore = ({
|
|
39
|
-
const
|
|
40
|
-
const
|
|
32
|
+
const createWalletStandardCore = ({ chains, id, label, namespace, preferredChainIds, registerDisconnector, trackChainChanges, wallet }) => {
|
|
33
|
+
const prefix = `${namespace}:`;
|
|
34
|
+
const preferred = new Set(preferredChainIds);
|
|
35
|
+
const pickChain = (advertised) => advertised.find((c) => preferred.has(c)) ?? advertised.find((c) => c.startsWith(prefix));
|
|
41
36
|
const initialChainId = pickChain(wallet.chains);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const events = getFeature(wallet, "standard:events", isStandardEventsFeature);
|
|
37
|
+
const connect = getFeature(wallet, "standard:connect", "connect");
|
|
38
|
+
if (initialChainId === void 0 || connect === void 0) return null;
|
|
39
|
+
const disconnect = getFeature(wallet, "standard:disconnect", "disconnect");
|
|
40
|
+
const events = getFeature(wallet, "standard:events", "on");
|
|
47
41
|
let currentChainId = initialChainId;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
42
|
+
const currentChain = () => resolveChain(currentChainId, chains);
|
|
43
|
+
const toAccounts = (accounts) => {
|
|
44
|
+
const chain = currentChain();
|
|
45
|
+
return accounts.map((a) => buildAccount(a.address, chain));
|
|
46
|
+
};
|
|
54
47
|
const listeners = /* @__PURE__ */ new Set();
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for (const listener of listeners) listener({
|
|
62
|
-
account: first,
|
|
63
|
-
accounts: built,
|
|
64
|
-
type: "accountChanged"
|
|
65
|
-
});
|
|
48
|
+
const emit = (event) => {
|
|
49
|
+
for (const listener of listeners) listener(event);
|
|
50
|
+
};
|
|
51
|
+
const toEvent = (accounts) => accounts.length === 0 ? { type: "disconnected" } : {
|
|
52
|
+
accounts: toAccounts(accounts),
|
|
53
|
+
type: "accountsChanged"
|
|
66
54
|
};
|
|
67
55
|
registerDisconnector?.(() => {
|
|
68
|
-
|
|
56
|
+
emit({ type: "disconnected" });
|
|
69
57
|
});
|
|
58
|
+
const resolveChainId = (chain) => {
|
|
59
|
+
if (chain === void 0) return currentChainId;
|
|
60
|
+
if (chain.namespace !== namespace) throw new Error(`${label} adapter received non-${label} chain "${chain.id}". Pass a chain with namespace "${namespace}".`);
|
|
61
|
+
if (!wallet.chains.includes(chain.id)) throw new Error(`Wallet ${wallet.name} does not advertise chain "${chain.id}". Available: ${wallet.chains.join(", ")}`);
|
|
62
|
+
return chain.id;
|
|
63
|
+
};
|
|
64
|
+
const subscribe = (listener) => {
|
|
65
|
+
listeners.add(listener);
|
|
66
|
+
const off = events?.on("change", (changes) => {
|
|
67
|
+
const next = trackChainChanges && changes.chains ? pickChain(changes.chains) : void 0;
|
|
68
|
+
if (next !== void 0) currentChainId = next;
|
|
69
|
+
if (changes.accounts !== void 0) listener(toEvent(changes.accounts));
|
|
70
|
+
else if (next !== void 0) listener(toEvent(wallet.accounts));
|
|
71
|
+
});
|
|
72
|
+
return () => {
|
|
73
|
+
listeners.delete(listener);
|
|
74
|
+
off?.();
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const advertisedInNamespace = wallet.chains.filter((c) => c.startsWith(prefix));
|
|
70
78
|
return {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
base: {
|
|
80
|
+
async connect(options) {
|
|
81
|
+
await connect.connect(options?.silent === true ? { silent: true } : void 0);
|
|
82
|
+
},
|
|
83
|
+
getAccounts: () => Promise.resolve(toAccounts(wallet.accounts)),
|
|
84
|
+
getSigner: () => Promise.resolve({
|
|
85
|
+
kind: "wallet-standard",
|
|
86
|
+
wallet
|
|
87
|
+
}),
|
|
88
|
+
icon: sanitizeIcon(wallet.icon),
|
|
89
|
+
id,
|
|
90
|
+
name: wallet.name,
|
|
91
|
+
...disconnect !== void 0 && { async disconnect() {
|
|
92
|
+
try {
|
|
93
|
+
await disconnect.disconnect();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logWarn(`[butr] ${label} Wallet Standard disconnect threw:`, error);
|
|
96
|
+
}
|
|
97
|
+
} },
|
|
98
|
+
...(events !== void 0 || registerDisconnector !== void 0) && { subscribe },
|
|
99
|
+
...advertisedInNamespace.length > 1 && { switchChain: async (chain) => {
|
|
100
|
+
currentChainId = resolveChainId(chain);
|
|
101
|
+
emit(toEvent(wallet.accounts));
|
|
102
|
+
await Promise.resolve();
|
|
103
|
+
} }
|
|
90
104
|
},
|
|
91
|
-
|
|
92
|
-
hasEvents: Boolean(events),
|
|
93
|
-
icon: sanitizeIcon(wallet.icon),
|
|
94
|
-
id,
|
|
95
|
-
name: wallet.name,
|
|
105
|
+
currentChain,
|
|
96
106
|
resolveAccount: (account) => {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
return
|
|
107
|
+
const match = account === void 0 ? wallet.accounts[0] : findAccount(wallet.accounts, account.walletAddress);
|
|
108
|
+
if (match === void 0) throw new Error(account === void 0 ? `Wallet ${wallet.name} has no connected account` : `Wallet ${wallet.name} does not expose account ${account.walletAddress}`);
|
|
109
|
+
return match;
|
|
100
110
|
},
|
|
101
|
-
|
|
102
|
-
listeners.add(listener);
|
|
103
|
-
let unsubWallet = null;
|
|
104
|
-
if (events !== void 0) {
|
|
105
|
-
const unsub = events.on("change", (changes) => {
|
|
106
|
-
if (trackChainChanges && changes.chains !== void 0) {
|
|
107
|
-
const next = pickChain(changes.chains);
|
|
108
|
-
if (next !== null) currentChainId = next;
|
|
109
|
-
}
|
|
110
|
-
if (changes.accounts !== void 0) {
|
|
111
|
-
if (changes.accounts.length === 0) {
|
|
112
|
-
listener({ type: "disconnected" });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const chain = toChain();
|
|
116
|
-
const built = changes.accounts.map((a) => buildAccount$1(a.address, chain));
|
|
117
|
-
const first = built[0];
|
|
118
|
-
if (first === void 0) return;
|
|
119
|
-
listener({
|
|
120
|
-
account: first,
|
|
121
|
-
accounts: built,
|
|
122
|
-
type: "accountChanged"
|
|
123
|
-
});
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (trackChainChanges && changes.chains !== void 0) notifyAccountChanged();
|
|
127
|
-
});
|
|
128
|
-
unsubWallet = () => {
|
|
129
|
-
unsub();
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return () => {
|
|
133
|
-
listeners.delete(listener);
|
|
134
|
-
unsubWallet?.();
|
|
135
|
-
};
|
|
136
|
-
},
|
|
137
|
-
switchChain: async (chain) => {
|
|
138
|
-
if (chain.namespace !== namespace) throw new Error(`${label} adapter received non-${platform} chain "${chain.id}". Pass a chain with namespace "${namespace}".`);
|
|
139
|
-
if (!wallet.chains.includes(chain.id)) throw new Error(`Wallet ${wallet.name} does not advertise chain "${chain.id}". Available: ${wallet.chains.join(", ")}`);
|
|
140
|
-
currentChainId = chain.id;
|
|
141
|
-
notifyAccountChanged();
|
|
142
|
-
await Promise.resolve();
|
|
143
|
-
},
|
|
144
|
-
toChain
|
|
111
|
+
resolveChainId
|
|
145
112
|
};
|
|
146
113
|
};
|
|
147
114
|
|
|
148
|
-
//#endregion
|
|
149
|
-
//#region src/capabilities.ts
|
|
150
|
-
/**
|
|
151
|
-
* Wallet Standard has no balance/receipt RPC, no EIP-2255 equivalent, and
|
|
152
|
-
* no silent account switch, so those flags are false everywhere.
|
|
153
|
-
* `switchChain` re-routes locally, so it needs a second advertised chain.
|
|
154
|
-
*/
|
|
155
|
-
const buildWalletCapabilities = (profile) => ({
|
|
156
|
-
getBalance: false,
|
|
157
|
-
getTransactionReceipt: false,
|
|
158
|
-
requestAccounts: false,
|
|
159
|
-
sendTransaction: profile.sendTransaction,
|
|
160
|
-
signIn: profile.signIn,
|
|
161
|
-
signMessage: profile.signMessage,
|
|
162
|
-
signTransaction: profile.signTransaction,
|
|
163
|
-
subscribe: profile.events,
|
|
164
|
-
switchAccount: false,
|
|
165
|
-
switchChain: profile.chainCount > 1
|
|
166
|
-
});
|
|
167
|
-
|
|
168
115
|
//#endregion
|
|
169
116
|
//#region src/discovery.ts
|
|
117
|
+
const isFeatureObject = (value) => typeof value === "object" && value !== null;
|
|
170
118
|
const toFeatureRecord = (features) => {
|
|
171
119
|
const record = {};
|
|
172
|
-
for (const [name, value] of Object.entries(features))
|
|
173
|
-
if (typeof value !== "object" || value === null) continue;
|
|
174
|
-
record[name] = value;
|
|
175
|
-
}
|
|
120
|
+
for (const [name, value] of Object.entries(features)) if (isFeatureObject(value)) record[name] = value;
|
|
176
121
|
return record;
|
|
177
122
|
};
|
|
178
123
|
const wrappers = /* @__PURE__ */ new WeakMap();
|
|
179
124
|
/**
|
|
180
125
|
* Reads through to the wallet: it re-points `accounts` on connect, so a copy
|
|
181
|
-
* pins the pre-connect empty list and every `
|
|
126
|
+
* pins the pre-connect empty list and every `getAccounts()` then resolves [].
|
|
182
127
|
* Memoised because `unregister` is matched to `register` by object identity.
|
|
183
128
|
*/
|
|
184
129
|
const mapWallet = (wallet) => {
|
|
@@ -202,7 +147,7 @@ const mapWallet = (wallet) => {
|
|
|
202
147
|
let warnedMissingApp = false;
|
|
203
148
|
const loadWalletStandardModule = async () => {
|
|
204
149
|
const imported = await import("@wallet-standard/app");
|
|
205
|
-
if (
|
|
150
|
+
if (!("getWallets" in imported) || typeof imported.getWallets !== "function") throw new Error("@wallet-standard/app has no getWallets export");
|
|
206
151
|
const source = imported.getWallets();
|
|
207
152
|
return { getWallets: () => ({
|
|
208
153
|
get: () => source.get().map(mapWallet),
|
|
@@ -268,5 +213,5 @@ const discoverWalletStandard = (onAdapter, build, loadModule = loadWalletStandar
|
|
|
268
213
|
};
|
|
269
214
|
|
|
270
215
|
//#endregion
|
|
271
|
-
export {
|
|
216
|
+
export { createWalletStandardCore, discoverWalletStandard, findAccount, getFeature, slugify };
|
|
272
217
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["buildAccount"],"sources":["../src/primitives.ts","../src/adapter-core.ts","../src/capabilities.ts","../src/discovery.ts"],"sourcesContent":["import type { WalletSigner } from \"@usebutr/core\";\n\nimport type {\n WalletStandardFeature,\n WalletStandardWallet,\n WalletStandardWalletAccount,\n} from \"./types\";\n\n/**\n * The platform prefix scopes the id so one multi-chain wallet (Phantom\n * SVM, Sui, BTC) yields distinct adapters on the shared `getWallets()` bus.\n */\nconst slugify = (platformPrefix: string, name: string): string => {\n const slug = name\n .trim()\n .toLowerCase()\n .replaceAll(/[^a-z0-9]+/gv, \"-\");\n return `wallet-standard:${platformPrefix}-${slug}`;\n};\n\n/**\n * The spec types `features` as a reverse-DNS-keyed `Record<string,\n * unknown>`, so the caller declares the shape it expects. This accessor is\n * the single boundary where that dynamism is acknowledged.\n */\ntype FeatureGuard<Feature> = (\n feature: WalletStandardFeature,\n) => feature is WalletStandardFeature & Feature;\n\nconst getFeature = <Feature>(\n wallet: WalletStandardWallet,\n name: string,\n isFeature: FeatureGuard<Feature>,\n): (WalletStandardFeature & Feature) | undefined => {\n const feature = wallet.features[name];\n if (feature === undefined || !isFeature(feature)) {\n return undefined;\n }\n return feature;\n};\n\nconst isWalletStandardWallet = (value: WalletSigner): value is WalletStandardWallet =>\n \"accounts\" in value &&\n Array.isArray(value.accounts) &&\n \"chains\" in value &&\n Array.isArray(value.chains) &&\n \"features\" in value &&\n typeof value.features === \"object\" &&\n value.features !== null &&\n \"icon\" in value &&\n typeof value.icon === \"string\" &&\n \"name\" in value &&\n typeof value.name === \"string\" &&\n \"version\" in value &&\n typeof value.version === \"string\";\n\nconst pickFirstAddress = (accounts: ReadonlyArray<WalletStandardWalletAccount>): string | null => {\n const first = accounts[0];\n return first === undefined ? null : first.address;\n};\n\n/**\n * Per-call routing for `sendTx` / `signMessage`: callers pass an `account`\n * from `ConnectedWallet.accounts` to sign with a non-active address, but\n * the wallet's own `accounts[]` stays the source of truth.\n */\nconst pickAccountByAddress = (\n accounts: ReadonlyArray<WalletStandardWalletAccount>,\n address: string,\n): WalletStandardWalletAccount | undefined =>\n accounts.find((a) => a.address === address) ?? accounts[0];\n\nexport { buildAccount } from \"@usebutr/core\";\nexport { getFeature, isWalletStandardWallet, pickAccountByAddress, pickFirstAddress, slugify };\n","import type { Account, ChainBase, ConnectorEvent } from \"@usebutr/core\";\nimport { buildAccount, logWarn, sanitizeIcon } from \"@usebutr/core\";\n\nimport { getFeature, pickAccountByAddress, pickFirstAddress } from \"./primitives\";\nimport type {\n StandardConnectFeature,\n StandardDisconnectFeature,\n StandardEventsFeature,\n WalletStandardFeature,\n WalletStandardWallet,\n WalletStandardWalletAccount,\n} from \"./types\";\n\nconst isStandardConnectFeature = (\n feature: WalletStandardFeature,\n): feature is WalletStandardFeature & StandardConnectFeature =>\n \"connect\" in feature && typeof feature.connect === \"function\";\n\nconst isStandardDisconnectFeature = (\n feature: WalletStandardFeature,\n): feature is WalletStandardFeature & StandardDisconnectFeature =>\n \"disconnect\" in feature && typeof feature.disconnect === \"function\";\n\nconst isStandardEventsFeature = (\n feature: WalletStandardFeature,\n): feature is WalletStandardFeature & StandardEventsFeature =>\n \"on\" in feature && typeof feature.on === \"function\";\n\ntype WalletStandardCoreInput = {\n /** CAIP-2 prefix including its colon (`solana:`, `sui:`, `bip122:`). */\n chainPrefix: string;\n /** Stable adapter id. Each platform package owns its own slug prefix. */\n id: string;\n /** How this adapter names itself in errors (`Bitcoin`, `SVM`, `Sui`). */\n label: string;\n /** CAIP-2 namespace without its colon. */\n namespace: string;\n /** How the chain family reads in errors (`Bitcoin`, `Solana`, `Sui`). */\n platform: string;\n /** Chain ids worth preferring over a bare prefix match. Matched against\n * the wallet's own ordering, so a wallet listing `mainnet-beta` before\n * `mainnet` keeps the chain it advertised first. */\n preferredChainIds: ReadonlyArray<string>;\n /** Called with a function that pushes a synthetic `disconnected` event\n * to all current subscribers. The discovery layer invokes it on Wallet\n * Standard `unregister`. */\n registerDisconnector?: (emit: () => void) => void;\n /** Re-point the active chain when the wallet reports a `chains` change.\n * Polkadot opts out: its wallets advertise a single relay chain, so a\n * re-point would only ever move the adapter off the chain it resolved. */\n trackChainChanges: boolean;\n wallet: WalletStandardWallet;\n};\n\ntype WalletStandardCore = {\n /** Chains the wallet advertises in this namespace; feeds capability gating. */\n chainCount: number;\n connect: (opts?: { silent?: boolean }) => Promise<void>;\n /** Active CAIP-2 chain id, for features that take a per-call `chain`. */\n currentChainId: () => string;\n disconnect: () => Promise<void>;\n getAccount: () => Promise<Account | null>;\n getAccounts: () => Promise<Array<Account>>;\n getSigner: () => Promise<WalletStandardWallet>;\n /** Whether the wallet advertises `standard:events`; feeds capability gating. */\n hasEvents: boolean;\n icon: string | undefined;\n id: string;\n name: string;\n /** Wallet Standard account a call should route through: the one matching\n * `account`, else the wallet's first exposed account. */\n resolveAccount: (account?: { walletAddress: string }) => WalletStandardWalletAccount;\n subscribe: (listener: (event: ConnectorEvent) => void) => () => void;\n switchChain: (chain: ChainBase) => Promise<void>;\n /** butr chain for the active chain id, for responses that mint their own\n * `Account` (Sign-In-With-Solana) rather than going through `getAccounts`. */\n toChain: () => ChainBase;\n};\n\n/**\n * Shared session plumbing for every Wallet Standard platform adapter.\n * `getSigner` hands back the raw Wallet Standard wallet so consumers can\n * wrap it in their own signing library.\n */\nconst createWalletStandardCore = ({\n chainPrefix,\n id,\n label,\n namespace,\n platform,\n preferredChainIds,\n registerDisconnector,\n trackChainChanges,\n wallet,\n}: WalletStandardCoreInput): WalletStandardCore | null => {\n const preferredChains = new Set(preferredChainIds);\n const pickChain = (chains: ReadonlyArray<string>): string | null =>\n chains.find((c) => preferredChains.has(c)) ??\n chains.find((c) => c.startsWith(chainPrefix)) ??\n null;\n\n const initialChainId = pickChain(wallet.chains);\n if (initialChainId === null) {\n return null;\n }\n const connect = getFeature(wallet, \"standard:connect\", isStandardConnectFeature);\n if (connect === undefined) {\n return null;\n }\n\n const disconnect = getFeature(wallet, \"standard:disconnect\", isStandardDisconnectFeature);\n const events = getFeature(wallet, \"standard:events\", isStandardEventsFeature);\n\n let currentChainId = initialChainId;\n const toChain = (): ChainBase => ({\n id: currentChainId,\n name: wallet.name,\n namespace,\n reference: currentChainId.slice(chainPrefix.length),\n });\n\n const listeners = new Set<(event: ConnectorEvent) => void>();\n const notifyAccountChanged = () => {\n if (wallet.accounts.length === 0) {\n return;\n }\n const chain = toChain();\n const built = wallet.accounts.map((a) => buildAccount(a.address, chain));\n const first = built[0];\n if (first === undefined) {\n return;\n }\n for (const listener of listeners) {\n listener({ account: first, accounts: built, type: \"accountChanged\" });\n }\n };\n\n registerDisconnector?.(() => {\n for (const listener of listeners) {\n listener({ type: \"disconnected\" });\n }\n });\n\n return {\n chainCount: wallet.chains.filter((c) => c.startsWith(chainPrefix)).length,\n\n async connect(opts) {\n await connect.connect(opts?.silent === true ? { silent: true } : undefined);\n },\n\n currentChainId: () => currentChainId,\n\n async disconnect() {\n if (disconnect !== undefined) {\n try {\n await disconnect.disconnect();\n } catch (error) {\n logWarn(`[butr] ${label} Wallet Standard disconnect threw:`, error);\n }\n }\n },\n\n getAccount: () => {\n const address = pickFirstAddress(wallet.accounts);\n return Promise.resolve(address === null ? null : buildAccount(address, toChain()));\n },\n\n getAccounts: () => {\n const chain = toChain();\n return Promise.resolve(wallet.accounts.map((a) => buildAccount(a.address, chain)));\n },\n\n getSigner: () => Promise.resolve(wallet),\n\n hasEvents: Boolean(events),\n\n icon: sanitizeIcon(wallet.icon),\n id,\n name: wallet.name,\n\n resolveAccount: (account) => {\n const wsAccount = account\n ? pickAccountByAddress(wallet.accounts, account.walletAddress)\n : wallet.accounts[0];\n if (wsAccount === undefined) {\n throw new Error(\"No connected account\");\n }\n return wsAccount;\n },\n\n subscribe(listener) {\n listeners.add(listener);\n let unsubWallet: (() => void) | null = null;\n if (events !== undefined) {\n const unsub = events.on(\"change\", (changes) => {\n if (trackChainChanges && changes.chains !== undefined) {\n const next = pickChain(changes.chains);\n if (next !== null) {\n currentChainId = next;\n }\n }\n\n if (changes.accounts !== undefined) {\n if (changes.accounts.length === 0) {\n listener({ type: \"disconnected\" });\n return;\n }\n const chain = toChain();\n const built = changes.accounts.map((a) => buildAccount(a.address, chain));\n const first = built[0];\n if (first === undefined) {\n return;\n }\n listener({ account: first, accounts: built, type: \"accountChanged\" });\n return;\n }\n\n if (trackChainChanges && changes.chains !== undefined) {\n notifyAccountChanged();\n }\n });\n unsubWallet = () => {\n unsub();\n };\n }\n return () => {\n listeners.delete(listener);\n unsubWallet?.();\n };\n },\n\n // Async so a rejected chain surfaces as a rejection. Declared\n // `Promise<void>`, this previously threw synchronously, so a caller\n // attaching `.catch()` instead of awaiting got an uncaught throw.\n switchChain: async (chain) => {\n if (chain.namespace !== namespace) {\n throw new Error(\n `${label} adapter received non-${platform} chain \"${chain.id}\". Pass a chain with namespace \"${namespace}\".`,\n );\n }\n if (!wallet.chains.includes(chain.id)) {\n throw new Error(\n `Wallet ${wallet.name} does not advertise chain \"${chain.id}\". Available: ${wallet.chains.join(\", \")}`,\n );\n }\n currentChainId = chain.id;\n notifyAccountChanged();\n await Promise.resolve();\n },\n\n toChain,\n };\n};\n\nexport type { WalletStandardCore };\nexport { createWalletStandardCore };\n","import type { WalletCapabilities } from \"@usebutr/core\";\n\ntype WalletCapabilityProfile = {\n /** Chains the wallet advertises in this namespace. */\n chainCount: number;\n /** Wallet advertises `standard:events`. */\n events: boolean;\n /** Wallet advertises a sign-and-broadcast feature:\n * `solana:signAndSendTransaction`, `sui:signAndExecuteTransaction`,\n * `bitcoin:sendTransfer`. */\n sendTransaction: boolean;\n /** Wallet advertises `solana:signIn`. No other namespace has an\n * equivalent, so every non-SVM caller passes `false`. */\n signIn: boolean;\n signMessage: boolean;\n /** Wallet advertises a sign-only feature: `solana:signTransaction`,\n * `sui:signTransaction`, `bitcoin:signPsbt`. */\n signTransaction: boolean;\n};\n\n/**\n * Wallet Standard has no balance/receipt RPC, no EIP-2255 equivalent, and\n * no silent account switch, so those flags are false everywhere.\n * `switchChain` re-routes locally, so it needs a second advertised chain.\n */\nconst buildWalletCapabilities = (profile: WalletCapabilityProfile): WalletCapabilities => ({\n getBalance: false,\n getTransactionReceipt: false,\n requestAccounts: false,\n sendTransaction: profile.sendTransaction,\n signIn: profile.signIn,\n signMessage: profile.signMessage,\n signTransaction: profile.signTransaction,\n subscribe: profile.events,\n switchAccount: false,\n switchChain: profile.chainCount > 1,\n});\n\nexport type { WalletCapabilityProfile };\nexport { buildWalletCapabilities };\n","import type { WalletAdapter } from \"@usebutr/core\";\nimport { logWarn } from \"@usebutr/core\";\nimport type { Wallets as ImportedWallets } from \"@wallet-standard/app\";\n\nimport type {\n WalletStandardAppModule,\n WalletStandardFeature,\n WalletStandardModuleLoader,\n WalletStandardWallet,\n} from \"./types\";\n\ntype ImportedWallet = ReturnType<ImportedWallets[\"get\"]>[number];\n\nconst toFeatureRecord = (features: ImportedWallet[\"features\"]) => {\n const record: Record<string, WalletStandardFeature> = {};\n for (const [name, value] of Object.entries(features)) {\n if (typeof value !== \"object\" || value === null) {\n continue;\n }\n // The feature object is stored as-is rather than spread: a spread would\n // detach its methods from the wallet's own object, breaking any\n // implementation whose `connect` / `signMessage` reads `this`.\n record[name] = value;\n }\n return record;\n};\n\nconst wrappers = new WeakMap<ImportedWallet, WalletStandardWallet>();\n\n/**\n * Reads through to the wallet: it re-points `accounts` on connect, so a copy\n * pins the pre-connect empty list and every `getAccount()` then resolves null.\n * Memoised because `unregister` is matched to `register` by object identity.\n */\nconst mapWallet = (wallet: ImportedWallet): WalletStandardWallet => {\n const cached = wrappers.get(wallet);\n if (cached !== undefined) {\n return cached;\n }\n const wrapper: WalletStandardWallet = {\n get accounts() {\n return wallet.accounts;\n },\n get chains() {\n return wallet.chains;\n },\n features: toFeatureRecord(wallet.features),\n icon: wallet.icon,\n name: wallet.name,\n version: wallet.version,\n };\n wrappers.set(wallet, wrapper);\n return wrapper;\n};\n\nlet warnedMissingApp = false;\n\n/**\n * Returns `null` when the wallet doesn't advertise this platform, so a\n * multi-chain wallet yields one adapter per platform. `registerDisconnector`\n * wires the synthetic `disconnected` fired on Wallet Standard `unregister`.\n */\ntype WalletStandardAdapterBuilder = (\n wallet: WalletStandardWallet,\n registerDisconnector: (emit: () => void) => void,\n) => WalletAdapter | null;\n\nconst loadWalletStandardModule = async (): Promise<WalletStandardAppModule> => {\n const imported = await import(\"@wallet-standard/app\");\n if (\n typeof imported !== \"object\" ||\n imported === null ||\n !(\"getWallets\" in imported) ||\n typeof imported.getWallets !== \"function\"\n ) {\n throw new Error(\"@wallet-standard/app has no getWallets export\");\n }\n const source = imported.getWallets();\n return {\n getWallets: () => ({\n get: () => source.get().map(mapWallet),\n on: (event, listener) =>\n source.on(event, (...wallets) => {\n listener(...wallets.map(mapWallet));\n }),\n }),\n };\n};\n\n/**\n * `@wallet-standard/app` is an optional peer dep: a failed import quietly\n * disables discovery. Disconnectors are keyed by wallet identity rather\n * than adapter id because `register` / `unregister` emit the same object.\n */\nconst discoverWalletStandard = (\n onAdapter: (adapter: WalletAdapter) => void,\n build: WalletStandardAdapterBuilder,\n loadModule: WalletStandardModuleLoader = loadWalletStandardModule,\n): (() => void) => {\n let cancelled = false;\n let internalUnsub: (() => void) | null = null;\n\n void (async () => {\n let mod: WalletStandardAppModule;\n try {\n mod = await loadModule();\n } catch (error) {\n if (!warnedMissingApp) {\n warnedMissingApp = true;\n logWarn(\n \"[butr] Wallet Standard wallet discovery is disabled: `@wallet-standard/app` could not be loaded. Install it (`npm i @wallet-standard/app`) to detect Wallet Standard wallets.\",\n error,\n );\n }\n return;\n }\n if (cancelled) {\n return;\n }\n\n const seenIds = new Set<string>();\n const disconnectors = new Map<WalletStandardWallet, () => void>();\n\n const tryAdd = (wallet: WalletStandardWallet) => {\n const adapter = build(wallet, (emit) => {\n disconnectors.set(wallet, emit);\n });\n if (!adapter || seenIds.has(adapter.id)) {\n return;\n }\n seenIds.add(adapter.id);\n onAdapter(adapter);\n };\n\n const app = mod.getWallets();\n for (const wallet of app.get()) {\n tryAdd(wallet);\n }\n\n const offRegister = app.on(\"register\", (...wallets) => {\n for (const wallet of wallets) {\n tryAdd(wallet);\n }\n });\n const offUnregister = app.on(\"unregister\", (...wallets) => {\n for (const wallet of wallets) {\n const emit = disconnectors.get(wallet);\n if (emit) {\n emit();\n disconnectors.delete(wallet);\n }\n }\n });\n internalUnsub = () => {\n offRegister();\n offUnregister();\n };\n })();\n\n return () => {\n cancelled = true;\n internalUnsub?.();\n internalUnsub = null;\n };\n};\n\nexport type { WalletStandardAdapterBuilder };\nexport { discoverWalletStandard, mapWallet };\n"],"mappings":";;;;;;;AAYA,MAAM,WAAW,gBAAwB,SAAyB;CAKhE,OAAO,mBAAmB,eAAe,GAJ5B,KACV,KAAK,CAAC,CACN,YAAY,CAAC,CACb,WAAW,6BAAa,GAAG,GACiB;AACjD;AAWA,MAAM,cACJ,QACA,MACA,cACkD;CAClD,MAAM,UAAU,OAAO,SAAS;CAChC,IAAI,YAAY,UAAa,CAAC,UAAU,OAAO,GAC7C;CAEF,OAAO;AACT;AAEA,MAAM,0BAA0B,UAC9B,cAAc,SACd,MAAM,QAAQ,MAAM,QAAQ,KAC5B,YAAY,SACZ,MAAM,QAAQ,MAAM,MAAM,KAC1B,cAAc,SACd,OAAO,MAAM,aAAa,YAC1B,MAAM,aAAa,QACnB,UAAU,SACV,OAAO,MAAM,SAAS,YACtB,UAAU,SACV,OAAO,MAAM,SAAS,YACtB,aAAa,SACb,OAAO,MAAM,YAAY;AAE3B,MAAM,oBAAoB,aAAwE;CAChG,MAAM,QAAQ,SAAS;CACvB,OAAO,UAAU,SAAY,OAAO,MAAM;AAC5C;;;;;;AAOA,MAAM,wBACJ,UACA,YAEA,SAAS,MAAM,MAAM,EAAE,YAAY,OAAO,KAAK,SAAS;;;;ACzD1D,MAAM,4BACJ,YAEA,aAAa,WAAW,OAAO,QAAQ,YAAY;AAErD,MAAM,+BACJ,YAEA,gBAAgB,WAAW,OAAO,QAAQ,eAAe;AAE3D,MAAM,2BACJ,YAEA,QAAQ,WAAW,OAAO,QAAQ,OAAO;;;;;;AA0D3C,MAAM,4BAA4B,EAChC,aACA,IACA,OACA,WACA,UACA,mBACA,sBACA,mBACA,aACwD;CACxD,MAAM,kBAAkB,IAAI,IAAI,iBAAiB;CACjD,MAAM,aAAa,WACjB,OAAO,MAAM,MAAM,gBAAgB,IAAI,CAAC,CAAC,KACzC,OAAO,MAAM,MAAM,EAAE,WAAW,WAAW,CAAC,KAC5C;CAEF,MAAM,iBAAiB,UAAU,OAAO,MAAM;CAC9C,IAAI,mBAAmB,MACrB,OAAO;CAET,MAAM,UAAU,WAAW,QAAQ,oBAAoB,wBAAwB;CAC/E,IAAI,YAAY,QACd,OAAO;CAGT,MAAM,aAAa,WAAW,QAAQ,uBAAuB,2BAA2B;CACxF,MAAM,SAAS,WAAW,QAAQ,mBAAmB,uBAAuB;CAE5E,IAAI,iBAAiB;CACrB,MAAM,iBAA4B;EAChC,IAAI;EACJ,MAAM,OAAO;EACb;EACA,WAAW,eAAe,MAAM,YAAY,MAAM;CACpD;CAEA,MAAM,4BAAY,IAAI,IAAqC;CAC3D,MAAM,6BAA6B;EACjC,IAAI,OAAO,SAAS,WAAW,GAC7B;EAEF,MAAM,QAAQ,QAAQ;EACtB,MAAM,QAAQ,OAAO,SAAS,KAAK,MAAMA,eAAa,EAAE,SAAS,KAAK,CAAC;EACvE,MAAM,QAAQ,MAAM;EACpB,IAAI,UAAU,QACZ;EAEF,KAAK,MAAM,YAAY,WACrB,SAAS;GAAE,SAAS;GAAO,UAAU;GAAO,MAAM;EAAiB,CAAC;CAExE;CAEA,6BAA6B;EAC3B,KAAK,MAAM,YAAY,WACrB,SAAS,EAAE,MAAM,eAAe,CAAC;CAErC,CAAC;CAED,OAAO;EACL,YAAY,OAAO,OAAO,QAAQ,MAAM,EAAE,WAAW,WAAW,CAAC,CAAC,CAAC;EAEnE,MAAM,QAAQ,MAAM;GAClB,MAAM,QAAQ,QAAQ,MAAM,WAAW,OAAO,EAAE,QAAQ,KAAK,IAAI,MAAS;EAC5E;EAEA,sBAAsB;EAEtB,MAAM,aAAa;GACjB,IAAI,eAAe,QACjB,IAAI;IACF,MAAM,WAAW,WAAW;GAC9B,SAAS,OAAO;IACd,QAAQ,UAAU,MAAM,qCAAqC,KAAK;GACpE;EAEJ;EAEA,kBAAkB;GAChB,MAAM,UAAU,iBAAiB,OAAO,QAAQ;GAChD,OAAO,QAAQ,QAAQ,YAAY,OAAO,OAAOA,eAAa,SAAS,QAAQ,CAAC,CAAC;EACnF;EAEA,mBAAmB;GACjB,MAAM,QAAQ,QAAQ;GACtB,OAAO,QAAQ,QAAQ,OAAO,SAAS,KAAK,MAAMA,eAAa,EAAE,SAAS,KAAK,CAAC,CAAC;EACnF;EAEA,iBAAiB,QAAQ,QAAQ,MAAM;EAEvC,WAAW,QAAQ,MAAM;EAEzB,MAAM,aAAa,OAAO,IAAI;EAC9B;EACA,MAAM,OAAO;EAEb,iBAAiB,YAAY;GAC3B,MAAM,YAAY,UACd,qBAAqB,OAAO,UAAU,QAAQ,aAAa,IAC3D,OAAO,SAAS;GACpB,IAAI,cAAc,QAChB,MAAM,IAAI,MAAM,sBAAsB;GAExC,OAAO;EACT;EAEA,UAAU,UAAU;GAClB,UAAU,IAAI,QAAQ;GACtB,IAAI,cAAmC;GACvC,IAAI,WAAW,QAAW;IACxB,MAAM,QAAQ,OAAO,GAAG,WAAW,YAAY;KAC7C,IAAI,qBAAqB,QAAQ,WAAW,QAAW;MACrD,MAAM,OAAO,UAAU,QAAQ,MAAM;MACrC,IAAI,SAAS,MACX,iBAAiB;KAErB;KAEA,IAAI,QAAQ,aAAa,QAAW;MAClC,IAAI,QAAQ,SAAS,WAAW,GAAG;OACjC,SAAS,EAAE,MAAM,eAAe,CAAC;OACjC;MACF;MACA,MAAM,QAAQ,QAAQ;MACtB,MAAM,QAAQ,QAAQ,SAAS,KAAK,MAAMA,eAAa,EAAE,SAAS,KAAK,CAAC;MACxE,MAAM,QAAQ,MAAM;MACpB,IAAI,UAAU,QACZ;MAEF,SAAS;OAAE,SAAS;OAAO,UAAU;OAAO,MAAM;MAAiB,CAAC;MACpE;KACF;KAEA,IAAI,qBAAqB,QAAQ,WAAW,QAC1C,qBAAqB;IAEzB,CAAC;IACD,oBAAoB;KAClB,MAAM;IACR;GACF;GACA,aAAa;IACX,UAAU,OAAO,QAAQ;IACzB,cAAc;GAChB;EACF;EAKA,aAAa,OAAO,UAAU;GAC5B,IAAI,MAAM,cAAc,WACtB,MAAM,IAAI,MACR,GAAG,MAAM,wBAAwB,SAAS,UAAU,MAAM,GAAG,kCAAkC,UAAU,GAC3G;GAEF,IAAI,CAAC,OAAO,OAAO,SAAS,MAAM,EAAE,GAClC,MAAM,IAAI,MACR,UAAU,OAAO,KAAK,6BAA6B,MAAM,GAAG,gBAAgB,OAAO,OAAO,KAAK,IAAI,GACrG;GAEF,iBAAiB,MAAM;GACvB,qBAAqB;GACrB,MAAM,QAAQ,QAAQ;EACxB;EAEA;CACF;AACF;;;;;;;;;ACnOA,MAAM,2BAA2B,aAA0D;CACzF,YAAY;CACZ,uBAAuB;CACvB,iBAAiB;CACjB,iBAAiB,QAAQ;CACzB,QAAQ,QAAQ;CAChB,aAAa,QAAQ;CACrB,iBAAiB,QAAQ;CACzB,WAAW,QAAQ;CACnB,eAAe;CACf,aAAa,QAAQ,aAAa;AACpC;;;;ACvBA,MAAM,mBAAmB,aAAyC;CAChE,MAAM,SAAgD,CAAC;CACvD,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,GAAG;EACpD,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC;EAKF,OAAO,QAAQ;CACjB;CACA,OAAO;AACT;AAEA,MAAM,2BAAW,IAAI,QAA8C;;;;;;AAOnE,MAAM,aAAa,WAAiD;CAClE,MAAM,SAAS,SAAS,IAAI,MAAM;CAClC,IAAI,WAAW,QACb,OAAO;CAET,MAAM,UAAgC;EACpC,IAAI,WAAW;GACb,OAAO,OAAO;EAChB;EACA,IAAI,SAAS;GACX,OAAO,OAAO;EAChB;EACA,UAAU,gBAAgB,OAAO,QAAQ;EACzC,MAAM,OAAO;EACb,MAAM,OAAO;EACb,SAAS,OAAO;CAClB;CACA,SAAS,IAAI,QAAQ,OAAO;CAC5B,OAAO;AACT;AAEA,IAAI,mBAAmB;AAYvB,MAAM,2BAA2B,YAA8C;CAC7E,MAAM,WAAW,MAAM,OAAO;CAC9B,IACE,OAAO,aAAa,YACpB,aAAa,QACb,EAAE,gBAAgB,aAClB,OAAO,SAAS,eAAe,YAE/B,MAAM,IAAI,MAAM,+CAA+C;CAEjE,MAAM,SAAS,SAAS,WAAW;CACnC,OAAO,EACL,mBAAmB;EACjB,WAAW,OAAO,IAAI,CAAC,CAAC,IAAI,SAAS;EACrC,KAAK,OAAO,aACV,OAAO,GAAG,QAAQ,GAAG,YAAY;GAC/B,SAAS,GAAG,QAAQ,IAAI,SAAS,CAAC;EACpC,CAAC;CACL,GACF;AACF;;;;;;AAOA,MAAM,0BACJ,WACA,OACA,aAAyC,6BACxB;CACjB,IAAI,YAAY;CAChB,IAAI,gBAAqC;CAEzC,CAAM,YAAY;EAChB,IAAI;EACJ,IAAI;GACF,MAAM,MAAM,WAAW;EACzB,SAAS,OAAO;GACd,IAAI,CAAC,kBAAkB;IACrB,mBAAmB;IACnB,QACE,iLACA,KACF;GACF;GACA;EACF;EACA,IAAI,WACF;EAGF,MAAM,0BAAU,IAAI,IAAY;EAChC,MAAM,gCAAgB,IAAI,IAAsC;EAEhE,MAAM,UAAU,WAAiC;GAC/C,MAAM,UAAU,MAAM,SAAS,SAAS;IACtC,cAAc,IAAI,QAAQ,IAAI;GAChC,CAAC;GACD,IAAI,CAAC,WAAW,QAAQ,IAAI,QAAQ,EAAE,GACpC;GAEF,QAAQ,IAAI,QAAQ,EAAE;GACtB,UAAU,OAAO;EACnB;EAEA,MAAM,MAAM,IAAI,WAAW;EAC3B,KAAK,MAAM,UAAU,IAAI,IAAI,GAC3B,OAAO,MAAM;EAGf,MAAM,cAAc,IAAI,GAAG,aAAa,GAAG,YAAY;GACrD,KAAK,MAAM,UAAU,SACnB,OAAO,MAAM;EAEjB,CAAC;EACD,MAAM,gBAAgB,IAAI,GAAG,eAAe,GAAG,YAAY;GACzD,KAAK,MAAM,UAAU,SAAS;IAC5B,MAAM,OAAO,cAAc,IAAI,MAAM;IACrC,IAAI,MAAM;KACR,KAAK;KACL,cAAc,OAAO,MAAM;IAC7B;GACF;EACF,CAAC;EACD,sBAAsB;GACpB,YAAY;GACZ,cAAc;EAChB;CACF,EAAC,CAAE;CAEH,aAAa;EACX,YAAY;EACZ,gBAAgB;EAChB,gBAAgB;CAClB;AACF"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/primitives.ts","../src/adapter-core.ts","../src/discovery.ts"],"sourcesContent":["import type {\n WalletStandardFeature,\n WalletStandardWallet,\n WalletStandardWalletAccount,\n} from \"./types\";\n\n/**\n * The platform prefix scopes the id so one multi-chain wallet (Phantom\n * SVM, Sui, BTC) yields distinct adapters on the shared `getWallets()` bus.\n */\nconst slugify = (platformPrefix: string, name: string): string => {\n const slug = name\n .trim()\n .toLowerCase()\n .replaceAll(/[^a-z0-9]+/gv, \"-\");\n return `wallet-standard:${platformPrefix}-${slug}`;\n};\n\nconst hasMethod = <Feature extends object>(\n feature: WalletStandardFeature,\n method: keyof Feature & string,\n): feature is WalletStandardFeature & Feature => typeof feature[method] === \"function\";\n\n/**\n * The spec keys `features` by reverse-DNS name with no fixed shape, so the\n * caller names the feature type and the method that proves it:\n * `getFeature<SolanaSignMessageFeature>(wallet, \"solana:signMessage\", \"signMessage\")`.\n */\nconst getFeature = <Feature extends object>(\n wallet: WalletStandardWallet,\n name: string,\n method: keyof Feature & string,\n): (WalletStandardFeature & Feature) | undefined => {\n const feature = wallet.features[name];\n return feature !== undefined && hasMethod<Feature>(feature, method) ? feature : undefined;\n};\n\n/** Wallet Standard account for a butr address. `undefined` for an address\n * the wallet does not expose: never another account in its place. */\nconst findAccount = (\n accounts: ReadonlyArray<WalletStandardWalletAccount>,\n address: string,\n): WalletStandardWalletAccount | undefined => accounts.find((a) => a.address === address);\n\nexport { findAccount, getFeature, slugify };\n","import type { Account, ChainBase, Connector, ConnectorEvent, WalletSigner } from \"@usebutr/core\";\nimport { buildAccount, logWarn, resolveChain, sanitizeIcon } from \"@usebutr/core\";\n\nimport { findAccount, getFeature } from \"./primitives\";\nimport type {\n StandardConnectFeature,\n StandardDisconnectFeature,\n StandardEventsFeature,\n WalletStandardWallet,\n WalletStandardWalletAccount,\n} from \"./types\";\n\ntype WalletStandardCoreInput = {\n /** The platform's chain registry, for chain names. A chain outside it is\n * named by its CAIP-2 id. */\n chains: ReadonlyArray<ChainBase>;\n /** Stable adapter id. Each platform package owns its own slug prefix. */\n id: string;\n /** How the platform reads in errors: `Solana`, `Sui`, `Bitcoin`. */\n label: string;\n /** CAIP-2 namespace without its colon: `solana`, `sui`, `bip122`. */\n namespace: string;\n /** Chain ids worth preferring over the first advertised one. Matched in\n * the wallet's own order, so a wallet listing `mainnet-beta` before\n * `mainnet` keeps the chain it advertised first. */\n preferredChainIds: ReadonlyArray<string>;\n /** Called with a function that pushes a synthetic `disconnected` event to\n * all current subscribers. Discovery invokes it on `unregister`. */\n registerDisconnector?: (emit: () => void) => void;\n /** Re-point the active chain when the wallet reports a `chains` change.\n * Polkadot opts out: its wallets advertise a single relay chain. */\n trackChainChanges: boolean;\n wallet: WalletStandardWallet;\n};\n\n/** The members every Wallet Standard adapter shares, ready to spread. */\ntype WalletStandardConnector = Pick<\n Connector,\n \"connect\" | \"disconnect\" | \"getAccounts\" | \"icon\" | \"id\" | \"name\" | \"subscribe\"\n> & {\n getSigner: () => Promise<WalletSigner>;\n switchChain?: (chain: ChainBase) => Promise<void>;\n};\n\ntype WalletStandardCore = {\n /** Spread into the adapter. `disconnect`, `subscribe` and `switchChain` are\n * present only when the wallet supports them. */\n base: WalletStandardConnector;\n /** The active chain, for results that mint their own `Account`. */\n currentChain: () => ChainBase;\n /** The Wallet Standard account a call signs with: the one matching\n * `account`, else the active one. Throws for an account the wallet does\n * not expose. */\n resolveAccount: (account?: Account) => WalletStandardWalletAccount;\n /** The chain id a call targets: `chain` when given, else the current one.\n * Throws for a chain outside the namespace or not advertised. */\n resolveChainId: (chain?: ChainBase) => string;\n};\n\n/**\n * `null` when the wallet advertises no chain in `namespace` or lacks\n * `standard:connect`, so a multi-chain wallet yields one adapter per\n * platform it actually speaks.\n */\nconst createWalletStandardCore = ({\n chains,\n id,\n label,\n namespace,\n preferredChainIds,\n registerDisconnector,\n trackChainChanges,\n wallet,\n}: WalletStandardCoreInput): WalletStandardCore | null => {\n const prefix = `${namespace}:`;\n const preferred = new Set(preferredChainIds);\n const pickChain = (advertised: ReadonlyArray<string>): string | undefined =>\n advertised.find((c) => preferred.has(c)) ?? advertised.find((c) => c.startsWith(prefix));\n\n const initialChainId = pickChain(wallet.chains);\n const connect = getFeature<StandardConnectFeature>(wallet, \"standard:connect\", \"connect\");\n if (initialChainId === undefined || connect === undefined) {\n return null;\n }\n const disconnect = getFeature<StandardDisconnectFeature>(\n wallet,\n \"standard:disconnect\",\n \"disconnect\",\n );\n const events = getFeature<StandardEventsFeature>(wallet, \"standard:events\", \"on\");\n\n let currentChainId = initialChainId;\n const currentChain = () => resolveChain(currentChainId, chains);\n const toAccounts = (accounts: ReadonlyArray<WalletStandardWalletAccount>) => {\n const chain = currentChain();\n return accounts.map((a) => buildAccount(a.address, chain));\n };\n\n const listeners = new Set<(event: ConnectorEvent) => void>();\n const emit = (event: ConnectorEvent) => {\n for (const listener of listeners) {\n listener(event);\n }\n };\n const toEvent = (accounts: ReadonlyArray<WalletStandardWalletAccount>): ConnectorEvent =>\n accounts.length === 0\n ? { type: \"disconnected\" }\n : { accounts: toAccounts(accounts), type: \"accountsChanged\" };\n registerDisconnector?.(() => {\n emit({ type: \"disconnected\" });\n });\n\n const resolveChainId = (chain?: ChainBase): string => {\n if (chain === undefined) {\n return currentChainId;\n }\n if (chain.namespace !== namespace) {\n throw new Error(\n `${label} adapter received non-${label} chain \"${chain.id}\". Pass a chain with namespace \"${namespace}\".`,\n );\n }\n if (!wallet.chains.includes(chain.id)) {\n throw new Error(\n `Wallet ${wallet.name} does not advertise chain \"${chain.id}\". Available: ${wallet.chains.join(\", \")}`,\n );\n }\n return chain.id;\n };\n\n const subscribe = (listener: (event: ConnectorEvent) => void) => {\n listeners.add(listener);\n // Each subscriber owns a wallet listener, so it delivers to that\n // subscriber alone: broadcasting would repeat a change per subscriber.\n const off = events?.on(\"change\", (changes) => {\n const next = trackChainChanges && changes.chains ? pickChain(changes.chains) : undefined;\n if (next !== undefined) {\n currentChainId = next;\n }\n if (changes.accounts !== undefined) {\n listener(toEvent(changes.accounts));\n } else if (next !== undefined) {\n listener(toEvent(wallet.accounts));\n }\n });\n return () => {\n listeners.delete(listener);\n off?.();\n };\n };\n\n const advertisedInNamespace = wallet.chains.filter((c) => c.startsWith(prefix));\n\n const base: WalletStandardConnector = {\n async connect(options) {\n await connect.connect(options?.silent === true ? { silent: true } : undefined);\n },\n getAccounts: () => Promise.resolve(toAccounts(wallet.accounts)),\n getSigner: () => Promise.resolve({ kind: \"wallet-standard\", wallet }),\n icon: sanitizeIcon(wallet.icon),\n id,\n name: wallet.name,\n ...(disconnect !== undefined && {\n async disconnect() {\n try {\n await disconnect.disconnect();\n } catch (error) {\n logWarn(`[butr] ${label} Wallet Standard disconnect threw:`, error);\n }\n },\n }),\n // Discovery wires `unregister` through the same listeners, so a wallet\n // without `standard:events` can still report its removal.\n ...((events !== undefined || registerDisconnector !== undefined) && { subscribe }),\n // Wallet Standard has no switch-network call: this re-points butr's view\n // (and every later call's `chain` input), which only means something\n // when the wallet advertises more than one chain.\n ...(advertisedInNamespace.length > 1 && {\n switchChain: async (chain: ChainBase) => {\n currentChainId = resolveChainId(chain);\n emit(toEvent(wallet.accounts));\n await Promise.resolve();\n },\n }),\n };\n\n return {\n base,\n currentChain,\n resolveAccount: (account) => {\n const match =\n account === undefined\n ? wallet.accounts[0]\n : findAccount(wallet.accounts, account.walletAddress);\n if (match === undefined) {\n throw new Error(\n account === undefined\n ? `Wallet ${wallet.name} has no connected account`\n : `Wallet ${wallet.name} does not expose account ${account.walletAddress}`,\n );\n }\n return match;\n },\n resolveChainId,\n };\n};\n\nexport type { WalletStandardConnector, WalletStandardCore, WalletStandardCoreInput };\nexport { createWalletStandardCore };\n","import type { WalletAdapter } from \"@usebutr/core\";\nimport { logWarn } from \"@usebutr/core\";\nimport type { Wallets as ImportedWallets } from \"@wallet-standard/app\";\n\nimport type {\n WalletStandardAppModule,\n WalletStandardFeature,\n WalletStandardModuleLoader,\n WalletStandardWallet,\n} from \"./types\";\n\ntype ImportedWallet = ReturnType<ImportedWallets[\"get\"]>[number];\n\n// Any object satisfies the open-ended feature shape; `getFeature` checks the\n// member it needs before a caller relies on it.\nconst isFeatureObject = (value: unknown): value is WalletStandardFeature =>\n typeof value === \"object\" && value !== null;\n\nconst toFeatureRecord = (features: ImportedWallet[\"features\"]) => {\n const record: Partial<Record<string, WalletStandardFeature>> = {};\n for (const [name, value] of Object.entries(features)) {\n // Stored as-is rather than spread: a spread would detach its methods from\n // the wallet's own object, breaking any implementation that reads `this`.\n if (isFeatureObject(value)) {\n record[name] = value;\n }\n }\n return record;\n};\n\nconst wrappers = new WeakMap<ImportedWallet, WalletStandardWallet>();\n\n/**\n * Reads through to the wallet: it re-points `accounts` on connect, so a copy\n * pins the pre-connect empty list and every `getAccounts()` then resolves [].\n * Memoised because `unregister` is matched to `register` by object identity.\n */\nconst mapWallet = (wallet: ImportedWallet): WalletStandardWallet => {\n const cached = wrappers.get(wallet);\n if (cached !== undefined) {\n return cached;\n }\n const wrapper: WalletStandardWallet = {\n get accounts() {\n return wallet.accounts;\n },\n get chains() {\n return wallet.chains;\n },\n features: toFeatureRecord(wallet.features),\n icon: wallet.icon,\n name: wallet.name,\n version: wallet.version,\n };\n wrappers.set(wallet, wrapper);\n return wrapper;\n};\n\nlet warnedMissingApp = false;\n\n/**\n * Returns `null` when the wallet doesn't advertise this platform, so a\n * multi-chain wallet yields one adapter per platform. `registerDisconnector`\n * wires the synthetic `disconnected` fired on Wallet Standard `unregister`.\n */\ntype WalletStandardAdapterBuilder = (\n wallet: WalletStandardWallet,\n registerDisconnector: (emit: () => void) => void,\n) => WalletAdapter | null;\n\nconst loadWalletStandardModule = async (): Promise<WalletStandardAppModule> => {\n const imported = await import(\"@wallet-standard/app\");\n if (!(\"getWallets\" in imported) || typeof imported.getWallets !== \"function\") {\n throw new Error(\"@wallet-standard/app has no getWallets export\");\n }\n const source = imported.getWallets();\n return {\n getWallets: () => ({\n get: () => source.get().map(mapWallet),\n on: (event, listener) =>\n source.on(event, (...wallets) => {\n listener(...wallets.map(mapWallet));\n }),\n }),\n };\n};\n\n/**\n * `@wallet-standard/app` is an optional peer dep: a failed import quietly\n * disables discovery. Disconnectors are keyed by wallet identity rather\n * than adapter id because `register` / `unregister` emit the same object.\n */\nconst discoverWalletStandard = (\n onAdapter: (adapter: WalletAdapter) => void,\n build: WalletStandardAdapterBuilder,\n loadModule: WalletStandardModuleLoader = loadWalletStandardModule,\n): (() => void) => {\n // SAFETY: the effect cleanup sets this after the await; TypeScript cannot see writes from closures\n let cancelled = false as boolean;\n let internalUnsub: (() => void) | null = null;\n\n void (async () => {\n let mod: WalletStandardAppModule;\n try {\n mod = await loadModule();\n } catch (error) {\n if (!warnedMissingApp) {\n warnedMissingApp = true;\n logWarn(\n \"[butr] Wallet Standard wallet discovery is disabled: `@wallet-standard/app` could not be loaded. Install it (`npm i @wallet-standard/app`) to detect Wallet Standard wallets.\",\n error,\n );\n }\n return;\n }\n if (cancelled) {\n return;\n }\n\n const seenIds = new Set<string>();\n const disconnectors = new Map<WalletStandardWallet, () => void>();\n\n const tryAdd = (wallet: WalletStandardWallet) => {\n const adapter = build(wallet, (emit) => {\n disconnectors.set(wallet, emit);\n });\n if (!adapter || seenIds.has(adapter.id)) {\n return;\n }\n seenIds.add(adapter.id);\n onAdapter(adapter);\n };\n\n const app = mod.getWallets();\n for (const wallet of app.get()) {\n tryAdd(wallet);\n }\n\n const offRegister = app.on(\"register\", (...wallets) => {\n for (const wallet of wallets) {\n tryAdd(wallet);\n }\n });\n const offUnregister = app.on(\"unregister\", (...wallets) => {\n for (const wallet of wallets) {\n const emit = disconnectors.get(wallet);\n if (emit) {\n emit();\n disconnectors.delete(wallet);\n }\n }\n });\n internalUnsub = () => {\n offRegister();\n offUnregister();\n };\n })();\n\n return () => {\n cancelled = true;\n internalUnsub?.();\n internalUnsub = null;\n };\n};\n\nexport type { WalletStandardAdapterBuilder };\nexport { discoverWalletStandard, mapWallet };\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebutr/wallet-standard-shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Shared Wallet Standard discovery primitives used by @usebutr/svm, @usebutr/sui, @usebutr/bitcoin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"butr",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@usebutr/core": "
|
|
43
|
+
"@usebutr/core": "3.0.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@wallet-standard/app": "^1.1.1",
|
|
47
47
|
"typescript": "^7.0.2",
|
|
48
|
-
"vitest": "^
|
|
48
|
+
"vitest": "^5.0.1",
|
|
49
49
|
"@repo/config-vitest": "0.0.0",
|
|
50
50
|
"@repo/typescript-config": "0.0.0"
|
|
51
51
|
},
|