@parity/product-sdk-terminal 0.0.0-dev.312.0 → 0.0.0-dev.454.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -16
- package/dist/chunk-EORPX5XL.js +267 -0
- package/dist/chunk-EORPX5XL.js.map +1 -0
- package/dist/chunk-GHOFKVUG.js +8 -133
- package/dist/chunk-GHOFKVUG.js.map +1 -1
- package/dist/{host-C3KVvc1j.d.ts → host-_G9M0isV.d.ts} +36 -10
- package/dist/host.d.ts +1 -1
- package/dist/host.js +2 -14
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +162 -37
- package/dist/index.js +158 -872
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +24 -234
- package/dist/testing.js.map +1 -1
- package/package.json +9 -9
- package/dist/chunk-YS3GW2XW.js +0 -1508
- package/dist/chunk-YS3GW2XW.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
|
```
|
|
@@ -63,7 +63,7 @@ Creates a terminal adapter backed by the host-papp SDK.
|
|
|
63
63
|
|
|
64
64
|
**Options:**
|
|
65
65
|
- `appId` -- unique app identifier (used as storage namespace)
|
|
66
|
-
- `endpoints?` -- statement store WebSocket endpoints (defaults to
|
|
66
|
+
- `endpoints?` -- statement store WebSocket endpoints (defaults to `StatementStoreNetworks.paseo`)
|
|
67
67
|
- `hostMetadata?` -- optional host environment info
|
|
68
68
|
- `storageDir?` -- override the on-disk session directory (defaults to `~/.polkadot-apps/`). Useful in tests and containerised environments.
|
|
69
69
|
|
|
@@ -71,34 +71,78 @@ Creates a terminal adapter backed by the host-papp SDK.
|
|
|
71
71
|
- `appId` -- the value you passed in (re-exposed so `createSessionSigner` can pull the productId from the adapter)
|
|
72
72
|
- `sso` -- auth component (`.authenticate()`, `.abortAuthentication()`, status subscriptions)
|
|
73
73
|
- `sessions` -- session manager (signing, disconnect)
|
|
74
|
-
- `destroy()` -- disconnect the WebSocket and release resources. Idempotent.
|
|
74
|
+
- `destroy()` -- disconnect the WebSocket and release resources. Idempotent. Drops `@novasamatech/statement-store`'s benign `Statement subscription error: Client destroyed` line, which it emits synchronously while disconnecting a live subscription — only that line; every other `console.error` passes through.
|
|
75
75
|
|
|
76
|
-
### `
|
|
76
|
+
### `StatementStoreNetworks`
|
|
77
|
+
|
|
78
|
+
The statement store endpoints terminal can pair against, keyed by network.
|
|
79
|
+
|
|
80
|
+
| Key | Network | Endpoint |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `paseo` | Paseo Next v2 | `wss://paseo-people-next-system-rpc.polkadot.io` |
|
|
83
|
+
| `previewnet` | zombienet, a step ahead of paseo | whatever host-papp's `SS_PREVIEW_STAGE_ENDPOINTS` resolves to, today `wss://previewnet.substrate.dev/people` |
|
|
84
|
+
|
|
85
|
+
Keyed the same way as `BULLETIN_RPCS` in `@parity/product-sdk-host`. There is deliberately no
|
|
86
|
+
`devnet` key: that people chain has a statement store, but no endpoint for it is published here, and
|
|
87
|
+
no phone build pairs against it. Pass `endpoints` explicitly to reach one.
|
|
88
|
+
|
|
89
|
+
Pairing only works when the terminal and the phone are on the **same** people chain, and the phone
|
|
90
|
+
picks its chain per build flavour. The Polkadot app's nightly build uses `paseo`, which is why that
|
|
91
|
+
is the default; a preview-flavour phone needs `endpoints: StatementStoreNetworks.previewnet`.
|
|
92
|
+
|
|
93
|
+
`SS_STABLE_STAGE_ENDPOINTS` is also exported. It is internal to the Parity network and does not
|
|
94
|
+
answer from outside it.
|
|
95
|
+
|
|
96
|
+
### `isBenignTeardownError(error): boolean`
|
|
97
|
+
|
|
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
|
+
|
|
100
|
+
### `createSessionSigner(session, adapter, publicKey?, options?): Promise<PolkadotSigner>`
|
|
77
101
|
|
|
78
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.
|
|
79
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
|
+
|
|
80
106
|
```ts
|
|
81
107
|
const [session] = adapter.sessions.sessions.read();
|
|
82
|
-
const signer = createSessionSigner(session, adapter);
|
|
108
|
+
const signer = await createSessionSigner(session, adapter);
|
|
83
109
|
await contract.publish.tx(domain, cid, { signer, origin });
|
|
84
110
|
```
|
|
85
111
|
|
|
86
|
-
### `createSessionSignerForAccount(session, ref): PolkadotSigner
|
|
112
|
+
### `createSessionSignerForAccount(session, ref, options?): Promise<PolkadotSigner>`
|
|
87
113
|
|
|
88
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.
|
|
89
115
|
|
|
90
116
|
`ref` is `{ productId: string; derivationIndex: number }`:
|
|
91
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.
|
|
92
|
-
- `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.
|
|
93
119
|
|
|
94
120
|
```ts
|
|
95
|
-
const subSigner = createSessionSignerForAccount(session, {
|
|
121
|
+
const subSigner = await createSessionSignerForAccount(session, {
|
|
96
122
|
productId: "my-product",
|
|
97
123
|
derivationIndex: 3,
|
|
98
124
|
});
|
|
99
125
|
```
|
|
100
126
|
|
|
101
|
-
|
|
127
|
+
Both factories accept `SessionSignerOptions`, extending `ProductSubtreeOptions` with optional `txExtVersion`. It defaults to the lowest pipeline version the chain metadata declares that holds every supplied extension, which is `0` on a chain with one pipeline. To force a version, pass `{ txExtVersion: 1 }` as the final `options` argument. The extension bytes supplied to `signTx` must match that version. PAPI 2.1.6's standard transaction builder encodes version `0`; this option does not change its encoder.
|
|
128
|
+
|
|
129
|
+
> **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.
|
|
130
|
+
|
|
131
|
+
### `getProductSubtreePublicKey(session, productId, options?): Promise<Uint8Array>`
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
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.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
await getProductSubtreePublicKey(session, adapter.appId, { appId: adapter.appId });
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`options` is `ProductSubtreeOptions`:
|
|
142
|
+
- `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.
|
|
143
|
+
- `storageDir` -- overrides `~/.polkadot-apps/`, matching `createTerminalAdapter({ storageDir })`.
|
|
144
|
+
|
|
145
|
+
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.
|
|
102
146
|
|
|
103
147
|
### `renderQrCode(data, options?): Promise<string>`
|
|
104
148
|
|
|
@@ -112,6 +156,19 @@ File-based storage adapter for Node.js. Data persists in `storageDir` (defaults
|
|
|
112
156
|
|
|
113
157
|
Waits for the session list to emit at least one entry, or resolves with `[]` after `timeoutMs`.
|
|
114
158
|
|
|
159
|
+
### `deriveEntropy(session, productId, key): Uint8Array`
|
|
160
|
+
|
|
161
|
+
Derives 32 bytes of deterministic entropy from a paired session, scoped to a product and a caller key (RFC-0007, host-spec §C.8). The bytes match what an in-container app gets from `@parity/product-sdk-host`'s `deriveEntropy` for the same wallet + product + key, so keys derived from them interoperate across web and terminal clients. Computed locally from the session's `rootEntropySource` — no round trip to the wallet.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const entropy = deriveEntropy(session, "my-cli.dot", new TextEncoder().encode("repo-key"));
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- `productId` -- the identifier the in-container deployment is served under: the same value you pass as `productId` to `getBulletinSigner`, **not** `adapter.appId`. Used verbatim and case-sensitively, and scoped per deployment — production, PR previews and `localhost:<port>` all derive different entropy.
|
|
168
|
+
- `key` -- caller key, 1..32 bytes.
|
|
169
|
+
|
|
170
|
+
Throws if the session predates RFC-0007 (no `rootEntropySource`; re-pair to fix). With several paired sessions, pin the intended wallet with `sessionRootPublicKey` first: the wrong session yields valid entropy for a different wallet. The result is raw key material — don't log it or persist it unwrapped.
|
|
171
|
+
|
|
115
172
|
## Allowance signers — the canonical path
|
|
116
173
|
|
|
117
174
|
For CLIs that need to write to Bulletin or publish to the Statement Store: ask the paired wallet for an allowance slot, get a `PolkadotSigner` back, sign extrinsics with it. This is what most consumers want.
|
|
@@ -238,20 +295,22 @@ For consumers moving from `@polkadot-apps/terminal` v0.2.0 / v0.3.0. Existing se
|
|
|
238
295
|
| Package name | `@polkadot-apps/terminal` | `@parity/product-sdk-terminal` |
|
|
239
296
|
| WASM loader hook | `--import @polkadot-apps/terminal/register` (required) | **removed** — no `--import` needed (host-papp ≥0.7.9 dropped `verifiablejs`) |
|
|
240
297
|
| `createTerminalAdapter` | `async` — returned `Promise<TerminalAdapter>` | **sync** — returns `TerminalAdapter` directly. Drop the `await`. |
|
|
241
|
-
| Default account signer | `createSessionSigner(session)` | `createSessionSigner(session, adapter)` — pass the adapter as second arg |
|
|
242
|
-
| Non-default sub-account signer | not exposed | `createSessionSignerForAccount(session, { productId, derivationIndex })` |
|
|
298
|
+
| Default account signer | `createSessionSigner(session)` | `await createSessionSigner(session, adapter)` — pass the adapter as second arg, and await it |
|
|
299
|
+
| Non-default sub-account signer | not exposed | `await createSessionSignerForAccount(session, { productId, derivationIndex })` |
|
|
243
300
|
| Override session storage dir | not supported (hard-coded `~/.polkadot-apps/`) | `createTerminalAdapter({ ..., storageDir })` option |
|
|
244
301
|
| E2E test helper for sessions | none | `createTestSession` from `@parity/product-sdk-terminal/testing` |
|
|
245
302
|
| Node version | any (bundled `ws`) | **≥21** (uses global `WebSocket`) |
|
|
246
|
-
| `destroy()` shutdown noise | emitted `Statement subscription error` to stderr |
|
|
303
|
+
| `destroy()` shutdown noise | emitted `Statement subscription error` to stderr | only the benign `Client destroyed` line dropped (synchronously, during disconnect); other `console.error` untouched |
|
|
247
304
|
|
|
248
305
|
### Why the signer API changed
|
|
249
306
|
|
|
250
|
-
`@novasamatech/host-papp` 0.7 replaced `SigningRawRequest.address` with `productAccountId: [productId, derivationIndex]
|
|
307
|
+
`@novasamatech/host-papp` 0.7 replaced `SigningRawRequest.address` with `productAccountId: [productId, derivationIndex]`, and 0.10 made the index a tagged `Index | Raw` selector. The wire format requires both fields, so a session-only argument is no longer enough — the signer needs to know *which sub-account of which product is asking*. We split that into two functions to keep the common case ergonomic:
|
|
251
308
|
|
|
252
|
-
- `createSessionSigner(session, adapter)` for the default account (uses `[adapter.appId, 0]`)
|
|
309
|
+
- `createSessionSigner(session, adapter)` for the default account (uses `[adapter.appId, { tag: "Index", value: 0 }]`)
|
|
253
310
|
- `createSessionSignerForAccount(session, { productId, derivationIndex })` for everything else
|
|
254
311
|
|
|
312
|
+
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.
|
|
313
|
+
|
|
255
314
|
The single-argument `createSessionSigner(session)` from `@polkadot-apps/terminal` no longer works against host-papp 0.7 regardless of which package you use.
|
|
256
315
|
|
|
257
316
|
### Migration steps
|
|
@@ -259,7 +318,7 @@ The single-argument `createSessionSigner(session)` from `@polkadot-apps/terminal
|
|
|
259
318
|
1. **Replace the dep**: `pnpm remove @polkadot-apps/terminal && pnpm add @parity/product-sdk-terminal`
|
|
260
319
|
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).
|
|
261
320
|
3. **Drop `await`** in front of `createTerminalAdapter(...)` calls.
|
|
262
|
-
4. **Update each `createSessionSigner` call site**: change `createSessionSigner(session)` → `createSessionSigner(session, adapter)`.
|
|
321
|
+
4. **Update each `createSessionSigner` call site**: change `createSessionSigner(session)` → `await createSessionSigner(session, adapter)`.
|
|
263
322
|
5. **Verify Node version** is ≥21 (`node --version`).
|
|
264
323
|
|
|
265
324
|
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.
|
|
@@ -300,7 +359,7 @@ const sessions = await waitForSessions(adapter);
|
|
|
300
359
|
|
|
301
360
|
After login and attestation, the paired wallet can sign both transactions and raw messages via the statement store. The `PolkadotSigner` returned by `createSessionSigner` routes each path to the right host-papp method:
|
|
302
361
|
|
|
303
|
-
- **Transactions** (`signTx
|
|
362
|
+
- **Transactions** (`signTx`, used by `submitAndWatch`, `signSubmitAndWatch`, and contract calls) go through `session.createTransaction`. The signer forwards the account, call data, `txExtVersion`, and every extension's encoded `extra` and `additionalSigned` bytes, including unknown extensions such as `AsPgas`. The wallet decodes the transaction for display and returns the signed extrinsic.
|
|
304
363
|
- **Raw bytes** (`signBytes`) go through `session.signRaw` with the `Bytes` tag. Mobile applies the standard `<Bytes>...</Bytes>` anti-phishing wrap before signing — appropriate for arbitrary data, the same behavior `signRaw` has across all Polkadot wallets.
|
|
305
364
|
|
|
306
365
|
> **Note on previous bugs.** Versions prior to `0.1.1` routed *all* signing through `signRaw` with no tag distinction, producing `BadProof`-rejected signatures. Versions `0.1.1` through `0.2.x` then used `polkadot-api/pjs-signer`, which threw `PJS does not support this signed-extension: <name>` on any extension outside its eight built-in mappers (notably `AsPgas`). The current path bypasses both pitfalls.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { createLogger } from '@parity/product-sdk-logger';
|
|
2
|
+
import { fromHex, toHex } from '@polkadot-api/utils';
|
|
3
|
+
import { readFile, mkdir, writeFile, rename } from 'fs/promises';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { numberToBytesLE, bytesToNumberLE } from '@noble/curves/utils.js';
|
|
7
|
+
import { createDerive, sr25519Derive, sr25519 } from '@polkadot-labs/hdkd-helpers';
|
|
8
|
+
import { getPolkadotSigner } from 'polkadot-api/signer';
|
|
9
|
+
|
|
10
|
+
// src/host.ts
|
|
11
|
+
var log = createLogger("terminal");
|
|
12
|
+
var DEFAULT_STORAGE_DIR = join(homedir(), ".polkadot-apps");
|
|
13
|
+
var CACHE_FILE_MODE = 384;
|
|
14
|
+
function sanitizeAppId(appId) {
|
|
15
|
+
return appId.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
16
|
+
}
|
|
17
|
+
function cacheFilePath(appId, kind, storageDir) {
|
|
18
|
+
return join(storageDir ?? DEFAULT_STORAGE_DIR, `${sanitizeAppId(appId)}_${kind}.json`);
|
|
19
|
+
}
|
|
20
|
+
async function loadJsonCache(path, version, label) {
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = await readFile(path, "utf-8");
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (e?.code === "ENOENT") return null;
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (parsed?.version !== version) {
|
|
31
|
+
log.warn(`${label} schema mismatch; starting fresh`, { path });
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
log.warn(`${label} parse failed; starting fresh`, { path, error: String(e) });
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function saveJsonCache(path, value) {
|
|
41
|
+
await mkdir(dirname(path), { recursive: true });
|
|
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 });
|
|
44
|
+
await rename(tmp, path);
|
|
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
|
+
}
|
|
77
|
+
function smartContractDest(dest) {
|
|
78
|
+
return dest.tag === "Index" ? `Index::${dest.value}` : `Raw::${toHex(dest.value)}`;
|
|
79
|
+
}
|
|
80
|
+
function cacheKey(resource) {
|
|
81
|
+
if (resource.tag === "SmartContractAllowance") {
|
|
82
|
+
return `${resource.tag}::${smartContractDest(resource.value)}`;
|
|
83
|
+
}
|
|
84
|
+
return resource.tag;
|
|
85
|
+
}
|
|
86
|
+
function pickOnExistingPolicy(cache, resources) {
|
|
87
|
+
if (resources.length === 0) return "Ignore";
|
|
88
|
+
const allSlotTableAndCached = resources.every((r) => {
|
|
89
|
+
if (r.tag !== "BulletInAllowance" && r.tag !== "StatementStoreAllowance") return false;
|
|
90
|
+
return Object.hasOwn(cache.entries, cacheKey(r));
|
|
91
|
+
});
|
|
92
|
+
return allSlotTableAndCached ? "Increase" : "Ignore";
|
|
93
|
+
}
|
|
94
|
+
function mergeOutcomes(cache, requested, outcomes) {
|
|
95
|
+
if (requested.length !== outcomes.length) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`mergeOutcomes: length mismatch \u2014 requested ${requested.length}, got ${outcomes.length}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const entries = { ...cache.entries };
|
|
101
|
+
let mutated = false;
|
|
102
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
103
|
+
const req = requested[i];
|
|
104
|
+
const out = outcomes[i];
|
|
105
|
+
if (out.tag !== "Allocated") continue;
|
|
106
|
+
const inner = out.value;
|
|
107
|
+
if (inner.tag !== req.tag) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`mergeOutcomes: variant mismatch at index ${i} \u2014 requested ${req.tag}, got Allocated<${inner.tag}>`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const key = cacheKey(req);
|
|
113
|
+
switch (inner.tag) {
|
|
114
|
+
case "BulletInAllowance":
|
|
115
|
+
case "StatementStoreAllowance":
|
|
116
|
+
entries[key] = {
|
|
117
|
+
tag: inner.tag,
|
|
118
|
+
slotAccountKey: toHex(inner.value.slotAccountKey)
|
|
119
|
+
};
|
|
120
|
+
mutated = true;
|
|
121
|
+
break;
|
|
122
|
+
case "SmartContractAllowance":
|
|
123
|
+
if (req.tag === "SmartContractAllowance") {
|
|
124
|
+
entries[key] = {
|
|
125
|
+
tag: "SmartContractAllowance",
|
|
126
|
+
dest: smartContractDest(req.value)
|
|
127
|
+
};
|
|
128
|
+
mutated = true;
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case "AutoSigning":
|
|
132
|
+
entries[key] = {
|
|
133
|
+
tag: "AutoSigning",
|
|
134
|
+
ringVrfDomainEntropy: toHex(inner.value.ringVrfDomainEntropy),
|
|
135
|
+
productRootPrivateKey: toHex(inner.value.productRootPrivateKey)
|
|
136
|
+
};
|
|
137
|
+
mutated = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return mutated ? { version: 2, entries } : cache;
|
|
142
|
+
}
|
|
143
|
+
function readCacheEntry(cache, resource) {
|
|
144
|
+
return cache.entries[cacheKey(resource)] ?? null;
|
|
145
|
+
}
|
|
146
|
+
function withCacheLock(appId, storageDir, fn) {
|
|
147
|
+
return withFileLock(cachePath(appId, storageDir), fn);
|
|
148
|
+
}
|
|
149
|
+
function canonicalSr25519SecretToEd25519Bytes(secret) {
|
|
150
|
+
const ed25519Scalar = numberToBytesLE(bytesToNumberLE(secret.subarray(0, 32)) << 3n, 32);
|
|
151
|
+
const out = new Uint8Array(64);
|
|
152
|
+
out.set(ed25519Scalar, 0);
|
|
153
|
+
out.set(secret.subarray(32, 64), 32);
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function buildKeypair(secret) {
|
|
157
|
+
if (secret.length === 32) {
|
|
158
|
+
const derive = createDerive({ seed: secret, curve: sr25519, derive: sr25519Derive });
|
|
159
|
+
const keypair = derive("");
|
|
160
|
+
return { publicKey: keypair.publicKey, sign: (data) => keypair.sign(data) };
|
|
161
|
+
}
|
|
162
|
+
if (secret.length === 64) {
|
|
163
|
+
const ed25519Secret = canonicalSr25519SecretToEd25519Bytes(secret);
|
|
164
|
+
return {
|
|
165
|
+
publicKey: sr25519.getPublicKey(ed25519Secret),
|
|
166
|
+
sign: (data) => sr25519.sign(data, ed25519Secret)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
throw new Error(
|
|
170
|
+
`createSlotAccountSigner: unexpected slotAccountKey length ${secret.length}. Expected 32 (mini-secret) or 64 (expanded secret).`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
function buildSignerFromEntry(entry) {
|
|
174
|
+
if (entry.tag !== "BulletInAllowance" && entry.tag !== "StatementStoreAllowance") {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`createSlotAccountSigner: ${entry.tag} does not carry a slot account key. Slot-table signing is defined for BulletInAllowance and StatementStoreAllowance only.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const secret = fromHex(entry.slotAccountKey);
|
|
180
|
+
const { publicKey, sign } = buildKeypair(secret);
|
|
181
|
+
return getPolkadotSigner(publicKey, "Sr25519", async (data) => sign(data));
|
|
182
|
+
}
|
|
183
|
+
async function createSlotAccountSigner(adapter, resource, productId) {
|
|
184
|
+
const cache = await loadCache(productId ?? adapter.appId, adapter.storageDir);
|
|
185
|
+
const entry = readCacheEntry(cache, resource);
|
|
186
|
+
if (!entry) return null;
|
|
187
|
+
return buildSignerFromEntry(entry);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/host.ts
|
|
191
|
+
var log2 = createLogger("terminal");
|
|
192
|
+
async function requestResourceAllocation(session, adapter, resources, options = {}) {
|
|
193
|
+
const productId = options.productId ?? adapter.appId;
|
|
194
|
+
return withCacheLock(productId, adapter.storageDir, async () => {
|
|
195
|
+
const cache = await loadCache(productId, adapter.storageDir);
|
|
196
|
+
const onExisting = options.onExisting ?? pickOnExistingPolicy(cache, resources);
|
|
197
|
+
log2.debug("requestResourceAllocation", {
|
|
198
|
+
productId,
|
|
199
|
+
resources: resources.map((r) => r.tag),
|
|
200
|
+
onExisting,
|
|
201
|
+
autoPolicy: options.onExisting === void 0
|
|
202
|
+
});
|
|
203
|
+
const outcomes = await sendResourceAllocation(session, productId, resources, onExisting);
|
|
204
|
+
const next = mergeOutcomes(cache, resources, outcomes);
|
|
205
|
+
if (next !== cache) {
|
|
206
|
+
await saveCache(productId, next, adapter.storageDir);
|
|
207
|
+
}
|
|
208
|
+
return outcomes;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function sendResourceAllocation(session, productId, resources, onExisting) {
|
|
212
|
+
const result = await session.requestResourceAllocation({
|
|
213
|
+
callingProductId: productId,
|
|
214
|
+
resources,
|
|
215
|
+
onExisting
|
|
216
|
+
});
|
|
217
|
+
if (result.isErr()) {
|
|
218
|
+
throw new Error(`requestResourceAllocation failed: ${result.error.message}`);
|
|
219
|
+
}
|
|
220
|
+
return result.value;
|
|
221
|
+
}
|
|
222
|
+
async function getCachedAllocation(adapter, resource, productId) {
|
|
223
|
+
const cache = await loadCache(productId ?? adapter.appId, adapter.storageDir);
|
|
224
|
+
return readCacheEntry(cache, resource);
|
|
225
|
+
}
|
|
226
|
+
async function ensureSlotAccountSigner(session, adapter, resource, productId) {
|
|
227
|
+
const resolvedProductId = productId ?? adapter.appId;
|
|
228
|
+
return withCacheLock(resolvedProductId, adapter.storageDir, async () => {
|
|
229
|
+
const cache = await loadCache(resolvedProductId, adapter.storageDir);
|
|
230
|
+
const hit = readCacheEntry(cache, resource);
|
|
231
|
+
if (hit) return buildSignerFromEntry(hit);
|
|
232
|
+
log2.debug("ensureSlotAccountSigner: cache miss, allocating", { resource: resource.tag });
|
|
233
|
+
const onExisting = pickOnExistingPolicy(cache, [resource]);
|
|
234
|
+
const outcomes = await sendResourceAllocation(
|
|
235
|
+
session,
|
|
236
|
+
resolvedProductId,
|
|
237
|
+
[resource],
|
|
238
|
+
onExisting
|
|
239
|
+
);
|
|
240
|
+
const next = mergeOutcomes(cache, [resource], outcomes);
|
|
241
|
+
if (next !== cache) {
|
|
242
|
+
await saveCache(resolvedProductId, next, adapter.storageDir);
|
|
243
|
+
}
|
|
244
|
+
const outcome = outcomes[0];
|
|
245
|
+
if (!outcome) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`ensureSlotAccountSigner: protocol violation \u2014 empty outcomes for ${resource.tag}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (outcome.tag !== "Allocated") {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`ensureSlotAccountSigner: allocation ${outcome.tag} for ${resource.tag}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const freshEntry = readCacheEntry(next, resource);
|
|
256
|
+
if (!freshEntry) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`ensureSlotAccountSigner: allocation succeeded but cache lookup returned null for ${resource.tag}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return buildSignerFromEntry(freshEntry);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export { cacheFilePath, createSlotAccountSigner, ensureSlotAccountSigner, getCachedAllocation, loadJsonCache, requestResourceAllocation, saveJsonCache, sendResourceAllocation, withFileLock };
|
|
266
|
+
//# sourceMappingURL=chunk-EORPX5XL.js.map
|
|
267
|
+
//# sourceMappingURL=chunk-EORPX5XL.js.map
|