@usebutr/wallet-standard-shared 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +41 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Account, ChainBase, ConnectorEvent, WalletAdapter, buildAccount } from "@usebutr/core";
|
|
1
|
+
import { Account, ChainBase, ConnectorEvent, WalletAdapter, WalletCapabilities, buildAccount } from "@usebutr/core";
|
|
2
2
|
//#region src/types.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Minimal type surface for the Wallet Standard protocol that every
|
|
@@ -164,6 +164,45 @@ type WalletStandardCore = {
|
|
|
164
164
|
*/
|
|
165
165
|
declare const createWalletStandardCore: ({ chainPrefix, id, label, namespace, platform, preferredChainIds, registerDisconnector, trackChainChanges, wallet }: WalletStandardCoreInput) => WalletStandardCore | null;
|
|
166
166
|
//#endregion
|
|
167
|
+
//#region src/capabilities.d.ts
|
|
168
|
+
type WalletCapabilityProfile = {
|
|
169
|
+
/** Chains the wallet advertises in this namespace. */
|
|
170
|
+
chainCount: number;
|
|
171
|
+
/** Wallet advertises `standard:events`. */
|
|
172
|
+
events: boolean;
|
|
173
|
+
/** Wallet advertises a sign-and-broadcast feature:
|
|
174
|
+
* `solana:signAndSendTransaction`, `sui:signAndExecuteTransaction`,
|
|
175
|
+
* `bitcoin:sendTransfer`. */
|
|
176
|
+
sendTransaction: boolean;
|
|
177
|
+
/** Wallet advertises `solana:signIn`. No other namespace has an
|
|
178
|
+
* equivalent, so every non-SVM caller passes `false`. */
|
|
179
|
+
signIn: boolean;
|
|
180
|
+
signMessage: boolean;
|
|
181
|
+
/** Wallet advertises a sign-only feature: `solana:signTransaction`,
|
|
182
|
+
* `sui:signTransaction`, `bitcoin:signPsbt`. */
|
|
183
|
+
signTransaction: boolean;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Wallet Standard → butr capability mapping, shared by every platform
|
|
187
|
+
* package. The flags that are constant across all of them:
|
|
188
|
+
*
|
|
189
|
+
* - `getBalance` / `getTransactionReceipt`: Wallet Standard exposes no
|
|
190
|
+
* balance/receipt RPC, and butr ships no RPC client of its own.
|
|
191
|
+
* - `requestAccounts`: no programmatic equivalent of EIP-2255; re-running
|
|
192
|
+
* `standard:connect` doesn't produce new accounts on any major Wallet
|
|
193
|
+
* Standard wallet in practice (Phantom, MetaMask Snap, Solflare,
|
|
194
|
+
* Backpack, Xverse, Talisman).
|
|
195
|
+
* - `switchAccount`: no protocol-level silent account switch; the user
|
|
196
|
+
* picks the account inside the wallet.
|
|
197
|
+
* - `switchChain`: true when the wallet advertises more than one chain in
|
|
198
|
+
* this namespace. The switch is local re-routing (the per-call `chain`
|
|
199
|
+
* input), so with a single chain there's nothing to switch to.
|
|
200
|
+
*
|
|
201
|
+
* Each platform package owns only the feature-name → flag mapping, since
|
|
202
|
+
* the feature names differ per namespace.
|
|
203
|
+
*/
|
|
204
|
+
declare const buildWalletCapabilities: (profile: WalletCapabilityProfile) => WalletCapabilities;
|
|
205
|
+
//#endregion
|
|
167
206
|
//#region src/discovery.d.ts
|
|
168
207
|
/**
|
|
169
208
|
* Callback supplied by per-platform discovery to build a `WalletAdapter`
|
|
@@ -207,5 +246,5 @@ type WalletStandardAdapterBuilder = (wallet: WalletStandardWallet, registerDisco
|
|
|
207
246
|
*/
|
|
208
247
|
declare const discoverWalletStandard: (onAdapter: (adapter: WalletAdapter) => void, build: WalletStandardAdapterBuilder) => (() => void);
|
|
209
248
|
//#endregion
|
|
210
|
-
export { type StandardConnectFeature, type StandardDisconnectFeature, type StandardEventsFeature, type StandardEventsListener, type WalletStandardAdapterBuilder, type WalletStandardAppModule, type WalletStandardCore, type WalletStandardWallet, type WalletStandardWalletAccount, type WalletsApp, buildAccount, createWalletStandardCore, discoverWalletStandard, getFeature, pickAccountByAddress, pickFirstAddress, slugify };
|
|
249
|
+
export { type StandardConnectFeature, type StandardDisconnectFeature, type StandardEventsFeature, type StandardEventsListener, type WalletCapabilityProfile, type WalletStandardAdapterBuilder, type WalletStandardAppModule, type WalletStandardCore, type WalletStandardWallet, type WalletStandardWalletAccount, type WalletsApp, buildAccount, buildWalletCapabilities, createWalletStandardCore, discoverWalletStandard, getFeature, pickAccountByAddress, pickFirstAddress, slugify };
|
|
211
250
|
//# 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/discovery.ts"],"mappings":";;;;;;;;;;;;;;;;KAeK;EACH;EACA,QAAQ;EACR,UAAU;;EAEV,YAAY;;KAGT;EACH,UAAU,cAAc;EACxB,QAAQ;;;EAGR,UAAU,SAAS;EACnB;EACA;EACA;;KAGG;EACH,WAAW,cAAc;EACzB,KACE,kCACA,cAAc,SAAS,cAAc;;KAIpC;EACH,kBAAkB;;KAGf;EACH,UAAU;IAAU;QAAuB;IACzC,UAAU,cAAc;;;KAIvB;EACH,kBAAkB;;KAGf,0BAA0B;EAC7B,WAAW,cAAc;EACzB,SAAS;EACT,WAAW;;KAGR;EACH,KAAK,iBAAiB,UAAU;;;;;;;;;;;;;;;cClD5B,UAAW,wBAAwB;;;;;;;;;;cAkBnC,aAAc,GAAG,QAAQ,sBAAsB,iBAAe;;;;;cAa9D,mBAAoB,UAAU,cAAc;;;;;;;;;cAa5C,uBACJ,UAAU,cAAc,8BACxB,oBACC;;;KChDE;;EAEH;;EAEA;;EAEA;;EAEA;;EAEA;;;;EAIA,mBAAmB;;;;EAInB,wBAAwB;;;;EAIxB;EACA,QAAQ;;KAGL;;EAEH;EACA,UAAU;IAAS;QAAuB;;EAE1C;EACA,kBAAkB;EAClB,kBAAkB,QAAQ;EAC1B,mBAAmB,QAAQ,MAAM;EACjC,iBAAiB;;EAEjB;EACA;EACA;EACA;;;EAGA,iBAAiB;IAAY;QAA4B;EACzD,YAAY,WAAW,OAAO;EAC9B,cAAc,OAAO,cAAc;;;EAGnC,eAAe;;;;;;;;;;;;;;;cAgBX,6BAA4B,aAAA,IAAA,OAAA,WAAA,UAAA,mBAAA,sBAAA,mBAAA,UAU/B,4BAA0B;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/primitives.ts","../src/adapter-core.ts","../src/capabilities.ts","../src/discovery.ts"],"mappings":";;;;;;;;;;;;;;;;KAeK;EACH;EACA,QAAQ;EACR,UAAU;;EAEV,YAAY;;KAGT;EACH,UAAU,cAAc;EACxB,QAAQ;;;EAGR,UAAU,SAAS;EACnB;EACA;EACA;;KAGG;EACH,WAAW,cAAc;EACzB,KACE,kCACA,cAAc,SAAS,cAAc;;KAIpC;EACH,kBAAkB;;KAGf;EACH,UAAU;IAAU;QAAuB;IACzC,UAAU,cAAc;;;KAIvB;EACH,kBAAkB;;KAGf,0BAA0B;EAC7B,WAAW,cAAc;EACzB,SAAS;EACT,WAAW;;KAGR;EACH,KAAK,iBAAiB,UAAU;;;;;;;;;;;;;;;cClD5B,UAAW,wBAAwB;;;;;;;;;;cAkBnC,aAAc,GAAG,QAAQ,sBAAsB,iBAAe;;;;;cAa9D,mBAAoB,UAAU,cAAc;;;;;;;;;cAa5C,uBACJ,UAAU,cAAc,8BACxB,oBACC;;;KChDE;;EAEH;;EAEA;;EAEA;;EAEA;;EAEA;;;;EAIA,mBAAmB;;;;EAInB,wBAAwB;;;;EAIxB;EACA,QAAQ;;KAGL;;EAEH;EACA,UAAU;IAAS;QAAuB;;EAE1C;EACA,kBAAkB;EAClB,kBAAkB,QAAQ;EAC1B,mBAAmB,QAAQ,MAAM;EACjC,iBAAiB;;EAEjB;EACA;EACA;EACA;;;EAGA,iBAAiB;IAAY;QAA4B;EACzD,YAAY,WAAW,OAAO;EAC9B,cAAc,OAAO,cAAc;;;EAGnC,eAAe;;;;;;;;;;;;;;;cAgBX,6BAA4B,aAAA,IAAA,OAAA,WAAA,UAAA,mBAAA,sBAAA,mBAAA,UAU/B,4BAA0B;;;KCpFxB;;EAEH;;EAEA;;;;EAIA;;;EAGA;EACA;;;EAGA;;;;;;;;;;;;;;;;;;;;;cAsBI,0BAA2B,SAAS,4BAA0B;;;;;;;;;;;;;;;;;KClB/D,gCACH,QAAQ,sBACR,uBAAuB,8BACpB;;;;;;;;;;;;;;;;;;;;;;;;;;cA2BC,yBACJ,YAAY,SAAS,wBACrB,OAAO"}
|
package/dist/index.js
CHANGED
|
@@ -171,6 +171,40 @@ const createWalletStandardCore = ({ chainPrefix, id, label, namespace, platform,
|
|
|
171
171
|
};
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/capabilities.ts
|
|
176
|
+
/**
|
|
177
|
+
* Wallet Standard → butr capability mapping, shared by every platform
|
|
178
|
+
* package. The flags that are constant across all of them:
|
|
179
|
+
*
|
|
180
|
+
* - `getBalance` / `getTransactionReceipt`: Wallet Standard exposes no
|
|
181
|
+
* balance/receipt RPC, and butr ships no RPC client of its own.
|
|
182
|
+
* - `requestAccounts`: no programmatic equivalent of EIP-2255; re-running
|
|
183
|
+
* `standard:connect` doesn't produce new accounts on any major Wallet
|
|
184
|
+
* Standard wallet in practice (Phantom, MetaMask Snap, Solflare,
|
|
185
|
+
* Backpack, Xverse, Talisman).
|
|
186
|
+
* - `switchAccount`: no protocol-level silent account switch; the user
|
|
187
|
+
* picks the account inside the wallet.
|
|
188
|
+
* - `switchChain`: true when the wallet advertises more than one chain in
|
|
189
|
+
* this namespace. The switch is local re-routing (the per-call `chain`
|
|
190
|
+
* input), so with a single chain there's nothing to switch to.
|
|
191
|
+
*
|
|
192
|
+
* Each platform package owns only the feature-name → flag mapping, since
|
|
193
|
+
* the feature names differ per namespace.
|
|
194
|
+
*/
|
|
195
|
+
const buildWalletCapabilities = (profile) => ({
|
|
196
|
+
getBalance: false,
|
|
197
|
+
getTransactionReceipt: false,
|
|
198
|
+
requestAccounts: false,
|
|
199
|
+
sendTransaction: profile.sendTransaction,
|
|
200
|
+
signIn: profile.signIn,
|
|
201
|
+
signMessage: profile.signMessage,
|
|
202
|
+
signTransaction: profile.signTransaction,
|
|
203
|
+
subscribe: profile.events,
|
|
204
|
+
switchAccount: false,
|
|
205
|
+
switchChain: profile.chainCount > 1
|
|
206
|
+
});
|
|
207
|
+
|
|
174
208
|
//#endregion
|
|
175
209
|
//#region src/discovery.ts
|
|
176
210
|
let warnedMissingApp = false;
|
|
@@ -253,5 +287,5 @@ const discoverWalletStandard = (onAdapter, build) => {
|
|
|
253
287
|
};
|
|
254
288
|
|
|
255
289
|
//#endregion
|
|
256
|
-
export { buildAccount, createWalletStandardCore, discoverWalletStandard, getFeature, pickAccountByAddress, pickFirstAddress, slugify };
|
|
290
|
+
export { buildAccount, buildWalletCapabilities, createWalletStandardCore, discoverWalletStandard, getFeature, pickAccountByAddress, pickFirstAddress, slugify };
|
|
257
291
|
//# 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/discovery.ts"],"sourcesContent":["import type { WalletStandardWallet, WalletStandardWalletAccount } from \"./types\";\n\n/**\n * Convert a wallet name into a stable kebab-case adapter id, scoped by\n * a platform prefix so adapters from different platforms (Phantom SVM,\n * Phantom Sui, Phantom BTC) coexist in the pool without colliding on\n * the shared `getWallets()` bus.\n *\n * @example\n * slugify(\"svm\", \"Phantom\") // \"wallet-standard:svm-phantom\"\n * slugify(\"sui\", \"Sui Wallet\") // \"wallet-standard:sui-sui-wallet\"\n * slugify(\"btc\", \"Phantom\") // \"wallet-standard:btc-phantom\"\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 * Read a Wallet Standard feature off a wallet, returning `undefined`\n * when absent. `features` is a reverse-DNS-keyed registry typed\n * `Record<string, unknown>` by the spec, so the caller declares the\n * concrete feature shape it expects; this accessor is the single\n * boundary where that dynamism is acknowledged. The `T` parameter is\n * intentionally caller-supplied (not inferable) and the assertion is\n * unavoidable for an untyped registry.\n */\n// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- caller-supplied feature shape; not inferable from args\nconst getFeature = <T>(wallet: WalletStandardWallet, name: string): T | undefined => {\n const feature: unknown = wallet.features[name];\n if (typeof feature !== \"object\" || feature === null) {\n return undefined;\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- untyped Wallet Standard feature registry; caller declares the shape\n return feature as T;\n};\n\n/**\n * Pick the first address out of a wallet's accounts array, returning\n * `null` when the wallet currently exposes none.\n */\nconst pickFirstAddress = (accounts: ReadonlyArray<WalletStandardWalletAccount>): string | null => {\n const first = accounts[0];\n return first === undefined ? null : first.address;\n};\n\n/**\n * Look up a wallet account by address, falling back to the first\n * exposed account when the requested address isn't present.\n *\n * Used by per-call routing in `sendTx` / `signMessage`: callers pass\n * an `account` from `ConnectedWallet.accounts` to sign with a non-\n * active address; the wallet's `accounts[]` is 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\n// Re-exported so this package's public surface keeps offering the shared\n// account builder now that the implementation lives in @usebutr/core\n// (next to `Account` and the reducer that relies on its id format).\nexport { buildAccount } from \"@usebutr/core\";\nexport { getFeature, 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 WalletStandardWallet,\n WalletStandardWalletAccount,\n} from \"./types\";\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<unknown>;\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 * Session plumbing every Wallet Standard platform adapter repeats: chain\n * resolution, the connect/disconnect handshake, account reads, the local\n * listener set that lets `switchChain` synthesise `accountChanged`, and\n * the `standard:events` bridge. Returns `null` for wallets butr can't\n * drive (no chain in this namespace, or no `standard:connect`), which is\n * the same `null` each platform adapter returns to its caller.\n *\n * Platform packages keep what genuinely differs: which features they read\n * off the wallet, how they encode transactions and messages, and their\n * balance units. `getSigner` hands back the raw Wallet Standard wallet so\n * consumers can 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 pickChain = (chains: ReadonlyArray<string>): string | null =>\n chains.find((c) => preferredChainIds.includes(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<StandardConnectFeature>(wallet, \"standard:connect\");\n if (connect === undefined) {\n return null;\n }\n\n const disconnect = getFeature<StandardDisconnectFeature>(wallet, \"standard:disconnect\");\n const events = getFeature<StandardEventsFeature>(wallet, \"standard:events\");\n\n let currentChainId = initialChainId;\n const toChain = (): ChainBase => ({\n id: currentChainId,\n // butr ships no chain-id → display-name table; the wallet name stands\n // in and consumers overlay their own labels via structural typing.\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 // Forward `silent` so eager hydration restores already-authorized\n // accounts without re-opening the wallet's approval UI. Wallets\n // that don't honour `silent` fall back to their normal behaviour.\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 // Chain change: the wallet switched network. Re-point\n // currentChainId so subsequent per-call `chain` inputs route\n // through it, mirroring `switchChain`'s local-state model.\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 // Forward the FULL accounts list; Wallet Standard's\n // change.accounts reflects the wallet's current exposure\n // set. Mirroring it into the pool entry keeps the array in\n // sync with what the wallet actually allows us to sign with,\n // so single-account-exposure wallets (Phantom Solana,\n // MetaMask Snap) don't accumulate stale addresses.\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 switchChain: (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 // The wallet must advertise this chain; otherwise the per-call\n // `chain` input would be rejected by the wallet later.\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 // Synthesise an `accountChanged` so butr's reducer updates the pool\n // entry's chain; the wallet itself has no event to fire here.\n notifyAccountChanged();\n return Promise.resolve();\n },\n\n toChain,\n };\n};\n\nexport type { WalletStandardCore };\nexport { createWalletStandardCore };\n","import type { WalletAdapter } from \"@usebutr/core\";\nimport { logWarn } from \"@usebutr/core\";\n\nimport type { WalletStandardAppModule, WalletStandardWallet } from \"./types\";\n\nlet warnedMissingApp = false;\n\n/**\n * Callback supplied by per-platform discovery to build a `WalletAdapter`\n * out of a Wallet Standard wallet announcement.\n *\n * Returns `null` when the wallet doesn't advertise this platform; that\n * lets a multi-chain wallet (e.g. Phantom: SVM + EVM + Sui + Bitcoin)\n * produce one adapter per platform without each builder needing to know\n * about the others.\n *\n * The second argument is a hook the builder calls during construction\n * to wire a synthetic `disconnected` event into its subscriber set.\n * The discovery loop fires the registered emitter when Wallet Standard\n * announces `unregister` for that wallet.\n */\ntype WalletStandardAdapterBuilder = (\n wallet: WalletStandardWallet,\n registerDisconnector: (emit: () => void) => void,\n) => WalletAdapter | null;\n\n/**\n * Subscribe to Wallet Standard announcements and turn matching wallets\n * into butr `WalletAdapter`s using a per-platform `build` callback.\n *\n * **Shared machinery** every platform package needs:\n * - Dynamic import of `@wallet-standard/app` (optional peer dep;\n * failure to resolve quietly disables discovery so consumers who\n * don't install it pay nothing).\n * - Cancellation: the returned unsubscribe is safe to call before the\n * dynamic import has resolved.\n * - Dedupe by adapter id, so re-announcements don't double-emit.\n * - `unregister` integration: when an extension is removed mid-session,\n * fire the disconnector the builder registered so connected pool\n * entries on the consumer side tear down cleanly.\n *\n * **What's platform-specific** lives entirely in `build`:\n * - Which `wallet.chains` to accept.\n * - Which features to gate on.\n * - The `id` slug prefix (use `slugify(prefix, wallet.name)`).\n * - The capability resolver, the feature decoding, the chain coercion.\n *\n * Disconnectors are keyed by the wallet object itself (identity equality),\n * not by adapter id; Wallet Standard's `register` / `unregister` emit\n * the same wallet object, so identity is the cheapest stable key.\n */\nconst discoverWalletStandard = (\n onAdapter: (adapter: WalletAdapter) => void,\n build: WalletStandardAdapterBuilder,\n): (() => void) => {\n let cancelled = false;\n let internalUnsub: (() => void) | null = null;\n\n void (async () => {\n let mod: WalletStandardAppModule;\n try {\n const imported: unknown = 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 // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- validated getWallets export above; the rest of the untyped module is trusted\n mod = imported as WalletStandardAppModule;\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 // wallet object → emitter that pushes synthetic `disconnected` to\n // this wallet's adapter's subscribers. Identity-keyed so the\n // unregister handler can find the right emitter cheaply.\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 };\n"],"mappings":"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"}
|
|
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 { WalletStandardWallet, WalletStandardWalletAccount } from \"./types\";\n\n/**\n * Convert a wallet name into a stable kebab-case adapter id, scoped by\n * a platform prefix so adapters from different platforms (Phantom SVM,\n * Phantom Sui, Phantom BTC) coexist in the pool without colliding on\n * the shared `getWallets()` bus.\n *\n * @example\n * slugify(\"svm\", \"Phantom\") // \"wallet-standard:svm-phantom\"\n * slugify(\"sui\", \"Sui Wallet\") // \"wallet-standard:sui-sui-wallet\"\n * slugify(\"btc\", \"Phantom\") // \"wallet-standard:btc-phantom\"\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 * Read a Wallet Standard feature off a wallet, returning `undefined`\n * when absent. `features` is a reverse-DNS-keyed registry typed\n * `Record<string, unknown>` by the spec, so the caller declares the\n * concrete feature shape it expects; this accessor is the single\n * boundary where that dynamism is acknowledged. The `T` parameter is\n * intentionally caller-supplied (not inferable) and the assertion is\n * unavoidable for an untyped registry.\n */\n// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- caller-supplied feature shape; not inferable from args\nconst getFeature = <T>(wallet: WalletStandardWallet, name: string): T | undefined => {\n const feature: unknown = wallet.features[name];\n if (typeof feature !== \"object\" || feature === null) {\n return undefined;\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- untyped Wallet Standard feature registry; caller declares the shape\n return feature as T;\n};\n\n/**\n * Pick the first address out of a wallet's accounts array, returning\n * `null` when the wallet currently exposes none.\n */\nconst pickFirstAddress = (accounts: ReadonlyArray<WalletStandardWalletAccount>): string | null => {\n const first = accounts[0];\n return first === undefined ? null : first.address;\n};\n\n/**\n * Look up a wallet account by address, falling back to the first\n * exposed account when the requested address isn't present.\n *\n * Used by per-call routing in `sendTx` / `signMessage`: callers pass\n * an `account` from `ConnectedWallet.accounts` to sign with a non-\n * active address; the wallet's `accounts[]` is 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\n// Re-exported so this package's public surface keeps offering the shared\n// account builder now that the implementation lives in @usebutr/core\n// (next to `Account` and the reducer that relies on its id format).\nexport { buildAccount } from \"@usebutr/core\";\nexport { getFeature, 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 WalletStandardWallet,\n WalletStandardWalletAccount,\n} from \"./types\";\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<unknown>;\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 * Session plumbing every Wallet Standard platform adapter repeats: chain\n * resolution, the connect/disconnect handshake, account reads, the local\n * listener set that lets `switchChain` synthesise `accountChanged`, and\n * the `standard:events` bridge. Returns `null` for wallets butr can't\n * drive (no chain in this namespace, or no `standard:connect`), which is\n * the same `null` each platform adapter returns to its caller.\n *\n * Platform packages keep what genuinely differs: which features they read\n * off the wallet, how they encode transactions and messages, and their\n * balance units. `getSigner` hands back the raw Wallet Standard wallet so\n * consumers can 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 pickChain = (chains: ReadonlyArray<string>): string | null =>\n chains.find((c) => preferredChainIds.includes(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<StandardConnectFeature>(wallet, \"standard:connect\");\n if (connect === undefined) {\n return null;\n }\n\n const disconnect = getFeature<StandardDisconnectFeature>(wallet, \"standard:disconnect\");\n const events = getFeature<StandardEventsFeature>(wallet, \"standard:events\");\n\n let currentChainId = initialChainId;\n const toChain = (): ChainBase => ({\n id: currentChainId,\n // butr ships no chain-id → display-name table; the wallet name stands\n // in and consumers overlay their own labels via structural typing.\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 // Forward `silent` so eager hydration restores already-authorized\n // accounts without re-opening the wallet's approval UI. Wallets\n // that don't honour `silent` fall back to their normal behaviour.\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 // Chain change: the wallet switched network. Re-point\n // currentChainId so subsequent per-call `chain` inputs route\n // through it, mirroring `switchChain`'s local-state model.\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 // Forward the FULL accounts list; Wallet Standard's\n // change.accounts reflects the wallet's current exposure\n // set. Mirroring it into the pool entry keeps the array in\n // sync with what the wallet actually allows us to sign with,\n // so single-account-exposure wallets (Phantom Solana,\n // MetaMask Snap) don't accumulate stale addresses.\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 switchChain: (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 // The wallet must advertise this chain; otherwise the per-call\n // `chain` input would be rejected by the wallet later.\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 // Synthesise an `accountChanged` so butr's reducer updates the pool\n // entry's chain; the wallet itself has no event to fire here.\n notifyAccountChanged();\n return 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 → butr capability mapping, shared by every platform\n * package. The flags that are constant across all of them:\n *\n * - `getBalance` / `getTransactionReceipt`: Wallet Standard exposes no\n * balance/receipt RPC, and butr ships no RPC client of its own.\n * - `requestAccounts`: no programmatic equivalent of EIP-2255; re-running\n * `standard:connect` doesn't produce new accounts on any major Wallet\n * Standard wallet in practice (Phantom, MetaMask Snap, Solflare,\n * Backpack, Xverse, Talisman).\n * - `switchAccount`: no protocol-level silent account switch; the user\n * picks the account inside the wallet.\n * - `switchChain`: true when the wallet advertises more than one chain in\n * this namespace. The switch is local re-routing (the per-call `chain`\n * input), so with a single chain there's nothing to switch to.\n *\n * Each platform package owns only the feature-name → flag mapping, since\n * the feature names differ per namespace.\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\";\n\nimport type { WalletStandardAppModule, WalletStandardWallet } from \"./types\";\n\nlet warnedMissingApp = false;\n\n/**\n * Callback supplied by per-platform discovery to build a `WalletAdapter`\n * out of a Wallet Standard wallet announcement.\n *\n * Returns `null` when the wallet doesn't advertise this platform; that\n * lets a multi-chain wallet (e.g. Phantom: SVM + EVM + Sui + Bitcoin)\n * produce one adapter per platform without each builder needing to know\n * about the others.\n *\n * The second argument is a hook the builder calls during construction\n * to wire a synthetic `disconnected` event into its subscriber set.\n * The discovery loop fires the registered emitter when Wallet Standard\n * announces `unregister` for that wallet.\n */\ntype WalletStandardAdapterBuilder = (\n wallet: WalletStandardWallet,\n registerDisconnector: (emit: () => void) => void,\n) => WalletAdapter | null;\n\n/**\n * Subscribe to Wallet Standard announcements and turn matching wallets\n * into butr `WalletAdapter`s using a per-platform `build` callback.\n *\n * **Shared machinery** every platform package needs:\n * - Dynamic import of `@wallet-standard/app` (optional peer dep;\n * failure to resolve quietly disables discovery so consumers who\n * don't install it pay nothing).\n * - Cancellation: the returned unsubscribe is safe to call before the\n * dynamic import has resolved.\n * - Dedupe by adapter id, so re-announcements don't double-emit.\n * - `unregister` integration: when an extension is removed mid-session,\n * fire the disconnector the builder registered so connected pool\n * entries on the consumer side tear down cleanly.\n *\n * **What's platform-specific** lives entirely in `build`:\n * - Which `wallet.chains` to accept.\n * - Which features to gate on.\n * - The `id` slug prefix (use `slugify(prefix, wallet.name)`).\n * - The capability resolver, the feature decoding, the chain coercion.\n *\n * Disconnectors are keyed by the wallet object itself (identity equality),\n * not by adapter id; Wallet Standard's `register` / `unregister` emit\n * the same wallet object, so identity is the cheapest stable key.\n */\nconst discoverWalletStandard = (\n onAdapter: (adapter: WalletAdapter) => void,\n build: WalletStandardAdapterBuilder,\n): (() => void) => {\n let cancelled = false;\n let internalUnsub: (() => void) | null = null;\n\n void (async () => {\n let mod: WalletStandardAppModule;\n try {\n const imported: unknown = 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 // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- validated getWallets export above; the rest of the untyped module is trusted\n mod = imported as WalletStandardAppModule;\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 // wallet object → emitter that pushes synthetic `disconnected` to\n // this wallet's adapter's subscribers. Identity-keyed so the\n // unregister handler can find the right emitter cheaply.\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 };\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAM,WAAW,gBAAwB,SAAyB;CAKhE,OAAO,mBAAmB,eAAe,GAJ5B,KACV,KAAK,CAAC,CACN,YAAY,CAAC,CACb,2BAAW,6BAAa,GAAG,GACiB;AACjD;;;;;;;;;;AAYA,MAAM,cAAiB,QAA8B,SAAgC;CACnF,MAAM,UAAmB,OAAO,SAAS;CACzC,IAAI,OAAO,YAAY,YAAY,YAAY,MAC7C;CAGF,OAAO;AACT;;;;;AAMA,MAAM,oBAAoB,aAAwE;CAChG,MAAM,QAAQ,SAAS;CACvB,OAAO,UAAU,SAAY,OAAO,MAAM;AAC5C;;;;;;;;;AAUA,MAAM,wBACJ,UACA,YAEA,SAAS,MAAM,MAAM,EAAE,YAAY,OAAO,KAAK,SAAS;;;;;;;;;;;;;;;;;ACe1D,MAAM,4BAA4B,EAChC,aACA,IACA,OACA,WACA,UACA,mBACA,sBACA,mBACA,aACwD;CACxD,MAAM,aAAa,WACjB,OAAO,MAAM,MAAM,kBAAkB,SAAS,CAAC,CAAC,KAChD,OAAO,MAAM,MAAM,EAAE,WAAW,WAAW,CAAC,KAC5C;CAEF,MAAM,iBAAiB,UAAU,OAAO,MAAM;CAC9C,IAAI,mBAAmB,MACrB,OAAO;CAET,MAAM,UAAU,WAAmC,QAAQ,kBAAkB;CAC7E,IAAI,YAAY,QACd,OAAO;CAGT,MAAM,aAAa,WAAsC,QAAQ,qBAAqB;CACtF,MAAM,SAAS,WAAkC,QAAQ,iBAAiB;CAE1E,IAAI,iBAAiB;CACrB,MAAM,iBAA4B;EAChC,IAAI;EAGJ,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;GAIlB,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;KAI7C,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;MAOA,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;EAEA,cAAc,UAAU;GACtB,IAAI,MAAM,cAAc,WACtB,MAAM,IAAI,MACR,GAAG,MAAM,wBAAwB,SAAS,UAAU,MAAM,GAAG,kCAAkC,UAAU,GAC3G;GAIF,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;GAGvB,qBAAqB;GACrB,OAAO,QAAQ,QAAQ;EACzB;EAEA;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;AC3NA,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;;;;AC7CA,IAAI,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CvB,MAAM,0BACJ,WACA,UACiB;CACjB,IAAI,YAAY;CAChB,IAAI,gBAAqC;CAEzC,CAAM,YAAY;EAChB,IAAI;EACJ,IAAI;GACF,MAAM,WAAoB,MAAM,OAAO;GACvC,IACE,OAAO,aAAa,YACpB,aAAa,QACb,EAAE,gBAAgB,aAClB,OAAO,SAAS,eAAe,YAE/B,MAAM,IAAI,MAAM,+CAA+C;GAGjE,MAAM;EACR,SAAS,OAAO;GACd,IAAI,CAAC,kBAAkB;IACrB,mBAAmB;IACnB,QACE,iLACA,KACF;GACF;GACA;EACF;EACA,IAAI,WACF;EAGF,MAAM,0BAAU,IAAI,IAAY;EAIhC,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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebutr/wallet-standard-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared Wallet Standard discovery primitives used by @usebutr/svm, @usebutr/sui, @usebutr/bitcoin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Pedro Filho <pedro@filho.me>",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@usebutr/core": "0.
|
|
25
|
+
"@usebutr/core": "1.0.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@wallet-standard/app": "^1.1.1",
|