@parity/product-sdk-terminal 0.9.1 → 0.10.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 +29 -9
- package/dist/{chunk-OKJQEB6P.js → chunk-EORPX5XL.js} +47 -31
- package/dist/chunk-EORPX5XL.js.map +1 -0
- package/dist/host.js +1 -1
- package/dist/index.d.ts +40 -33
- package/dist/index.js +81 -15
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-OKJQEB6P.js.map +0 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ const sessions = await waitForSessions(adapter, 2000);
|
|
|
50
50
|
// 5. Sign messages via the paired wallet
|
|
51
51
|
if (sessions.length > 0) {
|
|
52
52
|
const session = sessions[0];
|
|
53
|
-
const signer = createSessionSigner(session, adapter);
|
|
53
|
+
const signer = await createSessionSigner(session, adapter);
|
|
54
54
|
// use signer with polkadot-api transactions
|
|
55
55
|
}
|
|
56
56
|
```
|
|
@@ -97,26 +97,28 @@ answer from outside it.
|
|
|
97
97
|
|
|
98
98
|
Whether `error` is the benign teardown noise above (`Client destroyed`, matched on the message so it never swallows another package's `DestroyedError`). `destroy()` already drops it during its own teardown; export it so a consumer with its own `console.error` guard can drop the same line without reinventing the match.
|
|
99
99
|
|
|
100
|
-
### `createSessionSigner(session, adapter): PolkadotSigner
|
|
100
|
+
### `createSessionSigner(session, adapter): Promise<PolkadotSigner>`
|
|
101
101
|
|
|
102
102
|
Creates a `PolkadotSigner` backed by a QR-paired mobile wallet session, using the session's **default account** (`derivationIndex: 0`) under the adapter's `appId`. This is the right entry point for ~all CLI flows.
|
|
103
103
|
|
|
104
|
+
Async because the signer needs the product's subtree public key (RFC-0022), which only the paired wallet holds. That is one consent-free round trip the first time a product is seen; afterwards it is served from a cache on disk.
|
|
105
|
+
|
|
104
106
|
```ts
|
|
105
107
|
const [session] = adapter.sessions.sessions.read();
|
|
106
|
-
const signer = createSessionSigner(session, adapter);
|
|
108
|
+
const signer = await createSessionSigner(session, adapter);
|
|
107
109
|
await contract.publish.tx(domain, cid, { signer, origin });
|
|
108
110
|
```
|
|
109
111
|
|
|
110
|
-
### `createSessionSignerForAccount(session, ref): PolkadotSigner
|
|
112
|
+
### `createSessionSignerForAccount(session, ref): Promise<PolkadotSigner>`
|
|
111
113
|
|
|
112
114
|
Escape hatch for signing as a non-default sub-account of a paired session, or as a `productId` that differs from the adapter's `appId`. Most callers don't need this.
|
|
113
115
|
|
|
114
116
|
`ref` is `{ productId: string; derivationIndex: number }`:
|
|
115
117
|
- `productId` -- dotNS-style identifier of the requesting product. In normal usage this equals the adapter's `appId`; pass a different value only if you have an explicit reason.
|
|
116
|
-
- `derivationIndex` --
|
|
118
|
+
- `derivationIndex` -- child-key index within the product's subtree. `0` is the default account; non-zero indices reach additional sub-accounts of the same product.
|
|
117
119
|
|
|
118
120
|
```ts
|
|
119
|
-
const subSigner = createSessionSignerForAccount(session, {
|
|
121
|
+
const subSigner = await createSessionSignerForAccount(session, {
|
|
120
122
|
productId: "my-product",
|
|
121
123
|
derivationIndex: 3,
|
|
122
124
|
});
|
|
@@ -124,6 +126,22 @@ const subSigner = createSessionSignerForAccount(session, {
|
|
|
124
126
|
|
|
125
127
|
> **Wire format note:** `@novasamatech/host-papp` expects `productAccountId: [productId, { tag: "Index", value: derivationIndex }]` in `SigningRawRequest`. Both functions above hide that tuple — pass an adapter for the default case or a named-fields object for the escape hatch.
|
|
126
128
|
|
|
129
|
+
### `getProductSubtreePublicKey(session, productId, options?): Promise<Uint8Array>`
|
|
130
|
+
|
|
131
|
+
Fetches the sr25519 public key of `//product//{productId}` from the paired wallet (RFC-0022). Both signer factories call it for you; reach for it directly to warm the cache before going offline, or to derive a product address without building a signer.
|
|
132
|
+
|
|
133
|
+
The request is consent-free, so it raises no prompt on the phone, but it does need the phone reachable. The result is cached in memory and on disk, so only a cold cache reaches the wallet.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
await getProductSubtreePublicKey(session, adapter.appId, { appId: adapter.appId });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`options` is `ProductSubtreeOptions`:
|
|
140
|
+
- `appId` -- names the cache file (`~/.polkadot-apps/${appId}_ProductSubtrees.json`). Defaults to `productId`. **Pass your app id.** Anything that sweeps `~/.polkadot-apps/` by app-id prefix, such as a sign-out that clears local state, only finds the file if it is named after the app.
|
|
141
|
+
- `storageDir` -- overrides `~/.polkadot-apps/`, matching `createTerminalAdapter({ storageDir })`.
|
|
142
|
+
|
|
143
|
+
The cache file is written with mode `0600` and keyed by session and product, so re-pairing or switching wallet account does not reuse a stale key.
|
|
144
|
+
|
|
127
145
|
### `renderQrCode(data, options?): Promise<string>`
|
|
128
146
|
|
|
129
147
|
Render a string as a QR code using Unicode half-block characters for terminal display.
|
|
@@ -262,8 +280,8 @@ For consumers moving from `@polkadot-apps/terminal` v0.2.0 / v0.3.0. Existing se
|
|
|
262
280
|
| Package name | `@polkadot-apps/terminal` | `@parity/product-sdk-terminal` |
|
|
263
281
|
| WASM loader hook | `--import @polkadot-apps/terminal/register` (required) | **removed** — no `--import` needed (host-papp ≥0.7.9 dropped `verifiablejs`) |
|
|
264
282
|
| `createTerminalAdapter` | `async` — returned `Promise<TerminalAdapter>` | **sync** — returns `TerminalAdapter` directly. Drop the `await`. |
|
|
265
|
-
| Default account signer | `createSessionSigner(session)` | `createSessionSigner(session, adapter)` — pass the adapter as second arg |
|
|
266
|
-
| Non-default sub-account signer | not exposed | `createSessionSignerForAccount(session, { productId, derivationIndex })` |
|
|
283
|
+
| Default account signer | `createSessionSigner(session)` | `await createSessionSigner(session, adapter)` — pass the adapter as second arg, and await it |
|
|
284
|
+
| Non-default sub-account signer | not exposed | `await createSessionSignerForAccount(session, { productId, derivationIndex })` |
|
|
267
285
|
| Override session storage dir | not supported (hard-coded `~/.polkadot-apps/`) | `createTerminalAdapter({ ..., storageDir })` option |
|
|
268
286
|
| E2E test helper for sessions | none | `createTestSession` from `@parity/product-sdk-terminal/testing` |
|
|
269
287
|
| Node version | any (bundled `ws`) | **≥21** (uses global `WebSocket`) |
|
|
@@ -276,6 +294,8 @@ For consumers moving from `@polkadot-apps/terminal` v0.2.0 / v0.3.0. Existing se
|
|
|
276
294
|
- `createSessionSigner(session, adapter)` for the default account (uses `[adapter.appId, { tag: "Index", value: 0 }]`)
|
|
277
295
|
- `createSessionSignerForAccount(session, { productId, derivationIndex })` for everything else
|
|
278
296
|
|
|
297
|
+
Both return a promise since RFC-0022: a product account hangs off `//product//{productId}`, whose two hard junctions no public key can cross, so the subtree key has to be fetched from the paired wallet once per product.
|
|
298
|
+
|
|
279
299
|
The single-argument `createSessionSigner(session)` from `@polkadot-apps/terminal` no longer works against host-papp 0.7 regardless of which package you use.
|
|
280
300
|
|
|
281
301
|
### Migration steps
|
|
@@ -283,7 +303,7 @@ The single-argument `createSessionSigner(session)` from `@polkadot-apps/terminal
|
|
|
283
303
|
1. **Replace the dep**: `pnpm remove @polkadot-apps/terminal && pnpm add @parity/product-sdk-terminal`
|
|
284
304
|
2. **Remove the `--import @.../register` flag** from your `node` / `tsx` invocations or `package.json` scripts — the WASM loader hook no longer exists (and is no longer needed).
|
|
285
305
|
3. **Drop `await`** in front of `createTerminalAdapter(...)` calls.
|
|
286
|
-
4. **Update each `createSessionSigner` call site**: change `createSessionSigner(session)` → `createSessionSigner(session, adapter)`.
|
|
306
|
+
4. **Update each `createSessionSigner` call site**: change `createSessionSigner(session)` → `await createSessionSigner(session, adapter)`.
|
|
287
307
|
5. **Verify Node version** is ≥21 (`node --version`).
|
|
288
308
|
|
|
289
309
|
If your existing sessions don't appear after migrating, double-check that the `appId` is identical to what you used in `@polkadot-apps/terminal` — the on-disk file names depend on it.
|
|
@@ -14,40 +14,66 @@ var CACHE_FILE_MODE = 384;
|
|
|
14
14
|
function sanitizeAppId(appId) {
|
|
15
15
|
return appId.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
16
16
|
}
|
|
17
|
-
function
|
|
18
|
-
return join(storageDir ?? DEFAULT_STORAGE_DIR, `${sanitizeAppId(appId)}
|
|
17
|
+
function cacheFilePath(appId, kind, storageDir) {
|
|
18
|
+
return join(storageDir ?? DEFAULT_STORAGE_DIR, `${sanitizeAppId(appId)}_${kind}.json`);
|
|
19
19
|
}
|
|
20
|
-
function
|
|
21
|
-
return { version: 2, entries: {} };
|
|
22
|
-
}
|
|
23
|
-
async function loadCache(appId, storageDir) {
|
|
24
|
-
const path = cachePath(appId, storageDir);
|
|
20
|
+
async function loadJsonCache(path, version, label) {
|
|
25
21
|
let raw;
|
|
26
22
|
try {
|
|
27
23
|
raw = await readFile(path, "utf-8");
|
|
28
24
|
} catch (e) {
|
|
29
|
-
if (e?.code === "ENOENT") return
|
|
25
|
+
if (e?.code === "ENOENT") return null;
|
|
30
26
|
throw e;
|
|
31
27
|
}
|
|
32
28
|
try {
|
|
33
29
|
const parsed = JSON.parse(raw);
|
|
34
|
-
if (parsed?.version !==
|
|
35
|
-
log.warn(
|
|
36
|
-
return
|
|
30
|
+
if (parsed?.version !== version) {
|
|
31
|
+
log.warn(`${label} schema mismatch; starting fresh`, { path });
|
|
32
|
+
return null;
|
|
37
33
|
}
|
|
38
34
|
return parsed;
|
|
39
35
|
} catch (e) {
|
|
40
|
-
log.warn(
|
|
41
|
-
return
|
|
36
|
+
log.warn(`${label} parse failed; starting fresh`, { path, error: String(e) });
|
|
37
|
+
return null;
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
|
-
async function
|
|
45
|
-
const path = cachePath(appId, storageDir);
|
|
40
|
+
async function saveJsonCache(path, value) {
|
|
46
41
|
await mkdir(dirname(path), { recursive: true });
|
|
47
|
-
const tmp = `${path}.tmp`;
|
|
48
|
-
await writeFile(tmp, JSON.stringify(
|
|
42
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
43
|
+
await writeFile(tmp, JSON.stringify(value, null, 2), { mode: CACHE_FILE_MODE });
|
|
49
44
|
await rename(tmp, path);
|
|
50
45
|
}
|
|
46
|
+
var locks = /* @__PURE__ */ new Map();
|
|
47
|
+
function withFileLock(path, fn) {
|
|
48
|
+
const prev = locks.get(path) ?? Promise.resolve();
|
|
49
|
+
const next = prev.catch(() => {
|
|
50
|
+
}).then(fn);
|
|
51
|
+
locks.set(
|
|
52
|
+
path,
|
|
53
|
+
next.catch(() => {
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/host-cache.ts
|
|
60
|
+
function cachePath(appId, storageDir) {
|
|
61
|
+
return cacheFilePath(appId, "AllowanceKeys", storageDir);
|
|
62
|
+
}
|
|
63
|
+
function emptyCache() {
|
|
64
|
+
return { version: 2, entries: {} };
|
|
65
|
+
}
|
|
66
|
+
async function loadCache(appId, storageDir) {
|
|
67
|
+
const path = cachePath(appId, storageDir);
|
|
68
|
+
const cache = await loadJsonCache(path, 2, "allowance cache");
|
|
69
|
+
if (!cache || typeof cache.entries !== "object" || cache.entries === null) {
|
|
70
|
+
return emptyCache();
|
|
71
|
+
}
|
|
72
|
+
return cache;
|
|
73
|
+
}
|
|
74
|
+
async function saveCache(appId, cache, storageDir) {
|
|
75
|
+
await saveJsonCache(cachePath(appId, storageDir), cache);
|
|
76
|
+
}
|
|
51
77
|
function smartContractDest(dest) {
|
|
52
78
|
return dest.tag === "Index" ? `Index::${dest.value}` : `Raw::${toHex(dest.value)}`;
|
|
53
79
|
}
|
|
@@ -117,18 +143,8 @@ function mergeOutcomes(cache, requested, outcomes) {
|
|
|
117
143
|
function readCacheEntry(cache, resource) {
|
|
118
144
|
return cache.entries[cacheKey(resource)] ?? null;
|
|
119
145
|
}
|
|
120
|
-
var cacheLocks = /* @__PURE__ */ new Map();
|
|
121
146
|
function withCacheLock(appId, storageDir, fn) {
|
|
122
|
-
|
|
123
|
-
const prev = cacheLocks.get(key) ?? Promise.resolve();
|
|
124
|
-
const next = prev.catch(() => {
|
|
125
|
-
}).then(fn);
|
|
126
|
-
cacheLocks.set(
|
|
127
|
-
key,
|
|
128
|
-
next.catch(() => {
|
|
129
|
-
})
|
|
130
|
-
);
|
|
131
|
-
return next;
|
|
147
|
+
return withFileLock(cachePath(appId, storageDir), fn);
|
|
132
148
|
}
|
|
133
149
|
function canonicalSr25519SecretToEd25519Bytes(secret) {
|
|
134
150
|
const ed25519Scalar = numberToBytesLE(bytesToNumberLE(secret.subarray(0, 32)) << 3n, 32);
|
|
@@ -246,6 +262,6 @@ async function ensureSlotAccountSigner(session, adapter, resource, productId) {
|
|
|
246
262
|
});
|
|
247
263
|
}
|
|
248
264
|
|
|
249
|
-
export { createSlotAccountSigner, ensureSlotAccountSigner, getCachedAllocation, requestResourceAllocation, sendResourceAllocation };
|
|
250
|
-
//# sourceMappingURL=chunk-
|
|
251
|
-
//# sourceMappingURL=chunk-
|
|
265
|
+
export { cacheFilePath, createSlotAccountSigner, ensureSlotAccountSigner, getCachedAllocation, loadJsonCache, requestResourceAllocation, saveJsonCache, sendResourceAllocation, withFileLock };
|
|
266
|
+
//# sourceMappingURL=chunk-EORPX5XL.js.map
|
|
267
|
+
//# sourceMappingURL=chunk-EORPX5XL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/json-cache.ts","../src/host-cache.ts","../src/host-signer.ts","../src/host.ts"],"names":["log","createLogger"],"mappings":"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","file":"chunk-EORPX5XL.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Versioned JSON cache files, one per `appId` and kind.\n *\n * @internal\n */\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nconst log = createLogger(\"terminal\");\n\nexport const DEFAULT_STORAGE_DIR = join(homedir(), \".polkadot-apps\");\nconst CACHE_FILE_MODE = 0o600;\n\nexport interface VersionedCache {\n version: number;\n}\n\nfunction sanitizeAppId(appId: string): string {\n return appId.replace(/[^a-zA-Z0-9_.-]/g, \"_\");\n}\n\nexport function cacheFilePath(appId: string, kind: string, storageDir?: string): string {\n return join(storageDir ?? DEFAULT_STORAGE_DIR, `${sanitizeAppId(appId)}_${kind}.json`);\n}\n\n/**\n * Read a cache file, or `null` when absent, unparseable, or written by another\n * schema version. Dropped rather than half-read: its entries belong to a shape\n * this build no longer understands.\n */\nexport async function loadJsonCache<T extends VersionedCache>(\n path: string,\n version: number,\n label: string,\n): Promise<T | null> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf-8\");\n } catch (e: unknown) {\n if ((e as NodeJS.ErrnoException)?.code === \"ENOENT\") return null;\n throw e;\n }\n try {\n const parsed = JSON.parse(raw) as T;\n if (parsed?.version !== version) {\n log.warn(`${label} schema mismatch; starting fresh`, { path });\n return null;\n }\n return parsed;\n } catch (e) {\n log.warn(`${label} parse failed; starting fresh`, { path, error: String(e) });\n return null;\n }\n}\n\n/**\n * Temp + rename so a crash cannot leave a half-written file. The temp name is\n * unique per write because a shared one collides: the first rename consumes the\n * file the second writer is about to move.\n */\nexport async function saveJsonCache<T extends VersionedCache>(\n path: string,\n value: T,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;\n await writeFile(tmp, JSON.stringify(value, null, 2), { mode: CACHE_FILE_MODE });\n await rename(tmp, path);\n}\n\nconst locks = new Map<string, Promise<unknown>>();\n\n/**\n * Without this, concurrent callers each snapshot the file before the other\n * writes and the loser's entries are lost. Cross-process writes still race, but\n * converge: the Account Holder answers every caller alike for a given key.\n */\nexport function withFileLock<T>(path: string, fn: () => Promise<T>): Promise<T> {\n const prev = locks.get(path) ?? Promise.resolve();\n // Neutralize a prior rejection so one failure does not block later waiters.\n const next = prev.catch(() => {}).then(fn);\n locks.set(\n path,\n next.catch(() => {}),\n );\n return next;\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Persistent allowance-key cache. One JSON file per `appId`, 0o600.\n *\n * Cache key is the variant tag, except `SmartContractAllowance::{dest}`\n * which disambiguates per-account PGAS pre-warming.\n *\n * @internal\n */\nimport { toHex } from \"@polkadot-api/utils\";\n\nimport type { AllocatableResource, ApAllocationOutcome } from \"./host.js\";\nimport { cacheFilePath, loadJsonCache, saveJsonCache, withFileLock } from \"./json-cache.js\";\n\n/** One cached allowance entry. Hex strings are 0x-prefixed. */\nexport type CachedAllocation =\n | { tag: \"BulletInAllowance\"; slotAccountKey: string }\n | { tag: \"StatementStoreAllowance\"; slotAccountKey: string }\n | { tag: \"SmartContractAllowance\"; dest: string }\n | {\n tag: \"AutoSigning\";\n ringVrfDomainEntropy: string;\n productRootPrivateKey: string;\n };\n\ninterface AllowanceCacheV2 {\n version: 2;\n entries: Record<string, CachedAllocation>;\n}\n\nfunction cachePath(appId: string, storageDir?: string): string {\n return cacheFilePath(appId, \"AllowanceKeys\", storageDir);\n}\n\nfunction emptyCache(): AllowanceCacheV2 {\n return { version: 2, entries: {} };\n}\n\nexport async function loadCache(appId: string, storageDir?: string): Promise<AllowanceCacheV2> {\n const path = cachePath(appId, storageDir);\n const cache = await loadJsonCache<AllowanceCacheV2>(path, 2, \"allowance cache\");\n if (!cache || typeof cache.entries !== \"object\" || cache.entries === null) {\n return emptyCache();\n }\n return cache;\n}\n\nexport async function saveCache(\n appId: string,\n cache: AllowanceCacheV2,\n storageDir?: string,\n): Promise<void> {\n await saveJsonCache(cachePath(appId, storageDir), cache);\n}\n\ntype SmartContractDest = Extract<AllocatableResource, { tag: \"SmartContractAllowance\" }>[\"value\"];\n\n/** JSON-safe, so it doubles as the cached `dest`. */\nexport function smartContractDest(dest: SmartContractDest): string {\n return dest.tag === \"Index\" ? `Index::${dest.value}` : `Raw::${toHex(dest.value)}`;\n}\n\nexport function cacheKey(resource: AllocatableResource): string {\n if (resource.tag === \"SmartContractAllowance\") {\n return `${resource.tag}::${smartContractDest(resource.value)}`;\n }\n return resource.tag;\n}\n\n/**\n * Pick the wire `onExisting` policy from current cache state. `Ignore`\n * unless every requested resource is a slot-table variant (Bulletin /\n * SSS) AND already cached, in which case `Increase`. SC and AutoSigning\n * aren't slot-additive so they always force `Ignore`.\n */\nexport function pickOnExistingPolicy(\n cache: AllowanceCacheV2,\n resources: AllocatableResource[],\n): \"Ignore\" | \"Increase\" {\n if (resources.length === 0) return \"Ignore\";\n const allSlotTableAndCached = resources.every((r) => {\n if (r.tag !== \"BulletInAllowance\" && r.tag !== \"StatementStoreAllowance\") return false;\n return Object.hasOwn(cache.entries, cacheKey(r));\n });\n return allSlotTableAndCached ? \"Increase\" : \"Ignore\";\n}\n\n/**\n * Merge `Allocated` outcomes into the cache. Non-`Allocated` outcomes\n * leave the cache unchanged. Returns the input `cache` reference rather\n * than a copy when no entry was added, so callers can short-circuit a\n * disk write via reference equality.\n *\n * Throws on length mismatch — `outcomes` must be length-matched with\n * `requested`. A divergence is a wire contract violation, not data to\n * silently truncate.\n */\nexport function mergeOutcomes(\n cache: AllowanceCacheV2,\n requested: AllocatableResource[],\n outcomes: ApAllocationOutcome[],\n): AllowanceCacheV2 {\n if (requested.length !== outcomes.length) {\n throw new Error(\n `mergeOutcomes: length mismatch — requested ${requested.length}, got ${outcomes.length}`,\n );\n }\n const entries = { ...cache.entries };\n let mutated = false;\n for (let i = 0; i < outcomes.length; i++) {\n const req = requested[i];\n const out = outcomes[i];\n if (out.tag !== \"Allocated\") continue;\n const inner = out.value;\n // outcomes must align positionally AND by variant with the request.\n // A divergence is a wire contract violation: throw rather than\n // silently miscaching a key under the wrong cache slot.\n if (inner.tag !== req.tag) {\n throw new Error(\n `mergeOutcomes: variant mismatch at index ${i} — requested ${req.tag}, got Allocated<${inner.tag}>`,\n );\n }\n const key = cacheKey(req);\n switch (inner.tag) {\n case \"BulletInAllowance\":\n case \"StatementStoreAllowance\":\n entries[key] = {\n tag: inner.tag,\n slotAccountKey: toHex(inner.value.slotAccountKey),\n };\n mutated = true;\n break;\n case \"SmartContractAllowance\":\n // dest comes from the request; the response payload is undefined.\n // Variant alignment is enforced above, so req.tag is guaranteed\n // to be SmartContractAllowance here — the cast narrows it.\n if (req.tag === \"SmartContractAllowance\") {\n entries[key] = {\n tag: \"SmartContractAllowance\",\n dest: smartContractDest(req.value),\n };\n mutated = true;\n }\n break;\n case \"AutoSigning\":\n entries[key] = {\n tag: \"AutoSigning\",\n ringVrfDomainEntropy: toHex(inner.value.ringVrfDomainEntropy),\n productRootPrivateKey: toHex(inner.value.productRootPrivateKey),\n };\n mutated = true;\n break;\n }\n }\n return mutated ? { version: 2, entries } : cache;\n}\n\n/** Look up a single cached allocation, or `null` if absent. */\nexport function readCacheEntry(\n cache: AllowanceCacheV2,\n resource: AllocatableResource,\n): CachedAllocation | null {\n return cache.entries[cacheKey(resource)] ?? null;\n}\n\nexport function withCacheLock<T>(\n appId: string,\n storageDir: string | undefined,\n fn: () => Promise<T>,\n): Promise<T> {\n return withFileLock(cachePath(appId, storageDir), fn);\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, beforeEach } = import.meta.vitest;\n const { mkdtempSync, rmSync } = await import(\"node:fs\");\n const { mkdir, writeFile } = await import(\"node:fs/promises\");\n const { tmpdir } = await import(\"node:os\");\n const { join } = await import(\"node:path\");\n\n let storageDir: string;\n beforeEach(() => {\n storageDir = mkdtempSync(join(tmpdir(), \"host-cache-test-\"));\n return () => rmSync(storageDir, { recursive: true, force: true });\n });\n\n describe(\"cacheKey\", () => {\n test(\"returns the variant tag for slot-table resources and AutoSigning\", () => {\n expect(cacheKey({ tag: \"BulletInAllowance\", value: undefined })).toBe(\n \"BulletInAllowance\",\n );\n expect(cacheKey({ tag: \"StatementStoreAllowance\", value: undefined })).toBe(\n \"StatementStoreAllowance\",\n );\n expect(cacheKey({ tag: \"AutoSigning\", value: undefined })).toBe(\"AutoSigning\");\n });\n\n test(\"disambiguates SmartContractAllowance by dest\", () => {\n expect(\n cacheKey({ tag: \"SmartContractAllowance\", value: { tag: \"Index\", value: 0 } }),\n ).toBe(\"SmartContractAllowance::Index::0\");\n expect(\n cacheKey({ tag: \"SmartContractAllowance\", value: { tag: \"Index\", value: 7 } }),\n ).toBe(\"SmartContractAllowance::Index::7\");\n });\n\n // tsc misses this: a template literal accepts an object.\n test(\"an Index and a Raw dest never share a cache key\", () => {\n const raw = new Uint8Array(32);\n raw[0] = 7;\n const indexKey = cacheKey({\n tag: \"SmartContractAllowance\",\n value: { tag: \"Index\", value: 7 },\n });\n const rawKey = cacheKey({\n tag: \"SmartContractAllowance\",\n value: { tag: \"Raw\", value: raw },\n });\n\n expect(rawKey).not.toBe(indexKey);\n expect(rawKey).not.toContain(\"[object Object]\");\n expect(indexKey).not.toContain(\"[object Object]\");\n });\n\n test(\"two different Raw dests get different keys\", () => {\n const a = new Uint8Array(32);\n const b = new Uint8Array(32);\n b[31] = 1;\n\n expect(\n cacheKey({ tag: \"SmartContractAllowance\", value: { tag: \"Raw\", value: a } }),\n ).not.toBe(\n cacheKey({ tag: \"SmartContractAllowance\", value: { tag: \"Raw\", value: b } }),\n );\n });\n });\n\n describe(\"loadCache / saveCache round-trip\", () => {\n test(\"loadCache on missing file returns empty cache, not an error\", async () => {\n const cache = await loadCache(\"my-app\", storageDir);\n expect(cache).toEqual({ version: 2, entries: {} });\n });\n\n // v1 entries belong to sessions the x25519 change already invalidated.\n test(\"a version 1 file on disk is dropped, not read\", async () => {\n const stale = {\n version: 1,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: \"0xdeadbeef\" },\n },\n };\n await mkdir(storageDir, { recursive: true });\n await writeFile(\n join(storageDir, \"my-app_AllowanceKeys.json\"),\n JSON.stringify(stale, null, 2),\n );\n\n expect(await loadCache(\"my-app\", storageDir)).toEqual({ version: 2, entries: {} });\n });\n\n test(\"saveCache then loadCache returns the same content\", async () => {\n const original: AllowanceCacheV2 = {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: \"0xdeadbeef\" },\n },\n };\n await saveCache(\"my-app\", original, storageDir);\n const reloaded = await loadCache(\"my-app\", storageDir);\n expect(reloaded).toEqual(original);\n });\n\n test(\"two appIds in the same storageDir don't collide\", async () => {\n const a: AllowanceCacheV2 = {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: \"0x01\" },\n },\n };\n const b: AllowanceCacheV2 = {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: \"0x02\" },\n },\n };\n await saveCache(\"app-a\", a, storageDir);\n await saveCache(\"app-b\", b, storageDir);\n expect((await loadCache(\"app-a\", storageDir)).entries.BulletInAllowance).toEqual(\n a.entries.BulletInAllowance,\n );\n expect((await loadCache(\"app-b\", storageDir)).entries.BulletInAllowance).toEqual(\n b.entries.BulletInAllowance,\n );\n });\n\n test(\"appId with non-alphanumeric characters sanitizes to a safe path\", async () => {\n // Verifies a path-traversal-style appId can't escape storageDir.\n const cache: AllowanceCacheV2 = { version: 2, entries: {} };\n await expect(saveCache(\"../escape\", cache, storageDir)).resolves.toBeUndefined();\n const reloaded = await loadCache(\"../escape\", storageDir);\n expect(reloaded).toEqual(cache);\n });\n\n test(\"malformed cache file is treated as empty, not thrown\", async () => {\n await writeFile(cachePath(\"my-app\", storageDir), \"{ not valid json\", \"utf-8\");\n const cache = await loadCache(\"my-app\", storageDir);\n expect(cache).toEqual({ version: 2, entries: {} });\n });\n\n test(\"version mismatch is treated as empty, not thrown\", async () => {\n await writeFile(\n cachePath(\"my-app\", storageDir),\n JSON.stringify({ version: 99, entries: {} }),\n \"utf-8\",\n );\n const cache = await loadCache(\"my-app\", storageDir);\n expect(cache).toEqual({ version: 2, entries: {} });\n });\n\n test(\"file is written with 0o600 permissions (defense-in-depth)\", async () => {\n const { stat } = await import(\"node:fs/promises\");\n const cache: AllowanceCacheV2 = { version: 2, entries: {} };\n await saveCache(\"my-app\", cache, storageDir);\n const s = await stat(cachePath(\"my-app\", storageDir));\n // Mask to permission bits only — file type bits are above 0o777.\n expect(s.mode & 0o777).toBe(0o600);\n });\n });\n\n describe(\"pickOnExistingPolicy\", () => {\n test(\"empty request resolves to Ignore\", () => {\n const cache = emptyCache();\n expect(pickOnExistingPolicy(cache, [])).toBe(\"Ignore\");\n });\n\n test(\"no cached entries → Ignore\", () => {\n const cache = emptyCache();\n expect(\n pickOnExistingPolicy(cache, [{ tag: \"BulletInAllowance\", value: undefined }]),\n ).toBe(\"Ignore\");\n });\n\n test(\"all slot-table resources cached → Increase\", () => {\n const cache: AllowanceCacheV2 = {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: \"0x01\" },\n StatementStoreAllowance: {\n tag: \"StatementStoreAllowance\",\n slotAccountKey: \"0x02\",\n },\n },\n };\n expect(\n pickOnExistingPolicy(cache, [\n { tag: \"BulletInAllowance\", value: undefined },\n { tag: \"StatementStoreAllowance\", value: undefined },\n ]),\n ).toBe(\"Increase\");\n });\n\n test(\"mixed cached + uncached → Ignore (conservative)\", () => {\n const cache: AllowanceCacheV2 = {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: \"0x01\" },\n },\n };\n expect(\n pickOnExistingPolicy(cache, [\n { tag: \"BulletInAllowance\", value: undefined },\n { tag: \"StatementStoreAllowance\", value: undefined },\n ]),\n ).toBe(\"Ignore\");\n });\n\n test(\"AutoSigning in the request → Ignore even if cached (not slot-additive)\", () => {\n const cache: AllowanceCacheV2 = {\n version: 2,\n entries: {\n AutoSigning: {\n tag: \"AutoSigning\",\n ringVrfDomainEntropy: \"0x01\",\n productRootPrivateKey: \"0xaa\",\n },\n },\n };\n expect(pickOnExistingPolicy(cache, [{ tag: \"AutoSigning\", value: undefined }])).toBe(\n \"Ignore\",\n );\n });\n\n test(\"SC in the request → Ignore (not slot-additive; PGAS claim is per-call)\", () => {\n const cache: AllowanceCacheV2 = {\n version: 2,\n entries: {\n \"SmartContractAllowance::Index::5\": {\n tag: \"SmartContractAllowance\",\n dest: \"Index::5\",\n },\n },\n };\n expect(\n pickOnExistingPolicy(cache, [\n { tag: \"SmartContractAllowance\", value: { tag: \"Index\", value: 5 } },\n ]),\n ).toBe(\"Ignore\");\n });\n });\n\n describe(\"mergeOutcomes\", () => {\n test(\"ignores Rejected and NotAvailable\", () => {\n const cache = emptyCache();\n const requested: AllocatableResource[] = [\n { tag: \"BulletInAllowance\", value: undefined },\n { tag: \"StatementStoreAllowance\", value: undefined },\n ];\n const outcomes: ApAllocationOutcome[] = [\n { tag: \"Rejected\", value: undefined },\n { tag: \"NotAvailable\", value: undefined },\n ];\n const merged = mergeOutcomes(cache, requested, outcomes);\n expect(merged.entries).toEqual({});\n });\n\n test(\"stores Allocated Bulletin/SSS keys with hex encoding\", () => {\n const cache = emptyCache();\n const requested: AllocatableResource[] = [\n { tag: \"BulletInAllowance\", value: undefined },\n ];\n const outcomes: ApAllocationOutcome[] = [\n {\n tag: \"Allocated\",\n value: {\n tag: \"BulletInAllowance\",\n value: { slotAccountKey: new Uint8Array([0xde, 0xad, 0xbe, 0xef]) },\n },\n },\n ];\n const merged = mergeOutcomes(cache, requested, outcomes);\n expect(merged.entries.BulletInAllowance).toEqual({\n tag: \"BulletInAllowance\",\n slotAccountKey: \"0xdeadbeef\",\n });\n });\n\n test(\"stores SmartContractAllowance with dest from the request\", () => {\n const cache = emptyCache();\n const requested: AllocatableResource[] = [\n { tag: \"SmartContractAllowance\", value: { tag: \"Index\", value: 7 } },\n ];\n const outcomes: ApAllocationOutcome[] = [\n {\n tag: \"Allocated\",\n value: { tag: \"SmartContractAllowance\", value: undefined },\n },\n ];\n const merged = mergeOutcomes(cache, requested, outcomes);\n expect(merged.entries[\"SmartContractAllowance::Index::7\"]).toEqual({\n tag: \"SmartContractAllowance\",\n dest: \"Index::7\",\n });\n });\n\n test(\"stores AutoSigning subtree key + secret\", () => {\n const cache = emptyCache();\n const requested: AllocatableResource[] = [{ tag: \"AutoSigning\", value: undefined }];\n const outcomes: ApAllocationOutcome[] = [\n {\n tag: \"Allocated\",\n value: {\n tag: \"AutoSigning\",\n value: {\n ringVrfDomainEntropy: new Uint8Array([0xef]),\n productRootPrivateKey: new Uint8Array([0xab, 0xcd]),\n },\n },\n },\n ];\n const merged = mergeOutcomes(cache, requested, outcomes);\n expect(merged.entries.AutoSigning).toEqual({\n tag: \"AutoSigning\",\n ringVrfDomainEntropy: \"0xef\",\n productRootPrivateKey: \"0xabcd\",\n });\n });\n\n test(\"returns the same cache reference when no Allocated outcomes (no-op)\", () => {\n // Lets callers gate disk writes on reference equality.\n const cache: AllowanceCacheV2 = {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: \"0xaa\" },\n },\n };\n const requested: AllocatableResource[] = [\n { tag: \"BulletInAllowance\", value: undefined },\n { tag: \"StatementStoreAllowance\", value: undefined },\n ];\n const outcomes: ApAllocationOutcome[] = [\n { tag: \"Rejected\", value: undefined },\n { tag: \"NotAvailable\", value: undefined },\n ];\n expect(mergeOutcomes(cache, requested, outcomes)).toBe(cache);\n });\n\n test(\"empty inputs return the same cache reference\", () => {\n const cache = emptyCache();\n expect(mergeOutcomes(cache, [], [])).toBe(cache);\n });\n\n test(\"returns a new object reference when at least one entry is added\", () => {\n const cache = emptyCache();\n const requested: AllocatableResource[] = [\n { tag: \"BulletInAllowance\", value: undefined },\n ];\n const outcomes: ApAllocationOutcome[] = [\n {\n tag: \"Allocated\",\n value: {\n tag: \"BulletInAllowance\",\n value: { slotAccountKey: new Uint8Array([1]) },\n },\n },\n ];\n expect(mergeOutcomes(cache, requested, outcomes)).not.toBe(cache);\n });\n\n test(\"throws when outcomes do not length-match requested\", () => {\n const cache = emptyCache();\n const requested: AllocatableResource[] = [\n { tag: \"BulletInAllowance\", value: undefined },\n { tag: \"StatementStoreAllowance\", value: undefined },\n ];\n const outcomes: ApAllocationOutcome[] = [{ tag: \"Rejected\", value: undefined }];\n expect(() => mergeOutcomes(cache, requested, outcomes)).toThrow(/length mismatch/);\n });\n\n test(\"throws when an Allocated outcome's variant does not match the requested variant\", () => {\n // outcomes must align positionally AND by variant with the\n // request. Without this guard, a Bulletin key would get stored\n // under the StatementStoreAllowance cache slot.\n const cache = emptyCache();\n const requested: AllocatableResource[] = [\n { tag: \"StatementStoreAllowance\", value: undefined },\n ];\n const outcomes: ApAllocationOutcome[] = [\n {\n tag: \"Allocated\",\n value: {\n tag: \"BulletInAllowance\",\n value: { slotAccountKey: new Uint8Array([0xaa]) },\n },\n },\n ];\n expect(() => mergeOutcomes(cache, requested, outcomes)).toThrow(\n /variant mismatch at index 0 — requested StatementStoreAllowance, got Allocated<BulletInAllowance>/,\n );\n });\n\n test(\"preserves existing entries when merging new ones\", () => {\n const cache: AllowanceCacheV2 = {\n version: 2,\n entries: {\n StatementStoreAllowance: {\n tag: \"StatementStoreAllowance\",\n slotAccountKey: \"0xaa\",\n },\n },\n };\n const requested: AllocatableResource[] = [\n { tag: \"BulletInAllowance\", value: undefined },\n ];\n const outcomes: ApAllocationOutcome[] = [\n {\n tag: \"Allocated\",\n value: {\n tag: \"BulletInAllowance\",\n value: { slotAccountKey: new Uint8Array([0xbb]) },\n },\n },\n ];\n const merged = mergeOutcomes(cache, requested, outcomes);\n expect(Object.keys(merged.entries).sort()).toEqual([\n \"BulletInAllowance\",\n \"StatementStoreAllowance\",\n ]);\n expect(merged.entries.StatementStoreAllowance).toEqual({\n tag: \"StatementStoreAllowance\",\n slotAccountKey: \"0xaa\",\n });\n });\n });\n\n describe(\"readCacheEntry\", () => {\n test(\"returns the cached entry by resource discriminator\", () => {\n const cache: AllowanceCacheV2 = {\n version: 2,\n entries: {\n \"SmartContractAllowance::Index::5\": {\n tag: \"SmartContractAllowance\",\n dest: \"Index::5\",\n },\n },\n };\n expect(\n readCacheEntry(cache, {\n tag: \"SmartContractAllowance\",\n value: { tag: \"Index\", value: 5 },\n }),\n ).toEqual({\n tag: \"SmartContractAllowance\",\n dest: \"Index::5\",\n });\n // Different dest doesn't match.\n expect(\n readCacheEntry(cache, {\n tag: \"SmartContractAllowance\",\n value: { tag: \"Index\", value: 6 },\n }),\n ).toBeNull();\n });\n\n test(\"returns null when nothing cached\", () => {\n const cache = emptyCache();\n expect(\n readCacheEntry(cache, { tag: \"BulletInAllowance\", value: undefined }),\n ).toBeNull();\n });\n });\n\n describe(\"withCacheLock\", () => {\n test(\"serializes calls for the same (appId, storageDir)\", async () => {\n const order: string[] = [];\n const a = withCacheLock(\"app\", storageDir, async () => {\n order.push(\"a-start\");\n await new Promise((r) => setTimeout(r, 10));\n order.push(\"a-end\");\n });\n const b = withCacheLock(\"app\", storageDir, async () => {\n order.push(\"b-start\");\n order.push(\"b-end\");\n });\n await Promise.all([a, b]);\n expect(order).toEqual([\"a-start\", \"a-end\", \"b-start\", \"b-end\"]);\n });\n\n test(\"different (appId, storageDir) keys do not block each other\", async () => {\n let bRan = false;\n const a = withCacheLock(\"app-a\", storageDir, async () => {\n // b should run while a is still inside the await\n await new Promise((r) => setTimeout(r, 20));\n expect(bRan).toBe(true);\n });\n const b = withCacheLock(\"app-b\", storageDir, async () => {\n bRan = true;\n });\n await Promise.all([a, b]);\n });\n\n test(\"a rejection in one call does not block subsequent calls on the same key\", async () => {\n const a = withCacheLock(\"app\", storageDir, async () => {\n throw new Error(\"boom\");\n });\n await expect(a).rejects.toThrow(\"boom\");\n const b = await withCacheLock(\"app\", storageDir, async () => \"ok\");\n expect(b).toBe(\"ok\");\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Slot-account signer: reads the cached Bulletin/SSS allowance key and\n * returns a `PolkadotSigner` that signs locally with sr25519. SC and\n * AutoSigning are not slot-table variants and throw.\n *\n * @module\n */\nimport { bytesToNumberLE, numberToBytesLE } from \"@noble/curves/utils.js\";\nimport { fromHex } from \"@polkadot-api/utils\";\nimport { createDerive, sr25519, sr25519Derive } from \"@polkadot-labs/hdkd-helpers\";\nimport type { PolkadotSigner } from \"polkadot-api\";\nimport { getPolkadotSigner } from \"polkadot-api/signer\";\n\nimport type { TerminalAdapter } from \"./adapter.js\";\nimport { type CachedAllocation, loadCache, readCacheEntry } from \"./host-cache.js\";\nimport type { AllocatableResource } from \"./host.js\";\n\n// schnorrkel to_bytes() (canonical scalar) -> to_ed25519_bytes() form @scure expects (×8 cofactor).\nfunction canonicalSr25519SecretToEd25519Bytes(secret: Uint8Array): Uint8Array {\n const ed25519Scalar = numberToBytesLE(bytesToNumberLE(secret.subarray(0, 32)) << 3n, 32);\n const out = new Uint8Array(64);\n out.set(ed25519Scalar, 0);\n out.set(secret.subarray(32, 64), 32);\n return out;\n}\n\n// Wire encoding for slotAccountKey isn't pinned: mobile may send a\n// 32-byte mini-secret or a 64-byte expanded secret. Detect length and\n// route; any other length throws.\nfunction buildKeypair(secret: Uint8Array): {\n publicKey: Uint8Array;\n sign: (data: Uint8Array) => Uint8Array;\n} {\n if (secret.length === 32) {\n const derive = createDerive({ seed: secret, curve: sr25519, derive: sr25519Derive });\n const keypair = derive(\"\");\n return { publicKey: keypair.publicKey, sign: (data) => keypair.sign(data) };\n }\n if (secret.length === 64) {\n const ed25519Secret = canonicalSr25519SecretToEd25519Bytes(secret);\n return {\n publicKey: sr25519.getPublicKey(ed25519Secret),\n sign: (data) => sr25519.sign(data, ed25519Secret),\n };\n }\n throw new Error(\n `createSlotAccountSigner: unexpected slotAccountKey length ${secret.length}. Expected 32 (mini-secret) or 64 (expanded secret).`,\n );\n}\n\n/**\n * Build a `PolkadotSigner` from an already-loaded cache entry. Throws\n * for SC and AutoSigning — they don't carry a slot account key. See\n * the module docstring.\n *\n * Useful when the caller already holds the cache in memory, e.g. inside\n * a `withCacheLock` block, and wants to avoid re-reading disk.\n */\nexport function buildSignerFromEntry(entry: CachedAllocation): PolkadotSigner {\n if (entry.tag !== \"BulletInAllowance\" && entry.tag !== \"StatementStoreAllowance\") {\n throw new Error(\n `createSlotAccountSigner: ${entry.tag} does not carry a slot account key. Slot-table signing is defined for BulletInAllowance and StatementStoreAllowance only.`,\n );\n }\n const secret = fromHex(entry.slotAccountKey);\n const { publicKey, sign } = buildKeypair(secret);\n return getPolkadotSigner(publicKey, \"Sr25519\", async (data) => sign(data));\n}\n\n/**\n * `PolkadotSigner` backed by the cached slot account key. Returns `null`\n * when nothing's cached for `(productId, resource)`, where `productId`\n * defaults to `adapter.appId`. Throws for SC and AutoSigning (no slot\n * account key — see module docstring).\n *\n * Signs locally with sr25519 — no wallet round-trip on the hot path.\n *\n * @param productId Slot-cache namespace to read. Defaults to\n * `adapter.appId`; pass the same id the allocation was requested under\n * when the product id differs from the terminal's storage `appId`.\n *\n * @example\n * ```ts\n * const signer = await createSlotAccountSigner(adapter, {\n * tag: \"BulletInAllowance\", value: undefined,\n * });\n * if (signer) await tx.submitAndWatch(signer);\n * ```\n */\nexport async function createSlotAccountSigner(\n adapter: TerminalAdapter,\n resource: AllocatableResource,\n productId?: string,\n): Promise<PolkadotSigner | null> {\n const cache = await loadCache(productId ?? adapter.appId, adapter.storageDir);\n const entry = readCacheEntry(cache, resource);\n if (!entry) return null;\n return buildSignerFromEntry(entry);\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, beforeEach } = import.meta.vitest;\n const { mkdtempSync, rmSync } = await import(\"node:fs\");\n const { tmpdir } = await import(\"node:os\");\n const { join: pathJoin } = await import(\"node:path\");\n const { saveCache } = await import(\"./host-cache.js\");\n const { DEV_PHRASE, mnemonicToMiniSecret } = await import(\"@polkadot-labs/hdkd-helpers\");\n const { toHex } = await import(\"@polkadot-api/utils\");\n\n let storageDir: string;\n beforeEach(() => {\n storageDir = mkdtempSync(pathJoin(tmpdir(), \"host-signer-test-\"));\n return () => rmSync(storageDir, { recursive: true, force: true });\n });\n\n function fakeAdapter(appId: string): TerminalAdapter {\n return { appId, storageDir } as unknown as TerminalAdapter;\n }\n\n function knownSecret(): {\n secret: Uint8Array;\n publicKey: Uint8Array;\n hex: string;\n sign: (msg: Uint8Array) => Uint8Array;\n } {\n // Deterministic across runs — uses the standard substrate dev\n // mnemonic. `mnemonicToMiniSecret` returns the 32-byte mini-secret\n // form (one of the two shapes `createSlotAccountSigner` accepts).\n const secret = mnemonicToMiniSecret(DEV_PHRASE);\n const derive = createDerive({ seed: secret, curve: sr25519, derive: sr25519Derive });\n const keypair = derive(\"\");\n return {\n secret,\n publicKey: keypair.publicKey,\n hex: toHex(secret),\n sign: (msg) => keypair.sign(msg),\n };\n }\n\n describe(\"createSlotAccountSigner\", () => {\n test(\"returns null when no allocation is cached\", async () => {\n const adapter = fakeAdapter(\"p\");\n const signer = await createSlotAccountSigner(adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(signer).toBeNull();\n });\n\n test(\"returns a signer with the sr25519 public key derived from the cached secret\", async () => {\n const { hex, publicKey } = knownSecret();\n await saveCache(\n \"p\",\n {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: hex },\n },\n },\n storageDir,\n );\n\n const signer = await createSlotAccountSigner(fakeAdapter(\"p\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(signer).not.toBeNull();\n expect(signer?.publicKey).toEqual(publicKey);\n });\n\n test(\"signs locally and the signature verifies against the cached account\", async () => {\n const { hex, publicKey } = knownSecret();\n await saveCache(\n \"p\",\n {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: hex },\n },\n },\n storageDir,\n );\n\n const signer = await createSlotAccountSigner(fakeAdapter(\"p\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n // `signBytes` wraps with `<Bytes>...</Bytes>` like all PAPI\n // PolkadotSigners do; for direct payload signing PAPI calls\n // `signTx` which routes through the same `sign` callback we\n // installed. Test that callback directly by reaching for the\n // typed surface: PAPI exposes signTx but the easiest direct\n // verification is `signBytes` which is wired through our sign.\n const payload = new TextEncoder().encode(\"hello slot\");\n // signBytes wraps in <Bytes>...</Bytes> before calling our sign.\n // To verify, reconstruct the wrap manually.\n const wrapped = new Uint8Array([\n ...new TextEncoder().encode(\"<Bytes>\"),\n ...payload,\n ...new TextEncoder().encode(\"</Bytes>\"),\n ]);\n const signature = await signer!.signBytes(payload);\n expect(sr25519.verify(signature, wrapped, publicKey)).toBe(true);\n });\n\n test(\"64-byte expanded-secret form produces a valid signer\", async () => {\n // Canonical schnorrkel to_bytes() for DEV_PHRASE (scalar + nonce), the mobile wire form.\n const SECRET_64_HEX =\n \"0x05d65584630d16cd4af6d0bec10f34bb504a5dcb62dba2122d49f5a663763d0afd190cce74df356432b410bd64682309d6dedb27c76845daf388557cbac3ca34\";\n\n await saveCache(\n \"p\",\n {\n version: 2,\n entries: {\n BulletInAllowance: {\n tag: \"BulletInAllowance\",\n slotAccountKey: SECRET_64_HEX,\n },\n },\n },\n storageDir,\n );\n\n const signer = await createSlotAccountSigner(fakeAdapter(\"p\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(signer).not.toBeNull();\n\n // Public key from the 64-byte path matches the 32-byte path's\n // — same logical key, two encodings.\n const { publicKey: pubFrom32Bytes } = knownSecret();\n expect(signer?.publicKey).toEqual(pubFrom32Bytes);\n\n // Signature from the 64-byte branch verifies against the same\n // public key.\n const payload = new TextEncoder().encode(\"hello 64-byte path\");\n const wrapped = new Uint8Array([\n ...new TextEncoder().encode(\"<Bytes>\"),\n ...payload,\n ...new TextEncoder().encode(\"</Bytes>\"),\n ]);\n const sig = await signer!.signBytes(payload);\n expect(sr25519.verify(sig, wrapped, pubFrom32Bytes)).toBe(true);\n });\n\n test(\"works for StatementStoreAllowance the same way\", async () => {\n const { hex, publicKey } = knownSecret();\n await saveCache(\n \"p\",\n {\n version: 2,\n entries: {\n StatementStoreAllowance: {\n tag: \"StatementStoreAllowance\",\n slotAccountKey: hex,\n },\n },\n },\n storageDir,\n );\n\n const signer = await createSlotAccountSigner(fakeAdapter(\"p\"), {\n tag: \"StatementStoreAllowance\",\n value: undefined,\n });\n expect(signer?.publicKey).toEqual(publicKey);\n });\n\n test(\"throws for SmartContractAllowance (no slot account key)\", async () => {\n await saveCache(\n \"p\",\n {\n version: 2,\n entries: {\n \"SmartContractAllowance::Index::5\": {\n tag: \"SmartContractAllowance\",\n dest: \"Index::5\",\n },\n },\n },\n storageDir,\n );\n\n await expect(\n createSlotAccountSigner(fakeAdapter(\"p\"), {\n tag: \"SmartContractAllowance\",\n value: { tag: \"Index\", value: 5 },\n }),\n ).rejects.toThrow(/SmartContractAllowance does not carry a slot account key/);\n });\n\n test(\"throws for AutoSigning (subtree key, not slot account)\", async () => {\n await saveCache(\n \"p\",\n {\n version: 2,\n entries: {\n AutoSigning: {\n tag: \"AutoSigning\",\n ringVrfDomainEntropy: \"0xcd\",\n productRootPrivateKey: \"0xabcd\",\n },\n },\n },\n storageDir,\n );\n\n await expect(\n createSlotAccountSigner(fakeAdapter(\"p\"), {\n tag: \"AutoSigning\",\n value: undefined,\n }),\n ).rejects.toThrow(/AutoSigning does not carry a slot account key/);\n });\n\n test(\"explicit productId reads that cache namespace instead of adapter.appId\", async () => {\n const { hex, publicKey } = knownSecret();\n // Key cached under the product id, not the adapter's appId.\n await saveCache(\n \"my-product.dot\",\n {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: hex },\n },\n },\n storageDir,\n );\n\n const adapter = fakeAdapter(\"my-cli\");\n // Default (appId) namespace has nothing cached.\n expect(\n await createSlotAccountSigner(adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ).toBeNull();\n // The productId override finds the key.\n const signer = await createSlotAccountSigner(\n adapter,\n { tag: \"BulletInAllowance\", value: undefined },\n \"my-product.dot\",\n );\n expect(signer?.publicKey).toEqual(publicKey);\n });\n\n test(\"different appIds yield different signers from disjoint caches\", async () => {\n const a = knownSecret();\n // Distinct mini-secret → distinct derived keypair.\n const otherMiniSecret = new Uint8Array(32).fill(0x07);\n const otherKeypair = createDerive({\n seed: otherMiniSecret,\n curve: sr25519,\n derive: sr25519Derive,\n })(\"\");\n\n await saveCache(\n \"app-a\",\n {\n version: 2,\n entries: {\n BulletInAllowance: { tag: \"BulletInAllowance\", slotAccountKey: a.hex },\n },\n },\n storageDir,\n );\n await saveCache(\n \"app-b\",\n {\n version: 2,\n entries: {\n BulletInAllowance: {\n tag: \"BulletInAllowance\",\n slotAccountKey: toHex(otherMiniSecret),\n },\n },\n },\n storageDir,\n );\n\n const signerA = await createSlotAccountSigner(fakeAdapter(\"app-a\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n const signerB = await createSlotAccountSigner(fakeAdapter(\"app-b\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(signerA?.publicKey).toEqual(a.publicKey);\n expect(signerB?.publicKey).toEqual(otherKeypair.publicKey);\n expect(signerA?.publicKey).not.toEqual(signerB?.publicKey);\n });\n\n test(\"rejects an unexpected slotAccountKey length\", async () => {\n // Variable-length codec on the wire, but our keypair builder\n // only recognizes the standard 32 / 64 byte forms.\n await saveCache(\n \"p\",\n {\n version: 2,\n entries: {\n BulletInAllowance: {\n tag: \"BulletInAllowance\",\n slotAccountKey: toHex(new Uint8Array(48).fill(1)),\n },\n },\n },\n storageDir,\n );\n\n await expect(\n createSlotAccountSigner(fakeAdapter(\"p\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ).rejects.toThrow(/unexpected slotAccountKey length 48/);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Host-runner facet of `@parity/product-sdk-terminal`. A CLI using this\n * package plays the Host role per RFC-10; this module implements the\n * three §Stakeholders Host responsibilities (AP client, cache, signer)\n * over `@novasamatech/host-papp@0.10.0`'s `UserSession`.\n *\n * @module\n */\nimport type { UserSession } from \"@novasamatech/host-papp\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport type { ResultAsync } from \"neverthrow\";\nimport type { PolkadotSigner } from \"polkadot-api\";\n\nimport type { TerminalAdapter } from \"./adapter.js\";\nimport {\n type CachedAllocation,\n loadCache,\n mergeOutcomes,\n pickOnExistingPolicy,\n readCacheEntry,\n saveCache,\n withCacheLock,\n} from \"./host-cache.js\";\nimport { buildSignerFromEntry, createSlotAccountSigner } from \"./host-signer.js\";\n\n// `terminal` namespace matches adapter.ts / node-storage.ts.\nconst log = createLogger(\"terminal\");\n\n// Public re-exports — internal helpers (loadCache, mergeOutcomes, etc.)\n// stay private. CachedAllocation must be exported because it's the return\n// type of getCachedAllocation.\nexport type { CachedAllocation };\nexport { createSlotAccountSigner };\n\n// host-papp doesn't re-export these codec types from its root.\ntype ResourceAllocationRequest = Parameters<UserSession[\"requestResourceAllocation\"]>[0];\n\n/**\n * One resource a Host can request from the Account Holder. AutoSigning\n * currently returns `NotAvailable` on both Android and iOS wallets.\n */\nexport type AllocatableResource = ResourceAllocationRequest[\"resources\"][number];\n\n/**\n * `\"Ignore\"`: return existing keys if any, else allocate one slot.\n * `\"Increase\"`: add one slot to an existing allowance account.\n */\nexport type OnExistingAllowancePolicy = ResourceAllocationRequest[\"onExisting\"];\n\ntype ExtractOk<R> = R extends ResultAsync<infer T, unknown> ? T : never;\n\n/**\n * Per-resource outcome. `Allocated.value` carries the materialized payload\n * (slot account key for Bulletin/SSS; subtree key + secret for AutoSigning;\n * undefined for SC). Each entry is independent — no rollback on partial\n * success.\n */\nexport type ApAllocationOutcome = ExtractOk<\n ReturnType<UserSession[\"requestResourceAllocation\"]>\n>[number];\n\nexport interface RequestResourceAllocationOptions {\n /**\n * Override the auto-picked `onExisting`. Default: `Ignore` unless every\n * requested resource is a cached slot-table variant, then `Increase`.\n */\n onExisting?: OnExistingAllowancePolicy;\n /**\n * Product id the allocation is scoped to. Sent on the wire as\n * `callingProductId` and used as the slot-cache namespace. Defaults to\n * `adapter.appId`.\n *\n * Pass this when the product id the app signs as differs from the\n * terminal's storage `appId` — the wallet derives every per-product\n * artifact (including the on-chain account PGAS is minted to and\n * auto-mapped for) from this id, so allocating under the wrong id lands\n * the allowance on the wrong account. Mirrors the explicit `productId`\n * that {@link getBulletinSigner} already takes.\n */\n productId?: string;\n}\n\n/**\n * Send the AP `request_resource_allocation` message over the paired\n * session; block on the user's mobile dialog; return outcomes in request\n * order. Granted key material is cached on disk so subsequent calls skip\n * the wallet prompt.\n *\n * @throws If the session call fails (transport, timeout, AH protocol error).\n *\n * @example\n * ```ts\n * const [session] = adapter.sessions.sessions.read();\n * const outcomes = await requestResourceAllocation(session, adapter, [\n * { tag: \"BulletInAllowance\", value: undefined },\n * ]);\n * ```\n */\nexport async function requestResourceAllocation(\n session: UserSession,\n adapter: TerminalAdapter,\n resources: AllocatableResource[],\n options: RequestResourceAllocationOptions = {},\n): Promise<ApAllocationOutcome[]> {\n const productId = options.productId ?? adapter.appId;\n return withCacheLock(productId, adapter.storageDir, async () => {\n const cache = await loadCache(productId, adapter.storageDir);\n const onExisting = options.onExisting ?? pickOnExistingPolicy(cache, resources);\n log.debug(\"requestResourceAllocation\", {\n productId,\n resources: resources.map((r) => r.tag),\n onExisting,\n autoPolicy: options.onExisting === undefined,\n });\n\n const outcomes = await sendResourceAllocation(session, productId, resources, onExisting);\n\n const next = mergeOutcomes(cache, resources, outcomes);\n if (next !== cache) {\n await saveCache(productId, next, adapter.storageDir);\n }\n\n return outcomes;\n });\n}\n\n/**\n * Send an AP `request_resource_allocation` message over the paired session and\n * return the outcomes in request order. This is the raw wire call: no disk\n * cache, no `onExisting` auto-pick, no adapter. Callers that want the caching /\n * slot-reuse behaviour should use {@link requestResourceAllocation} instead;\n * this primitive exists for consumers (e.g. `@parity/product-sdk-auth`) that\n * only hold a `productId` and manage their own policy.\n *\n * @throws If the session call fails (transport, timeout, AH protocol error).\n */\nexport async function sendResourceAllocation(\n session: UserSession,\n productId: string,\n resources: AllocatableResource[],\n onExisting: OnExistingAllowancePolicy,\n): Promise<ApAllocationOutcome[]> {\n const result = await session.requestResourceAllocation({\n callingProductId: productId,\n resources,\n onExisting,\n });\n\n if (result.isErr()) {\n throw new Error(`requestResourceAllocation failed: ${result.error.message}`);\n }\n\n return result.value;\n}\n\n/**\n * Read a cached allowance entry without going over the wire. Returns\n * `null` if nothing's cached for `(productId, resource)`, where\n * `productId` defaults to `adapter.appId`. For `SmartContractAllowance`,\n * the same `dest` must be passed — entries are keyed per-dest.\n *\n * @param productId Slot-cache namespace to read. Defaults to\n * `adapter.appId`; pass the same id the allocation was requested under\n * (see {@link RequestResourceAllocationOptions.productId}).\n */\nexport async function getCachedAllocation(\n adapter: TerminalAdapter,\n resource: AllocatableResource,\n productId?: string,\n): Promise<CachedAllocation | null> {\n const cache = await loadCache(productId ?? adapter.appId, adapter.storageDir);\n return readCacheEntry(cache, resource);\n}\n\n/**\n * Cache hit → return signer. Cache miss → call\n * {@link requestResourceAllocation} for `[resource]`, then build the signer.\n * Throws on `Rejected` / `NotAvailable`.\n *\n * @param productId Product id the allocation is scoped to (wire\n * `callingProductId` + slot-cache namespace). Defaults to `adapter.appId`\n * (see {@link RequestResourceAllocationOptions.productId}).\n *\n * @example\n * ```ts\n * const signer = await ensureSlotAccountSigner(session, adapter, {\n * tag: \"BulletInAllowance\", value: undefined,\n * });\n * await someBulletinTx.submitAndWatch(signer);\n * ```\n */\nexport async function ensureSlotAccountSigner(\n session: UserSession,\n adapter: TerminalAdapter,\n resource: AllocatableResource,\n productId?: string,\n): Promise<PolkadotSigner> {\n const resolvedProductId = productId ?? adapter.appId;\n // The cache-hit check and the allocation path must share the same\n // critical section: otherwise two concurrent calls for the same\n // resource both see an empty cache, then both serialize through the\n // lock but each issues its own wallet prompt and burns a slot.\n return withCacheLock(resolvedProductId, adapter.storageDir, async () => {\n // Single loadCache for the whole critical section — we hold the\n // lock, so no other writer can change disk under us. The fast\n // cache-hit path and the slow allocate-then-build path both\n // reuse this in-memory `cache` reference.\n const cache = await loadCache(resolvedProductId, adapter.storageDir);\n const hit = readCacheEntry(cache, resource);\n if (hit) return buildSignerFromEntry(hit);\n\n log.debug(\"ensureSlotAccountSigner: cache miss, allocating\", { resource: resource.tag });\n\n // Use the cache-free `sendResourceAllocation` primitive, not the public\n // `requestResourceAllocation` wrapper: `withCacheLock` is non-reentrant,\n // so calling the wrapper from inside this critical section would\n // deadlock on the second lock acquisition. We do the merge → save here.\n const onExisting = pickOnExistingPolicy(cache, [resource]);\n const outcomes = await sendResourceAllocation(\n session,\n resolvedProductId,\n [resource],\n onExisting,\n );\n const next = mergeOutcomes(cache, [resource], outcomes);\n if (next !== cache) {\n await saveCache(resolvedProductId, next, adapter.storageDir);\n }\n\n const outcome = outcomes[0];\n if (!outcome) {\n // Unreachable: mergeOutcomes throws on length mismatch first.\n // Kept as a typed-non-null guard.\n throw new Error(\n `ensureSlotAccountSigner: protocol violation — empty outcomes for ${resource.tag}`,\n );\n }\n if (outcome.tag !== \"Allocated\") {\n throw new Error(\n `ensureSlotAccountSigner: allocation ${outcome.tag} for ${resource.tag}`,\n );\n }\n\n // Build the signer from the in-memory post-merge cache — no third\n // disk read.\n const freshEntry = readCacheEntry(next, resource);\n if (!freshEntry) {\n // Unreachable: the entry was just merged in.\n // Kept as a typed-non-null guard against mergeOutcomes regressions.\n throw new Error(\n `ensureSlotAccountSigner: allocation succeeded but cache lookup returned null for ${resource.tag}`,\n );\n }\n return buildSignerFromEntry(freshEntry);\n });\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { ok, err } = await import(\"neverthrow\");\n const { mkdtempSync, rmSync } = await import(\"node:fs\");\n const { tmpdir } = await import(\"node:os\");\n const { join: pathJoin } = await import(\"node:path\");\n // Needed by the ensureSlotAccountSigner suite. Lifted up here because\n // top-level await is only allowed at module top-level (or inside this\n // top-level vitest block), not inside `describe`.\n const { mnemonicToMiniSecret, DEV_PHRASE } = await import(\"@polkadot-labs/hdkd-helpers\");\n const { toHex } = await import(\"@polkadot-api/utils\");\n\n // Deriving from `UserSession` only catches a reshape of a variant this repo\n // constructs; a bump to any other variant compiles clean.\n type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2\n ? true\n : false;\n type Expect<T extends true> = T;\n\n type PinnedAllocatableResource =\n | { tag: \"StatementStoreAllowance\"; value: undefined }\n | {\n tag: \"SmartContractAllowance\";\n value: { tag: \"Index\"; value: number } | { tag: \"Raw\"; value: Uint8Array };\n }\n | { tag: \"AutoSigning\"; value: undefined }\n | { tag: \"BulletInAllowance\"; value: undefined };\n\n type _AllocatableResourceIsPinned = Expect<\n Equal<AllocatableResource, PinnedAllocatableResource>\n >;\n type _OnExistingPolicyIsPinned = Expect<\n Equal<OnExistingAllowancePolicy, \"Ignore\" | \"Increase\">\n >;\n\n type PinnedAllocatedResource =\n | { tag: \"StatementStoreAllowance\"; value: { slotAccountKey: Uint8Array } }\n | { tag: \"SmartContractAllowance\"; value: undefined }\n | {\n tag: \"AutoSigning\";\n value: { productRootPrivateKey: Uint8Array; ringVrfDomainEntropy: Uint8Array };\n }\n | { tag: \"BulletInAllowance\"; value: { slotAccountKey: Uint8Array } };\n\n type _ApAllocationOutcomeIsPinned = Expect<\n Equal<\n ApAllocationOutcome,\n | { tag: \"Rejected\"; value: undefined }\n | { tag: \"Allocated\"; value: PinnedAllocatedResource }\n | { tag: \"NotAvailable\"; value: undefined }\n >\n >;\n\n let testStorageDir: string;\n beforeEach(() => {\n // Per-test temp dir so cache writes don't leak between tests or\n // touch the real default storage directory. The wrapper inherits\n // `storageDir` from `adapter.storageDir`; we pin it here.\n testStorageDir = mkdtempSync(pathJoin(tmpdir(), \"host-test-\"));\n return () => rmSync(testStorageDir, { recursive: true, force: true });\n });\n\n function fakeAdapter(appId: string): TerminalAdapter {\n // appId + storageDir are the only fields the wrapper touches.\n return { appId, storageDir: testStorageDir } as unknown as TerminalAdapter;\n }\n\n type RequestCapture = ResourceAllocationRequest;\n type StubReturn = Awaited<ReturnType<UserSession[\"requestResourceAllocation\"]>>;\n\n function makeSession(opts: {\n requestResourceAllocation?: (req: RequestCapture) => Promise<StubReturn>;\n }): UserSession {\n return {\n requestResourceAllocation: vi.fn(\n opts.requestResourceAllocation ??\n (async () => {\n throw new Error(\"requestResourceAllocation not stubbed in this test\");\n }),\n ),\n } as unknown as UserSession;\n }\n\n describe(\"requestResourceAllocation\", () => {\n test(\"forwards resources verbatim and returns outcomes in order\", async () => {\n const captured: RequestCapture[] = [];\n const stubbed = [\n {\n tag: \"Allocated\" as const,\n value: {\n tag: \"BulletInAllowance\" as const,\n value: { slotAccountKey: new Uint8Array([1, 2, 3]) },\n },\n },\n { tag: \"Rejected\" as const, value: undefined },\n ];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok(stubbed) as StubReturn;\n },\n });\n\n const outcomes = await requestResourceAllocation(session, fakeAdapter(\"my-app\"), [\n { tag: \"BulletInAllowance\", value: undefined },\n { tag: \"StatementStoreAllowance\", value: undefined },\n ]);\n\n expect(captured).toHaveLength(1);\n expect(captured[0].callingProductId).toBe(\"my-app\");\n expect(captured[0].resources.map((r) => r.tag)).toEqual([\n \"BulletInAllowance\",\n \"StatementStoreAllowance\",\n ]);\n expect(outcomes).toEqual(stubbed);\n });\n\n test(\"uses adapter.appId as callingProductId by default\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok([]) as StubReturn;\n },\n });\n\n await requestResourceAllocation(session, fakeAdapter(\"alt-product.dot\"), []);\n\n expect(captured[0].callingProductId).toBe(\"alt-product.dot\");\n });\n\n test(\"options.productId overrides adapter.appId as callingProductId\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok([]) as StubReturn;\n },\n });\n\n await requestResourceAllocation(session, fakeAdapter(\"my-cli\"), [], {\n productId: \"my-product.dot\",\n });\n\n expect(captured[0].callingProductId).toBe(\"my-product.dot\");\n });\n\n test(\"auto-picks Ignore for an empty resources array\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok([]) as StubReturn;\n },\n });\n\n await requestResourceAllocation(session, fakeAdapter(\"p\"), []);\n\n expect(captured[0].onExisting).toBe(\"Ignore\");\n });\n\n test(\"forwards onExisting when explicitly set to 'Increase'\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok([{ tag: \"Rejected\" as const, value: undefined }]) as StubReturn;\n },\n });\n\n await requestResourceAllocation(\n session,\n fakeAdapter(\"p\"),\n [{ tag: \"BulletInAllowance\", value: undefined }],\n { onExisting: \"Increase\" },\n );\n\n expect(captured[0].onExisting).toBe(\"Increase\");\n });\n\n test(\"forwards SmartContractAllowance derivation index\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok([{ tag: \"Rejected\" as const, value: undefined }]) as StubReturn;\n },\n });\n\n await requestResourceAllocation(session, fakeAdapter(\"p\"), [\n { tag: \"SmartContractAllowance\", value: { tag: \"Index\", value: 7 } },\n ]);\n\n const sent = captured[0].resources[0];\n expect(sent.tag).toBe(\"SmartContractAllowance\");\n if (sent.tag === \"SmartContractAllowance\") {\n expect(sent.value).toEqual({ tag: \"Index\", value: 7 });\n }\n });\n\n test(\"throws a clear error when the session call fails\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () =>\n err(new Error(\"mobile disconnected\")) as StubReturn,\n });\n\n await expect(\n requestResourceAllocation(session, fakeAdapter(\"p\"), [\n { tag: \"BulletInAllowance\", value: undefined },\n ]),\n ).rejects.toThrow(\"requestResourceAllocation failed: mobile disconnected\");\n });\n\n test(\"forwards AutoSigning and returns NotAvailable verbatim (mobile-default path)\", async () => {\n // Both Android (paritytech/polkadot-app-android-v2#563) and iOS\n // (paritytech/polkadot-app-ios-v2#759) currently return NotAvailable\n // for any AutoSigning request. The wrapper must round-trip both\n // the request variant and the NotAvailable outcome without\n // filtering, transforming, or throwing — that's the only state\n // real users see for one of the four AllocatableResource variants\n // today.\n const captured: RequestCapture[] = [];\n const stubbed = [{ tag: \"NotAvailable\" as const, value: undefined }];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok(stubbed) as StubReturn;\n },\n });\n\n const outcomes = await requestResourceAllocation(session, fakeAdapter(\"p\"), [\n { tag: \"AutoSigning\", value: undefined },\n ]);\n\n const sent = captured[0].resources[0];\n expect(sent.tag).toBe(\"AutoSigning\");\n expect(outcomes).toEqual(stubbed);\n });\n\n test(\"passes empty resources through (no-op request still hits the wire)\", async () => {\n // Wrappers shouldn't second-guess the caller: an empty resource\n // list is a legitimate (if degenerate) request and the wire\n // accepts it. The Account Holder may use it as a probe.\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok([]) as StubReturn;\n },\n });\n\n const outcomes = await requestResourceAllocation(session, fakeAdapter(\"p\"), []);\n\n expect(captured).toHaveLength(1);\n expect(captured[0].resources).toEqual([]);\n expect(outcomes).toEqual([]);\n });\n });\n\n describe(\"requestResourceAllocation — cache integration\", () => {\n function bulletinAllocated(slotKey: Uint8Array): StubReturn {\n return ok([\n {\n tag: \"Allocated\" as const,\n value: {\n tag: \"BulletInAllowance\" as const,\n value: { slotAccountKey: slotKey },\n },\n },\n ]) as StubReturn;\n }\n\n test(\"first call sends Ignore when cache is empty\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return bulletinAllocated(new Uint8Array([1, 2, 3]));\n },\n });\n\n await requestResourceAllocation(session, fakeAdapter(\"p\"), [\n { tag: \"BulletInAllowance\", value: undefined },\n ]);\n\n expect(captured[0].onExisting).toBe(\"Ignore\");\n });\n\n test(\"second call for same slot-table resource auto-picks Increase\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return bulletinAllocated(new Uint8Array([1, 2, 3]));\n },\n });\n\n const adapter = fakeAdapter(\"p\");\n const req: AllocatableResource[] = [{ tag: \"BulletInAllowance\", value: undefined }];\n\n await requestResourceAllocation(session, adapter, req);\n await requestResourceAllocation(session, adapter, req);\n\n expect(captured.map((c) => c.onExisting)).toEqual([\"Ignore\", \"Increase\"]);\n });\n\n test(\"explicit options.onExisting overrides the auto-pick\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return bulletinAllocated(new Uint8Array([1, 2, 3]));\n },\n });\n\n const adapter = fakeAdapter(\"p\");\n const req: AllocatableResource[] = [{ tag: \"BulletInAllowance\", value: undefined }];\n\n // Even after a cache-populating first call, passing Ignore\n // explicitly forces Ignore — useful for callers that want\n // idempotent \"ensure I have allowance\" semantics rather than\n // the additive-scaling default.\n await requestResourceAllocation(session, adapter, req);\n await requestResourceAllocation(session, adapter, req, { onExisting: \"Ignore\" });\n\n expect(captured.map((c) => c.onExisting)).toEqual([\"Ignore\", \"Ignore\"]);\n });\n\n test(\"populates cache on Allocated and survives across separate calls\", async () => {\n const slotKey = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);\n const session = makeSession({\n requestResourceAllocation: async () => bulletinAllocated(slotKey),\n });\n\n const adapter = fakeAdapter(\"p\");\n await requestResourceAllocation(session, adapter, [\n { tag: \"BulletInAllowance\", value: undefined },\n ]);\n\n // Second call reads cache via the public lookup, no wire.\n const cached = await getCachedAllocation(adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(cached).toEqual({\n tag: \"BulletInAllowance\",\n slotAccountKey: \"0xdeadbeef\",\n });\n });\n\n test(\"does not cache Rejected or NotAvailable outcomes\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () =>\n ok([\n { tag: \"Rejected\" as const, value: undefined },\n { tag: \"NotAvailable\" as const, value: undefined },\n ]) as StubReturn,\n });\n\n const adapter = fakeAdapter(\"p\");\n await requestResourceAllocation(session, adapter, [\n { tag: \"BulletInAllowance\", value: undefined },\n { tag: \"StatementStoreAllowance\", value: undefined },\n ]);\n\n expect(\n await getCachedAllocation(adapter, { tag: \"BulletInAllowance\", value: undefined }),\n ).toBeNull();\n expect(\n await getCachedAllocation(adapter, {\n tag: \"StatementStoreAllowance\",\n value: undefined,\n }),\n ).toBeNull();\n });\n\n test(\"mixed cached + uncached request stays on Ignore (conservative)\", async () => {\n // Bulletin cached, SSS not. Auto-pick must NOT send Increase\n // — the wire takes one policy for the whole request, and\n // Increase against an uncached resource is at-best a no-op\n // and at-worst a mis-allocation.\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n // Length-match the request: 1 or 2 outcomes as needed.\n return ok(\n req.resources.map((_r, i) =>\n i === 0\n ? {\n tag: \"Allocated\" as const,\n value: {\n tag: \"BulletInAllowance\" as const,\n value: { slotAccountKey: new Uint8Array([1]) },\n },\n }\n : { tag: \"Rejected\" as const, value: undefined },\n ),\n ) as StubReturn;\n },\n });\n\n const adapter = fakeAdapter(\"p\");\n // Populate Bulletin in cache.\n await requestResourceAllocation(session, adapter, [\n { tag: \"BulletInAllowance\", value: undefined },\n ]);\n\n // Now request both Bulletin (cached) and SSS (uncached).\n await requestResourceAllocation(session, adapter, [\n { tag: \"BulletInAllowance\", value: undefined },\n { tag: \"StatementStoreAllowance\", value: undefined },\n ]);\n\n expect(captured[captured.length - 1].onExisting).toBe(\"Ignore\");\n });\n\n test(\"AutoSigning never auto-Increases even when cached\", async () => {\n // AutoSigning isn't slot-additive; auto-pick must stay on Ignore.\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return ok([\n {\n tag: \"Allocated\" as const,\n value: {\n tag: \"AutoSigning\" as const,\n value: {\n ringVrfDomainEntropy: new Uint8Array([0xcd]),\n productRootPrivateKey: new Uint8Array([0xab]),\n },\n },\n },\n ]) as StubReturn;\n },\n });\n\n const adapter = fakeAdapter(\"p\");\n const req: AllocatableResource[] = [{ tag: \"AutoSigning\", value: undefined }];\n\n await requestResourceAllocation(session, adapter, req);\n await requestResourceAllocation(session, adapter, req);\n\n expect(captured.map((c) => c.onExisting)).toEqual([\"Ignore\", \"Ignore\"]);\n });\n\n test(\"parallel calls for different resources both persist their keys (no last-writer-wins)\", async () => {\n // Regression test for the load → wire → save race: without\n // serialization, both calls snapshot an empty cache, do their\n // wire round-trip, then save — the second save clobbers the\n // first. Both keys must end up in the cache.\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n // Force interleaving: each call yields before responding.\n await new Promise((r) => setTimeout(r, 5));\n const tag = req.resources[0]?.tag;\n return ok([\n {\n tag: \"Allocated\" as const,\n value:\n tag === \"BulletInAllowance\"\n ? {\n tag: \"BulletInAllowance\" as const,\n value: { slotAccountKey: new Uint8Array([0xaa]) },\n }\n : {\n tag: \"StatementStoreAllowance\" as const,\n value: { slotAccountKey: new Uint8Array([0xbb]) },\n },\n },\n ]) as StubReturn;\n },\n });\n const adapter = fakeAdapter(\"p\");\n\n await Promise.all([\n requestResourceAllocation(session, adapter, [\n { tag: \"BulletInAllowance\", value: undefined },\n ]),\n requestResourceAllocation(session, adapter, [\n { tag: \"StatementStoreAllowance\", value: undefined },\n ]),\n ]);\n\n expect(\n await getCachedAllocation(adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ).not.toBeNull();\n expect(\n await getCachedAllocation(adapter, {\n tag: \"StatementStoreAllowance\",\n value: undefined,\n }),\n ).not.toBeNull();\n });\n\n test(\"options.productId scopes the cache namespace, not adapter.appId\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () => bulletinAllocated(new Uint8Array([1, 2, 3])),\n });\n\n const adapter = fakeAdapter(\"my-cli\");\n await requestResourceAllocation(\n session,\n adapter,\n [{ tag: \"BulletInAllowance\", value: undefined }],\n { productId: \"my-product.dot\" },\n );\n\n // Cached under the product id...\n expect(\n await getCachedAllocation(\n adapter,\n { tag: \"BulletInAllowance\", value: undefined },\n \"my-product.dot\",\n ),\n ).not.toBeNull();\n // ...not under the adapter's appId.\n expect(\n await getCachedAllocation(adapter, { tag: \"BulletInAllowance\", value: undefined }),\n ).toBeNull();\n });\n\n test(\"two appIds maintain independent caches in the same storageDir\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () => bulletinAllocated(new Uint8Array([1, 2, 3])),\n });\n\n const adapterA = fakeAdapter(\"app-a\");\n const adapterB = fakeAdapter(\"app-b\");\n\n await requestResourceAllocation(session, adapterA, [\n { tag: \"BulletInAllowance\", value: undefined },\n ]);\n\n // app-b's cache is empty even though app-a's is populated.\n expect(\n await getCachedAllocation(adapterB, {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ).toBeNull();\n expect(\n await getCachedAllocation(adapterA, {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ).not.toBeNull();\n });\n });\n\n describe(\"getCachedAllocation\", () => {\n test(\"returns null when nothing is cached\", async () => {\n const adapter = fakeAdapter(\"p\");\n const cached = await getCachedAllocation(adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(cached).toBeNull();\n });\n\n test(\"disambiguates SmartContractAllowance entries by dest\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () =>\n ok([\n {\n tag: \"Allocated\" as const,\n value: {\n tag: \"SmartContractAllowance\" as const,\n value: undefined,\n },\n },\n ]) as StubReturn,\n });\n const adapter = fakeAdapter(\"p\");\n await requestResourceAllocation(session, adapter, [\n { tag: \"SmartContractAllowance\", value: { tag: \"Index\", value: 5 } },\n ]);\n\n expect(\n await getCachedAllocation(adapter, {\n tag: \"SmartContractAllowance\",\n value: { tag: \"Index\", value: 5 },\n }),\n ).toEqual({ tag: \"SmartContractAllowance\", dest: \"Index::5\" });\n // Different dest doesn't match.\n expect(\n await getCachedAllocation(adapter, {\n tag: \"SmartContractAllowance\",\n value: { tag: \"Index\", value: 6 },\n }),\n ).toBeNull();\n });\n });\n\n describe(\"ensureSlotAccountSigner\", () => {\n // Deterministic mini-secret so the signer is constructable.\n const slotKey = mnemonicToMiniSecret(DEV_PHRASE);\n\n function bulletinAllocatedResponse() {\n return ok([\n {\n tag: \"Allocated\" as const,\n value: {\n tag: \"BulletInAllowance\" as const,\n value: { slotAccountKey: slotKey },\n },\n },\n ]) as StubReturn;\n }\n\n test(\"cache hit returns a signer without invoking the AP wire\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return bulletinAllocatedResponse();\n },\n });\n const adapter = fakeAdapter(\"p\");\n\n // Populate cache.\n await requestResourceAllocation(session, adapter, [\n { tag: \"BulletInAllowance\", value: undefined },\n ]);\n expect(captured).toHaveLength(1);\n\n // ensureSlotAccountSigner should find the cached key and NOT\n // call the wire again.\n const signer = await ensureSlotAccountSigner(session, adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(signer.publicKey).toBeInstanceOf(Uint8Array);\n expect(captured).toHaveLength(1); // still 1 — no extra round-trip\n });\n\n test(\"cache miss triggers allocation and returns a fresh signer\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return bulletinAllocatedResponse();\n },\n });\n const adapter = fakeAdapter(\"p\");\n\n // No prior call. ensureSlotAccountSigner should round-trip.\n const signer = await ensureSlotAccountSigner(session, adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(signer.publicKey).toBeInstanceOf(Uint8Array);\n expect(captured).toHaveLength(1);\n // Implicit allocation requests exactly one resource.\n expect(captured[0].resources.map((r) => r.tag)).toEqual([\"BulletInAllowance\"]);\n });\n\n test(\"Rejected outcome surfaces as a throw — no signer to return\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () =>\n ok([{ tag: \"Rejected\" as const, value: undefined }]) as StubReturn,\n });\n\n await expect(\n ensureSlotAccountSigner(session, fakeAdapter(\"p\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ).rejects.toThrow(/allocation Rejected for BulletInAllowance/);\n });\n\n test(\"empty outcomes array from the wire surfaces as a length-mismatch throw\", async () => {\n // mergeOutcomes enforces the length contract and throws before\n // the downstream `outcomes[0]` defensive guard is reached.\n const session = makeSession({\n requestResourceAllocation: async () => ok([]) as StubReturn,\n });\n\n await expect(\n ensureSlotAccountSigner(session, fakeAdapter(\"p\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ).rejects.toThrow(/length mismatch — requested 1, got 0/);\n });\n\n test(\"NotAvailable outcome surfaces as a throw\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () =>\n ok([{ tag: \"NotAvailable\" as const, value: undefined }]) as StubReturn,\n });\n\n await expect(\n ensureSlotAccountSigner(session, fakeAdapter(\"p\"), {\n tag: \"StatementStoreAllowance\",\n value: undefined,\n }),\n ).rejects.toThrow(/allocation NotAvailable for StatementStoreAllowance/);\n });\n\n test(\"transport error propagates from the underlying allocation call\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () =>\n err(new Error(\"mobile timed out\")) as StubReturn,\n });\n\n await expect(\n ensureSlotAccountSigner(session, fakeAdapter(\"p\"), {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ).rejects.toThrow(/mobile timed out/);\n });\n\n test(\"explicit productId scopes the wire id and the cache namespace\", async () => {\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n return bulletinAllocatedResponse();\n },\n });\n const adapter = fakeAdapter(\"my-cli\");\n\n const signer = await ensureSlotAccountSigner(\n session,\n adapter,\n { tag: \"BulletInAllowance\", value: undefined },\n \"my-product.dot\",\n );\n expect(signer.publicKey).toBeInstanceOf(Uint8Array);\n expect(captured[0].callingProductId).toBe(\"my-product.dot\");\n\n // Cached under the product id, so a second call with the same\n // productId is a hit (no extra wire round-trip)...\n await ensureSlotAccountSigner(\n session,\n adapter,\n { tag: \"BulletInAllowance\", value: undefined },\n \"my-product.dot\",\n );\n expect(captured).toHaveLength(1);\n\n // ...while the default (appId) namespace stays empty.\n expect(\n await getCachedAllocation(adapter, { tag: \"BulletInAllowance\", value: undefined }),\n ).toBeNull();\n });\n\n test(\"populates the cache so a later call is a hit\", async () => {\n const session = makeSession({\n requestResourceAllocation: async () => bulletinAllocatedResponse(),\n });\n const adapter = fakeAdapter(\"p\");\n\n await ensureSlotAccountSigner(session, adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n\n // After the implicit allocation, getCachedAllocation should see the key.\n const cached = await getCachedAllocation(adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n });\n expect(cached?.tag).toBe(\"BulletInAllowance\");\n });\n\n test(\"parallel calls for the SAME resource share one allocation (no double-prompt)\", async () => {\n // Regression: previously the cache-hit check ran outside the\n // lock, so two concurrent same-resource calls both passed it,\n // then both hit the wire under the serialized lock — burning\n // two slots and prompting the wallet twice. Both calls must\n // resolve from a single allocation.\n const captured: RequestCapture[] = [];\n const session = makeSession({\n requestResourceAllocation: async (req) => {\n captured.push(req);\n // Force a yield so the second caller has a chance to\n // race past the cache check if the lock isn't around it.\n await new Promise((r) => setTimeout(r, 5));\n return bulletinAllocatedResponse();\n },\n });\n const adapter = fakeAdapter(\"p\");\n\n const [signerA, signerB] = await Promise.all([\n ensureSlotAccountSigner(session, adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ensureSlotAccountSigner(session, adapter, {\n tag: \"BulletInAllowance\",\n value: undefined,\n }),\n ]);\n\n // Exactly one wire round-trip — the second caller must observe\n // the freshly-cached key, not start a second allocation.\n expect(captured).toHaveLength(1);\n // Both calls return functioning signers backed by the same key.\n expect(signerA.publicKey).toBeInstanceOf(Uint8Array);\n expect(signerB.publicKey).toBeInstanceOf(Uint8Array);\n expect(toHex(signerA.publicKey)).toBe(toHex(signerB.publicKey));\n });\n });\n}\n"]}
|
package/dist/host.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createSlotAccountSigner, ensureSlotAccountSigner, getCachedAllocation, requestResourceAllocation, sendResourceAllocation } from './chunk-
|
|
1
|
+
export { createSlotAccountSigner, ensureSlotAccountSigner, getCachedAllocation, requestResourceAllocation, sendResourceAllocation } from './chunk-EORPX5XL.js';
|
|
2
2
|
//# sourceMappingURL=host.js.map
|
|
3
3
|
//# sourceMappingURL=host.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,27 @@ declare const StatementStoreNetworks: {
|
|
|
24
24
|
/** Network keys with built-in endpoints in {@link StatementStoreNetworks}. */
|
|
25
25
|
type StatementStoreEnvironment = keyof typeof StatementStoreNetworks;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The product-subtree public key of `//product//{productId}` (RFC-0022).
|
|
29
|
+
*
|
|
30
|
+
* The request is consent-free and cacheable. The disk layer is what makes that
|
|
31
|
+
* worth having: without it every CLI command would need the phone reachable
|
|
32
|
+
* just to render a product address.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
interface ProductSubtreeOptions {
|
|
36
|
+
/** Names the cache file. Defaults to the productId. */
|
|
37
|
+
appId?: string;
|
|
38
|
+
storageDir?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fetch the subtree public key for `productId`, reaching the wallet only on a
|
|
42
|
+
* cold cache.
|
|
43
|
+
*
|
|
44
|
+
* @throws Error when the wallet rejects the request or returns a malformed key.
|
|
45
|
+
*/
|
|
46
|
+
declare function getProductSubtreePublicKey(session: UserSession, productId: string, options?: ProductSubtreeOptions): Promise<Uint8Array>;
|
|
47
|
+
|
|
27
48
|
/**
|
|
28
49
|
* Create a PolkadotSigner from a QR-paired session.
|
|
29
50
|
*
|
|
@@ -53,10 +74,10 @@ type StatementStoreEnvironment = keyof typeof StatementStoreNetworks;
|
|
|
53
74
|
* const [session] = adapter.sessions.sessions.read();
|
|
54
75
|
*
|
|
55
76
|
* // Default account — uses [adapter.appId, 0]:
|
|
56
|
-
* const signer = createSessionSigner(session, adapter);
|
|
77
|
+
* const signer = await createSessionSigner(session, adapter);
|
|
57
78
|
*
|
|
58
79
|
* // Non-default derivation index, or a different productId:
|
|
59
|
-
* const subSigner = createSessionSignerForAccount(session, {
|
|
80
|
+
* const subSigner = await createSessionSignerForAccount(session, {
|
|
60
81
|
* productId: "my-product",
|
|
61
82
|
* derivationIndex: 3,
|
|
62
83
|
* });
|
|
@@ -82,46 +103,32 @@ interface ProductAccountRef {
|
|
|
82
103
|
* The **product account's** sr25519 public key (32 bytes), as derived by the
|
|
83
104
|
* host for `[productId, derivationIndex]`.
|
|
84
105
|
*
|
|
85
|
-
* PAPI stamps this into the extrinsic's signer address and verifies
|
|
86
|
-
*
|
|
87
|
-
* wallet's selected/root account (`session.remoteAccount.accountId`). A
|
|
88
|
-
* mismatch produces an invalid signature.
|
|
106
|
+
* PAPI stamps this into the extrinsic's signer address and verifies against
|
|
107
|
+
* it, so a mismatch produces an invalid signature.
|
|
89
108
|
*
|
|
90
|
-
* When omitted, the signer
|
|
91
|
-
*
|
|
92
|
-
* for every product account. Supply it explicitly only to avoid the
|
|
93
|
-
* re-derivation or when you've already derived it elsewhere.
|
|
109
|
+
* When omitted, the signer fetches the subtree key and derives it. Supply it
|
|
110
|
+
* to skip that round trip.
|
|
94
111
|
*/
|
|
95
112
|
publicKey?: Uint8Array;
|
|
96
113
|
}
|
|
97
114
|
declare const INCOMPLETE_SESSION_MESSAGE = "Stored login session is missing the root account public key. Run \"logout\" and then \"login\" to pair again.";
|
|
98
115
|
/**
|
|
99
|
-
* The
|
|
100
|
-
*
|
|
101
|
-
* parent key product accounts soft-derive from. host-papp's live `UserSession`
|
|
102
|
-
* doesn't surface it on the public type, so we read it structurally.
|
|
116
|
+
* The wallet's own root account key. Product accounts do **not** derive from it:
|
|
117
|
+
* RFC-0022 puts two hard junctions in between.
|
|
103
118
|
*
|
|
104
119
|
* @throws {@link INCOMPLETE_SESSION_MESSAGE} if the session predates the
|
|
105
|
-
* `rootAccountId` field
|
|
120
|
+
* `rootAccountId` field.
|
|
106
121
|
*/
|
|
107
122
|
declare function sessionRootPublicKey(session: UserSession): Uint8Array;
|
|
108
123
|
/**
|
|
109
|
-
*
|
|
110
|
-
* root account.
|
|
111
|
-
*
|
|
112
|
-
* This is the single source of truth for product-account math. The session
|
|
113
|
-
* signer uses it to stamp the correct signer address; consumers that need the
|
|
114
|
-
* product address *without* building a signer (e.g. a login-status display
|
|
115
|
-
* triple) call it directly so the displayed address can't desync from what the
|
|
116
|
-
* signer actually signs with.
|
|
124
|
+
* The product account's sr25519 public key for `ref`.
|
|
117
125
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* privately via `mnemonic + "/product/{productId}/{derivationIndex}"`.
|
|
126
|
+
* The one place product-account math happens, so a displayed address cannot
|
|
127
|
+
* desync from what the signer signs with.
|
|
121
128
|
*
|
|
122
|
-
* @throws
|
|
129
|
+
* @throws Error when the wallet is unreachable on a cold cache.
|
|
123
130
|
*/
|
|
124
|
-
declare function deriveProductPublicKey(session: UserSession, ref: ProductAccountRef): Uint8Array
|
|
131
|
+
declare function deriveProductPublicKey(session: UserSession, ref: ProductAccountRef, options?: ProductSubtreeOptions): Promise<Uint8Array>;
|
|
125
132
|
/**
|
|
126
133
|
* Create a `PolkadotSigner` backed by a QR-paired mobile wallet session,
|
|
127
134
|
* using the session's **default account** (`derivationIndex: 0`).
|
|
@@ -132,10 +139,10 @@ declare function deriveProductPublicKey(session: UserSession, ref: ProductAccoun
|
|
|
132
139
|
* @param adapter The {@link TerminalAdapter} that loaded the session. Its `appId`
|
|
133
140
|
* is used as the `productId` in the wire request.
|
|
134
141
|
* @param publicKey The product account's sr25519 public key for
|
|
135
|
-
* `[adapter.appId, 0]`. Optional — when omitted it's
|
|
136
|
-
*
|
|
142
|
+
* `[adapter.appId, 0]`. Optional — when omitted it's fetched and derived.
|
|
143
|
+
* See {@link ProductAccountRef.publicKey}.
|
|
137
144
|
*/
|
|
138
|
-
declare function createSessionSigner(session: UserSession, adapter: TerminalAdapter, publicKey?: Uint8Array): PolkadotSigner
|
|
145
|
+
declare function createSessionSigner(session: UserSession, adapter: TerminalAdapter, publicKey?: Uint8Array, options?: ProductSubtreeOptions): Promise<PolkadotSigner>;
|
|
139
146
|
/**
|
|
140
147
|
* Create a `PolkadotSigner` for a specific sub-account of a paired session.
|
|
141
148
|
*
|
|
@@ -146,7 +153,7 @@ declare function createSessionSigner(session: UserSession, adapter: TerminalAdap
|
|
|
146
153
|
* @param session The paired user session.
|
|
147
154
|
* @param ref The product account to sign as: `{ productId, derivationIndex }`.
|
|
148
155
|
*/
|
|
149
|
-
declare function createSessionSignerForAccount(session: UserSession, ref: ProductAccountRef): PolkadotSigner
|
|
156
|
+
declare function createSessionSignerForAccount(session: UserSession, ref: ProductAccountRef, options?: ProductSubtreeOptions): Promise<PolkadotSigner>;
|
|
150
157
|
|
|
151
158
|
/**
|
|
152
159
|
* Get a `PolkadotSigner` for a Bulletin allowance slot.
|
|
@@ -275,4 +282,4 @@ declare function renderQrCode(data: string, options?: QrRenderOptions): Promise<
|
|
|
275
282
|
*/
|
|
276
283
|
declare function createNodeStorageAdapter(appId: string, storageDir?: string): StorageAdapter;
|
|
277
284
|
|
|
278
|
-
export { INCOMPLETE_SESSION_MESSAGE, type ProductAccountRef, type QrRenderOptions, type StatementStoreEnvironment, StatementStoreNetworks, TerminalAdapter, createNodeStorageAdapter, createSessionSigner, createSessionSignerForAccount, deriveProductPublicKey, getBulletinSigner, getStatementStoreProver, hasBulletinAllowance, hasStatementStoreAllowance, renderQrCode, sessionRootPublicKey, waitForSessions };
|
|
285
|
+
export { INCOMPLETE_SESSION_MESSAGE, type ProductAccountRef, type ProductSubtreeOptions, type QrRenderOptions, type StatementStoreEnvironment, StatementStoreNetworks, TerminalAdapter, createNodeStorageAdapter, createSessionSigner, createSessionSignerForAccount, deriveProductPublicKey, getBulletinSigner, getProductSubtreePublicKey, getStatementStoreProver, hasBulletinAllowance, hasStatementStoreAllowance, renderQrCode, sessionRootPublicKey, waitForSessions };
|