@parity/product-sdk-host 0.0.0-dev.312.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/LICENSE +201 -0
- package/dist/chain-discovery-nLrPzb3d.d.ts +70 -0
- package/dist/chunk-GDXSV7JV.js +50 -0
- package/dist/chunk-GDXSV7JV.js.map +1 -0
- package/dist/index.d.ts +1210 -0
- package/dist/index.js +1163 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +105 -0
- package/dist/testing.js +177 -0
- package/dist/testing.js.map +1 -0
- package/package.json +46 -0
- package/src/accounts.ts +1052 -0
- package/src/chain-discovery.ts +287 -0
- package/src/chain-spec.ts +272 -0
- package/src/chain-transaction.ts +241 -0
- package/src/chains.ts +46 -0
- package/src/chat.ts +122 -0
- package/src/container.ts +339 -0
- package/src/entropy.ts +105 -0
- package/src/errors.ts +239 -0
- package/src/features.ts +172 -0
- package/src/index.ts +149 -0
- package/src/navigation.ts +128 -0
- package/src/notifications.ts +113 -0
- package/src/papi-provider.ts +741 -0
- package/src/payments.ts +117 -0
- package/src/permissions.ts +236 -0
- package/src/result.ts +13 -0
- package/src/testing.ts +493 -0
- package/src/theme.ts +78 -0
- package/src/transport.ts +181 -0
- package/src/truapi.ts +313 -0
- package/src/types.ts +101 -0
- package/src/worker.ts +261 -0
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Host wallet accounts, backed by `truApi.account.*` and `truApi.signing.*`.
|
|
5
|
+
*
|
|
6
|
+
* `getAccountsProvider()` returns the full accounts surface — user identity
|
|
7
|
+
* (`getUserId` / `requestLogin`), the user's existing wallet accounts
|
|
8
|
+
* (`getLegacyAccounts`), app-scoped product accounts (`getProductAccount` /
|
|
9
|
+
* `getProductAccountAlias`), Ring VRF proofs (`createRingVRFProof`), sr25519 VRF
|
|
10
|
+
* signatures over a caller-supplied Merlin transcript (`signVrf`), connection
|
|
11
|
+
* status, and PAPI `PolkadotSigner` factories for both product and legacy
|
|
12
|
+
* accounts.
|
|
13
|
+
*
|
|
14
|
+
* The signer factories build a PAPI `PolkadotSigner` directly over
|
|
15
|
+
* `truApi.signing.createTransaction` (product) /
|
|
16
|
+
* `createTransactionWithLegacyAccount` (legacy) — `signTx` derives the
|
|
17
|
+
* metadata-driven `txExtVersion` and maps the signed extensions to the host's
|
|
18
|
+
* wire shape; `signBytes` calls `signing.signRaw(WithLegacyAccount)`. No PJS
|
|
19
|
+
* bridge is involved, so opaque signed extensions (e.g. Paseo Next's `AsPgas`)
|
|
20
|
+
* survive end-to-end.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
|
|
26
|
+
import type { ResultAsync } from "neverthrow";
|
|
27
|
+
import { AccountId, type PolkadotSigner } from "polkadot-api";
|
|
28
|
+
|
|
29
|
+
import type {
|
|
30
|
+
ContextualAlias as WireAlias,
|
|
31
|
+
HostAccountConnectionStatusSubscribeItem,
|
|
32
|
+
HostAccountCreateProofResponse as WireRingVRFProof,
|
|
33
|
+
HostRequestLoginResponse,
|
|
34
|
+
LegacyAccount as WireLegacyAccount,
|
|
35
|
+
ProductAccount as WireProductAccount,
|
|
36
|
+
ProductAccountId,
|
|
37
|
+
ProductProofContext,
|
|
38
|
+
RegisteredRingVrfKey as WireRegisteredRingVrfKey,
|
|
39
|
+
RingLocation,
|
|
40
|
+
RingVrfKeyDisclosure,
|
|
41
|
+
TrUApiClient,
|
|
42
|
+
VersionedHostAccountCreateProofError,
|
|
43
|
+
VersionedHostAccountGetAliasError,
|
|
44
|
+
VersionedHostAccountGetError,
|
|
45
|
+
VersionedHostAccountListRingVrfKeysError,
|
|
46
|
+
VersionedHostAccountRegisterRingVrfKeyError,
|
|
47
|
+
VersionedHostAccountRingVrfSignError,
|
|
48
|
+
VersionedHostAccountSignVrfError,
|
|
49
|
+
VersionedHostGetLegacyAccountsError,
|
|
50
|
+
VersionedHostGetUserIdError,
|
|
51
|
+
VersionedHostRequestLoginError,
|
|
52
|
+
VrfSignature as WireVrfSignature,
|
|
53
|
+
VrfTranscriptItem as WireVrfTranscriptItem,
|
|
54
|
+
scale,
|
|
55
|
+
} from "@parity/truapi";
|
|
56
|
+
|
|
57
|
+
import { getClient, subscribeWithInterrupt } from "./transport.js";
|
|
58
|
+
import { fromHex, toHex, unwrapHostResult } from "./truapi.js";
|
|
59
|
+
import type { HostSubscription } from "./types.js";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ring VRF request shapes, re-exported from `@parity/truapi`:
|
|
63
|
+
* - `RingLocation` — where the ring lives (`{ chainId, junctions }`; junctions
|
|
64
|
+
* address the ring via `PalletInstance` / `CollectionId` steps).
|
|
65
|
+
* - `ProductProofContext` — the product-scoped proof context
|
|
66
|
+
* (`{ productId, suffix }`), expanded by the host into the 32-byte context a
|
|
67
|
+
* proof or alias is bound to.
|
|
68
|
+
* - `DerivationIndex` — the tagged selector `ProductProofContext.suffix`
|
|
69
|
+
* carries: `{ tag: "Index", value: number }` for a plain index, or
|
|
70
|
+
* `{ tag: "Raw", value: HexString }` for a raw 32-byte index.
|
|
71
|
+
*/
|
|
72
|
+
export type {
|
|
73
|
+
DerivationIndex,
|
|
74
|
+
ProductProofContext,
|
|
75
|
+
RingLocation,
|
|
76
|
+
RingVrfKeyDisclosure,
|
|
77
|
+
} from "@parity/truapi";
|
|
78
|
+
|
|
79
|
+
// The account/alias shapes come from `@parity/truapi`'s generated specs; we
|
|
80
|
+
// derive the SDK-facing views from them so the field inventory tracks the
|
|
81
|
+
// protocol automatically, and override only the fields the adapter re-encodes:
|
|
82
|
+
// byte fields decoded from `0x`-prefixed `HexString`s to `Uint8Array`s, and
|
|
83
|
+
// the tagged derivation-index selector kept as a plain `number` (wrapped back
|
|
84
|
+
// into `Index` at the wire boundary). Shapes re-exported verbatim (e.g.
|
|
85
|
+
// `ProductProofContext`) track the wire as-is. Same pattern as
|
|
86
|
+
// `@parity/product-sdk-statement-store`.
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* One of the user's existing wallet accounts, surfaced through the host and
|
|
90
|
+
* identified by its public key and an optional name. Contrast with
|
|
91
|
+
* {@link ProductAccount}, which is also user-controlled but derived by the
|
|
92
|
+
* host for a specific app rather than picked from the user's existing keys.
|
|
93
|
+
*
|
|
94
|
+
* Derived from `@parity/truapi`'s `LegacyAccount`, with `publicKey` decoded to bytes.
|
|
95
|
+
*/
|
|
96
|
+
export type HostAccount = Omit<WireLegacyAccount, "publicKey"> & {
|
|
97
|
+
/** Raw public key bytes. */
|
|
98
|
+
publicKey: Uint8Array;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A product account — an app-scoped derived account managed by the host wallet.
|
|
103
|
+
*
|
|
104
|
+
* The host derives a unique keypair for each app (identified by `dotNsIdentifier`)
|
|
105
|
+
* so apps get their own account that the user controls but is scoped to the app.
|
|
106
|
+
*
|
|
107
|
+
* Combines `@parity/truapi`'s `ProductAccountId` (the `{ dotNsIdentifier,
|
|
108
|
+
* derivationIndex }` lookup key) with the `ProductAccount` payload, with
|
|
109
|
+
* `publicKey` decoded to bytes and `derivationIndex` kept as the plain
|
|
110
|
+
* numeric index (the adapter wraps it into the wire's tagged
|
|
111
|
+
* {@link DerivationIndex} selector).
|
|
112
|
+
*/
|
|
113
|
+
export type ProductAccount = Omit<ProductAccountId, "derivationIndex"> &
|
|
114
|
+
Omit<WireProductAccount, "publicKey"> & {
|
|
115
|
+
/** Plain account index within the product subtree. */
|
|
116
|
+
derivationIndex: number;
|
|
117
|
+
/** Raw public key bytes. */
|
|
118
|
+
publicKey: Uint8Array;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* How callers address a product account: app identifier plus an optional index,
|
|
123
|
+
* defaulting to 0. A {@link ProductAccount} satisfies this, so an account from
|
|
124
|
+
* {@link AccountsProvider.getProductAccount} can be passed straight back in.
|
|
125
|
+
*/
|
|
126
|
+
export type ProductAccountLookup = Omit<ProductAccountId, "derivationIndex"> & {
|
|
127
|
+
/** Plain account index within the product subtree. Defaults to 0. */
|
|
128
|
+
derivationIndex?: number;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
declare const ringVrfKeyHandleBrand: unique symbol;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Opaque public name of a registered ring-VRF key.
|
|
135
|
+
*
|
|
136
|
+
* Handles come from {@link AccountsProvider.listRingVrfKeys}; product code
|
|
137
|
+
* cannot construct one from a derivation index.
|
|
138
|
+
*/
|
|
139
|
+
export type RingVrfKeyHandle = {
|
|
140
|
+
readonly [ringVrfKeyHandleBrand]: "RingVrfKeyHandle";
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** Ring-VRF member public key, decoded from the wire's hex string. */
|
|
144
|
+
export type RingVrfPublicKey = Uint8Array;
|
|
145
|
+
|
|
146
|
+
/** Registered key metadata returned by the host. */
|
|
147
|
+
export type RegisteredRingVrfKey = Omit<WireRegisteredRingVrfKey, "handle" | "publicKey"> & {
|
|
148
|
+
/** Opaque handle to pass back for alias and proof requests. */
|
|
149
|
+
handle: RingVrfKeyHandle;
|
|
150
|
+
/** Present when public-key disclosure was granted. */
|
|
151
|
+
publicKey?: RingVrfPublicKey;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
function sameRingLocation(a: RingLocation, b: RingLocation): boolean {
|
|
155
|
+
if (
|
|
156
|
+
a.chainId.toLowerCase() !== b.chainId.toLowerCase() ||
|
|
157
|
+
a.junctions.length !== b.junctions.length
|
|
158
|
+
) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return a.junctions.every((junction, index) => {
|
|
162
|
+
const candidate = b.junctions[index];
|
|
163
|
+
if (junction.tag === "PalletInstance") {
|
|
164
|
+
return candidate.tag === "PalletInstance" && junction.value === candidate.value;
|
|
165
|
+
}
|
|
166
|
+
return (
|
|
167
|
+
candidate.tag === "CollectionId" &&
|
|
168
|
+
junction.value.toLowerCase() === candidate.value.toLowerCase()
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Select a registered key by its declared ring and return its opaque handle.
|
|
175
|
+
*
|
|
176
|
+
* Consumers must not hard-code another product's derivation index. Registry
|
|
177
|
+
* order breaks ties when an owner declares multiple keys for the same ring.
|
|
178
|
+
*/
|
|
179
|
+
export function findRingVrfKeyHandle(
|
|
180
|
+
keys: RegisteredRingVrfKey[],
|
|
181
|
+
ring: RingLocation,
|
|
182
|
+
): RingVrfKeyHandle | undefined {
|
|
183
|
+
return keys.find((key) => key.rings.some((candidate) => sameRingLocation(candidate, ring)))
|
|
184
|
+
?.handle;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A contextual alias obtained from Ring VRF.
|
|
189
|
+
*
|
|
190
|
+
* Proves account membership in a ring without revealing which account.
|
|
191
|
+
*
|
|
192
|
+
* Derived from `@parity/truapi`'s `ContextualAlias`, with both fields decoded to bytes.
|
|
193
|
+
*/
|
|
194
|
+
export type ContextualAlias = { [K in keyof WireAlias]: Uint8Array };
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A Ring VRF proof plus the values needed to verify it downstream (e.g.
|
|
198
|
+
* against a precompile): the alias it commits to, and the ring member index /
|
|
199
|
+
* revision the proof was generated against.
|
|
200
|
+
*
|
|
201
|
+
* Derived from `@parity/truapi`'s `HostAccountCreateProofResponse`, with the
|
|
202
|
+
* byte fields decoded.
|
|
203
|
+
*/
|
|
204
|
+
export type RingVRFProof = Omit<WireRingVRFProof, "proof" | "contextualAlias"> & {
|
|
205
|
+
/** Raw ring VRF proof bytes. */
|
|
206
|
+
proof: Uint8Array;
|
|
207
|
+
/** Alias derived for the request's context. */
|
|
208
|
+
contextualAlias: ContextualAlias;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* One `append_message(label, value)` call replayed against a VRF transcript.
|
|
213
|
+
* Merlin labels are ASCII by convention: use `utf8ToBytes("round")`.
|
|
214
|
+
*
|
|
215
|
+
* Derived from `@parity/truapi`'s `VrfTranscriptItem`, decoded to bytes.
|
|
216
|
+
*/
|
|
217
|
+
export type VrfTranscriptItem = { [K in keyof WireVrfTranscriptItem]: Uint8Array };
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* An sr25519 VRF signature: the pre-output and its DLEQ proof.
|
|
221
|
+
*
|
|
222
|
+
* Derived from `@parity/truapi`'s `VrfSignature`, decoded to bytes.
|
|
223
|
+
*/
|
|
224
|
+
export type VrfSignature = { [K in keyof WireVrfSignature]: Uint8Array };
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Accounts provider handle, backed by `truApi.account.*` / `truApi.signing.*`.
|
|
228
|
+
* Surfaces the user's wallet accounts, app-scoped product accounts, Ring VRF,
|
|
229
|
+
* user identity, connection status, and `PolkadotSigner` factories.
|
|
230
|
+
*
|
|
231
|
+
* Lookup methods return a neverthrow `ResultAsync` (use `.match(ok, err)`);
|
|
232
|
+
* the signer factories return a synchronous PAPI `PolkadotSigner`. The `err`
|
|
233
|
+
* channel carries truapi's canonical `CallErrorValue` envelope around the
|
|
234
|
+
* per-call versioned domain error, exactly as the generated client returns it.
|
|
235
|
+
*/
|
|
236
|
+
export interface AccountsProvider {
|
|
237
|
+
getUserId(): ResultAsync<
|
|
238
|
+
{ primaryUsername: string },
|
|
239
|
+
scale.CallErrorValue<VersionedHostGetUserIdError>
|
|
240
|
+
>;
|
|
241
|
+
requestLogin(
|
|
242
|
+
reason?: string,
|
|
243
|
+
): ResultAsync<HostRequestLoginResponse, scale.CallErrorValue<VersionedHostRequestLoginError>>;
|
|
244
|
+
getProductAccount(
|
|
245
|
+
dotNsIdentifier: string,
|
|
246
|
+
derivationIndex?: number,
|
|
247
|
+
): ResultAsync<ProductAccount, scale.CallErrorValue<VersionedHostAccountGetError>>;
|
|
248
|
+
/**
|
|
249
|
+
* Register a ring-VRF key owned by the calling product.
|
|
250
|
+
*
|
|
251
|
+
* `index` is the plain derivation index within the product's ring-VRF
|
|
252
|
+
* domain; the adapter wraps it into the wire's tagged selector.
|
|
253
|
+
*
|
|
254
|
+
* Registration returns the key's public key. Call {@link listRingVrfKeys}
|
|
255
|
+
* afterward to obtain the opaque handle required by alias and proof calls.
|
|
256
|
+
*/
|
|
257
|
+
registerRingVrfKey(
|
|
258
|
+
index: number,
|
|
259
|
+
ring: RingLocation,
|
|
260
|
+
): ResultAsync<
|
|
261
|
+
RingVrfPublicKey,
|
|
262
|
+
scale.CallErrorValue<VersionedHostAccountRegisterRingVrfKeyError>
|
|
263
|
+
>;
|
|
264
|
+
/** List an owner's registered ring-VRF keys. */
|
|
265
|
+
listRingVrfKeys(
|
|
266
|
+
owner: string,
|
|
267
|
+
disclosure?: RingVrfKeyDisclosure,
|
|
268
|
+
): ResultAsync<
|
|
269
|
+
RegisteredRingVrfKey[],
|
|
270
|
+
scale.CallErrorValue<VersionedHostAccountListRingVrfKeysError>
|
|
271
|
+
>;
|
|
272
|
+
/** Derive a contextual alias with an explicitly registered ring-VRF key. */
|
|
273
|
+
getProductAccountAlias(
|
|
274
|
+
keyHandle: RingVrfKeyHandle,
|
|
275
|
+
context: ProductProofContext,
|
|
276
|
+
location: RingLocation,
|
|
277
|
+
): ResultAsync<ContextualAlias, scale.CallErrorValue<VersionedHostAccountGetAliasError>>;
|
|
278
|
+
getLegacyAccounts(): ResultAsync<
|
|
279
|
+
HostAccount[],
|
|
280
|
+
scale.CallErrorValue<VersionedHostGetLegacyAccountsError>
|
|
281
|
+
>;
|
|
282
|
+
/**
|
|
283
|
+
* Generate a Ring VRF proof with an explicitly registered key, binding
|
|
284
|
+
* `message` to the product-scoped `context`.
|
|
285
|
+
*/
|
|
286
|
+
createRingVRFProof(
|
|
287
|
+
keyHandle: RingVrfKeyHandle,
|
|
288
|
+
context: ProductProofContext,
|
|
289
|
+
location: RingLocation,
|
|
290
|
+
message: Uint8Array,
|
|
291
|
+
): ResultAsync<RingVRFProof, scale.CallErrorValue<VersionedHostAccountCreateProofError>>;
|
|
292
|
+
/**
|
|
293
|
+
* Sign `message` directly with an explicitly registered ring-VRF key.
|
|
294
|
+
*
|
|
295
|
+
* Unlike {@link createRingVRFProof} this proves nothing about ring
|
|
296
|
+
* membership; it is the plain signature under the member key, for
|
|
297
|
+
* protocols that carry their own proof.
|
|
298
|
+
*/
|
|
299
|
+
ringVrfSign(
|
|
300
|
+
keyHandle: RingVrfKeyHandle,
|
|
301
|
+
message: Uint8Array,
|
|
302
|
+
): ResultAsync<Uint8Array, scale.CallErrorValue<VersionedHostAccountRingVrfSignError>>;
|
|
303
|
+
/**
|
|
304
|
+
* Produce an sr25519 VRF signature from a product account (RFC-0023).
|
|
305
|
+
*
|
|
306
|
+
* The host builds a Merlin transcript from `transcriptLabel` and `items`,
|
|
307
|
+
* then signs it with the account's key. Unlike {@link createRingVRFProof},
|
|
308
|
+
* this names the signing account instead of proving ring membership.
|
|
309
|
+
*
|
|
310
|
+
* The caller owns four things the types cannot enforce:
|
|
311
|
+
*
|
|
312
|
+
* - Domain separation. A label borrowed from another protocol makes the
|
|
313
|
+
* output replayable across both.
|
|
314
|
+
* - Freshness. The VRF is deterministic, so per-round values belong in
|
|
315
|
+
* `items`.
|
|
316
|
+
* - Size. Hosts cap the transcript at 32 items and 8 KiB total.
|
|
317
|
+
* - Authorization. An `AutoSigning` allowance makes these calls silent. It
|
|
318
|
+
* is not VRF-scoped, so it covers other signing by that account too.
|
|
319
|
+
*
|
|
320
|
+
* Hosts predating the call reject it through the error channel.
|
|
321
|
+
*/
|
|
322
|
+
signVrf(
|
|
323
|
+
account: ProductAccountLookup,
|
|
324
|
+
transcriptLabel: Uint8Array,
|
|
325
|
+
items: VrfTranscriptItem[],
|
|
326
|
+
): ResultAsync<VrfSignature, scale.CallErrorValue<VersionedHostAccountSignVrfError>>;
|
|
327
|
+
/**
|
|
328
|
+
* Build a `PolkadotSigner` for a product account. Signing routes through the
|
|
329
|
+
* host's `createTransaction` path: the host decodes the metadata and forwards
|
|
330
|
+
* the opaque signed-extension bytes, so unknown extensions survive end-to-end.
|
|
331
|
+
*/
|
|
332
|
+
getProductAccountSigner(account: ProductAccount): PolkadotSigner;
|
|
333
|
+
/**
|
|
334
|
+
* Build a `PolkadotSigner` for one of the user's existing wallet accounts.
|
|
335
|
+
* `name` is accepted for callsite ergonomics but unused — the signer is
|
|
336
|
+
* derived from `publicKey` alone.
|
|
337
|
+
*/
|
|
338
|
+
getLegacyAccountSigner(account: { publicKey: Uint8Array; name?: string }): PolkadotSigner;
|
|
339
|
+
subscribeAccountConnectionStatus(
|
|
340
|
+
callback: (status: HostAccountConnectionStatusSubscribeItem) => void,
|
|
341
|
+
): HostSubscription;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Derive the host's extrinsic-extension version from SCALE-encoded metadata:
|
|
346
|
+
* v4 → 0, otherwise the latest supported version. `unifyMetadata` normalizes
|
|
347
|
+
* v14/v15 so `.extrinsic.version` is an array.
|
|
348
|
+
*
|
|
349
|
+
* Indirected through {@link deps} so the SCALE decode (which needs a real
|
|
350
|
+
* metadata blob) can be stubbed in unit tests while the rest of the `signTx`
|
|
351
|
+
* flow — genesis extraction, extension mapping, the host call — is exercised.
|
|
352
|
+
*/
|
|
353
|
+
function deriveTxExtVersion(metadata: Uint8Array): number {
|
|
354
|
+
const versions = unifyMetadata(decAnyMetadata(metadata)).extrinsic.version;
|
|
355
|
+
if (versions.length === 0) {
|
|
356
|
+
throw new Error("No extrinsic version found in metadata");
|
|
357
|
+
}
|
|
358
|
+
const latestVersion = versions.reduce((acc, v) => Math.max(acc, v), 0);
|
|
359
|
+
return latestVersion === 4 ? 0 : latestVersion;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Internal seam so `import.meta.vitest` can stub the metadata decode. @internal */
|
|
363
|
+
const deps = { deriveTxExtVersion };
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Map a PAPI `signTx` call's signed extensions onto the host's
|
|
367
|
+
* `TxPayloadExtension` wire shape (hex-encoded `extra` / `additionalSigned`).
|
|
368
|
+
*/
|
|
369
|
+
function toHostExtensions(
|
|
370
|
+
signedExtensions: Record<
|
|
371
|
+
string,
|
|
372
|
+
{ identifier: string; value: Uint8Array; additionalSigned: Uint8Array }
|
|
373
|
+
>,
|
|
374
|
+
) {
|
|
375
|
+
return Object.values(signedExtensions).map((ext) => ({
|
|
376
|
+
id: ext.identifier,
|
|
377
|
+
extra: toHex(ext.value),
|
|
378
|
+
additionalSigned: toHex(ext.additionalSigned),
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Build the wire `ProductAccountId`: default the index to 0, wrap it as `Index`.
|
|
384
|
+
*
|
|
385
|
+
* Destructured rather than spread, so passing a full {@link ProductAccount}
|
|
386
|
+
* cannot leak its `publicKey` onto the wire.
|
|
387
|
+
*/
|
|
388
|
+
function toWireProductAccountId({
|
|
389
|
+
dotNsIdentifier,
|
|
390
|
+
derivationIndex = 0,
|
|
391
|
+
}: ProductAccountLookup): ProductAccountId {
|
|
392
|
+
return { dotNsIdentifier, derivationIndex: { tag: "Index", value: derivationIndex } };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Build an {@link AccountsProvider} over a TruAPI client's `account` / `signing` domains. */
|
|
396
|
+
function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
|
|
397
|
+
const account = client.account;
|
|
398
|
+
const signing = client.signing;
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
getUserId() {
|
|
402
|
+
return account.getUserId().map((response) => ({
|
|
403
|
+
primaryUsername: response.primaryUsername,
|
|
404
|
+
}));
|
|
405
|
+
},
|
|
406
|
+
requestLogin(reason) {
|
|
407
|
+
return account.requestLogin({ reason });
|
|
408
|
+
},
|
|
409
|
+
getProductAccount(dotNsIdentifier, derivationIndex = 0) {
|
|
410
|
+
return account
|
|
411
|
+
.getAccount({
|
|
412
|
+
productAccountId: toWireProductAccountId({ dotNsIdentifier, derivationIndex }),
|
|
413
|
+
})
|
|
414
|
+
.map((response) => ({
|
|
415
|
+
publicKey: fromHex(response.account.publicKey),
|
|
416
|
+
dotNsIdentifier,
|
|
417
|
+
derivationIndex,
|
|
418
|
+
}));
|
|
419
|
+
},
|
|
420
|
+
registerRingVrfKey(index, ring) {
|
|
421
|
+
return account
|
|
422
|
+
.registerRingVrfKey({ index: { tag: "Index", value: index }, ring })
|
|
423
|
+
.map(fromHex);
|
|
424
|
+
},
|
|
425
|
+
listRingVrfKeys(owner, disclosure = "Anonymized") {
|
|
426
|
+
return account.listRingVrfKeys({ owner, disclosure }).map((keys) =>
|
|
427
|
+
keys.map((key) => ({
|
|
428
|
+
...key,
|
|
429
|
+
handle: key.handle as unknown as RingVrfKeyHandle,
|
|
430
|
+
publicKey: key.publicKey === undefined ? undefined : fromHex(key.publicKey),
|
|
431
|
+
})),
|
|
432
|
+
);
|
|
433
|
+
},
|
|
434
|
+
getProductAccountAlias(keyHandle, context, location) {
|
|
435
|
+
return account
|
|
436
|
+
.getAccountAlias({
|
|
437
|
+
keyHandle: keyHandle as unknown as ProductAccountId,
|
|
438
|
+
context,
|
|
439
|
+
ringLocation: location,
|
|
440
|
+
})
|
|
441
|
+
.map((response) => ({
|
|
442
|
+
context: fromHex(response.context),
|
|
443
|
+
alias: fromHex(response.alias),
|
|
444
|
+
}));
|
|
445
|
+
},
|
|
446
|
+
getLegacyAccounts() {
|
|
447
|
+
return account.getLegacyAccounts().map((response) =>
|
|
448
|
+
response.accounts.map((a) => ({
|
|
449
|
+
publicKey: fromHex(a.publicKey),
|
|
450
|
+
name: a.name,
|
|
451
|
+
})),
|
|
452
|
+
);
|
|
453
|
+
},
|
|
454
|
+
createRingVRFProof(keyHandle, context, location, message) {
|
|
455
|
+
return account
|
|
456
|
+
.createAccountProof({
|
|
457
|
+
keyHandle: keyHandle as unknown as ProductAccountId,
|
|
458
|
+
context,
|
|
459
|
+
ringLocation: location,
|
|
460
|
+
message: toHex(message),
|
|
461
|
+
})
|
|
462
|
+
.map((response) => ({
|
|
463
|
+
proof: fromHex(response.proof),
|
|
464
|
+
contextualAlias: {
|
|
465
|
+
context: fromHex(response.contextualAlias.context),
|
|
466
|
+
alias: fromHex(response.contextualAlias.alias),
|
|
467
|
+
},
|
|
468
|
+
ringIndex: response.ringIndex,
|
|
469
|
+
ringRevision: response.ringRevision,
|
|
470
|
+
}));
|
|
471
|
+
},
|
|
472
|
+
ringVrfSign(keyHandle, message) {
|
|
473
|
+
return account
|
|
474
|
+
.ringVrfSign({
|
|
475
|
+
keyHandle: keyHandle as unknown as ProductAccountId,
|
|
476
|
+
message: toHex(message),
|
|
477
|
+
})
|
|
478
|
+
.map(fromHex);
|
|
479
|
+
},
|
|
480
|
+
signVrf(account_, transcriptLabel, items) {
|
|
481
|
+
return account
|
|
482
|
+
.signVrf({
|
|
483
|
+
account: toWireProductAccountId(account_),
|
|
484
|
+
transcriptLabel: toHex(transcriptLabel),
|
|
485
|
+
items: items.map(({ label, value }) => ({
|
|
486
|
+
label: toHex(label),
|
|
487
|
+
value: toHex(value),
|
|
488
|
+
})),
|
|
489
|
+
})
|
|
490
|
+
.map((response) => ({
|
|
491
|
+
preOutput: fromHex(response.preOutput),
|
|
492
|
+
proof: fromHex(response.proof),
|
|
493
|
+
}));
|
|
494
|
+
},
|
|
495
|
+
getProductAccountSigner(account_) {
|
|
496
|
+
const productAccountId = toWireProductAccountId(account_);
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
publicKey: account_.publicKey,
|
|
500
|
+
async signTx(callData, signedExtensions, metadata) {
|
|
501
|
+
const checkGenesis = signedExtensions.CheckGenesis;
|
|
502
|
+
if (!checkGenesis) {
|
|
503
|
+
throw new Error("Can't find genesis hash on transaction");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const response = await unwrapHostResult(
|
|
507
|
+
signing.createTransaction({
|
|
508
|
+
signer: productAccountId,
|
|
509
|
+
genesisHash: toHex(checkGenesis.additionalSigned),
|
|
510
|
+
callData: toHex(callData),
|
|
511
|
+
extensions: toHostExtensions(signedExtensions),
|
|
512
|
+
txExtVersion: deps.deriveTxExtVersion(metadata),
|
|
513
|
+
}),
|
|
514
|
+
"createTransaction failed",
|
|
515
|
+
);
|
|
516
|
+
return fromHex(response.transaction);
|
|
517
|
+
},
|
|
518
|
+
async signBytes(data) {
|
|
519
|
+
const response = await unwrapHostResult(
|
|
520
|
+
signing.signRaw({
|
|
521
|
+
account: productAccountId,
|
|
522
|
+
payload: { tag: "Bytes", value: { bytes: toHex(data) } },
|
|
523
|
+
}),
|
|
524
|
+
"signRaw failed",
|
|
525
|
+
);
|
|
526
|
+
return fromHex(response.signature);
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
},
|
|
530
|
+
getLegacyAccountSigner(account_) {
|
|
531
|
+
// `createTransactionWithLegacyAccount` identifies the signer by its
|
|
532
|
+
// raw account id (hex public key); `signRawWithLegacyAccount` takes an
|
|
533
|
+
// SS58 address the wallet can match. Compute both up front.
|
|
534
|
+
const signerHex = toHex(account_.publicKey);
|
|
535
|
+
const ss58Address = AccountId().dec(account_.publicKey);
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
publicKey: account_.publicKey,
|
|
539
|
+
async signTx(callData, signedExtensions, metadata) {
|
|
540
|
+
const checkGenesis = signedExtensions.CheckGenesis;
|
|
541
|
+
if (!checkGenesis) {
|
|
542
|
+
throw new Error("Can't find genesis hash on transaction");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const response = await unwrapHostResult(
|
|
546
|
+
signing.createTransactionWithLegacyAccount({
|
|
547
|
+
signer: signerHex,
|
|
548
|
+
genesisHash: toHex(checkGenesis.additionalSigned),
|
|
549
|
+
callData: toHex(callData),
|
|
550
|
+
extensions: toHostExtensions(signedExtensions),
|
|
551
|
+
txExtVersion: deps.deriveTxExtVersion(metadata),
|
|
552
|
+
}),
|
|
553
|
+
"createTransactionWithLegacyAccount failed",
|
|
554
|
+
);
|
|
555
|
+
return fromHex(response.transaction);
|
|
556
|
+
},
|
|
557
|
+
async signBytes(data) {
|
|
558
|
+
const response = await unwrapHostResult(
|
|
559
|
+
signing.signRawWithLegacyAccount({
|
|
560
|
+
signer: ss58Address,
|
|
561
|
+
payload: { tag: "Bytes", value: { bytes: toHex(data) } },
|
|
562
|
+
}),
|
|
563
|
+
"signRawWithLegacyAccount failed",
|
|
564
|
+
);
|
|
565
|
+
return fromHex(response.signature);
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
subscribeAccountConnectionStatus(callback) {
|
|
570
|
+
return subscribeWithInterrupt(account.connectionStatusSubscribe(), callback);
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get the accounts provider for managing host accounts, backed by
|
|
577
|
+
* `truApi.account.*` / `truApi.signing.*`. Returns `null` when running outside
|
|
578
|
+
* a host container.
|
|
579
|
+
*
|
|
580
|
+
* @returns The accounts provider, or `null` if unavailable.
|
|
581
|
+
*/
|
|
582
|
+
export async function getAccountsProvider(): Promise<AccountsProvider | null> {
|
|
583
|
+
const client = await getClient();
|
|
584
|
+
return client ? adaptAccountsProvider(client) : null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (import.meta.vitest) {
|
|
588
|
+
const { test, expect, vi } = import.meta.vitest;
|
|
589
|
+
|
|
590
|
+
/** Minimal fake of the truapi account/signing domains used to test the adapter. */
|
|
591
|
+
function makeFakeClient(opts: { onCall?: (method: string, args: unknown) => void } = {}) {
|
|
592
|
+
const okMatch = (value: unknown) => ({
|
|
593
|
+
// neverthrow ResultAsync surface used by the adapter: .map + .match.
|
|
594
|
+
map: (fn: (v: unknown) => unknown) => okMatch(fn(value)),
|
|
595
|
+
match: (ok: (v: unknown) => unknown, _err: (e: unknown) => unknown) => ok(value),
|
|
596
|
+
});
|
|
597
|
+
const method = (name: string, response: unknown) => (args: unknown) => {
|
|
598
|
+
opts.onCall?.(name, args);
|
|
599
|
+
return okMatch(response);
|
|
600
|
+
};
|
|
601
|
+
return {
|
|
602
|
+
account: {
|
|
603
|
+
getUserId: method("getUserId", { primaryUsername: "alice.dot" }),
|
|
604
|
+
getAccount: method("getAccount", { account: { publicKey: "0xaa" } }),
|
|
605
|
+
registerRingVrfKey: method("registerRingVrfKey", "0x0304"),
|
|
606
|
+
ringVrfSign: method("ringVrfSign", "0xba5eba11"),
|
|
607
|
+
listRingVrfKeys: method("listRingVrfKeys", [
|
|
608
|
+
{
|
|
609
|
+
handle: {
|
|
610
|
+
dotNsIdentifier: "people.dot",
|
|
611
|
+
derivationIndex: { tag: "Index", value: 0 },
|
|
612
|
+
},
|
|
613
|
+
rings: [
|
|
614
|
+
{
|
|
615
|
+
chainId: "0x01",
|
|
616
|
+
junctions: [{ tag: "PalletInstance", value: 1 }],
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
handle: {
|
|
622
|
+
dotNsIdentifier: "people.dot",
|
|
623
|
+
derivationIndex: { tag: "Index", value: 1 },
|
|
624
|
+
},
|
|
625
|
+
rings: [
|
|
626
|
+
{
|
|
627
|
+
chainId: "0x02",
|
|
628
|
+
junctions: [{ tag: "CollectionId", value: "0xaabb" }],
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
publicKey: "0x0102",
|
|
632
|
+
},
|
|
633
|
+
]),
|
|
634
|
+
getAccountAlias: method("getAccountAlias", { context: "0x01", alias: "0x02" }),
|
|
635
|
+
getLegacyAccounts: method("getLegacyAccounts", {
|
|
636
|
+
accounts: [{ publicKey: "0xbb", name: "Bob" }],
|
|
637
|
+
}),
|
|
638
|
+
createAccountProof: method("createAccountProof", {
|
|
639
|
+
proof: "0xc0ffee",
|
|
640
|
+
contextualAlias: { context: "0x01", alias: "0x02" },
|
|
641
|
+
ringIndex: 3,
|
|
642
|
+
ringRevision: 7,
|
|
643
|
+
}),
|
|
644
|
+
signVrf: method("signVrf", { preOutput: "0xaa11", proof: "0xbb22" }),
|
|
645
|
+
connectionStatusSubscribe: () => ({
|
|
646
|
+
subscribe: () => ({ unsubscribe: vi.fn() }),
|
|
647
|
+
[Symbol.observable as symbol]() {
|
|
648
|
+
return this;
|
|
649
|
+
},
|
|
650
|
+
}),
|
|
651
|
+
},
|
|
652
|
+
signing: {
|
|
653
|
+
createTransaction: method("createTransaction", { transaction: "0xdead" }),
|
|
654
|
+
createTransactionWithLegacyAccount: method("createTransactionWithLegacyAccount", {
|
|
655
|
+
transaction: "0xfeed",
|
|
656
|
+
}),
|
|
657
|
+
signRaw: method("signRaw", { signature: "0xbeef" }),
|
|
658
|
+
signRawWithLegacyAccount: method("signRawWithLegacyAccount", {
|
|
659
|
+
signature: "0xcafe",
|
|
660
|
+
}),
|
|
661
|
+
},
|
|
662
|
+
} as unknown as TrUApiClient;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
test("getAccountsProvider returns null outside a container", async () => {
|
|
666
|
+
expect(await getAccountsProvider()).toBeNull();
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("getProductAccount decodes the public key and carries the identifier", async () => {
|
|
670
|
+
const calls: Array<[string, unknown]> = [];
|
|
671
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
672
|
+
const provider = adaptAccountsProvider(client);
|
|
673
|
+
const account = await provider.getProductAccount("app.dot", 2).match(
|
|
674
|
+
(a) => a,
|
|
675
|
+
() => null,
|
|
676
|
+
);
|
|
677
|
+
expect(calls[0]).toEqual([
|
|
678
|
+
"getAccount",
|
|
679
|
+
{
|
|
680
|
+
productAccountId: {
|
|
681
|
+
dotNsIdentifier: "app.dot",
|
|
682
|
+
derivationIndex: { tag: "Index", value: 2 },
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
]);
|
|
686
|
+
expect(account).toEqual({
|
|
687
|
+
publicKey: fromHex("0xaa"),
|
|
688
|
+
dotNsIdentifier: "app.dot",
|
|
689
|
+
derivationIndex: 2,
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("getProductAccount defaults the derivation index in both the request and the result", async () => {
|
|
694
|
+
const calls: Array<[string, unknown]> = [];
|
|
695
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
696
|
+
const provider = adaptAccountsProvider(client);
|
|
697
|
+
const account = await provider.getProductAccount("app.dot").match(
|
|
698
|
+
(a) => a,
|
|
699
|
+
() => null,
|
|
700
|
+
);
|
|
701
|
+
expect(calls[0]).toEqual([
|
|
702
|
+
"getAccount",
|
|
703
|
+
{
|
|
704
|
+
productAccountId: {
|
|
705
|
+
dotNsIdentifier: "app.dot",
|
|
706
|
+
derivationIndex: { tag: "Index", value: 0 },
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
]);
|
|
710
|
+
// The resolved index must reach the caller too, not just the wire.
|
|
711
|
+
expect(account?.derivationIndex).toBe(0);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("registerRingVrfKey wraps the numeric index and decodes the public key", async () => {
|
|
715
|
+
const calls: Array<[string, unknown]> = [];
|
|
716
|
+
const provider = adaptAccountsProvider(
|
|
717
|
+
makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
|
|
718
|
+
);
|
|
719
|
+
const ring: RingLocation = {
|
|
720
|
+
chainId: "0x01",
|
|
721
|
+
junctions: [{ tag: "PalletInstance", value: 67 }],
|
|
722
|
+
};
|
|
723
|
+
const index = 2;
|
|
724
|
+
const publicKey = await provider.registerRingVrfKey(index, ring).match(
|
|
725
|
+
(value) => value,
|
|
726
|
+
() => null,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
expect(calls[0]).toEqual([
|
|
730
|
+
"registerRingVrfKey",
|
|
731
|
+
{ index: { tag: "Index", value: 2 }, ring },
|
|
732
|
+
]);
|
|
733
|
+
expect(publicKey).toEqual(fromHex("0x0304"));
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test("listRingVrfKeys selects by ring without exposing a raw index", async () => {
|
|
737
|
+
const calls: Array<[string, unknown]> = [];
|
|
738
|
+
const provider = adaptAccountsProvider(
|
|
739
|
+
makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
|
|
740
|
+
);
|
|
741
|
+
const keys = await provider.listRingVrfKeys("people.dot", "PublicKey").match(
|
|
742
|
+
(value) => value,
|
|
743
|
+
() => [],
|
|
744
|
+
);
|
|
745
|
+
expect(calls[0]).toEqual([
|
|
746
|
+
"listRingVrfKeys",
|
|
747
|
+
{ owner: "people.dot", disclosure: "PublicKey" },
|
|
748
|
+
]);
|
|
749
|
+
expect(keys[1].publicKey).toEqual(fromHex("0x0102"));
|
|
750
|
+
expect(
|
|
751
|
+
findRingVrfKeyHandle(keys, {
|
|
752
|
+
chainId: "0x02",
|
|
753
|
+
junctions: [{ tag: "CollectionId", value: "0xAABB" }],
|
|
754
|
+
}),
|
|
755
|
+
).toEqual(keys[1].handle);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test("getProductAccountAlias passes the selected key handle", async () => {
|
|
759
|
+
const calls: Array<[string, unknown]> = [];
|
|
760
|
+
const provider = adaptAccountsProvider(
|
|
761
|
+
makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
|
|
762
|
+
);
|
|
763
|
+
const keys = await provider.listRingVrfKeys("people.dot").match(
|
|
764
|
+
(value) => value,
|
|
765
|
+
() => [],
|
|
766
|
+
);
|
|
767
|
+
calls.length = 0;
|
|
768
|
+
const keyHandle = keys[1].handle;
|
|
769
|
+
const context: ProductProofContext = {
|
|
770
|
+
productId: "app.dot",
|
|
771
|
+
suffix: { tag: "Index", value: 0 },
|
|
772
|
+
};
|
|
773
|
+
const ring: RingLocation = {
|
|
774
|
+
chainId: "0x01",
|
|
775
|
+
junctions: [{ tag: "PalletInstance", value: 1 }],
|
|
776
|
+
};
|
|
777
|
+
const alias = await provider.getProductAccountAlias(keyHandle, context, ring).match(
|
|
778
|
+
(value) => value,
|
|
779
|
+
() => null,
|
|
780
|
+
);
|
|
781
|
+
expect(calls[0]).toEqual(["getAccountAlias", { keyHandle, context, ringLocation: ring }]);
|
|
782
|
+
expect(alias).toEqual({ context: fromHex("0x01"), alias: fromHex("0x02") });
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("ringVrfSign passes the selected handle and decodes the signature", async () => {
|
|
786
|
+
const calls: Array<[string, unknown]> = [];
|
|
787
|
+
const provider = adaptAccountsProvider(
|
|
788
|
+
makeFakeClient({ onCall: (method, args) => calls.push([method, args]) }),
|
|
789
|
+
);
|
|
790
|
+
const keys = await provider.listRingVrfKeys("people.dot").match(
|
|
791
|
+
(value) => value,
|
|
792
|
+
() => [],
|
|
793
|
+
);
|
|
794
|
+
calls.length = 0;
|
|
795
|
+
const keyHandle = keys[1].handle;
|
|
796
|
+
const signature = await provider.ringVrfSign(keyHandle, new Uint8Array([1, 2, 3])).match(
|
|
797
|
+
(value) => value,
|
|
798
|
+
() => null,
|
|
799
|
+
);
|
|
800
|
+
expect(calls[0]).toEqual([
|
|
801
|
+
"ringVrfSign",
|
|
802
|
+
{ keyHandle, message: toHex(new Uint8Array([1, 2, 3])) },
|
|
803
|
+
]);
|
|
804
|
+
expect(signature).toEqual(fromHex("0xba5eba11"));
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("createRingVRFProof hex-encodes the message and decodes the proof response", async () => {
|
|
808
|
+
const calls: Array<[string, unknown]> = [];
|
|
809
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
810
|
+
const provider = adaptAccountsProvider(client);
|
|
811
|
+
const keyHandle = (
|
|
812
|
+
await provider.listRingVrfKeys("people.dot").match(
|
|
813
|
+
(value) => value,
|
|
814
|
+
() => [],
|
|
815
|
+
)
|
|
816
|
+
)[0].handle;
|
|
817
|
+
calls.length = 0;
|
|
818
|
+
const proof = await provider
|
|
819
|
+
.createRingVRFProof(
|
|
820
|
+
keyHandle,
|
|
821
|
+
{ productId: "app.dot", suffix: { tag: "Index", value: 0 } },
|
|
822
|
+
{ chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 1 }] },
|
|
823
|
+
new Uint8Array([1, 2, 3]),
|
|
824
|
+
)
|
|
825
|
+
.match(
|
|
826
|
+
(p) => p,
|
|
827
|
+
() => null,
|
|
828
|
+
);
|
|
829
|
+
expect(calls[0][0]).toBe("createAccountProof");
|
|
830
|
+
expect(calls[0][1]).toEqual({
|
|
831
|
+
keyHandle: {
|
|
832
|
+
dotNsIdentifier: "people.dot",
|
|
833
|
+
derivationIndex: { tag: "Index", value: 0 },
|
|
834
|
+
},
|
|
835
|
+
context: { productId: "app.dot", suffix: { tag: "Index", value: 0 } },
|
|
836
|
+
ringLocation: { chainId: "0x01", junctions: [{ tag: "PalletInstance", value: 1 }] },
|
|
837
|
+
message: toHex(new Uint8Array([1, 2, 3])),
|
|
838
|
+
});
|
|
839
|
+
expect(proof).toEqual({
|
|
840
|
+
proof: fromHex("0xc0ffee"),
|
|
841
|
+
contextualAlias: { context: fromHex("0x01"), alias: fromHex("0x02") },
|
|
842
|
+
ringIndex: 3,
|
|
843
|
+
ringRevision: 7,
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("signVrf hex-encodes the transcript and decodes the signature", async () => {
|
|
848
|
+
const calls: Array<[string, unknown]> = [];
|
|
849
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
850
|
+
const provider = adaptAccountsProvider(client);
|
|
851
|
+
const transcriptLabel = new Uint8Array([1, 2, 3]);
|
|
852
|
+
const itemLabel = new Uint8Array([4]);
|
|
853
|
+
const itemValue = new Uint8Array([5, 6]);
|
|
854
|
+
const signature = await provider
|
|
855
|
+
.signVrf({ dotNsIdentifier: "app.dot", derivationIndex: 3 }, transcriptLabel, [
|
|
856
|
+
{ label: itemLabel, value: itemValue },
|
|
857
|
+
])
|
|
858
|
+
.match(
|
|
859
|
+
(s) => s,
|
|
860
|
+
() => null,
|
|
861
|
+
);
|
|
862
|
+
expect(calls[0]).toEqual([
|
|
863
|
+
"signVrf",
|
|
864
|
+
{
|
|
865
|
+
account: {
|
|
866
|
+
dotNsIdentifier: "app.dot",
|
|
867
|
+
derivationIndex: { tag: "Index", value: 3 },
|
|
868
|
+
},
|
|
869
|
+
transcriptLabel: toHex(transcriptLabel),
|
|
870
|
+
items: [{ label: toHex(itemLabel), value: toHex(itemValue) }],
|
|
871
|
+
},
|
|
872
|
+
]);
|
|
873
|
+
expect(signature).toEqual({ preOutput: fromHex("0xaa11"), proof: fromHex("0xbb22") });
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test("signVrf defaults the derivation index", async () => {
|
|
877
|
+
const calls: Array<[string, unknown]> = [];
|
|
878
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
879
|
+
const provider = adaptAccountsProvider(client);
|
|
880
|
+
await provider.signVrf({ dotNsIdentifier: "app.dot" }, new Uint8Array([1]), []).match(
|
|
881
|
+
(s) => s,
|
|
882
|
+
() => null,
|
|
883
|
+
);
|
|
884
|
+
expect((calls[0][1] as { account: unknown }).account).toEqual({
|
|
885
|
+
dotNsIdentifier: "app.dot",
|
|
886
|
+
derivationIndex: { tag: "Index", value: 0 },
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test("signVrf sends only the id fields when given a full product account", async () => {
|
|
891
|
+
const calls: Array<[string, unknown]> = [];
|
|
892
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
893
|
+
const provider = adaptAccountsProvider(client);
|
|
894
|
+
const account: ProductAccount = {
|
|
895
|
+
dotNsIdentifier: "app.dot",
|
|
896
|
+
derivationIndex: 1,
|
|
897
|
+
publicKey: new Uint8Array(32).fill(0xaa),
|
|
898
|
+
};
|
|
899
|
+
await provider.signVrf(account, new Uint8Array([1]), []).match(
|
|
900
|
+
(s) => s,
|
|
901
|
+
() => null,
|
|
902
|
+
);
|
|
903
|
+
expect(Object.keys((calls[0][1] as { account: object }).account)).toEqual([
|
|
904
|
+
"dotNsIdentifier",
|
|
905
|
+
"derivationIndex",
|
|
906
|
+
]);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test("the product signer signs bytes via signing.signRaw", async () => {
|
|
910
|
+
const calls: Array<[string, unknown]> = [];
|
|
911
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
912
|
+
const provider = adaptAccountsProvider(client);
|
|
913
|
+
const signer = provider.getProductAccountSigner({
|
|
914
|
+
dotNsIdentifier: "app.dot",
|
|
915
|
+
derivationIndex: 0,
|
|
916
|
+
publicKey: new Uint8Array(32).fill(0xaa),
|
|
917
|
+
});
|
|
918
|
+
const signature = await signer.signBytes(new Uint8Array([9, 9]));
|
|
919
|
+
expect(calls.at(-1)).toEqual([
|
|
920
|
+
"signRaw",
|
|
921
|
+
{
|
|
922
|
+
account: {
|
|
923
|
+
dotNsIdentifier: "app.dot",
|
|
924
|
+
derivationIndex: { tag: "Index", value: 0 },
|
|
925
|
+
},
|
|
926
|
+
payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([9, 9])) } },
|
|
927
|
+
},
|
|
928
|
+
]);
|
|
929
|
+
expect(signature).toEqual(fromHex("0xbeef"));
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
test("the legacy signer signs bytes via signing.signRawWithLegacyAccount (by SS58 address)", async () => {
|
|
933
|
+
const calls: Array<[string, unknown]> = [];
|
|
934
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
935
|
+
const provider = adaptAccountsProvider(client);
|
|
936
|
+
const publicKey = new Uint8Array(32).fill(0xbb);
|
|
937
|
+
const signer = provider.getLegacyAccountSigner({ publicKey });
|
|
938
|
+
const signature = await signer.signBytes(new Uint8Array([7, 7]));
|
|
939
|
+
// signRawWithLegacyAccount identifies the signer by SS58 address, not raw pubkey.
|
|
940
|
+
expect(calls.at(-1)).toEqual([
|
|
941
|
+
"signRawWithLegacyAccount",
|
|
942
|
+
{
|
|
943
|
+
signer: AccountId().dec(publicKey),
|
|
944
|
+
payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([7, 7])) } },
|
|
945
|
+
},
|
|
946
|
+
]);
|
|
947
|
+
expect(signature).toEqual(fromHex("0xcafe"));
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test("the legacy signer's signTx throws without a CheckGenesis extension", async () => {
|
|
951
|
+
const provider = adaptAccountsProvider(makeFakeClient());
|
|
952
|
+
const signer = provider.getLegacyAccountSigner({
|
|
953
|
+
publicKey: new Uint8Array(32).fill(0xbb),
|
|
954
|
+
});
|
|
955
|
+
await expect(signer.signTx(new Uint8Array([1]), {}, new Uint8Array(), 0)).rejects.toThrow(
|
|
956
|
+
"Can't find genesis hash on transaction",
|
|
957
|
+
);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Signed extensions PAPI hands to `signTx`. `CheckGenesis.additionalSigned`
|
|
961
|
+
// carries the genesis hash the signer pulls out; the rest are mapped to the
|
|
962
|
+
// host's `{ id, extra, additionalSigned }` wire shape.
|
|
963
|
+
const sampleExtensions = {
|
|
964
|
+
CheckGenesis: {
|
|
965
|
+
identifier: "CheckGenesis",
|
|
966
|
+
value: new Uint8Array([]),
|
|
967
|
+
additionalSigned: new Uint8Array([0x01, 0x02]),
|
|
968
|
+
},
|
|
969
|
+
CheckNonce: {
|
|
970
|
+
identifier: "CheckNonce",
|
|
971
|
+
value: new Uint8Array([0x05]),
|
|
972
|
+
additionalSigned: new Uint8Array([]),
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
const expectedHostExtensions = [
|
|
976
|
+
{
|
|
977
|
+
id: "CheckGenesis",
|
|
978
|
+
extra: toHex(new Uint8Array([])),
|
|
979
|
+
additionalSigned: toHex(new Uint8Array([0x01, 0x02])),
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
id: "CheckNonce",
|
|
983
|
+
extra: toHex(new Uint8Array([0x05])),
|
|
984
|
+
additionalSigned: toHex(new Uint8Array([])),
|
|
985
|
+
},
|
|
986
|
+
];
|
|
987
|
+
|
|
988
|
+
test("the product signer's signTx builds createTransaction from genesis + extensions", async () => {
|
|
989
|
+
// Stub the metadata decode (needs a real SCALE blob) so the rest of the
|
|
990
|
+
// signTx flow — genesis extraction, extension mapping, the host call,
|
|
991
|
+
// response decode — is exercised against a fixed txExtVersion.
|
|
992
|
+
vi.spyOn(deps, "deriveTxExtVersion").mockReturnValue(0);
|
|
993
|
+
const calls: Array<[string, unknown]> = [];
|
|
994
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
995
|
+
const provider = adaptAccountsProvider(client);
|
|
996
|
+
const signer = provider.getProductAccountSigner({
|
|
997
|
+
dotNsIdentifier: "app.dot",
|
|
998
|
+
derivationIndex: 0,
|
|
999
|
+
publicKey: new Uint8Array(32).fill(0xaa),
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const signed = await signer.signTx(
|
|
1003
|
+
new Uint8Array([0xca, 0x11]),
|
|
1004
|
+
sampleExtensions,
|
|
1005
|
+
new Uint8Array([0x6d]),
|
|
1006
|
+
0,
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
expect(calls.at(-1)).toEqual([
|
|
1010
|
+
"createTransaction",
|
|
1011
|
+
{
|
|
1012
|
+
signer: { dotNsIdentifier: "app.dot", derivationIndex: { tag: "Index", value: 0 } },
|
|
1013
|
+
genesisHash: toHex(new Uint8Array([0x01, 0x02])),
|
|
1014
|
+
callData: toHex(new Uint8Array([0xca, 0x11])),
|
|
1015
|
+
extensions: expectedHostExtensions,
|
|
1016
|
+
txExtVersion: 0,
|
|
1017
|
+
},
|
|
1018
|
+
]);
|
|
1019
|
+
expect(signed).toEqual(fromHex("0xdead"));
|
|
1020
|
+
vi.restoreAllMocks();
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
test("the legacy signer's signTx builds createTransactionWithLegacyAccount (signer = hex pubkey)", async () => {
|
|
1024
|
+
vi.spyOn(deps, "deriveTxExtVersion").mockReturnValue(0);
|
|
1025
|
+
const calls: Array<[string, unknown]> = [];
|
|
1026
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
1027
|
+
const provider = adaptAccountsProvider(client);
|
|
1028
|
+
const publicKey = new Uint8Array(32).fill(0xbb);
|
|
1029
|
+
const signer = provider.getLegacyAccountSigner({ publicKey });
|
|
1030
|
+
|
|
1031
|
+
const signed = await signer.signTx(
|
|
1032
|
+
new Uint8Array([0xca, 0x11]),
|
|
1033
|
+
sampleExtensions,
|
|
1034
|
+
new Uint8Array([0x6d]),
|
|
1035
|
+
0,
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
expect(calls.at(-1)).toEqual([
|
|
1039
|
+
"createTransactionWithLegacyAccount",
|
|
1040
|
+
{
|
|
1041
|
+
// createTransactionWithLegacyAccount identifies the signer by raw hex pubkey.
|
|
1042
|
+
signer: toHex(publicKey),
|
|
1043
|
+
genesisHash: toHex(new Uint8Array([0x01, 0x02])),
|
|
1044
|
+
callData: toHex(new Uint8Array([0xca, 0x11])),
|
|
1045
|
+
extensions: expectedHostExtensions,
|
|
1046
|
+
txExtVersion: 0,
|
|
1047
|
+
},
|
|
1048
|
+
]);
|
|
1049
|
+
expect(signed).toEqual(fromHex("0xfeed"));
|
|
1050
|
+
vi.restoreAllMocks();
|
|
1051
|
+
});
|
|
1052
|
+
}
|