@parity/product-sdk-host 0.10.3 → 0.12.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/dist/index.d.ts +681 -462
- package/dist/index.js +890 -219
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/accounts.ts +544 -0
- package/src/chain-spec.ts +272 -0
- package/src/chain-transaction.ts +241 -0
- package/src/chat.ts +81 -85
- package/src/container.ts +211 -246
- package/src/entropy.ts +63 -25
- package/src/errors.ts +198 -0
- package/src/features.ts +172 -0
- package/src/index.ts +47 -22
- package/src/navigation.ts +128 -0
- package/src/notifications.ts +59 -69
- package/src/papi-provider.ts +673 -0
- package/src/payments.ts +77 -61
- package/src/permissions.ts +107 -105
- package/src/result.ts +56 -0
- package/src/theme.ts +35 -63
- package/src/transport.ts +71 -0
- package/src/truapi.ts +166 -409
- package/src/types.ts +69 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parity/product-sdk-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Host container detection and storage access for Polkadot Desktop and Mobile environments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
"src"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@
|
|
22
|
-
"@
|
|
21
|
+
"@parity/truapi": "^0.3.2",
|
|
22
|
+
"@polkadot-api/json-rpc-provider": "^0.2.0",
|
|
23
|
+
"@polkadot-api/substrate-bindings": "^0.20.3",
|
|
24
|
+
"neverthrow": "^8.2.0",
|
|
23
25
|
"polkadot-api": "^2.1.6",
|
|
24
26
|
"@parity/product-sdk-logger": "0.1.1"
|
|
25
27
|
},
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
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`), connection
|
|
10
|
+
* status, and PAPI `PolkadotSigner` factories for both product and legacy
|
|
11
|
+
* accounts.
|
|
12
|
+
*
|
|
13
|
+
* The signer factories build a PAPI `PolkadotSigner` directly over
|
|
14
|
+
* `truApi.signing.createTransaction` (product) /
|
|
15
|
+
* `createTransactionWithLegacyAccount` (legacy) — `signTx` derives the
|
|
16
|
+
* metadata-driven `txExtVersion` and maps the signed extensions to the host's
|
|
17
|
+
* wire shape; `signBytes` calls `signing.signRaw(WithLegacyAccount)`. No PJS
|
|
18
|
+
* bridge is involved, so opaque signed extensions (e.g. Paseo Next's `AsPgas`)
|
|
19
|
+
* survive end-to-end.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
|
|
25
|
+
import type { ResultAsync } from "neverthrow";
|
|
26
|
+
import { AccountId, type PolkadotSigner } from "polkadot-api";
|
|
27
|
+
|
|
28
|
+
import type {
|
|
29
|
+
HostAccountConnectionStatusSubscribeItem,
|
|
30
|
+
HostAccountCreateProofError,
|
|
31
|
+
HostAccountGetAliasResponse as WireAlias,
|
|
32
|
+
HostAccountGetError,
|
|
33
|
+
HostGetUserIdError,
|
|
34
|
+
HostRequestLoginError,
|
|
35
|
+
HostRequestLoginResponse,
|
|
36
|
+
LegacyAccount as WireLegacyAccount,
|
|
37
|
+
ProductAccount as WireProductAccount,
|
|
38
|
+
ProductAccountId,
|
|
39
|
+
RingLocation,
|
|
40
|
+
TrUApiClient,
|
|
41
|
+
} from "@parity/truapi";
|
|
42
|
+
|
|
43
|
+
import { getClient, subscribeWithInterrupt } from "./transport.js";
|
|
44
|
+
import { fromHex, toHex, unwrapHostResult } from "./truapi.js";
|
|
45
|
+
import type { HostSubscription } from "./types.js";
|
|
46
|
+
|
|
47
|
+
/** Ring location for Ring VRF proofs (`{ genesisHash, ringRootHash, hints? }`). Re-exported from `@parity/truapi`. */
|
|
48
|
+
export type { RingLocation } from "@parity/truapi";
|
|
49
|
+
|
|
50
|
+
// The account/alias shapes come from `@parity/truapi`'s generated specs; we
|
|
51
|
+
// derive the SDK-facing views from them so the field inventory tracks the
|
|
52
|
+
// protocol automatically, and override only the byte fields the adapter
|
|
53
|
+
// decodes (the wire types carry `0x`-prefixed `HexString`s, whereas these
|
|
54
|
+
// surface decoded `Uint8Array`s). Same pattern as `@parity/product-sdk-statement-store`.
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One of the user's existing wallet accounts, surfaced through the host and
|
|
58
|
+
* identified by its public key and an optional name. Contrast with
|
|
59
|
+
* {@link ProductAccount}, which is also user-controlled but derived by the
|
|
60
|
+
* host for a specific app rather than picked from the user's existing keys.
|
|
61
|
+
*
|
|
62
|
+
* Derived from `@parity/truapi`'s `LegacyAccount`, with `publicKey` decoded to bytes.
|
|
63
|
+
*/
|
|
64
|
+
export type HostAccount = Omit<WireLegacyAccount, "publicKey"> & {
|
|
65
|
+
/** Raw public key bytes. */
|
|
66
|
+
publicKey: Uint8Array;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A product account — an app-scoped derived account managed by the host wallet.
|
|
71
|
+
*
|
|
72
|
+
* The host derives a unique keypair for each app (identified by `dotNsIdentifier`)
|
|
73
|
+
* so apps get their own account that the user controls but is scoped to the app.
|
|
74
|
+
*
|
|
75
|
+
* Combines `@parity/truapi`'s `ProductAccountId` (the `{ dotNsIdentifier,
|
|
76
|
+
* derivationIndex }` lookup key) with the `ProductAccount` payload, with
|
|
77
|
+
* `publicKey` decoded to bytes.
|
|
78
|
+
*/
|
|
79
|
+
export type ProductAccount = ProductAccountId &
|
|
80
|
+
Omit<WireProductAccount, "publicKey"> & {
|
|
81
|
+
/** Raw public key bytes. */
|
|
82
|
+
publicKey: Uint8Array;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A contextual alias obtained from Ring VRF.
|
|
87
|
+
*
|
|
88
|
+
* Proves account membership in a ring without revealing which account.
|
|
89
|
+
*
|
|
90
|
+
* Derived from `@parity/truapi`'s alias response, with both fields decoded to bytes.
|
|
91
|
+
*/
|
|
92
|
+
export type ContextualAlias = { [K in keyof WireAlias]: Uint8Array };
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Accounts provider handle, backed by `truApi.account.*` / `truApi.signing.*`.
|
|
96
|
+
* Surfaces the user's wallet accounts, app-scoped product accounts, Ring VRF,
|
|
97
|
+
* user identity, connection status, and `PolkadotSigner` factories.
|
|
98
|
+
*
|
|
99
|
+
* Lookup methods return a neverthrow `ResultAsync` (use `.match(ok, err)`);
|
|
100
|
+
* the signer factories return a synchronous PAPI `PolkadotSigner`.
|
|
101
|
+
*/
|
|
102
|
+
export interface AccountsProvider {
|
|
103
|
+
getUserId(): ResultAsync<{ primaryUsername: string }, HostGetUserIdError>;
|
|
104
|
+
requestLogin(reason?: string): ResultAsync<HostRequestLoginResponse, HostRequestLoginError>;
|
|
105
|
+
getProductAccount(
|
|
106
|
+
dotNsIdentifier: string,
|
|
107
|
+
derivationIndex?: number,
|
|
108
|
+
): ResultAsync<ProductAccount, HostAccountGetError>;
|
|
109
|
+
getProductAccountAlias(
|
|
110
|
+
dotNsIdentifier: string,
|
|
111
|
+
derivationIndex?: number,
|
|
112
|
+
): ResultAsync<ContextualAlias, HostAccountGetError>;
|
|
113
|
+
getLegacyAccounts(): ResultAsync<HostAccount[], HostAccountGetError>;
|
|
114
|
+
createRingVRFProof(
|
|
115
|
+
dotNsIdentifier: string,
|
|
116
|
+
derivationIndex: number,
|
|
117
|
+
location: RingLocation,
|
|
118
|
+
message: Uint8Array,
|
|
119
|
+
): ResultAsync<Uint8Array, HostAccountCreateProofError>;
|
|
120
|
+
/**
|
|
121
|
+
* Build a `PolkadotSigner` for a product account. Signing routes through the
|
|
122
|
+
* host's `createTransaction` path: the host decodes the metadata and forwards
|
|
123
|
+
* the opaque signed-extension bytes, so unknown extensions survive end-to-end.
|
|
124
|
+
*/
|
|
125
|
+
getProductAccountSigner(account: ProductAccount): PolkadotSigner;
|
|
126
|
+
/**
|
|
127
|
+
* Build a `PolkadotSigner` for one of the user's existing wallet accounts.
|
|
128
|
+
* `name` is accepted for callsite ergonomics but unused — the signer is
|
|
129
|
+
* derived from `publicKey` alone.
|
|
130
|
+
*/
|
|
131
|
+
getLegacyAccountSigner(account: { publicKey: Uint8Array; name?: string }): PolkadotSigner;
|
|
132
|
+
subscribeAccountConnectionStatus(
|
|
133
|
+
callback: (status: HostAccountConnectionStatusSubscribeItem) => void,
|
|
134
|
+
): HostSubscription;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Derive the host's extrinsic-extension version from SCALE-encoded metadata:
|
|
139
|
+
* v4 → 0, otherwise the latest supported version. `unifyMetadata` normalizes
|
|
140
|
+
* v14/v15 so `.extrinsic.version` is an array.
|
|
141
|
+
*
|
|
142
|
+
* Indirected through {@link deps} so the SCALE decode (which needs a real
|
|
143
|
+
* metadata blob) can be stubbed in unit tests while the rest of the `signTx`
|
|
144
|
+
* flow — genesis extraction, extension mapping, the host call — is exercised.
|
|
145
|
+
*/
|
|
146
|
+
function deriveTxExtVersion(metadata: Uint8Array): number {
|
|
147
|
+
const versions = unifyMetadata(decAnyMetadata(metadata)).extrinsic.version;
|
|
148
|
+
if (versions.length === 0) {
|
|
149
|
+
throw new Error("No extrinsic version found in metadata");
|
|
150
|
+
}
|
|
151
|
+
const latestVersion = versions.reduce((acc, v) => Math.max(acc, v), 0);
|
|
152
|
+
return latestVersion === 4 ? 0 : latestVersion;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Internal seam so `import.meta.vitest` can stub the metadata decode. @internal */
|
|
156
|
+
const deps = { deriveTxExtVersion };
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Map a PAPI `signTx` call's signed extensions onto the host's
|
|
160
|
+
* `TxPayloadExtension` wire shape (hex-encoded `extra` / `additionalSigned`).
|
|
161
|
+
*/
|
|
162
|
+
function toHostExtensions(
|
|
163
|
+
signedExtensions: Record<
|
|
164
|
+
string,
|
|
165
|
+
{ identifier: string; value: Uint8Array; additionalSigned: Uint8Array }
|
|
166
|
+
>,
|
|
167
|
+
) {
|
|
168
|
+
return Object.values(signedExtensions).map((ext) => ({
|
|
169
|
+
id: ext.identifier,
|
|
170
|
+
extra: toHex(ext.value),
|
|
171
|
+
additionalSigned: toHex(ext.additionalSigned),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Build an {@link AccountsProvider} over a TruAPI client's `account` / `signing` domains. */
|
|
176
|
+
function adaptAccountsProvider(client: TrUApiClient): AccountsProvider {
|
|
177
|
+
const account = client.account;
|
|
178
|
+
const signing = client.signing;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
getUserId() {
|
|
182
|
+
return account.getUserId().map((response) => ({
|
|
183
|
+
primaryUsername: response.primaryUsername,
|
|
184
|
+
}));
|
|
185
|
+
},
|
|
186
|
+
requestLogin(reason) {
|
|
187
|
+
return account.requestLogin({ reason });
|
|
188
|
+
},
|
|
189
|
+
getProductAccount(dotNsIdentifier, derivationIndex = 0) {
|
|
190
|
+
return account
|
|
191
|
+
.getAccount({ productAccountId: { dotNsIdentifier, derivationIndex } })
|
|
192
|
+
.map((response) => ({
|
|
193
|
+
publicKey: fromHex(response.account.publicKey),
|
|
194
|
+
dotNsIdentifier,
|
|
195
|
+
derivationIndex,
|
|
196
|
+
}));
|
|
197
|
+
},
|
|
198
|
+
getProductAccountAlias(dotNsIdentifier, derivationIndex = 0) {
|
|
199
|
+
return account
|
|
200
|
+
.getAccountAlias({ productAccountId: { dotNsIdentifier, derivationIndex } })
|
|
201
|
+
.map((response) => ({
|
|
202
|
+
context: fromHex(response.context),
|
|
203
|
+
alias: fromHex(response.alias),
|
|
204
|
+
}));
|
|
205
|
+
},
|
|
206
|
+
getLegacyAccounts() {
|
|
207
|
+
return account.getLegacyAccounts().map((response) =>
|
|
208
|
+
response.accounts.map((a) => ({
|
|
209
|
+
publicKey: fromHex(a.publicKey),
|
|
210
|
+
name: a.name,
|
|
211
|
+
})),
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
createRingVRFProof(dotNsIdentifier, derivationIndex, location, message) {
|
|
215
|
+
return account
|
|
216
|
+
.createAccountProof({
|
|
217
|
+
productAccountId: { dotNsIdentifier, derivationIndex },
|
|
218
|
+
ringLocation: location,
|
|
219
|
+
context: toHex(message),
|
|
220
|
+
})
|
|
221
|
+
.map((response) => fromHex(response.proof));
|
|
222
|
+
},
|
|
223
|
+
getProductAccountSigner(account_) {
|
|
224
|
+
const productAccountId = {
|
|
225
|
+
dotNsIdentifier: account_.dotNsIdentifier,
|
|
226
|
+
derivationIndex: account_.derivationIndex,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
publicKey: account_.publicKey,
|
|
231
|
+
async signTx(callData, signedExtensions, metadata) {
|
|
232
|
+
const checkGenesis = signedExtensions.CheckGenesis;
|
|
233
|
+
if (!checkGenesis) {
|
|
234
|
+
throw new Error("Can't find genesis hash on transaction");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const response = await unwrapHostResult(
|
|
238
|
+
signing.createTransaction({
|
|
239
|
+
signer: productAccountId,
|
|
240
|
+
genesisHash: toHex(checkGenesis.additionalSigned),
|
|
241
|
+
callData: toHex(callData),
|
|
242
|
+
extensions: toHostExtensions(signedExtensions),
|
|
243
|
+
txExtVersion: deps.deriveTxExtVersion(metadata),
|
|
244
|
+
}),
|
|
245
|
+
"createTransaction failed",
|
|
246
|
+
);
|
|
247
|
+
return fromHex(response.transaction);
|
|
248
|
+
},
|
|
249
|
+
async signBytes(data) {
|
|
250
|
+
const response = await unwrapHostResult(
|
|
251
|
+
signing.signRaw({
|
|
252
|
+
account: productAccountId,
|
|
253
|
+
payload: { tag: "Bytes", value: { bytes: toHex(data) } },
|
|
254
|
+
}),
|
|
255
|
+
"signRaw failed",
|
|
256
|
+
);
|
|
257
|
+
return fromHex(response.signature);
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
getLegacyAccountSigner(account_) {
|
|
262
|
+
// `createTransactionWithLegacyAccount` identifies the signer by its
|
|
263
|
+
// raw account id (hex public key); `signRawWithLegacyAccount` takes an
|
|
264
|
+
// SS58 address the wallet can match. Compute both up front.
|
|
265
|
+
const signerHex = toHex(account_.publicKey);
|
|
266
|
+
const ss58Address = AccountId().dec(account_.publicKey);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
publicKey: account_.publicKey,
|
|
270
|
+
async signTx(callData, signedExtensions, metadata) {
|
|
271
|
+
const checkGenesis = signedExtensions.CheckGenesis;
|
|
272
|
+
if (!checkGenesis) {
|
|
273
|
+
throw new Error("Can't find genesis hash on transaction");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const response = await unwrapHostResult(
|
|
277
|
+
signing.createTransactionWithLegacyAccount({
|
|
278
|
+
signer: signerHex,
|
|
279
|
+
genesisHash: toHex(checkGenesis.additionalSigned),
|
|
280
|
+
callData: toHex(callData),
|
|
281
|
+
extensions: toHostExtensions(signedExtensions),
|
|
282
|
+
txExtVersion: deps.deriveTxExtVersion(metadata),
|
|
283
|
+
}),
|
|
284
|
+
"createTransactionWithLegacyAccount failed",
|
|
285
|
+
);
|
|
286
|
+
return fromHex(response.transaction);
|
|
287
|
+
},
|
|
288
|
+
async signBytes(data) {
|
|
289
|
+
const response = await unwrapHostResult(
|
|
290
|
+
signing.signRawWithLegacyAccount({
|
|
291
|
+
signer: ss58Address,
|
|
292
|
+
payload: { tag: "Bytes", value: { bytes: toHex(data) } },
|
|
293
|
+
}),
|
|
294
|
+
"signRawWithLegacyAccount failed",
|
|
295
|
+
);
|
|
296
|
+
return fromHex(response.signature);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
subscribeAccountConnectionStatus(callback) {
|
|
301
|
+
return subscribeWithInterrupt(account.connectionStatusSubscribe(), callback);
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get the accounts provider for managing host accounts, backed by
|
|
308
|
+
* `truApi.account.*` / `truApi.signing.*`. Returns `null` when running outside
|
|
309
|
+
* a host container.
|
|
310
|
+
*
|
|
311
|
+
* @returns The accounts provider, or `null` if unavailable.
|
|
312
|
+
*/
|
|
313
|
+
export async function getAccountsProvider(): Promise<AccountsProvider | null> {
|
|
314
|
+
const client = await getClient();
|
|
315
|
+
return client ? adaptAccountsProvider(client) : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (import.meta.vitest) {
|
|
319
|
+
const { test, expect, vi } = import.meta.vitest;
|
|
320
|
+
|
|
321
|
+
/** Minimal fake of the truapi account/signing domains used to test the adapter. */
|
|
322
|
+
function makeFakeClient(opts: { onCall?: (method: string, args: unknown) => void } = {}) {
|
|
323
|
+
const okMatch = (value: unknown) => ({
|
|
324
|
+
// neverthrow ResultAsync surface used by the adapter: .map + .match.
|
|
325
|
+
map: (fn: (v: unknown) => unknown) => okMatch(fn(value)),
|
|
326
|
+
match: (ok: (v: unknown) => unknown, _err: (e: unknown) => unknown) => ok(value),
|
|
327
|
+
});
|
|
328
|
+
const method = (name: string, response: unknown) => (args: unknown) => {
|
|
329
|
+
opts.onCall?.(name, args);
|
|
330
|
+
return okMatch(response);
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
account: {
|
|
334
|
+
getUserId: method("getUserId", { primaryUsername: "alice.dot" }),
|
|
335
|
+
getAccount: method("getAccount", { account: { publicKey: "0xaa" } }),
|
|
336
|
+
getAccountAlias: method("getAccountAlias", { context: "0x01", alias: "0x02" }),
|
|
337
|
+
getLegacyAccounts: method("getLegacyAccounts", {
|
|
338
|
+
accounts: [{ publicKey: "0xbb", name: "Bob" }],
|
|
339
|
+
}),
|
|
340
|
+
createAccountProof: method("createAccountProof", { proof: "0xc0ffee" }),
|
|
341
|
+
connectionStatusSubscribe: () => ({
|
|
342
|
+
subscribe: () => ({ unsubscribe: vi.fn() }),
|
|
343
|
+
[Symbol.observable as symbol]() {
|
|
344
|
+
return this;
|
|
345
|
+
},
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
signing: {
|
|
349
|
+
createTransaction: method("createTransaction", { transaction: "0xdead" }),
|
|
350
|
+
createTransactionWithLegacyAccount: method("createTransactionWithLegacyAccount", {
|
|
351
|
+
transaction: "0xfeed",
|
|
352
|
+
}),
|
|
353
|
+
signRaw: method("signRaw", { signature: "0xbeef" }),
|
|
354
|
+
signRawWithLegacyAccount: method("signRawWithLegacyAccount", {
|
|
355
|
+
signature: "0xcafe",
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
} as unknown as TrUApiClient;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
test("getAccountsProvider returns null outside a container", async () => {
|
|
362
|
+
expect(await getAccountsProvider()).toBeNull();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("getProductAccount decodes the public key and carries the identifier", async () => {
|
|
366
|
+
const calls: Array<[string, unknown]> = [];
|
|
367
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
368
|
+
const provider = adaptAccountsProvider(client);
|
|
369
|
+
const account = await provider.getProductAccount("app.dot", 2).match(
|
|
370
|
+
(a) => a,
|
|
371
|
+
() => null,
|
|
372
|
+
);
|
|
373
|
+
expect(calls[0]).toEqual([
|
|
374
|
+
"getAccount",
|
|
375
|
+
{ productAccountId: { dotNsIdentifier: "app.dot", derivationIndex: 2 } },
|
|
376
|
+
]);
|
|
377
|
+
expect(account).toEqual({
|
|
378
|
+
publicKey: fromHex("0xaa"),
|
|
379
|
+
dotNsIdentifier: "app.dot",
|
|
380
|
+
derivationIndex: 2,
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("createRingVRFProof hex-encodes the message as the proof context", async () => {
|
|
385
|
+
const calls: Array<[string, unknown]> = [];
|
|
386
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
387
|
+
const provider = adaptAccountsProvider(client);
|
|
388
|
+
const proof = await provider
|
|
389
|
+
.createRingVRFProof(
|
|
390
|
+
"app.dot",
|
|
391
|
+
0,
|
|
392
|
+
{ genesisHash: "0x01", ringRootHash: "0x02" },
|
|
393
|
+
new Uint8Array([1, 2, 3]),
|
|
394
|
+
)
|
|
395
|
+
.match(
|
|
396
|
+
(p) => p,
|
|
397
|
+
() => null,
|
|
398
|
+
);
|
|
399
|
+
expect(calls[0][0]).toBe("createAccountProof");
|
|
400
|
+
expect((calls[0][1] as { context: string }).context).toBe(toHex(new Uint8Array([1, 2, 3])));
|
|
401
|
+
expect(proof).toEqual(fromHex("0xc0ffee"));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("the product signer signs bytes via signing.signRaw", async () => {
|
|
405
|
+
const calls: Array<[string, unknown]> = [];
|
|
406
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
407
|
+
const provider = adaptAccountsProvider(client);
|
|
408
|
+
const signer = provider.getProductAccountSigner({
|
|
409
|
+
dotNsIdentifier: "app.dot",
|
|
410
|
+
derivationIndex: 0,
|
|
411
|
+
publicKey: new Uint8Array(32).fill(0xaa),
|
|
412
|
+
});
|
|
413
|
+
const signature = await signer.signBytes(new Uint8Array([9, 9]));
|
|
414
|
+
expect(calls.at(-1)).toEqual([
|
|
415
|
+
"signRaw",
|
|
416
|
+
{
|
|
417
|
+
account: { dotNsIdentifier: "app.dot", derivationIndex: 0 },
|
|
418
|
+
payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([9, 9])) } },
|
|
419
|
+
},
|
|
420
|
+
]);
|
|
421
|
+
expect(signature).toEqual(fromHex("0xbeef"));
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("the legacy signer signs bytes via signing.signRawWithLegacyAccount (by SS58 address)", async () => {
|
|
425
|
+
const calls: Array<[string, unknown]> = [];
|
|
426
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
427
|
+
const provider = adaptAccountsProvider(client);
|
|
428
|
+
const publicKey = new Uint8Array(32).fill(0xbb);
|
|
429
|
+
const signer = provider.getLegacyAccountSigner({ publicKey });
|
|
430
|
+
const signature = await signer.signBytes(new Uint8Array([7, 7]));
|
|
431
|
+
// signRawWithLegacyAccount identifies the signer by SS58 address, not raw pubkey.
|
|
432
|
+
expect(calls.at(-1)).toEqual([
|
|
433
|
+
"signRawWithLegacyAccount",
|
|
434
|
+
{
|
|
435
|
+
signer: AccountId().dec(publicKey),
|
|
436
|
+
payload: { tag: "Bytes", value: { bytes: toHex(new Uint8Array([7, 7])) } },
|
|
437
|
+
},
|
|
438
|
+
]);
|
|
439
|
+
expect(signature).toEqual(fromHex("0xcafe"));
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("the legacy signer's signTx throws without a CheckGenesis extension", async () => {
|
|
443
|
+
const provider = adaptAccountsProvider(makeFakeClient());
|
|
444
|
+
const signer = provider.getLegacyAccountSigner({
|
|
445
|
+
publicKey: new Uint8Array(32).fill(0xbb),
|
|
446
|
+
});
|
|
447
|
+
await expect(signer.signTx(new Uint8Array([1]), {}, new Uint8Array(), 0)).rejects.toThrow(
|
|
448
|
+
"Can't find genesis hash on transaction",
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Signed extensions PAPI hands to `signTx`. `CheckGenesis.additionalSigned`
|
|
453
|
+
// carries the genesis hash the signer pulls out; the rest are mapped to the
|
|
454
|
+
// host's `{ id, extra, additionalSigned }` wire shape.
|
|
455
|
+
const sampleExtensions = {
|
|
456
|
+
CheckGenesis: {
|
|
457
|
+
identifier: "CheckGenesis",
|
|
458
|
+
value: new Uint8Array([]),
|
|
459
|
+
additionalSigned: new Uint8Array([0x01, 0x02]),
|
|
460
|
+
},
|
|
461
|
+
CheckNonce: {
|
|
462
|
+
identifier: "CheckNonce",
|
|
463
|
+
value: new Uint8Array([0x05]),
|
|
464
|
+
additionalSigned: new Uint8Array([]),
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
const expectedHostExtensions = [
|
|
468
|
+
{
|
|
469
|
+
id: "CheckGenesis",
|
|
470
|
+
extra: toHex(new Uint8Array([])),
|
|
471
|
+
additionalSigned: toHex(new Uint8Array([0x01, 0x02])),
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: "CheckNonce",
|
|
475
|
+
extra: toHex(new Uint8Array([0x05])),
|
|
476
|
+
additionalSigned: toHex(new Uint8Array([])),
|
|
477
|
+
},
|
|
478
|
+
];
|
|
479
|
+
|
|
480
|
+
test("the product signer's signTx builds createTransaction from genesis + extensions", async () => {
|
|
481
|
+
// Stub the metadata decode (needs a real SCALE blob) so the rest of the
|
|
482
|
+
// signTx flow — genesis extraction, extension mapping, the host call,
|
|
483
|
+
// response decode — is exercised against a fixed txExtVersion.
|
|
484
|
+
vi.spyOn(deps, "deriveTxExtVersion").mockReturnValue(0);
|
|
485
|
+
const calls: Array<[string, unknown]> = [];
|
|
486
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
487
|
+
const provider = adaptAccountsProvider(client);
|
|
488
|
+
const signer = provider.getProductAccountSigner({
|
|
489
|
+
dotNsIdentifier: "app.dot",
|
|
490
|
+
derivationIndex: 0,
|
|
491
|
+
publicKey: new Uint8Array(32).fill(0xaa),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const signed = await signer.signTx(
|
|
495
|
+
new Uint8Array([0xca, 0x11]),
|
|
496
|
+
sampleExtensions,
|
|
497
|
+
new Uint8Array([0x6d]),
|
|
498
|
+
0,
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
expect(calls.at(-1)).toEqual([
|
|
502
|
+
"createTransaction",
|
|
503
|
+
{
|
|
504
|
+
signer: { dotNsIdentifier: "app.dot", derivationIndex: 0 },
|
|
505
|
+
genesisHash: toHex(new Uint8Array([0x01, 0x02])),
|
|
506
|
+
callData: toHex(new Uint8Array([0xca, 0x11])),
|
|
507
|
+
extensions: expectedHostExtensions,
|
|
508
|
+
txExtVersion: 0,
|
|
509
|
+
},
|
|
510
|
+
]);
|
|
511
|
+
expect(signed).toEqual(fromHex("0xdead"));
|
|
512
|
+
vi.restoreAllMocks();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("the legacy signer's signTx builds createTransactionWithLegacyAccount (signer = hex pubkey)", async () => {
|
|
516
|
+
vi.spyOn(deps, "deriveTxExtVersion").mockReturnValue(0);
|
|
517
|
+
const calls: Array<[string, unknown]> = [];
|
|
518
|
+
const client = makeFakeClient({ onCall: (m, a) => calls.push([m, a]) });
|
|
519
|
+
const provider = adaptAccountsProvider(client);
|
|
520
|
+
const publicKey = new Uint8Array(32).fill(0xbb);
|
|
521
|
+
const signer = provider.getLegacyAccountSigner({ publicKey });
|
|
522
|
+
|
|
523
|
+
const signed = await signer.signTx(
|
|
524
|
+
new Uint8Array([0xca, 0x11]),
|
|
525
|
+
sampleExtensions,
|
|
526
|
+
new Uint8Array([0x6d]),
|
|
527
|
+
0,
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
expect(calls.at(-1)).toEqual([
|
|
531
|
+
"createTransactionWithLegacyAccount",
|
|
532
|
+
{
|
|
533
|
+
// createTransactionWithLegacyAccount identifies the signer by raw hex pubkey.
|
|
534
|
+
signer: toHex(publicKey),
|
|
535
|
+
genesisHash: toHex(new Uint8Array([0x01, 0x02])),
|
|
536
|
+
callData: toHex(new Uint8Array([0xca, 0x11])),
|
|
537
|
+
extensions: expectedHostExtensions,
|
|
538
|
+
txExtVersion: 0,
|
|
539
|
+
},
|
|
540
|
+
]);
|
|
541
|
+
expect(signed).toEqual(fromHex("0xfeed"));
|
|
542
|
+
vi.restoreAllMocks();
|
|
543
|
+
});
|
|
544
|
+
}
|