@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.
- package/LICENSE +201 -0
- package/dist/address/index.d.ts +9 -0
- package/dist/address/index.js +3 -0
- package/dist/address/index.js.map +1 -0
- package/dist/chain/index.d.ts +1 -0
- package/dist/chain/index.js +3 -0
- package/dist/chain/index.js.map +1 -0
- package/dist/chunk-BRZNHTLQ.js +89 -0
- package/dist/chunk-BRZNHTLQ.js.map +1 -0
- package/dist/chunk-DHRCWMY5.js +240 -0
- package/dist/chunk-DHRCWMY5.js.map +1 -0
- package/dist/chunk-JVJ552RU.js +3 -0
- package/dist/chunk-JVJ552RU.js.map +1 -0
- package/dist/cloud-storage/index.d.ts +1 -0
- package/dist/cloud-storage/index.js +3 -0
- package/dist/cloud-storage/index.js.map +1 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +3 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/crypto/index.d.ts +1 -0
- package/dist/crypto/index.js +3 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/dotns-bswWAmR2.d.ts +136 -0
- package/dist/host/index.d.ts +1 -0
- package/dist/host/index.js +3 -0
- package/dist/host/index.js.map +1 -0
- package/dist/identity/index.d.ts +491 -0
- package/dist/identity/index.js +691 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/individuality/index.d.ts +1 -0
- package/dist/individuality/index.js +3 -0
- package/dist/individuality/index.js.map +1 -0
- package/dist/local-storage/index.d.ts +1 -0
- package/dist/local-storage/index.js +3 -0
- package/dist/local-storage/index.js.map +1 -0
- package/dist/react/index.d.ts +181 -0
- package/dist/react/index.js +198 -0
- package/dist/react/index.js.map +1 -0
- package/dist/testing/index.d.ts +61 -0
- package/dist/testing/index.js +149 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-B5Zx4RNq.d.ts +211 -0
- package/dist/wallet/index.d.ts +1 -0
- package/dist/wallet/index.js +3 -0
- package/dist/wallet/index.js.map +1 -0
- package/package.json +118 -0
- package/src/address/index.ts +8 -0
- package/src/chain/index.ts +8 -0
- package/src/cloud-storage/index.ts +8 -0
- package/src/contracts/index.ts +8 -0
- package/src/core/createApp.ts +408 -0
- package/src/core/index.ts +12 -0
- package/src/core/logger.ts +15 -0
- package/src/core/types.ts +233 -0
- package/src/crypto/index.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/identity/dotns-abis.test.ts +43 -0
- package/src/identity/dotns-abis.ts +274 -0
- package/src/identity/dotns-errors.ts +34 -0
- package/src/identity/dotns-namehash.test.ts +155 -0
- package/src/identity/dotns-namehash.ts +149 -0
- package/src/identity/dotns-owner.test.ts +92 -0
- package/src/identity/dotns-registry.test.ts +1154 -0
- package/src/identity/dotns-registry.ts +870 -0
- package/src/identity/dotns.test.ts +128 -0
- package/src/identity/dotns.ts +221 -0
- package/src/identity/index.ts +61 -0
- package/src/identity/product-account.test.ts +130 -0
- package/src/identity/product-account.ts +115 -0
- package/src/identity/types.ts +87 -0
- package/src/index.ts +61 -0
- package/src/individuality/contract.test.ts +135 -0
- package/src/individuality/index.ts +8 -0
- package/src/local-storage/index.ts +8 -0
- package/src/react/context.ts +35 -0
- package/src/react/index.ts +43 -0
- package/src/react/provider.tsx +78 -0
- package/src/react/useChain.ts +35 -0
- package/src/react/useLocalStorage.ts +127 -0
- package/src/react/useWallet.ts +156 -0
- package/src/testing.ts +297 -0
- 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
|
+
}
|