kaspa-wallet-standard 0.1.0 → 0.2.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/README.md CHANGED
@@ -5,8 +5,10 @@ any dApp can find any wallet — and any wallet can join every dApp — without
5
5
  other. Inspired by Ethereum's [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) and Solana's
6
6
  [Wallet Standard](https://github.com/wallet-standard/wallet-standard).
7
7
 
8
- > **Status: proposed / draft**, open for community adoption and headed for a [KIP](https://github.com/kaspanet/kips).
9
- > The full specification is in **[SPEC.md](SPEC.md)**. The wire contract is frozen see SPEC §7.
8
+ > **Status: reference implementation of [KIP-12](https://github.com/kaspanet/kips/pull/21) (draft, under revival).**
9
+ > This standard has been folded into KIP-12, which is now the authoritative specification; this package
10
+ > implements it exactly — canonical names only (`kaspa:provider` announce, bare network ids,
11
+ > `chainChanged`). [SPEC.md](SPEC.md) tracks the same contract. Versioning policy: SPEC §7.
10
12
 
11
13
  ```bash
12
14
  npm install kaspa-wallet-standard
@@ -55,7 +57,7 @@ const provider = {
55
57
  getPublicKey: () => w.request('kas:get_account').then(a => a.publicKey),
56
58
  signMessage: (m) => w.request('kas:sign_message', m),
57
59
  signPskt: ({ txJsonString, options }) => w.request('kas:sign_tx', {
58
- networkId: /* your network id */ 'kaspa_testnet_10',
60
+ networkId: /* whatever id YOUR bridge expects — this is the wallet's own dialect */ 'testnet-10',
59
61
  txJson: txJsonString,
60
62
  scripts: options.signInputs.map(s => ({ inputIndex: s.index, scriptHex: '', signType: 'All' })),
61
63
  }),
@@ -74,7 +76,7 @@ const detail = Object.freeze({
74
76
  info: Object.freeze({ uuid: crypto.randomUUID(), name: 'YourWallet', icon: 'data:…', rdns: 'com.yourwallet' }),
75
77
  provider: window.yourwallet,
76
78
  });
77
- const announce = () => window.dispatchEvent(new CustomEvent('kaspa:announceProvider', { detail }));
79
+ const announce = () => window.dispatchEvent(new CustomEvent('kaspa:provider', { detail }));
78
80
  window.addEventListener('kaspa:requestProvider', announce);
79
81
  announce();
80
82
  ```
@@ -93,7 +95,10 @@ const unsubscribe = requestKaspaWallets(({ info, provider }) => {
93
95
  ```
94
96
 
95
97
  Then on click: `const [address] = await provider.requestAccounts();`. Capability-check optional methods
96
- before using them, and refuse any `info.icon` that isn't a `data:` URI.
98
+ before using them. `requestKaspaWallets` already strips any non-`data:` `info.icon` for you (a remote URL
99
+ is a tracking/spoofing vector), so what reaches your callback is safe to render. The announce is **not** an
100
+ identity proof, though — any script can announce any name/rdns, so require an explicit user connect before
101
+ signing and treat silent `getAccounts()` restore as display-only. See [SPEC §8](SPEC.md#8-security-considerations).
97
102
 
98
103
  ## Who's using it
99
104
 
@@ -105,10 +110,12 @@ Using it in your wallet or dApp? Open a PR to add yourself.
105
110
 
106
111
  ## Status & contributing
107
112
 
108
- This is a **proposed** standard. The goal is ratification as a KIP once the handshake is proven across at
109
- least two independently-developed wallets. If you're a wallet or dApp author — especially if you'd want a
110
- field changed **before** it freezes into a KIP please [open an issue](../../issues). See
111
- [SPEC.md](SPEC.md) for the full contract, security model, and versioning policy.
113
+ This package is the reference implementation of **KIP-12**, the (draft) Kaspa wallet provider and
114
+ discovery standard the original draft lives at [kaspanet/kips#21](https://github.com/kaspanet/kips/pull/21)
115
+ and a revived, consolidated revision is being prepared with the original authors. The bar for
116
+ ratification: the handshake proven across at least two independently-developed wallets. If you're a
117
+ wallet or dApp author, review happens on the KIP; implementation issues are welcome
118
+ [here](../../issues). See [SPEC.md](SPEC.md) for the contract, security model, and versioning policy.
112
119
 
113
120
  ## License
114
121
 
package/SPEC.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # Kaspa Wallet Standard
2
2
 
3
- **Status: Proposed (draft).** This is an open proposal for how Kaspa dApps and wallets discover and talk
4
- to each other. It is **not** (yet) a ratified [Kaspa Improvement Proposal](https://github.com/kaspanet/kips)
5
- it is offered for community adoption and feedback, with the explicit goal of becoming a KIP once it has
6
- proven out across independent wallets. Comments, issues, and PRs are welcome.
3
+ **Status: folded into [KIP-12](https://github.com/kaspanet/kips/pull/21) (draft, under revival).**
4
+ This document began as an independent proposal and has since been consolidated into KIP-12, the
5
+ Kaspa wallet provider and discovery standard **the KIP is the authoritative specification**; this
6
+ file tracks the same contract as the companion to the reference implementation in this package.
7
+ Standards review happens on the KIP; implementation issues are welcome here.
7
8
 
8
- **Version:** 0.1 · **Wire contract:** frozen (see §7).
9
+ **Version:** 0.2 · **Naming:** KIP-12 canonical only (see §7 for the versioning policy).
9
10
 
10
11
  ---
11
12
 
@@ -30,7 +31,7 @@ It has two parts: a **provider interface** (§3–4) and a **discovery handshake
30
31
 
31
32
  - **Provider** — the object a wallet injects/exposes that a dApp calls to get accounts, network, and
32
33
  signatures (§3).
33
- - **Announce** — a wallet advertising its provider via the `kaspa:announceProvider` event (§5).
34
+ - **Announce** — a wallet advertising its provider via the `kaspa:provider` event (§5).
34
35
  - **dApp** — any web page that wants to use a Kaspa wallet.
35
36
  - Keywords **MUST**, **SHOULD**, **MAY** are used per [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
36
37
 
@@ -50,7 +51,7 @@ method is **optional** and MUST be capability-checked by the dApp (`typeof p.sig
50
51
  | `signMessage(msg): Promise<string>` | no | KIP-5 message signing → Schnorr signature hex. |
51
52
  | `signPskt({ txJsonString, options }): Promise<string>` | no | Sign specific inputs of a transaction (§4). |
52
53
  | `disconnect(origin?): Promise<void>` | no | Drop the site's authorization. |
53
- | `on / removeListener(event, handler)` | no | `accountsChanged`, `networkChanged`. |
54
+ | `on / removeListener(event, handler)` | no | `accountsChanged`, `chainChanged`. |
54
55
 
55
56
  A wallet that implements only `requestAccounts` is valid — it will connect and display balances in a
56
57
  dApp, which simply disables features that need the missing methods.
@@ -74,7 +75,7 @@ array of `{ index, sighashType }`, and returns the re-serialized signed transact
74
75
 
75
76
  Two events on `window`, mirroring EIP-6963 replay semantics:
76
77
 
77
- - **`kaspa:announceProvider`** — a `CustomEvent` dispatched by the **wallet**. `detail` is a **frozen**
78
+ - **`kaspa:provider`** — a `CustomEvent` dispatched by the **wallet**. `detail` is a **frozen**
78
79
  `{ info, provider }` (see §5.1). MUST be dispatched on load, and re-dispatched on every
79
80
  `kaspa:requestProvider`.
80
81
  - **`kaspa:requestProvider`** — a plain `Event` dispatched by the **dApp** to ask present wallets to
@@ -83,7 +84,7 @@ Two events on `window`, mirroring EIP-6963 replay semantics:
83
84
  **Sequence**
84
85
 
85
86
  1. On load, the wallet registers a `kaspa:requestProvider` listener that re-announces, then announces once.
86
- 2. The dApp registers its `kaspa:announceProvider` listener (kept alive for the page lifetime), then
87
+ 2. The dApp registers its `kaspa:provider` listener (kept alive for the page lifetime), then
87
88
  dispatches `kaspa:requestProvider`.
88
89
  3. Late arrival is covered from both directions: a late wallet announces unprompted on load; a late dApp's
89
90
  request triggers a replay from every wallet already present.
@@ -100,33 +101,55 @@ type KaspaProviderInfo = {
100
101
  type KaspaProviderDetail = { info: KaspaProviderInfo; provider: KaspaProvider };
101
102
  ```
102
103
 
103
- - The wallet **MUST** freeze `detail` (and `detail.info`) so page scripts cannot swap the provider out.
104
- - The wallet **SHOULD** provide a stable `rdns`; without it a dApp cannot silently restore the session
105
- after a reload (there is no stable identity to match).
106
- - The dApp **SHOULD** dedupe announces by `info.rdns ?? info.uuid`, first-announce-wins.
104
+ - The wallet **MUST** freeze `detail` (and `detail.info`) so a page script cannot mutate an announce
105
+ **in flight** (swap the `provider` or a field on an already-dispatched event). Freezing protects the
106
+ integrity of a *single* announce only it does **not** authenticate the announcer, and a hostile
107
+ script can still dispatch its **own** competing announce. Freezing is not a trust signal (see §8).
108
+ - The wallet **SHOULD** provide a stable `rdns`; without it a dApp cannot restore the session after a
109
+ reload (there is no stable identity to match). `rdns` is a convenience key, **not** an authenticity
110
+ claim — any script can announce any `rdns` (see §8).
111
+ - The dApp **SHOULD** dedupe announces by `info.rdns ?? info.uuid` (first-announce-wins). Because `rdns`
112
+ is spoofable, a dApp **MAY** surface a *second* announce for an already-seen `rdns` as a warning rather
113
+ than silently discarding it.
107
114
 
108
115
  ## 6. Network ids
109
116
 
110
- Canonical strings (also exported as `KASPA_NETWORKS`):
117
+ Canonical strings (also exported as `KASPA_NETWORKS`) — the node's own network ids, per KIP-12:
111
118
 
112
- `kaspa_mainnet` · `kaspa_testnet_10` · `kaspa_testnet_11` · `kaspa_devnet`
119
+ `mainnet` · `testnet-10` · `testnet-11` · `devnet`
113
120
 
114
- ## 7. Compatibility and versioning (frozen contract)
121
+ Some injected wallet APIs speak a `kaspa_`-prefixed dialect (`kaspa_mainnet`, `kaspa_testnet_10`);
122
+ adapters SHOULD normalize those to canonical — the package exports `normalizeKaspaNetworkId()` for
123
+ exactly this.
115
124
 
116
- The **wire contract is frozen**: the two event names, and every field defined above, never change
117
- meaning or type. The standard evolves **only by adding new OPTIONAL fields/methods**. A wallet or dApp
118
- built against this document keeps working against every future version. Breaking changes, if ever
119
- unavoidable, would ship under a **new event name** never by mutating these.
125
+ ## 7. Compatibility and versioning
126
+
127
+ **v0.2 is a clean break to KIP-12 canonical naming** (announce event, network ids, change event) —
128
+ made while this package's own deployments were the only consumers of the v0.1 names, which are
129
+ retired outright (no dual bridge). From v0.2 the contract is stable under KIP-12's own rule: the
130
+ event names and every field defined above never change meaning or type; the standard evolves **only
131
+ by adding new OPTIONAL fields/methods**; breaking changes, if ever unavoidable, ship under a **new
132
+ event name** — never by mutating these.
120
133
 
121
134
  ## 8. Security considerations
122
135
 
123
136
  - `name`/`icon` are **display hints, not trust signals**. An announce proves a provider is *present*, not
124
137
  that it is *who it claims to be*. dApps MUST NOT grant trust based on them, and MUST refuse non-`data:`
125
- icons (a remote URL is a tracking/spoofing vector).
126
- - Any page script can dispatch `kaspa:announceProvider`. Treat the provider as untrusted until the user
127
- explicitly connects; the connect prompt is the trust boundary.
138
+ icons (a remote URL is a tracking/spoofing vector). The reference `requestKaspaWallets` **enforces this
139
+ for you** it strips any non-`data:` icon (delivers it as `''`) before handing the announce to your
140
+ callback.
141
+ - Any page script can dispatch `kaspa:provider`, including one that claims another wallet's
142
+ `name`/`rdns`. Treat the provider as untrusted until the user explicitly connects; the wallet's own
143
+ connect/sign prompt — rendered by the extension, outside page control — is the trust boundary, not the
144
+ announce.
145
+ - **Silent session restore is display-only.** A dApp MAY use a remembered `rdns` to silently re-populate
146
+ *displayed* accounts via `getAccounts()` after a reload, but it **MUST** require a fresh explicit user
147
+ connect gesture before calling `signPskt` (or any signing). Never route a signature to a provider the
148
+ user did not explicitly (re-)select this session — `rdns` alone is spoofable and is not consent.
128
149
  - The fund-safety rules in §4 are the load-bearing security property. A wallet that signs sloppily can
129
- lose user funds even though the handshake itself is benign.
150
+ lose user funds even though the handshake itself is benign. Note a spoofed in-page provider still
151
+ **cannot forge a signature** (it holds no keys); the realistic risk of announce-spoofing is *display
152
+ spoofing / phishing setup*, which the connect-gesture boundary above is designed to contain.
130
153
 
131
154
  ## 9. Reference implementation & adoption
132
155
 
@@ -136,9 +159,12 @@ unavoidable, would ship under a **new event name** — never by mutating these.
136
159
  discovery handshake in production, and ships built-in adapters for KasWare and Kastle behind the same
137
160
  provider interface.
138
161
 
139
- ## 10. Path to standardization
162
+ ## 10. Standardization status
140
163
 
141
- This document is intended to graduate into a **KIP**. The bar we're aiming for before proposing
142
- ratification: the handshake proven across **at least two independently-developed wallets**, and the
143
- provider interface exercised by covenant-grade `signPskt` on-chain. Wallet and dApp authors who adopt it
144
- (or who want fields changed *before* it freezes into a KIP) are invited to open an issue.
164
+ This document **has been folded into KIP-12** ([original draft PR](https://github.com/kaspanet/kips/pull/21);
165
+ a revived, consolidated revision is being prepared with the original authors) the KIP is where the
166
+ standard lives and where review happens. The bar before proposing ratification stands: the handshake
167
+ proven across **at least two independently-developed wallets** (in progress: KRON's dApp side against
168
+ the Rift wallet PoC, plus the KasWare/Kastle adapters), and the provider interface exercised by
169
+ covenant-grade `signPskt` on-chain. Wallet and dApp authors: raise standards questions on the KIP,
170
+ implementation issues here.
package/dist/index.d.ts CHANGED
@@ -1,19 +1,25 @@
1
1
  /** Canonical network ids used by `getNetwork()` / `switchNetwork()`. String literals are the contract;
2
2
  * this object is a convenience so integrators don't hand-type them. */
3
3
  declare const KASPA_NETWORKS: {
4
- readonly MAINNET: "kaspa_mainnet";
5
- readonly TESTNET_10: "kaspa_testnet_10";
6
- readonly TESTNET_11: "kaspa_testnet_11";
7
- readonly DEVNET: "kaspa_devnet";
4
+ readonly MAINNET: "mainnet";
5
+ readonly TESTNET_10: "testnet-10";
6
+ readonly TESTNET_11: "testnet-11";
7
+ readonly DEVNET: "devnet";
8
8
  };
9
9
  type KaspaNetworkId = (typeof KASPA_NETWORKS)[keyof typeof KASPA_NETWORKS];
10
+ /** Normalize any wallet-reported network id to the canonical KIP-12 form: legacy `kaspa_`-prefixed
11
+ * variants (`kaspa_mainnet`, `kaspa_testnet_10` — v0.1 of this package, KasWare's injected API) map to
12
+ * the node's bare ids (`mainnet`, `testnet-10`). Canonical ids pass through unchanged. */
13
+ declare const normalizeKaspaNetworkId: (id: string) => string;
10
14
  /** Identity a wallet announces about itself. `name`/`icon` are DISPLAY hints — never trust signals. */
11
15
  type KaspaProviderInfo = {
12
16
  /** UUIDv4, freshly generated per page load — instance identity, used only for dedupe. */
13
17
  uuid: string;
14
18
  /** Human-readable wallet name shown in pickers, e.g. "Kastle". */
15
19
  name: string;
16
- /** Wallet icon as a `data:` URI (SVG/PNG). dApps should refuse to render remote URLs. */
20
+ /** Wallet icon as a `data:` URI (SVG/PNG) a DISPLAY hint, never a trust signal. A remote URL is a
21
+ * tracking/spoofing vector, so the dApp-side {@link requestKaspaWallets} STRIPS any non-`data:` icon
22
+ * (delivers it as `''`) — a dApp can never be handed a remote URL through the handshake. */
17
23
  icon: string;
18
24
  /** Reverse-DNS identifier, e.g. "com.kasware" — STABLE across page loads and versions. Strongly
19
25
  * recommended: it is what lets a dApp silently restore a session with your wallet after a reload. */
@@ -25,6 +31,95 @@ type KaspaSignInput = {
25
31
  index: number;
26
32
  sighashType: number;
27
33
  };
34
+ /** Argument of {@link KaspaProvider.signPskt}: a Kaspa Safe-JSON transaction plus the explicit list of
35
+ * inputs to sign. The wallet MUST sign ONLY these inputs and leave the rest untouched (fund-safety). */
36
+ interface KaspaSignPsktArg {
37
+ txJsonString: string;
38
+ options: {
39
+ signInputs: KaspaSignInput[];
40
+ };
41
+ }
42
+ /** Simple transfer (SPEC §4.2). Amount is sompi as a base-10 **string** — float KAS is FORBIDDEN in the
43
+ * wire contract: IEEE-754 doubles cannot represent every sompi value exactly, and a rounding error in an
44
+ * amount is a fund-safety defect. */
45
+ interface KaspaSimpleTransfer {
46
+ to: string;
47
+ amountSompi: string;
48
+ }
49
+ /** Provider events and their payloads (SPEC §6). */
50
+ interface KaspaProviderEventMap {
51
+ /** The authorized account list changed (switch, revoke). */
52
+ accountsChanged: string[];
53
+ /** The wallet switched networks; payload is a network id (see {@link KaspaNetworkId}). */
54
+ chainChanged: string;
55
+ }
56
+ type KaspaProviderEvent = keyof KaspaProviderEventMap;
57
+ /** Canonical `request()` registry (SPEC §7): every §3 method under a namespaced name, so a
58
+ * `request()`-only client stays possible. Future KIPs/protocols extend this map via declaration
59
+ * merging; wallets serve vendor methods under their own namespace (e.g. `"rift:…"`). */
60
+ interface KaspaRequestMap {
61
+ 'kaspa:requestAccounts': {
62
+ args: [];
63
+ result: string[];
64
+ };
65
+ 'kaspa:getAccounts': {
66
+ args: [];
67
+ result: string[];
68
+ };
69
+ 'kaspa:getNetwork': {
70
+ args: [];
71
+ result: string;
72
+ };
73
+ 'kaspa:switchNetwork': {
74
+ args: [networkId: string];
75
+ result: void;
76
+ };
77
+ 'kaspa:getPublicKey': {
78
+ args: [];
79
+ result: string;
80
+ };
81
+ 'kaspa:signMessage': {
82
+ args: [message: string];
83
+ result: string;
84
+ };
85
+ 'kaspa:signPskt': {
86
+ args: [arg: KaspaSignPsktArg];
87
+ result: string;
88
+ };
89
+ 'kaspa:sendTransaction': {
90
+ args: [tx: KaspaSimpleTransfer];
91
+ result: string;
92
+ };
93
+ 'kaspa:signPskb': {
94
+ args: [pskb: string];
95
+ result: string;
96
+ };
97
+ 'kaspa:broadcastPskb': {
98
+ args: [pskb: string];
99
+ result: string[];
100
+ };
101
+ 'kaspa:disconnect': {
102
+ args: [origin?: string];
103
+ result: void;
104
+ };
105
+ }
106
+ type KaspaRequestMethod = keyof KaspaRequestMap;
107
+ /** Standard rejection codes (SPEC §8, mirroring EIP-1193 for developer familiarity). */
108
+ declare const KIP12_ERRORS: {
109
+ /** The user rejected the request. */
110
+ readonly USER_REJECTED: 4001;
111
+ /** The origin is not authorized (call `requestAccounts` first). */
112
+ readonly UNAUTHORIZED: 4100;
113
+ /** The wallet does not support the requested method. */
114
+ readonly UNSUPPORTED_METHOD: 4200;
115
+ /** The wallet is locked or unavailable. */
116
+ readonly WALLET_UNAVAILABLE: 4900;
117
+ };
118
+ interface Kip12Error {
119
+ message: string;
120
+ code?: number | string;
121
+ details?: unknown;
122
+ }
28
123
  /**
29
124
  * The raw provider surface a wallet exposes. Deliberately shaped like the widely-deployed injected APIs
30
125
  * (KasWare's `window.kasware`) so an existing wallet can usually announce its injected object as-is.
@@ -50,21 +145,30 @@ interface KaspaProvider {
50
145
  getPublicKey?(): Promise<string>;
51
146
  /** KIP-5 message signing; resolves to the Schnorr signature hex. */
52
147
  signMessage?(message: string): Promise<string>;
53
- /** Sign ONLY the listed inputs of a Kaspa Safe-JSON transaction and return the signed Safe JSON. */
54
- signPskt?(arg: {
55
- txJsonString: string;
56
- options: {
57
- signInputs: KaspaSignInput[];
58
- };
59
- }): Promise<string>;
148
+ /** Sign ONLY the listed inputs of a Kaspa Safe-JSON transaction and return the signed Safe JSON
149
+ * the covenant-grade signing primitive (fund-safety rules apply). */
150
+ signPskt?(arg: KaspaSignPsktArg): Promise<string>;
151
+ /** Simple transfer (SPEC §4.2); resolves to the transaction id. */
152
+ sendTransaction?(tx: KaspaSimpleTransfer): Promise<string>;
153
+ /** Sign a PSKB bundle (SPEC §4.3); the per-input discipline of {@link signPskt} applies. */
154
+ signPskb?(pskb: string): Promise<string>;
155
+ /** Broadcast a signed PSKB; resolves to transaction ids. */
156
+ broadcastPskb?(pskb: string): Promise<string[]>;
60
157
  disconnect?(origin?: string): Promise<void>;
61
- on?(event: 'accountsChanged' | 'networkChanged', handler: (...args: any[]) => void): void;
62
- removeListener?(event: string, handler: (...args: any[]) => void): void;
158
+ /** Subscribe to a provider event; the handler payload is inferred from the event
159
+ * ({@link KaspaProviderEventMap}). `chainChanged` is the KIP-12 network-change event. */
160
+ on?<E extends KaspaProviderEvent>(event: E, handler: (payload: KaspaProviderEventMap[E]) => void): void;
161
+ removeListener?<E extends KaspaProviderEvent>(event: E, handler: (payload: KaspaProviderEventMap[E]) => void): void;
162
+ /** Typed protocol-extension escape hatch (SPEC §7). Extends the surface, never the privileges — a
163
+ * method served here is still subject to the authorization gate and fund-safety rules. An unknown
164
+ * method MUST reject with code 4200 ({@link KIP12_ERRORS}). */
165
+ request?<M extends KaspaRequestMethod>(method: M, args: KaspaRequestMap[M]['args']): Promise<KaspaRequestMap[M]['result']>;
63
166
  }
64
167
  /** Dispatched on `window` by a dApp to ask all present wallets to (re-)announce themselves. */
65
168
  declare const KASPA_REQUEST_PROVIDER_EVENT = "kaspa:requestProvider";
66
- /** Dispatched on `window` by a wallet; `detail` is a frozen {@link KaspaProviderDetail}. */
67
- declare const KASPA_ANNOUNCE_PROVIDER_EVENT = "kaspa:announceProvider";
169
+ /** Dispatched on `window` by a wallet; `detail` is a frozen {@link KaspaProviderDetail}.
170
+ * KIP-12 canonical name. */
171
+ declare const KASPA_ANNOUNCE_PROVIDER_EVENT = "kaspa:provider";
68
172
  /** The `detail` of a `kaspa:announceProvider` CustomEvent. */
69
173
  type KaspaProviderDetail = {
70
174
  info: KaspaProviderInfo;
@@ -80,9 +184,17 @@ declare function announceKaspaWallet(info: KaspaProviderInfo, provider: KaspaPro
80
184
  * dApp SIDE — register `onAnnounce` (fires once per announce event, including replays; dedupe by
81
185
  * `info.rdns ?? info.uuid` yourself), then request announcements from wallets already present. Keep the
82
186
  * subscription alive for the page lifetime to catch late-injecting wallets. Returns an unsubscribe.
83
- * Malformed announces (missing identity or `requestAccounts`) are dropped, never delivered. No-op
84
- * outside a window context.
187
+ *
188
+ * Two receiver-side safety filters run before an announce reaches `onAnnounce`:
189
+ * • Malformed announces (missing `uuid`/`name`, or no `requestAccounts`) are dropped, never delivered.
190
+ * • A non-`data:` `icon` (a remote-URL tracking/spoofing vector, SPEC §8) is STRIPPED to `''` — the
191
+ * wallet is still surfaced, just without the unsafe icon. An absent or valid `data:` icon passes
192
+ * through untouched (the announce stays the wallet's original frozen object).
193
+ *
194
+ * NEITHER filter authenticates the announcer: any page script can announce (SPEC §8), so treat a provider
195
+ * as untrusted until the user explicitly connects — that connect gesture is the trust boundary, not this
196
+ * handshake. No-op outside a window context.
85
197
  */
86
198
  declare function requestKaspaWallets(onAnnounce: (detail: KaspaProviderDetail) => void): () => void;
87
199
 
88
- export { KASPA_ANNOUNCE_PROVIDER_EVENT, KASPA_NETWORKS, KASPA_REQUEST_PROVIDER_EVENT, type KaspaNetworkId, type KaspaProvider, type KaspaProviderDetail, type KaspaProviderInfo, type KaspaSignInput, announceKaspaWallet, requestKaspaWallets };
200
+ export { KASPA_ANNOUNCE_PROVIDER_EVENT, KASPA_NETWORKS, KASPA_REQUEST_PROVIDER_EVENT, KIP12_ERRORS, type KaspaNetworkId, type KaspaProvider, type KaspaProviderDetail, type KaspaProviderEvent, type KaspaProviderEventMap, type KaspaProviderInfo, type KaspaRequestMap, type KaspaRequestMethod, type KaspaSignInput, type KaspaSignPsktArg, type KaspaSimpleTransfer, type Kip12Error, announceKaspaWallet, normalizeKaspaNetworkId, requestKaspaWallets };
package/dist/index.js CHANGED
@@ -1,12 +1,23 @@
1
1
  // src/index.ts
2
2
  var KASPA_NETWORKS = {
3
- MAINNET: "kaspa_mainnet",
4
- TESTNET_10: "kaspa_testnet_10",
5
- TESTNET_11: "kaspa_testnet_11",
6
- DEVNET: "kaspa_devnet"
3
+ MAINNET: "mainnet",
4
+ TESTNET_10: "testnet-10",
5
+ TESTNET_11: "testnet-11",
6
+ DEVNET: "devnet"
7
+ };
8
+ var normalizeKaspaNetworkId = (id) => id.startsWith("kaspa_") ? id.slice("kaspa_".length).replace(/_/g, "-") : id;
9
+ var KIP12_ERRORS = {
10
+ /** The user rejected the request. */
11
+ USER_REJECTED: 4001,
12
+ /** The origin is not authorized (call `requestAccounts` first). */
13
+ UNAUTHORIZED: 4100,
14
+ /** The wallet does not support the requested method. */
15
+ UNSUPPORTED_METHOD: 4200,
16
+ /** The wallet is locked or unavailable. */
17
+ WALLET_UNAVAILABLE: 4900
7
18
  };
8
19
  var KASPA_REQUEST_PROVIDER_EVENT = "kaspa:requestProvider";
9
- var KASPA_ANNOUNCE_PROVIDER_EVENT = "kaspa:announceProvider";
20
+ var KASPA_ANNOUNCE_PROVIDER_EVENT = "kaspa:provider";
10
21
  function announceKaspaWallet(info, provider) {
11
22
  if (typeof window === "undefined") return () => {
12
23
  };
@@ -16,6 +27,7 @@ function announceKaspaWallet(info, provider) {
16
27
  announce();
17
28
  return () => window.removeEventListener(KASPA_REQUEST_PROVIDER_EVENT, announce);
18
29
  }
30
+ var isSafeIcon = (icon) => typeof icon === "string" && /^data:/i.test(icon.trim());
19
31
  function requestKaspaWallets(onAnnounce) {
20
32
  if (typeof window === "undefined") return () => {
21
33
  };
@@ -23,6 +35,10 @@ function requestKaspaWallets(onAnnounce) {
23
35
  const detail = e.detail;
24
36
  if (!detail?.info?.uuid || !detail?.info?.name) return;
25
37
  if (typeof detail.provider?.requestAccounts !== "function") return;
38
+ if (detail.info.icon != null && !isSafeIcon(detail.info.icon)) {
39
+ onAnnounce({ info: { ...detail.info, icon: "" }, provider: detail.provider });
40
+ return;
41
+ }
26
42
  onAnnounce(detail);
27
43
  };
28
44
  window.addEventListener(KASPA_ANNOUNCE_PROVIDER_EVENT, listener);
@@ -33,7 +49,9 @@ export {
33
49
  KASPA_ANNOUNCE_PROVIDER_EVENT,
34
50
  KASPA_NETWORKS,
35
51
  KASPA_REQUEST_PROVIDER_EVENT,
52
+ KIP12_ERRORS,
36
53
  announceKaspaWallet,
54
+ normalizeKaspaNetworkId,
37
55
  requestKaspaWallets
38
56
  };
39
57
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Kaspa Wallet Standard — reference implementation.\n//\n// A PROPOSED standard (see SPEC.md) for how a Kaspa dApp and a Kaspa wallet find and talk to each other,\n// with two parts:\n// • Part 1 — the provider interface a wallet exposes (accounts, network, signing).\n// • Part 2 — an EIP-6963-style discovery handshake so a dApp finds every present wallet with zero\n// per-wallet code, and any future wallet self-registers by dispatching one event.\n//\n// This package has ZERO runtime dependencies and is safe to import in Node (all browser access is guarded).\n// The wire contract — the two event names and the existing fields below — is FROZEN: it never changes.\n// Evolution happens only by adding new OPTIONAL fields. A wallet written against this file keeps working\n// against every future version.\n\n// ============================================================================================\n// Part 1 — Provider interface\n// ============================================================================================\n\n/** Canonical network ids used by `getNetwork()` / `switchNetwork()`. String literals are the contract;\n * this object is a convenience so integrators don't hand-type them. */\nexport const KASPA_NETWORKS = {\n MAINNET: 'kaspa_mainnet',\n TESTNET_10: 'kaspa_testnet_10',\n TESTNET_11: 'kaspa_testnet_11',\n DEVNET: 'kaspa_devnet',\n} as const;\n\nexport type KaspaNetworkId = (typeof KASPA_NETWORKS)[keyof typeof KASPA_NETWORKS];\n\n/** Identity a wallet announces about itself. `name`/`icon` are DISPLAY hints — never trust signals. */\nexport type KaspaProviderInfo = {\n /** UUIDv4, freshly generated per page load — instance identity, used only for dedupe. */\n uuid: string;\n /** Human-readable wallet name shown in pickers, e.g. \"Kastle\". */\n name: string;\n /** Wallet icon as a `data:` URI (SVG/PNG). dApps should refuse to render remote URLs. */\n icon: string;\n /** Reverse-DNS identifier, e.g. \"com.kasware\" — STABLE across page loads and versions. Strongly\n * recommended: it is what lets a dApp silently restore a session with your wallet after a reload. */\n rdns?: string;\n};\n\n/** One input a wallet is asked to sign, by position. `sighashType` 1 = SIGHASH_ALL (the only value\n * KRON uses today); a wallet MUST refuse a type it does not implement rather than guess. */\nexport type KaspaSignInput = { index: number; sighashType: number };\n\n/**\n * The raw provider surface a wallet exposes. Deliberately shaped like the widely-deployed injected APIs\n * (KasWare's `window.kasware`) so an existing wallet can usually announce its injected object as-is.\n *\n * Only `requestAccounts` is MANDATORY. Everything else is OPTIONAL and MUST be capability-checked by the\n * dApp (`typeof provider.signPskt === 'function'`) — a dApp degrades gracefully (e.g. disables trading)\n * when a method is absent, rather than refusing to list the wallet.\n *\n * FUND-SAFETY RULE (wallet side): `signPskt` MUST sign ONLY the inputs listed in `options.signInputs`,\n * and MUST leave every other input untouched. Kaspa covenant transactions carry pre-authorized inputs\n * that must NOT be re-signed; a signature over an unlisted input, or over the wrong sighash, is a\n * fund-safety bug, not a cosmetic mismatch. See SPEC.md §\"Security considerations\".\n */\nexport interface KaspaProvider {\n /** Connect: prompt the user if needed; resolve to the authorized address list (active address first). */\n requestAccounts(): Promise<string[]>;\n /** Already-authorized accounts WITHOUT prompting (empty array if none) — enables silent session restore. */\n getAccounts?(): Promise<string[]>;\n /** Current network id (see {@link KaspaNetworkId}). */\n getNetwork?(): Promise<string>;\n switchNetwork?(networkId: string): Promise<void>;\n /** The active account's public key hex (compressed 33-byte or x-only 32-byte — both accepted). */\n getPublicKey?(): Promise<string>;\n /** KIP-5 message signing; resolves to the Schnorr signature hex. */\n signMessage?(message: string): Promise<string>;\n /** Sign ONLY the listed inputs of a Kaspa Safe-JSON transaction and return the signed Safe JSON. */\n signPskt?(arg: { txJsonString: string; options: { signInputs: KaspaSignInput[] } }): Promise<string>;\n disconnect?(origin?: string): Promise<void>;\n on?(event: 'accountsChanged' | 'networkChanged', handler: (...args: any[]) => void): void;\n removeListener?(event: string, handler: (...args: any[]) => void): void;\n}\n\n// ============================================================================================\n// Part 2 — Discovery handshake\n// ============================================================================================\n\n/** Dispatched on `window` by a dApp to ask all present wallets to (re-)announce themselves. */\nexport const KASPA_REQUEST_PROVIDER_EVENT = 'kaspa:requestProvider';\n/** Dispatched on `window` by a wallet; `detail` is a frozen {@link KaspaProviderDetail}. */\nexport const KASPA_ANNOUNCE_PROVIDER_EVENT = 'kaspa:announceProvider';\n\n/** The `detail` of a `kaspa:announceProvider` CustomEvent. */\nexport type KaspaProviderDetail = { info: KaspaProviderInfo; provider: KaspaProvider };\n\n/**\n * WALLET SIDE — call once when your content script loads. Announces immediately AND replays on every\n * `kaspa:requestProvider` (so a dApp that loads after you still finds you). Returns an unsubscribe\n * (rarely needed; e.g. extension teardown). No-op outside a window context.\n */\nexport function announceKaspaWallet(info: KaspaProviderInfo, provider: KaspaProvider): () => void {\n if (typeof window === 'undefined') return () => {};\n const detail: KaspaProviderDetail = Object.freeze({ info: Object.freeze({ ...info }), provider });\n const announce = () =>\n window.dispatchEvent(new CustomEvent(KASPA_ANNOUNCE_PROVIDER_EVENT, { detail }));\n window.addEventListener(KASPA_REQUEST_PROVIDER_EVENT, announce);\n announce();\n return () => window.removeEventListener(KASPA_REQUEST_PROVIDER_EVENT, announce);\n}\n\n/**\n * dApp SIDE — register `onAnnounce` (fires once per announce event, including replays; dedupe by\n * `info.rdns ?? info.uuid` yourself), then request announcements from wallets already present. Keep the\n * subscription alive for the page lifetime to catch late-injecting wallets. Returns an unsubscribe.\n * Malformed announces (missing identity or `requestAccounts`) are dropped, never delivered. No-op\n * outside a window context.\n */\nexport function requestKaspaWallets(onAnnounce: (detail: KaspaProviderDetail) => void): () => void {\n if (typeof window === 'undefined') return () => {};\n const listener = (e: Event) => {\n const detail = (e as CustomEvent<KaspaProviderDetail>).detail;\n if (!detail?.info?.uuid || !detail?.info?.name) return;\n if (typeof detail.provider?.requestAccounts !== 'function') return;\n onAnnounce(detail);\n };\n window.addEventListener(KASPA_ANNOUNCE_PROVIDER_EVENT, listener);\n window.dispatchEvent(new Event(KASPA_REQUEST_PROVIDER_EVENT));\n return () => window.removeEventListener(KASPA_ANNOUNCE_PROVIDER_EVENT, listener);\n}\n"],"mappings":";AAmBO,IAAM,iBAAiB;AAAA,EAC5B,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,QAAQ;AACV;AA0DO,IAAM,+BAA+B;AAErC,IAAM,gCAAgC;AAUtC,SAAS,oBAAoB,MAAyB,UAAqC;AAChG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,SAA8B,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,EAAE,GAAG,KAAK,CAAC,GAAG,SAAS,CAAC;AAChG,QAAM,WAAW,MACf,OAAO,cAAc,IAAI,YAAY,+BAA+B,EAAE,OAAO,CAAC,CAAC;AACjF,SAAO,iBAAiB,8BAA8B,QAAQ;AAC9D,WAAS;AACT,SAAO,MAAM,OAAO,oBAAoB,8BAA8B,QAAQ;AAChF;AASO,SAAS,oBAAoB,YAA+D;AACjG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,MAAa;AAC7B,UAAM,SAAU,EAAuC;AACvD,QAAI,CAAC,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,KAAM;AAChD,QAAI,OAAO,OAAO,UAAU,oBAAoB,WAAY;AAC5D,eAAW,MAAM;AAAA,EACnB;AACA,SAAO,iBAAiB,+BAA+B,QAAQ;AAC/D,SAAO,cAAc,IAAI,MAAM,4BAA4B,CAAC;AAC5D,SAAO,MAAM,OAAO,oBAAoB,+BAA+B,QAAQ;AACjF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Kaspa Wallet Standard — reference implementation.\n//\n// A PROPOSED standard (see SPEC.md) for how a Kaspa dApp and a Kaspa wallet find and talk to each other,\n// with two parts:\n// • Part 1 — the provider interface a wallet exposes (accounts, network, signing).\n// • Part 2 — an EIP-6963-style discovery handshake so a dApp finds every present wallet with zero\n// per-wallet code, and any future wallet self-registers by dispatching one event.\n//\n// This package has ZERO runtime dependencies and is safe to import in Node (all browser access is guarded).\n//\n// v0.2 — KIP-12. This package is the reference implementation of the revived KIP-12 and speaks ONLY its\n// canonical names: `kaspa:provider` announce, the node's bare network ids (`mainnet`, `testnet-10`),\n// `chainChanged`. The v0.1 names (`kaspa:announceProvider`, `kaspa_mainnet`, `networkChanged`) are gone —\n// a clean break, made while KRON was the only deployed consumer (updated in lockstep). From here the\n// wire contract evolves per KIP-12 itself: add-only fields/methods; breaking = new event name.\n//\n// The type surface below is kept at 1:1 parity with the KIP's formal interfaces (`kip-0012/interfaces.ts`\n// in kaspanet/kips) — same provider methods, `request()` registry, event map, and error codes. The two\n// runtime helpers (announce/request) implement the discovery handshake + receiver-side security filters\n// that the spec describes; a wallet/dApp gets a correct, zero-dependency implementation for free.\n\n// ============================================================================================\n// Part 1 — Provider interface\n// ============================================================================================\n\n/** Canonical network ids used by `getNetwork()` / `switchNetwork()`. String literals are the contract;\n * this object is a convenience so integrators don't hand-type them. */\nexport const KASPA_NETWORKS = {\n MAINNET: 'mainnet',\n TESTNET_10: 'testnet-10',\n TESTNET_11: 'testnet-11',\n DEVNET: 'devnet',\n} as const;\n\nexport type KaspaNetworkId = (typeof KASPA_NETWORKS)[keyof typeof KASPA_NETWORKS];\n\n/** Normalize any wallet-reported network id to the canonical KIP-12 form: legacy `kaspa_`-prefixed\n * variants (`kaspa_mainnet`, `kaspa_testnet_10` — v0.1 of this package, KasWare's injected API) map to\n * the node's bare ids (`mainnet`, `testnet-10`). Canonical ids pass through unchanged. */\nexport const normalizeKaspaNetworkId = (id: string): string =>\n id.startsWith('kaspa_') ? id.slice('kaspa_'.length).replace(/_/g, '-') : id;\n\n/** Identity a wallet announces about itself. `name`/`icon` are DISPLAY hints — never trust signals. */\nexport type KaspaProviderInfo = {\n /** UUIDv4, freshly generated per page load — instance identity, used only for dedupe. */\n uuid: string;\n /** Human-readable wallet name shown in pickers, e.g. \"Kastle\". */\n name: string;\n /** Wallet icon as a `data:` URI (SVG/PNG) — a DISPLAY hint, never a trust signal. A remote URL is a\n * tracking/spoofing vector, so the dApp-side {@link requestKaspaWallets} STRIPS any non-`data:` icon\n * (delivers it as `''`) — a dApp can never be handed a remote URL through the handshake. */\n icon: string;\n /** Reverse-DNS identifier, e.g. \"com.kasware\" — STABLE across page loads and versions. Strongly\n * recommended: it is what lets a dApp silently restore a session with your wallet after a reload. */\n rdns?: string;\n};\n\n/** One input a wallet is asked to sign, by position. `sighashType` 1 = SIGHASH_ALL (the only value\n * KRON uses today); a wallet MUST refuse a type it does not implement rather than guess. */\nexport type KaspaSignInput = { index: number; sighashType: number };\n\n/** Argument of {@link KaspaProvider.signPskt}: a Kaspa Safe-JSON transaction plus the explicit list of\n * inputs to sign. The wallet MUST sign ONLY these inputs and leave the rest untouched (fund-safety). */\nexport interface KaspaSignPsktArg {\n txJsonString: string;\n options: { signInputs: KaspaSignInput[] };\n}\n\n/** Simple transfer (SPEC §4.2). Amount is sompi as a base-10 **string** — float KAS is FORBIDDEN in the\n * wire contract: IEEE-754 doubles cannot represent every sompi value exactly, and a rounding error in an\n * amount is a fund-safety defect. */\nexport interface KaspaSimpleTransfer {\n to: string;\n amountSompi: string;\n}\n\n/** Provider events and their payloads (SPEC §6). */\nexport interface KaspaProviderEventMap {\n /** The authorized account list changed (switch, revoke). */\n accountsChanged: string[];\n /** The wallet switched networks; payload is a network id (see {@link KaspaNetworkId}). */\n chainChanged: string;\n}\n\nexport type KaspaProviderEvent = keyof KaspaProviderEventMap;\n\n/** Canonical `request()` registry (SPEC §7): every §3 method under a namespaced name, so a\n * `request()`-only client stays possible. Future KIPs/protocols extend this map via declaration\n * merging; wallets serve vendor methods under their own namespace (e.g. `\"rift:…\"`). */\nexport interface KaspaRequestMap {\n 'kaspa:requestAccounts': { args: []; result: string[] };\n 'kaspa:getAccounts': { args: []; result: string[] };\n 'kaspa:getNetwork': { args: []; result: string };\n 'kaspa:switchNetwork': { args: [networkId: string]; result: void };\n 'kaspa:getPublicKey': { args: []; result: string };\n 'kaspa:signMessage': { args: [message: string]; result: string };\n 'kaspa:signPskt': { args: [arg: KaspaSignPsktArg]; result: string };\n 'kaspa:sendTransaction': { args: [tx: KaspaSimpleTransfer]; result: string };\n 'kaspa:signPskb': { args: [pskb: string]; result: string };\n 'kaspa:broadcastPskb': { args: [pskb: string]; result: string[] };\n 'kaspa:disconnect': { args: [origin?: string]; result: void };\n}\n\nexport type KaspaRequestMethod = keyof KaspaRequestMap;\n\n/** Standard rejection codes (SPEC §8, mirroring EIP-1193 for developer familiarity). */\nexport const KIP12_ERRORS = {\n /** The user rejected the request. */\n USER_REJECTED: 4001,\n /** The origin is not authorized (call `requestAccounts` first). */\n UNAUTHORIZED: 4100,\n /** The wallet does not support the requested method. */\n UNSUPPORTED_METHOD: 4200,\n /** The wallet is locked or unavailable. */\n WALLET_UNAVAILABLE: 4900,\n} as const;\n\nexport interface Kip12Error {\n message: string;\n code?: number | string;\n details?: unknown;\n}\n\n/**\n * The raw provider surface a wallet exposes. Deliberately shaped like the widely-deployed injected APIs\n * (KasWare's `window.kasware`) so an existing wallet can usually announce its injected object as-is.\n *\n * Only `requestAccounts` is MANDATORY. Everything else is OPTIONAL and MUST be capability-checked by the\n * dApp (`typeof provider.signPskt === 'function'`) — a dApp degrades gracefully (e.g. disables trading)\n * when a method is absent, rather than refusing to list the wallet.\n *\n * FUND-SAFETY RULE (wallet side): `signPskt` MUST sign ONLY the inputs listed in `options.signInputs`,\n * and MUST leave every other input untouched. Kaspa covenant transactions carry pre-authorized inputs\n * that must NOT be re-signed; a signature over an unlisted input, or over the wrong sighash, is a\n * fund-safety bug, not a cosmetic mismatch. See SPEC.md §\"Security considerations\".\n */\nexport interface KaspaProvider {\n /** Connect: prompt the user if needed; resolve to the authorized address list (active address first). */\n requestAccounts(): Promise<string[]>;\n /** Already-authorized accounts WITHOUT prompting (empty array if none) — enables silent session restore. */\n getAccounts?(): Promise<string[]>;\n /** Current network id (see {@link KaspaNetworkId}). */\n getNetwork?(): Promise<string>;\n switchNetwork?(networkId: string): Promise<void>;\n /** The active account's public key hex (compressed 33-byte or x-only 32-byte — both accepted). */\n getPublicKey?(): Promise<string>;\n /** KIP-5 message signing; resolves to the Schnorr signature hex. */\n signMessage?(message: string): Promise<string>;\n /** Sign ONLY the listed inputs of a Kaspa Safe-JSON transaction and return the signed Safe JSON —\n * the covenant-grade signing primitive (fund-safety rules apply). */\n signPskt?(arg: KaspaSignPsktArg): Promise<string>;\n /** Simple transfer (SPEC §4.2); resolves to the transaction id. */\n sendTransaction?(tx: KaspaSimpleTransfer): Promise<string>;\n /** Sign a PSKB bundle (SPEC §4.3); the per-input discipline of {@link signPskt} applies. */\n signPskb?(pskb: string): Promise<string>;\n /** Broadcast a signed PSKB; resolves to transaction ids. */\n broadcastPskb?(pskb: string): Promise<string[]>;\n disconnect?(origin?: string): Promise<void>;\n /** Subscribe to a provider event; the handler payload is inferred from the event\n * ({@link KaspaProviderEventMap}). `chainChanged` is the KIP-12 network-change event. */\n on?<E extends KaspaProviderEvent>(event: E, handler: (payload: KaspaProviderEventMap[E]) => void): void;\n removeListener?<E extends KaspaProviderEvent>(event: E, handler: (payload: KaspaProviderEventMap[E]) => void): void;\n /** Typed protocol-extension escape hatch (SPEC §7). Extends the surface, never the privileges — a\n * method served here is still subject to the authorization gate and fund-safety rules. An unknown\n * method MUST reject with code 4200 ({@link KIP12_ERRORS}). */\n request?<M extends KaspaRequestMethod>(\n method: M,\n args: KaspaRequestMap[M]['args'],\n ): Promise<KaspaRequestMap[M]['result']>;\n}\n\n// ============================================================================================\n// Part 2 — Discovery handshake\n// ============================================================================================\n\n/** Dispatched on `window` by a dApp to ask all present wallets to (re-)announce themselves. */\nexport const KASPA_REQUEST_PROVIDER_EVENT = 'kaspa:requestProvider';\n/** Dispatched on `window` by a wallet; `detail` is a frozen {@link KaspaProviderDetail}.\n * KIP-12 canonical name. */\nexport const KASPA_ANNOUNCE_PROVIDER_EVENT = 'kaspa:provider';\n\n/** The `detail` of a `kaspa:announceProvider` CustomEvent. */\nexport type KaspaProviderDetail = { info: KaspaProviderInfo; provider: KaspaProvider };\n\n/**\n * WALLET SIDE — call once when your content script loads. Announces immediately AND replays on every\n * `kaspa:requestProvider` (so a dApp that loads after you still finds you). Returns an unsubscribe\n * (rarely needed; e.g. extension teardown). No-op outside a window context.\n */\nexport function announceKaspaWallet(info: KaspaProviderInfo, provider: KaspaProvider): () => void {\n if (typeof window === 'undefined') return () => {};\n const detail: KaspaProviderDetail = Object.freeze({ info: Object.freeze({ ...info }), provider });\n const announce = () =>\n window.dispatchEvent(new CustomEvent(KASPA_ANNOUNCE_PROVIDER_EVENT, { detail }));\n window.addEventListener(KASPA_REQUEST_PROVIDER_EVENT, announce);\n announce();\n return () => window.removeEventListener(KASPA_REQUEST_PROVIDER_EVENT, announce);\n}\n\n/** An icon is safe to render only as an inline `data:` URI. A remote URL is a tracking/spoofing vector\n * (SPEC §8), so the dApp-side handshake refuses it. */\nconst isSafeIcon = (icon: unknown): icon is string => typeof icon === 'string' && /^data:/i.test(icon.trim());\n\n/**\n * dApp SIDE — register `onAnnounce` (fires once per announce event, including replays; dedupe by\n * `info.rdns ?? info.uuid` yourself), then request announcements from wallets already present. Keep the\n * subscription alive for the page lifetime to catch late-injecting wallets. Returns an unsubscribe.\n *\n * Two receiver-side safety filters run before an announce reaches `onAnnounce`:\n * • Malformed announces (missing `uuid`/`name`, or no `requestAccounts`) are dropped, never delivered.\n * • A non-`data:` `icon` (a remote-URL tracking/spoofing vector, SPEC §8) is STRIPPED to `''` — the\n * wallet is still surfaced, just without the unsafe icon. An absent or valid `data:` icon passes\n * through untouched (the announce stays the wallet's original frozen object).\n *\n * NEITHER filter authenticates the announcer: any page script can announce (SPEC §8), so treat a provider\n * as untrusted until the user explicitly connects — that connect gesture is the trust boundary, not this\n * handshake. No-op outside a window context.\n */\nexport function requestKaspaWallets(onAnnounce: (detail: KaspaProviderDetail) => void): () => void {\n if (typeof window === 'undefined') return () => {};\n const listener = (e: Event) => {\n const detail = (e as CustomEvent<KaspaProviderDetail>).detail;\n if (!detail?.info?.uuid || !detail?.info?.name) return;\n if (typeof detail.provider?.requestAccounts !== 'function') return;\n if (detail.info.icon != null && !isSafeIcon(detail.info.icon)) {\n // Strip the unsafe icon but keep the wallet: deliver a sanitized copy; the wallet's frozen\n // original is left untouched (a valid/absent icon skips this and passes through frozen).\n onAnnounce({ info: { ...detail.info, icon: '' }, provider: detail.provider });\n return;\n }\n onAnnounce(detail);\n };\n window.addEventListener(KASPA_ANNOUNCE_PROVIDER_EVENT, listener);\n window.dispatchEvent(new Event(KASPA_REQUEST_PROVIDER_EVENT));\n return () => window.removeEventListener(KASPA_ANNOUNCE_PROVIDER_EVENT, listener);\n}\n"],"mappings":";AA2BO,IAAM,iBAAiB;AAAA,EAC5B,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,QAAQ;AACV;AAOO,IAAM,0BAA0B,CAAC,OACtC,GAAG,WAAW,QAAQ,IAAI,GAAG,MAAM,SAAS,MAAM,EAAE,QAAQ,MAAM,GAAG,IAAI;AAkEpE,IAAM,eAAe;AAAA;AAAA,EAE1B,eAAe;AAAA;AAAA,EAEf,cAAc;AAAA;AAAA,EAEd,oBAAoB;AAAA;AAAA,EAEpB,oBAAoB;AACtB;AA6DO,IAAM,+BAA+B;AAGrC,IAAM,gCAAgC;AAUtC,SAAS,oBAAoB,MAAyB,UAAqC;AAChG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,SAA8B,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,EAAE,GAAG,KAAK,CAAC,GAAG,SAAS,CAAC;AAChG,QAAM,WAAW,MACf,OAAO,cAAc,IAAI,YAAY,+BAA+B,EAAE,OAAO,CAAC,CAAC;AACjF,SAAO,iBAAiB,8BAA8B,QAAQ;AAC9D,WAAS;AACT,SAAO,MAAM,OAAO,oBAAoB,8BAA8B,QAAQ;AAChF;AAIA,IAAM,aAAa,CAAC,SAAkC,OAAO,SAAS,YAAY,UAAU,KAAK,KAAK,KAAK,CAAC;AAiBrG,SAAS,oBAAoB,YAA+D;AACjG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,MAAa;AAC7B,UAAM,SAAU,EAAuC;AACvD,QAAI,CAAC,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,KAAM;AAChD,QAAI,OAAO,OAAO,UAAU,oBAAoB,WAAY;AAC5D,QAAI,OAAO,KAAK,QAAQ,QAAQ,CAAC,WAAW,OAAO,KAAK,IAAI,GAAG;AAG7D,iBAAW,EAAE,MAAM,EAAE,GAAG,OAAO,MAAM,MAAM,GAAG,GAAG,UAAU,OAAO,SAAS,CAAC;AAC5E;AAAA,IACF;AACA,eAAW,MAAM;AAAA,EACnB;AACA,SAAO,iBAAiB,+BAA+B,QAAQ;AAC/D,SAAO,cAAc,IAAI,MAAM,4BAA4B,CAAC;AAC5D,SAAO,MAAM,OAAO,oBAAoB,+BAA+B,QAAQ;AACjF;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kaspa-wallet-standard",
3
- "version": "0.1.0",
4
- "description": "A proposed standard for dApp↔wallet interoperability on Kaspa: a provider interface plus an EIP-6963-style discovery handshake so any wallet can announce itself to any dApp. Zero dependencies.",
3
+ "version": "0.2.0",
4
+ "description": "A proposed standard for dApp\u2194wallet interoperability on Kaspa: a provider interface plus an EIP-6963-style discovery handshake so any wallet can announce itself to any dApp. Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "Shawn Pearce",
7
7
  "type": "module",