@parity/product-sdk 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.
Files changed (88) hide show
  1. package/LICENSE +201 -0
  2. package/dist/address/index.d.ts +9 -0
  3. package/dist/address/index.js +3 -0
  4. package/dist/address/index.js.map +1 -0
  5. package/dist/chain/index.d.ts +1 -0
  6. package/dist/chain/index.js +3 -0
  7. package/dist/chain/index.js.map +1 -0
  8. package/dist/chunk-BRZNHTLQ.js +89 -0
  9. package/dist/chunk-BRZNHTLQ.js.map +1 -0
  10. package/dist/chunk-DHRCWMY5.js +240 -0
  11. package/dist/chunk-DHRCWMY5.js.map +1 -0
  12. package/dist/chunk-JVJ552RU.js +3 -0
  13. package/dist/chunk-JVJ552RU.js.map +1 -0
  14. package/dist/cloud-storage/index.d.ts +1 -0
  15. package/dist/cloud-storage/index.js +3 -0
  16. package/dist/cloud-storage/index.js.map +1 -0
  17. package/dist/contracts/index.d.ts +1 -0
  18. package/dist/contracts/index.js +3 -0
  19. package/dist/contracts/index.js.map +1 -0
  20. package/dist/core/index.d.ts +12 -0
  21. package/dist/core/index.js +5 -0
  22. package/dist/core/index.js.map +1 -0
  23. package/dist/crypto/index.d.ts +1 -0
  24. package/dist/crypto/index.js +3 -0
  25. package/dist/crypto/index.js.map +1 -0
  26. package/dist/dotns-bswWAmR2.d.ts +136 -0
  27. package/dist/host/index.d.ts +1 -0
  28. package/dist/host/index.js +3 -0
  29. package/dist/host/index.js.map +1 -0
  30. package/dist/identity/index.d.ts +491 -0
  31. package/dist/identity/index.js +691 -0
  32. package/dist/identity/index.js.map +1 -0
  33. package/dist/index.d.ts +62 -0
  34. package/dist/index.js +12 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/individuality/index.d.ts +1 -0
  37. package/dist/individuality/index.js +3 -0
  38. package/dist/individuality/index.js.map +1 -0
  39. package/dist/local-storage/index.d.ts +1 -0
  40. package/dist/local-storage/index.js +3 -0
  41. package/dist/local-storage/index.js.map +1 -0
  42. package/dist/react/index.d.ts +181 -0
  43. package/dist/react/index.js +198 -0
  44. package/dist/react/index.js.map +1 -0
  45. package/dist/testing/index.d.ts +61 -0
  46. package/dist/testing/index.js +149 -0
  47. package/dist/testing/index.js.map +1 -0
  48. package/dist/types-B5Zx4RNq.d.ts +211 -0
  49. package/dist/wallet/index.d.ts +1 -0
  50. package/dist/wallet/index.js +3 -0
  51. package/dist/wallet/index.js.map +1 -0
  52. package/package.json +118 -0
  53. package/src/address/index.ts +8 -0
  54. package/src/chain/index.ts +8 -0
  55. package/src/cloud-storage/index.ts +8 -0
  56. package/src/contracts/index.ts +8 -0
  57. package/src/core/createApp.ts +408 -0
  58. package/src/core/index.ts +12 -0
  59. package/src/core/logger.ts +15 -0
  60. package/src/core/types.ts +233 -0
  61. package/src/crypto/index.ts +8 -0
  62. package/src/host/index.ts +8 -0
  63. package/src/identity/dotns-abis.test.ts +43 -0
  64. package/src/identity/dotns-abis.ts +274 -0
  65. package/src/identity/dotns-errors.ts +34 -0
  66. package/src/identity/dotns-namehash.test.ts +155 -0
  67. package/src/identity/dotns-namehash.ts +149 -0
  68. package/src/identity/dotns-owner.test.ts +92 -0
  69. package/src/identity/dotns-registry.test.ts +1154 -0
  70. package/src/identity/dotns-registry.ts +870 -0
  71. package/src/identity/dotns.test.ts +128 -0
  72. package/src/identity/dotns.ts +221 -0
  73. package/src/identity/index.ts +61 -0
  74. package/src/identity/product-account.test.ts +130 -0
  75. package/src/identity/product-account.ts +115 -0
  76. package/src/identity/types.ts +87 -0
  77. package/src/index.ts +61 -0
  78. package/src/individuality/contract.test.ts +135 -0
  79. package/src/individuality/index.ts +8 -0
  80. package/src/local-storage/index.ts +8 -0
  81. package/src/react/context.ts +35 -0
  82. package/src/react/index.ts +43 -0
  83. package/src/react/provider.tsx +78 -0
  84. package/src/react/useChain.ts +35 -0
  85. package/src/react/useLocalStorage.ts +127 -0
  86. package/src/react/useWallet.ts +156 -0
  87. package/src/testing.ts +297 -0
  88. package/src/wallet/index.ts +11 -0
