kaspa-wallet-standard 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/SPEC.md +24 -8
- package/dist/index.d.ts +13 -3
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,7 +93,10 @@ const unsubscribe = requestKaspaWallets(({ info, provider }) => {
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
Then on click: `const [address] = await provider.requestAccounts();`. Capability-check optional methods
|
|
96
|
-
before using them
|
|
96
|
+
before using them. `requestKaspaWallets` already strips any non-`data:` `info.icon` for you (a remote URL
|
|
97
|
+
is a tracking/spoofing vector), so what reaches your callback is safe to render. The announce is **not** an
|
|
98
|
+
identity proof, though — any script can announce any name/rdns, so require an explicit user connect before
|
|
99
|
+
signing and treat silent `getAccounts()` restore as display-only. See [SPEC §8](SPEC.md#8-security-considerations).
|
|
97
100
|
|
|
98
101
|
## Who's using it
|
|
99
102
|
|
package/SPEC.md
CHANGED
|
@@ -100,10 +100,16 @@ type KaspaProviderInfo = {
|
|
|
100
100
|
type KaspaProviderDetail = { info: KaspaProviderInfo; provider: KaspaProvider };
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
- The wallet **MUST** freeze `detail` (and `detail.info`) so page
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
- The wallet **MUST** freeze `detail` (and `detail.info`) so a page script cannot mutate an announce
|
|
104
|
+
**in flight** (swap the `provider` or a field on an already-dispatched event). Freezing protects the
|
|
105
|
+
integrity of a *single* announce only — it does **not** authenticate the announcer, and a hostile
|
|
106
|
+
script can still dispatch its **own** competing announce. Freezing is not a trust signal (see §8).
|
|
107
|
+
- The wallet **SHOULD** provide a stable `rdns`; without it a dApp cannot restore the session after a
|
|
108
|
+
reload (there is no stable identity to match). `rdns` is a convenience key, **not** an authenticity
|
|
109
|
+
claim — any script can announce any `rdns` (see §8).
|
|
110
|
+
- The dApp **SHOULD** dedupe announces by `info.rdns ?? info.uuid` (first-announce-wins). Because `rdns`
|
|
111
|
+
is spoofable, a dApp **MAY** surface a *second* announce for an already-seen `rdns` as a warning rather
|
|
112
|
+
than silently discarding it.
|
|
107
113
|
|
|
108
114
|
## 6. Network ids
|
|
109
115
|
|
|
@@ -122,11 +128,21 @@ unavoidable, would ship under a **new event name** — never by mutating these.
|
|
|
122
128
|
|
|
123
129
|
- `name`/`icon` are **display hints, not trust signals**. An announce proves a provider is *present*, not
|
|
124
130
|
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
|
-
|
|
127
|
-
|
|
131
|
+
icons (a remote URL is a tracking/spoofing vector). The reference `requestKaspaWallets` **enforces this
|
|
132
|
+
for you** — it strips any non-`data:` icon (delivers it as `''`) before handing the announce to your
|
|
133
|
+
callback.
|
|
134
|
+
- Any page script can dispatch `kaspa:announceProvider`, including one that claims another wallet's
|
|
135
|
+
`name`/`rdns`. Treat the provider as untrusted until the user explicitly connects; the wallet's own
|
|
136
|
+
connect/sign prompt — rendered by the extension, outside page control — is the trust boundary, not the
|
|
137
|
+
announce.
|
|
138
|
+
- **Silent session restore is display-only.** A dApp MAY use a remembered `rdns` to silently re-populate
|
|
139
|
+
*displayed* accounts via `getAccounts()` after a reload, but it **MUST** require a fresh explicit user
|
|
140
|
+
connect gesture before calling `signPskt` (or any signing). Never route a signature to a provider the
|
|
141
|
+
user did not explicitly (re-)select this session — `rdns` alone is spoofable and is not consent.
|
|
128
142
|
- 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.
|
|
143
|
+
lose user funds even though the handshake itself is benign. Note a spoofed in-page provider still
|
|
144
|
+
**cannot forge a signature** (it holds no keys); the realistic risk of announce-spoofing is *display
|
|
145
|
+
spoofing / phishing setup*, which the connect-gesture boundary above is designed to contain.
|
|
130
146
|
|
|
131
147
|
## 9. Reference implementation & adoption
|
|
132
148
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ type KaspaProviderInfo = {
|
|
|
13
13
|
uuid: string;
|
|
14
14
|
/** Human-readable wallet name shown in pickers, e.g. "Kastle". */
|
|
15
15
|
name: string;
|
|
16
|
-
/** Wallet icon as a `data:` URI (SVG/PNG)
|
|
16
|
+
/** Wallet icon as a `data:` URI (SVG/PNG) — a DISPLAY hint, never a trust signal. A remote URL is a
|
|
17
|
+
* tracking/spoofing vector, so the dApp-side {@link requestKaspaWallets} STRIPS any non-`data:` icon
|
|
18
|
+
* (delivers it as `''`) — a dApp can never be handed a remote URL through the handshake. */
|
|
17
19
|
icon: string;
|
|
18
20
|
/** Reverse-DNS identifier, e.g. "com.kasware" — STABLE across page loads and versions. Strongly
|
|
19
21
|
* recommended: it is what lets a dApp silently restore a session with your wallet after a reload. */
|
|
@@ -80,8 +82,16 @@ declare function announceKaspaWallet(info: KaspaProviderInfo, provider: KaspaPro
|
|
|
80
82
|
* dApp SIDE — register `onAnnounce` (fires once per announce event, including replays; dedupe by
|
|
81
83
|
* `info.rdns ?? info.uuid` yourself), then request announcements from wallets already present. Keep the
|
|
82
84
|
* subscription alive for the page lifetime to catch late-injecting wallets. Returns an unsubscribe.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
+
*
|
|
86
|
+
* Two receiver-side safety filters run before an announce reaches `onAnnounce`:
|
|
87
|
+
* • Malformed announces (missing `uuid`/`name`, or no `requestAccounts`) are dropped, never delivered.
|
|
88
|
+
* • A non-`data:` `icon` (a remote-URL tracking/spoofing vector, SPEC §8) is STRIPPED to `''` — the
|
|
89
|
+
* wallet is still surfaced, just without the unsafe icon. An absent or valid `data:` icon passes
|
|
90
|
+
* through untouched (the announce stays the wallet's original frozen object).
|
|
91
|
+
*
|
|
92
|
+
* NEITHER filter authenticates the announcer: any page script can announce (SPEC §8), so treat a provider
|
|
93
|
+
* as untrusted until the user explicitly connects — that connect gesture is the trust boundary, not this
|
|
94
|
+
* handshake. No-op outside a window context.
|
|
85
95
|
*/
|
|
86
96
|
declare function requestKaspaWallets(onAnnounce: (detail: KaspaProviderDetail) => void): () => void;
|
|
87
97
|
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ function announceKaspaWallet(info, provider) {
|
|
|
16
16
|
announce();
|
|
17
17
|
return () => window.removeEventListener(KASPA_REQUEST_PROVIDER_EVENT, announce);
|
|
18
18
|
}
|
|
19
|
+
var isSafeIcon = (icon) => typeof icon === "string" && /^data:/i.test(icon.trim());
|
|
19
20
|
function requestKaspaWallets(onAnnounce) {
|
|
20
21
|
if (typeof window === "undefined") return () => {
|
|
21
22
|
};
|
|
@@ -23,6 +24,10 @@ function requestKaspaWallets(onAnnounce) {
|
|
|
23
24
|
const detail = e.detail;
|
|
24
25
|
if (!detail?.info?.uuid || !detail?.info?.name) return;
|
|
25
26
|
if (typeof detail.provider?.requestAccounts !== "function") return;
|
|
27
|
+
if (detail.info.icon != null && !isSafeIcon(detail.info.icon)) {
|
|
28
|
+
onAnnounce({ info: { ...detail.info, icon: "" }, provider: detail.provider });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
26
31
|
onAnnounce(detail);
|
|
27
32
|
};
|
|
28
33
|
window.addEventListener(KASPA_ANNOUNCE_PROVIDER_EVENT, listener);
|
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).
|
|
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) — 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/**\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/** 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":";AAmBO,IAAM,iBAAiB;AAAA,EAC5B,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,QAAQ;AACV;AA4DO,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;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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kaspa-wallet-standard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
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.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Shawn Pearce",
|