@@ -0,0 +1,128 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { describe, expect, test } from "vitest";
4
+ import { stripSuffix } from "./dotns-namehash.js";
5
+ import { isResolvableDotNsName, isValidDotNsName, normalizeDotNsName } from "./dotns.js";
6
+
7
+ // The two deployments these helpers actually have to serve. Every case below
8
+ // runs against `.paseo` as well as `.dot`, because a helper that only behaves
9
+ // on a four-character suffix is the defect being fixed.
10
+ const DOT = ".dot";
11
+ const PASEO = ".paseo";
12
+
13
+ describe("stripSuffix", () => {
14
+ test("removes the suffix by its own length", () => {
15
+ expect(stripSuffix("alice.paseo", PASEO)).toBe("alice");
16
+ expect(stripSuffix("alice.dot", DOT)).toBe("alice");
17
+ });
18
+
19
+ test("a six-character suffix does not lose two characters to a hardcoded 4", () => {
20
+ // `slice(0, -4)` returns "alice.pa" here, which then hashes and queries
21
+ // as the subname `pa` under `alice`.
22
+ expect(stripSuffix("alice.paseo", PASEO)).not.toBe("alice.pa");
23
+ });
24
+
25
+ test("leaves a name that does not carry the suffix untouched", () => {
26
+ expect(stripSuffix("alice", PASEO)).toBe("alice");
27
+ expect(stripSuffix("alice.dot", PASEO)).toBe("alice.dot");
28
+ });
29
+
30
+ test("keeps subname structure", () => {
31
+ expect(stripSuffix("bob.alice.paseo", PASEO)).toBe("bob.alice");
32
+ });
33
+
34
+ test("a bare suffix strips to the empty label", () => {
35
+ expect(stripSuffix(".paseo", PASEO)).toBe("");
36
+ });
37
+ });
38
+
39
+ describe("normalizeDotNsName", () => {
40
+ test("appends the deployment's suffix to a bare label", () => {
41
+ expect(normalizeDotNsName("alice", PASEO)).toBe("alice.paseo");
42
+ expect(normalizeDotNsName("alice", DOT)).toBe("alice.dot");
43
+ });
44
+
45
+ test("is idempotent for a name that already carries the suffix", () => {
46
+ expect(normalizeDotNsName("alice.paseo", PASEO)).toBe("alice.paseo");
47
+ });
48
+
49
+ test("leaves a foreign suffix alone instead of burying it", () => {
50
+ // The rule this pins: appending only when there is no dot at all means
51
+ // `alice.dot` survives to be refused by validation. The `endsWith`
52
+ // rule would produce `alice.dot.paseo`, which is a well-formed subname
53
+ // and would resolve silently to a node nobody owns.
54
+ expect(normalizeDotNsName("alice.dot", PASEO)).toBe("alice.dot");
55
+ expect(normalizeDotNsName("alice.dot", PASEO)).not.toBe("alice.dot.paseo");
56
+ });
57
+
58
+ test("leaves a bare multi-label name alone", () => {
59
+ // Recorded behaviour change: this used to become `bob.alice.dot` and
60
+ // resolve. It is now left as-is and refused by validation, because
61
+ // telling `bob.alice` from `alice.dot` needs a list of known TLDs.
62
+ expect(normalizeDotNsName("bob.alice", PASEO)).toBe("bob.alice");
63
+ });
64
+
65
+ test("lowercases, trims, and drops a single trailing root dot", () => {
66
+ // Matches `normalize_host` in truapi-server, so a name arriving from a
67
+ // URL keys the same way here as it does in the host.
68
+ expect(normalizeDotNsName(" Alice.PASEO. ", PASEO)).toBe("alice.paseo");
69
+ expect(normalizeDotNsName("ALICE", PASEO)).toBe("alice.paseo");
70
+ });
71
+
72
+ test("drops only one trailing dot, so a doubled dot stays visible", () => {
73
+ // `alice.paseo..` is malformed and must reach validation as such
74
+ // rather than being quietly repaired into a valid name.
75
+ expect(normalizeDotNsName("alice.paseo..", PASEO)).toBe("alice.paseo.");
76
+ });
77
+ });
78
+
79
+ describe("isValidDotNsName", () => {
80
+ test("accepts one label under the deployment's suffix", () => {
81
+ expect(isValidDotNsName("alice.paseo", PASEO)).toBe(true);
82
+ expect(isValidDotNsName("alice.dot", DOT)).toBe(true);
83
+ });
84
+
85
+ test("refuses another deployment's suffix", () => {
86
+ expect(isValidDotNsName("alice.dot", PASEO)).toBe(false);
87
+ expect(isValidDotNsName("alice.paseo", DOT)).toBe(false);
88
+ });
89
+
90
+ test("refuses a subname, which the registrar cannot mint", () => {
91
+ expect(isValidDotNsName("bob.alice.paseo", PASEO)).toBe(false);
92
+ });
93
+
94
+ test("applies the label rule to the label, not to the suffix's length", () => {
95
+ // With a hardcoded `slice(0, -4)` the label here would be `dim2.pa`,
96
+ // which fails the label regex and would reject a registrable name.
97
+ expect(isValidDotNsName("dim2.paseo", PASEO)).toBe(true);
98
+ });
99
+
100
+ test("keeps the on-chain label rules", () => {
101
+ expect(isValidDotNsName("ab.paseo", PASEO)).toBe(false); // under 3 chars
102
+ expect(isValidDotNsName("-alice.paseo", PASEO)).toBe(false); // edge hyphen
103
+ expect(isValidDotNsName("al ice.paseo", PASEO)).toBe(false); // space
104
+ expect(isValidDotNsName(`${"a".repeat(64)}.paseo`, PASEO)).toBe(false); // over 63
105
+ });
106
+ });
107
+
108
+ describe("isResolvableDotNsName", () => {
109
+ test("accepts a subname, which the registry supports even though the registrar does not", () => {
110
+ expect(isResolvableDotNsName("bob.alice.paseo", PASEO)).toBe(true);
111
+ expect(isResolvableDotNsName("alice.paseo", PASEO)).toBe(true);
112
+ });
113
+
114
+ test("refuses another deployment's suffix", () => {
115
+ expect(isResolvableDotNsName("bob.alice.dot", PASEO)).toBe(false);
116
+ });
117
+
118
+ test("extracts the labels by suffix length, not by a constant 4", () => {
119
+ // `slice(0, -4)` would split `dim2.pa` into ["dim2", "pa"] and reject
120
+ // it on the 3-character minimum.
121
+ expect(isResolvableDotNsName("dim2.paseo", PASEO)).toBe(true);
122
+ });
123
+
124
+ test("refuses an empty label anywhere in the chain", () => {
125
+ expect(isResolvableDotNsName("bob..alice.paseo", PASEO)).toBe(false);
126
+ expect(isResolvableDotNsName(".paseo", PASEO)).toBe(false);
127
+ });
128
+ });
@@ -0,0 +1,221 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * DotNS (Polkadot Name Service) utilities
5
+ *
6
+ * Name validation and normalization. Every helper here takes the deployment's
7
+ * TLD suffix rather than assuming `.dot`: the TLD is fixed per network when the
8
+ * contracts initialise, so the same string is a valid name on one deployment
9
+ * and meaningless on another. `resolveTld` in `./dotns-registry.js` is what
10
+ * asks the chain which suffix is in play.
11
+ */
12
+
13
+ import { accountIdBytes } from "@parity/product-sdk-address";
14
+ import { bytesToHex, hexToBytes } from "@parity/product-sdk-crypto";
15
+ import { stripSuffix } from "./dotns-namehash.js";
16
+ import type {
17
+ ChainDefinition,
18
+ PalletsTypedef,
19
+ PlainDescriptor,
20
+ RuntimeDescriptor,
21
+ SS58String,
22
+ StorageDescriptor,
23
+ TxDescriptor,
24
+ } from "polkadot-api";
25
+
26
+ type AnyDescriptorEntry<T> = Record<string, Record<string, T>>;
27
+
28
+ type PeopleUsernameStorage = {
29
+ Resources: {
30
+ UsernameOwnerOf: StorageDescriptor<[Uint8Array], SS58String, true, never>;
31
+ };
32
+ };
33
+
34
+ type PeopleUsernamePallets = PalletsTypedef<
35
+ PeopleUsernameStorage,
36
+ AnyDescriptorEntry<TxDescriptor<any>>,
37
+ AnyDescriptorEntry<PlainDescriptor<any>>,
38
+ AnyDescriptorEntry<PlainDescriptor<any>>,
39
+ AnyDescriptorEntry<PlainDescriptor<any>>,
40
+ AnyDescriptorEntry<RuntimeDescriptor<any, any>>
41
+ >;
42
+
43
+ /**
44
+ * Descriptor narrowed to "any chain that exposes `Resources.UsernameOwnerOf`".
45
+ *
46
+ * Used as the input to `signMessageWithDotNsIdentity` so the SDK doesn't pin
47
+ * a specific People-chain genesis — anything with the right storage shape
48
+ * (paseo-individuality today, future People Lite, etc.) is accepted.
49
+ */
50
+ export type PeopleUsernameChain = ChainDefinition & {
51
+ descriptors: Promise<unknown> & {
52
+ pallets: PeopleUsernamePallets;
53
+ };
54
+ };
55
+
56
+ /**
57
+ * Minimal typed-api shape required to resolve a username on a People chain.
58
+ *
59
+ * This is a narrow structural type — anything with the right
60
+ * `query.Resources.UsernameOwnerOf.getValue` shape works, including a real
61
+ * `TypedApi<PeopleUsernameChain>` slice of a `ChainClient` or a hand-rolled
62
+ * test double.
63
+ */
64
+ export type PeopleUsernameQueryApi = {
65
+ query: {
66
+ Resources: {
67
+ UsernameOwnerOf: {
68
+ getValue: (key: Uint8Array) => Promise<SS58String | undefined>;
69
+ };
70
+ };
71
+ };
72
+ };
73
+
74
+ /**
75
+ * Whether `name` is a single registrable label under `suffix`.
76
+ *
77
+ * `suffix` is the deployment's own TLD, including its leading dot, as
78
+ * `resolveTld` reports it — `".dot"` on Paseo Asset Hub Previewnet, `".paseo"`
79
+ * on Next V2. It is required rather than defaulted, because a name is only
80
+ * valid *relative to a deployment*: `alice.dot` is registrable on one network
81
+ * and meaningless on the other, and a default would silently pick one.
82
+ *
83
+ * @param name - Name to validate, already normalized by {@link normalizeDotNsName}
84
+ * @param suffix - The deployment's TLD suffix, e.g. `".paseo"`
85
+ */
86
+ export function isValidDotNsName(name: string, suffix: string): boolean {
87
+ if (!name.endsWith(suffix)) return false;
88
+ return isValidLabel(stripSuffix(name, suffix));
89
+ }
90
+
91
+ /**
92
+ * One canonical DNS label, matching the on-chain rule.
93
+ *
94
+ * `StringUtils.isSingleLabel` allows lowercase ASCII, digits and non-edge
95
+ * hyphens up to 63 characters, and the registrar additionally requires 3.
96
+ */
97
+ function isValidLabel(label: string): boolean {
98
+ if (label.length < 3 || label.length > 63) return false;
99
+ return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(label);
100
+ }
101
+
102
+ /**
103
+ * Whether a name can be resolved: one or more valid labels under `suffix`.
104
+ *
105
+ * Broader than {@link isValidDotNsName} on purpose. The registrar only mints
106
+ * single labels, so registration keeps the stricter rule, but the registry
107
+ * supports subnodes and `namehash` hashes them, so `bob.alice.paseo` is
108
+ * resolvable even though it is not registrable.
109
+ *
110
+ * @param name - Name to check, already normalized by {@link normalizeDotNsName}
111
+ * @param suffix - The deployment's TLD suffix, e.g. `".paseo"`
112
+ */
113
+ export function isResolvableDotNsName(name: string, suffix: string): boolean {
114
+ if (!name.endsWith(suffix)) return false;
115
+ const labels = stripSuffix(name, suffix).split(".");
116
+ return labels.length > 0 && labels.every(isValidLabel);
117
+ }
118
+
119
+ /**
120
+ * Normalize a DotNS name for the deployment identified by `suffix`.
121
+ *
122
+ * Case-folds, applies NFC, trims surrounding whitespace, and drops a single
123
+ * trailing root dot, so `Alice.PASEO.` keys the same as `alice.paseo`. This
124
+ * mirrors `normalize_host` in truapi-server, which is what turns a URL into a
125
+ * dotNS identifier — a name arriving from navigation has to key identically
126
+ * here or the two layers disagree about the same input.
127
+ *
128
+ * The NFC step is defensive only: {@link isValidLabel} and the on-chain
129
+ * `StringUtils.isSingleLabel` both allow lowercase ASCII alone, so no name that
130
+ * survives validation can differ between its composed and decomposed spellings.
131
+ * It costs nothing and it keeps this function honest if the label rule ever
132
+ * widens.
133
+ *
134
+ * **The suffix is appended only when `name` contains no dot at all.** A bare
135
+ * label is the friendly path and gets the deployment's suffix; anything already
136
+ * carrying a dot is left exactly as written, so a name from another deployment
137
+ * survives to be refused by validation rather than being buried under our own
138
+ * suffix. Appending on `!endsWith(suffix)` instead would turn `alice.dot` into
139
+ * `alice.dot.paseo`, which is a well-formed subname and would resolve silently
140
+ * to a node nobody owns.
141
+ *
142
+ * The cost of that rule: a bare multi-label name such as `bob.alice` is no
143
+ * longer completed to `bob.alice.paseo`. Distinguishing it from `alice.dot`
144
+ * would require a hardcoded list of every network's TLD, and there are already
145
+ * more TLDs deployed than any such list contains.
146
+ *
147
+ * @param name - Name to normalize
148
+ * @param suffix - The deployment's TLD suffix, e.g. `".paseo"`
149
+ */
150
+ export function normalizeDotNsName(name: string, suffix: string): string {
151
+ const folded = name.trim().normalize("NFC").toLowerCase();
152
+ const normalized = folded.endsWith(".") ? folded.slice(0, -1) : folded;
153
+ return normalized.includes(".") ? normalized : normalized + suffix;
154
+ }
155
+
156
+ // `resolveDotNs` / `reverseDotNs` / `isDotNsAvailable` now live in
157
+ // `./dotns-registry.ts` (real `Result`-returning registry calls), replacing the
158
+ // throwing skeletons that used to sit here. See that module + the design doc.
159
+
160
+ /**
161
+ * Resolve a People / People Lite username to its owning account.
162
+ *
163
+ * Queries `Resources.UsernameOwnerOf` on the caller-supplied typed-api fragment.
164
+ * The returned value is the SS58 address the storage yields, or `null` when no
165
+ * owner is registered for that username.
166
+ *
167
+ * Returns SS58 rather than hex because that is both the form the storage yields
168
+ * and the form `Resources.Consumers` is keyed by, so reading an account's
169
+ * usernames from this result needs no conversion. Callers that want the raw
170
+ * 32-byte key use `accountIdBytes` from `@parity/product-sdk-address`, and
171
+ * {@link accountIdToHex} for the hex form.
172
+ *
173
+ * The `username` is UTF-8 encoded as-is — no normalization is applied. Pass
174
+ * the exact byte string the chain stores (typically with the `.dot` suffix).
175
+ *
176
+ * @internal Exposed for unit testing. Consumers should use
177
+ * `wallet.signMessageWithDotNsIdentity` instead, which orchestrates the
178
+ * chain-connection lifecycle.
179
+ */
180
+ export async function resolvePeopleUsernameOwner(
181
+ username: string,
182
+ peopleApi: PeopleUsernameQueryApi,
183
+ ): Promise<SS58String | null> {
184
+ const owner = await peopleApi.query.Resources.UsernameOwnerOf.getValue(
185
+ new TextEncoder().encode(username),
186
+ );
187
+ return owner ?? null;
188
+ }
189
+
190
+ function assertHex(value: string): `0x${string}` {
191
+ if (!/^0x[0-9a-fA-F]*$/.test(value)) {
192
+ throw new Error(`Expected 0x-prefixed hex string, got ${value}`);
193
+ }
194
+ return value as `0x${string}`;
195
+ }
196
+
197
+ export function accountIdHexToBytes(accountId: `0x${string}`): Uint8Array {
198
+ const bytes = hexToBytes(assertHex(accountId).slice(2));
199
+ if (bytes.length !== 32) {
200
+ throw new Error(`Expected 32-byte AccountId, got ${bytes.length} bytes`);
201
+ }
202
+ return bytes;
203
+ }
204
+
205
+ /** Throws on any input that is not exactly 32 bytes, rather than padding it. */
206
+ export function accountIdBytesToHex(bytes: Uint8Array): `0x${string}` {
207
+ if (bytes.length !== 32) {
208
+ throw new Error(`Expected 32-byte AccountId, got ${bytes.length} bytes`);
209
+ }
210
+ return `0x${bytesToHex(bytes)}`;
211
+ }
212
+
213
+ /**
214
+ * The inverse of {@link accountIdHexToBytes}.
215
+ *
216
+ * No in-repo caller: this is the migration path for consumers, after
217
+ * `resolvePeopleUsernameOwner` changed from hex to SS58. Not dead code.
218
+ */
219
+ export function accountIdToHex(address: string): `0x${string}` {
220
+ return accountIdBytesToHex(accountIdBytes(address));
221
+ }
@@ -0,0 +1,61 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @parity/product-sdk/identity
5
+ *
6
+ * Identity utilities: DotNS name resolution, plus the deprecated context-alias
7
+ * helpers.
8
+ *
9
+ * For real Ring VRF aliases and proofs, use `SignerManager`'s
10
+ * `getProductAccountAlias` and `createRingVRFProof` from
11
+ * `@parity/product-sdk-signer`.
12
+ */
13
+
14
+ // DotNS utilities (pure helpers + People-chain username path)
15
+ export {
16
+ isValidDotNsName,
17
+ isResolvableDotNsName,
18
+ normalizeDotNsName,
19
+ accountIdHexToBytes,
20
+ accountIdToHex,
21
+ resolvePeopleUsernameOwner,
22
+ } from "./dotns.js";
23
+ export type { PeopleUsernameChain, PeopleUsernameQueryApi } from "./dotns.js";
24
+
25
+ // DotNS registry reads + writes (Revive contract)
26
+ export {
27
+ resolveDotNs,
28
+ reverseDotNs,
29
+ isDotNsAvailable,
30
+ setDotNsRecord,
31
+ prepareDotNsRegistration,
32
+ } from "./dotns-registry.js";
33
+ export type {
34
+ DotNsClientOptions,
35
+ RegisterDotNsArgs,
36
+ SetRecordArgs,
37
+ DotNsRegistration,
38
+ } from "./dotns-registry.js";
39
+ export { DotNsError } from "./dotns-errors.js";
40
+ export type { DotNsErrorReason } from "./dotns-errors.js";
41
+ export {
42
+ namehash,
43
+ dotNsTld,
44
+ isConsistentDotNsTld,
45
+ stripSuffix,
46
+ DOT_NODE,
47
+ DOT_TLD,
48
+ } from "./dotns-namehash.js";
49
+ export type { DotNsTld } from "./dotns-namehash.js";
50
+ export { POP_STATUS, DOTNS_ADDRESSES } from "./dotns-abis.js";
51
+
52
+ // Context alias utilities (deprecated)
53
+ export { deriveContextAlias, verifyContextAlias } from "./product-account.js";
54
+
55
+ // Types
56
+ export type {
57
+ DotNsRecord,
58
+ ContextAliasInfo,
59
+ VerificationResult,
60
+ OnChainIdentity,
61
+ } from "./types.js";
@@ -0,0 +1,130 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Frozen vectors for the deprecated context-alias helpers.
5
+ *
6
+ * These pin current behaviour so the deprecation cannot silently alter the
7
+ * derivation. Never re-derive: a caller may use the value as a plain
8
+ * identifier, where different output bytes would be a silent break.
9
+ */
10
+ import { ss58Decode, ss58Encode } from "@parity/product-sdk-address";
11
+ import { blake2b256 } from "@parity/product-sdk-crypto";
12
+ import { describe, expect, it } from "vitest";
13
+ import * as identity from "./index.js";
14
+ import { deriveContextAlias, verifyContextAlias } from "./product-account.js";
15
+
16
+ const ALICE = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
17
+ const CONTEXT = "voting-round-1";
18
+
19
+ /** Recomputes the documented formula from primitives, independent of the implementation. */
20
+ function expectedAliasPublicKey(parentAddress: string, context: string): Uint8Array {
21
+ const { publicKey } = ss58Decode(parentAddress);
22
+ const contextBytes = new TextEncoder().encode(context);
23
+ const combined = new Uint8Array(publicKey.length + contextBytes.length);
24
+ combined.set(publicKey, 0);
25
+ combined.set(contextBytes, publicKey.length);
26
+ return blake2b256(combined);
27
+ }
28
+
29
+ describe("deriveContextAlias", () => {
30
+ it("returns blake2b256(parentPublicKey || context), encoded at the given prefix", () => {
31
+ const alias = deriveContextAlias(ALICE, CONTEXT);
32
+ const expected = expectedAliasPublicKey(ALICE, CONTEXT);
33
+
34
+ expect(alias.address).toBe(ss58Encode(expected, 42));
35
+ });
36
+
37
+ it("matches a frozen vector for a known parent and context", () => {
38
+ const alias = deriveContextAlias(ALICE, CONTEXT);
39
+
40
+ expect(alias.address).toBe("5GPTdhxkG9wNKhCSN1vZrCwrf3fwvAf4j487vqrkVzvRMH6F");
41
+ expect(alias.h160Address).toBe("0x93d10a91de267ba79b27fbec373e5a3b2e86a283");
42
+ expect(alias.parentAddress).toBe(ALICE);
43
+ expect(alias.context).toBe(CONTEXT);
44
+ });
45
+
46
+ it("defaults to ss58 prefix 42", () => {
47
+ expect(deriveContextAlias(ALICE, CONTEXT).address).toBe(
48
+ deriveContextAlias(ALICE, CONTEXT, 42).address,
49
+ );
50
+ });
51
+
52
+ it("changes only the encoding when the ss58 prefix changes, not the key", () => {
53
+ const at42 = deriveContextAlias(ALICE, CONTEXT, 42);
54
+ const at0 = deriveContextAlias(ALICE, CONTEXT, 0);
55
+
56
+ expect(at0.address).not.toBe(at42.address);
57
+ expect(Array.from(ss58Decode(at0.address).publicKey)).toEqual(
58
+ Array.from(ss58Decode(at42.address).publicKey),
59
+ );
60
+ });
61
+
62
+ it("derives a different alias for a different context", () => {
63
+ expect(deriveContextAlias(ALICE, "round-1").address).not.toBe(
64
+ deriveContextAlias(ALICE, "round-2").address,
65
+ );
66
+ });
67
+ });
68
+
69
+ describe("verifyContextAlias", () => {
70
+ it("accepts an alias derived from the same parent and context", () => {
71
+ const alias = deriveContextAlias(ALICE, CONTEXT);
72
+
73
+ expect(verifyContextAlias(alias.address, ALICE, CONTEXT)).toBe(true);
74
+ });
75
+
76
+ it("rejects a different context", () => {
77
+ const alias = deriveContextAlias(ALICE, CONTEXT);
78
+
79
+ expect(verifyContextAlias(alias.address, ALICE, "voting-round-2")).toBe(false);
80
+ });
81
+
82
+ it("rejects a different parent", () => {
83
+ const alias = deriveContextAlias(ALICE, CONTEXT);
84
+ const bob = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty";
85
+
86
+ expect(verifyContextAlias(alias.address, bob, CONTEXT)).toBe(false);
87
+ });
88
+
89
+ it("returns false rather than throwing on an undecodable address", () => {
90
+ expect(verifyContextAlias("not-an-address", ALICE, CONTEXT)).toBe(false);
91
+ });
92
+
93
+ // Guards the byte-level comparison: `addressesEqual` from
94
+ // @parity/product-sdk-address compares SS58 strings exactly and would
95
+ // return false here, silently narrowing behaviour.
96
+ it("compares public keys, so it accepts an alias encoded at another prefix", () => {
97
+ const alias = deriveContextAlias(ALICE, CONTEXT, 42);
98
+ const reencoded = ss58Encode(ss58Decode(alias.address).publicKey, 0);
99
+
100
+ expect(reencoded).not.toBe(alias.address);
101
+ expect(verifyContextAlias(reencoded, ALICE, CONTEXT)).toBe(true);
102
+ });
103
+ });
104
+
105
+ describe("the identity export surface", () => {
106
+ // These threw on every call, so no working consumer could exist. Deleting
107
+ // turns a guaranteed runtime throw into a compile error.
108
+ it("no longer exports the unimplemented ring helpers", () => {
109
+ for (const name of ["deriveAnonymousAlias", "createRingProof", "verifyRingProof"]) {
110
+ expect(name in identity).toBe(false);
111
+ }
112
+ });
113
+
114
+ it("still exports the DotNS helpers and the deprecated context-alias pair", () => {
115
+ for (const name of [
116
+ "resolveDotNs",
117
+ "reverseDotNs",
118
+ "isDotNsAvailable",
119
+ "resolvePeopleUsernameOwner",
120
+ "isValidDotNsName",
121
+ "normalizeDotNsName",
122
+ "accountIdHexToBytes",
123
+ "accountIdToHex",
124
+ "deriveContextAlias",
125
+ "verifyContextAlias",
126
+ ]) {
127
+ expect(name in identity).toBe(true);
128
+ }
129
+ });
130
+ });
@@ -0,0 +1,115 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Context alias derivation (deprecated, removal in 0.23.0)
5
+ *
6
+ * Derives a deterministic, context-bound alias from a parent account using blake2b-256.
7
+ *
8
+ * The output is a hash, not a key, so no signing key exists for the addresses
9
+ * returned here. See the notes on each function for what to use instead.
10
+ *
11
+ * NOTE: this is NOT the canonical sr25519 product-account derivation used by
12
+ * mobile, desktop, and dotli hosts. For that, use
13
+ * `@parity/product-sdk-keys::deriveProductAccountPublicKey`.
14
+ */
15
+
16
+ import { createLogger } from "@parity/product-sdk-logger";
17
+ import { blake2b256 } from "@parity/product-sdk-crypto";
18
+ import { ss58Encode, ss58Decode, deriveH160 } from "@parity/product-sdk-address";
19
+ import type { ContextAliasInfo } from "./types.js";
20
+
21
+ const log = createLogger("identity");
22
+
23
+ /**
24
+ * Derive a context-bound alias from a parent account.
25
+ *
26
+ * The alias is deterministically derived using:
27
+ * aliasPublicKey = blake2b256(parentPublicKey || context)
28
+ *
29
+ * @deprecated Returns addresses that no key can spend. The alias public key is a
30
+ * blake2b-256 hash rather than a derived key, so no secret corresponds to the
31
+ * SS58 address or to the H160: both can receive value and neither can ever
32
+ * send it. Removal: `@parity/product-sdk` 0.23.0. Replace by intent:
33
+ *
34
+ * - An account that holds or spends value:
35
+ * `SignerManager.getProductAccount(dotNsIdentifier, index)` from
36
+ * `@parity/product-sdk-signer`. Host-backed and actually signable.
37
+ * - The address offline, with no host: `deriveProductAccountPublicKey` from
38
+ * `@parity/product-sdk-keys`, the canonical sr25519 soft derivation.
39
+ * - An unlinkable per-context alias: select a registered ring-VRF key, then call
40
+ * `SignerManager.getProductAccountAlias(keyHandle, context, location)` or
41
+ * `createRingVRFProof(keyHandle, context, location, message)`.
42
+ * - A context-scoped identifier, never used as an account: `blake2b256` from
43
+ * `@parity/product-sdk/crypto`. Same bytes, without address packaging that
44
+ * invites the mistake.
45
+ *
46
+ * @param parentAddress - Parent account SS58 address
47
+ * @param context - Context string for derivation (e.g. an app id or scope label)
48
+ * @param ss58Prefix - SS58 prefix (default: 42)
49
+ * @returns Context alias info
50
+ */
51
+ export function deriveContextAlias(
52
+ parentAddress: string,
53
+ context: string,
54
+ ss58Prefix = 42,
55
+ ): ContextAliasInfo {
56
+ const { publicKey: parentPublicKey } = ss58Decode(parentAddress);
57
+
58
+ // Derive alias public key: blake2b-256(parentPublicKey || context)
59
+ const contextBytes = new TextEncoder().encode(context);
60
+ const combined = new Uint8Array(parentPublicKey.length + contextBytes.length);
61
+ combined.set(parentPublicKey, 0);
62
+ combined.set(contextBytes, parentPublicKey.length);
63
+
64
+ const aliasPublicKey = blake2b256(combined);
65
+ const address = ss58Encode(aliasPublicKey, ss58Prefix);
66
+ const h160Address = deriveH160(aliasPublicKey);
67
+
68
+ log.debug("Derived context alias", {
69
+ parentAddress,
70
+ context,
71
+ address,
72
+ });
73
+
74
+ return {
75
+ address,
76
+ h160Address,
77
+ parentAddress,
78
+ context,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Verify that a context alias was derived from a parent account.
84
+ *
85
+ * @deprecated Recomputes {@link deriveContextAlias} from two public values and
86
+ * compares the result, so it confirms a derivation relationship and nothing
87
+ * more. No secret enters the operation at any point, which means there is
88
+ * nothing here to authenticate: a passing result does not show that anyone
89
+ * controls either account. Removal: `@parity/product-sdk` 0.23.0.
90
+ *
91
+ * @param aliasAddress - Context alias SS58 address
92
+ * @param parentAddress - Claimed parent address
93
+ * @param context - Context string used for derivation
94
+ * @returns True if derivation is valid
95
+ */
96
+ export function verifyContextAlias(
97
+ aliasAddress: string,
98
+ parentAddress: string,
99
+ context: string,
100
+ ): boolean {
101
+ try {
102
+ const derived = deriveContextAlias(parentAddress, context);
103
+ const { publicKey: aliasKey } = ss58Decode(aliasAddress);
104
+ const { publicKey: derivedKey } = ss58Decode(derived.address);
105
+
106
+ // Compare public keys
107
+ if (aliasKey.length !== derivedKey.length) return false;
108
+ for (let i = 0; i < aliasKey.length; i++) {
109
+ if (aliasKey[i] !== derivedKey[i]) return false;
110
+ }
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }