@parity/product-sdk 0.13.0 → 0.14.1

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.
@@ -6,11 +6,70 @@
6
6
  * Provides name resolution for .dot domains
7
7
  */
8
8
 
9
+ import { accountIdBytes } from "@parity/product-sdk-address";
10
+ import { bytesToHex, hexToBytes } from "@parity/product-sdk-crypto";
9
11
  import { createLogger } from "@parity/product-sdk-logger";
12
+ import type {
13
+ ChainDefinition,
14
+ PalletsTypedef,
15
+ PlainDescriptor,
16
+ RuntimeDescriptor,
17
+ SS58String,
18
+ StorageDescriptor,
19
+ TxDescriptor,
20
+ } from "polkadot-api";
10
21
  import type { DotNsRecord } from "./types.js";
11
22
 
12
23
  const log = createLogger("identity");
13
24
 
25
+ type AnyDescriptorEntry<T> = Record<string, Record<string, T>>;
26
+
27
+ type PeopleUsernameStorage = {
28
+ Resources: {
29
+ UsernameOwnerOf: StorageDescriptor<[Uint8Array], SS58String, true, never>;
30
+ };
31
+ };
32
+
33
+ type PeopleUsernamePallets = PalletsTypedef<
34
+ PeopleUsernameStorage,
35
+ AnyDescriptorEntry<TxDescriptor<any>>,
36
+ AnyDescriptorEntry<PlainDescriptor<any>>,
37
+ AnyDescriptorEntry<PlainDescriptor<any>>,
38
+ AnyDescriptorEntry<PlainDescriptor<any>>,
39
+ AnyDescriptorEntry<RuntimeDescriptor<any, any>>
40
+ >;
41
+
42
+ /**
43
+ * Descriptor narrowed to "any chain that exposes `Resources.UsernameOwnerOf`".
44
+ *
45
+ * Used as the input to `signMessageWithDotNsIdentity` so the SDK doesn't pin
46
+ * a specific People-chain genesis — anything with the right storage shape
47
+ * (paseo-individuality today, future People Lite, etc.) is accepted.
48
+ */
49
+ export type PeopleUsernameChain = ChainDefinition & {
50
+ descriptors: Promise<unknown> & {
51
+ pallets: PeopleUsernamePallets;
52
+ };
53
+ };
54
+
55
+ /**
56
+ * Minimal typed-api shape required to resolve a username on a People chain.
57
+ *
58
+ * This is a narrow structural type — anything with the right
59
+ * `query.Resources.UsernameOwnerOf.getValue` shape works, including a real
60
+ * `TypedApi<PeopleUsernameChain>` slice of a `ChainClient` or a hand-rolled
61
+ * test double.
62
+ */
63
+ export type PeopleUsernameQueryApi = {
64
+ query: {
65
+ Resources: {
66
+ UsernameOwnerOf: {
67
+ getValue: (key: Uint8Array) => Promise<SS58String | undefined>;
68
+ };
69
+ };
70
+ };
71
+ };
72
+
14
73
  /**
15
74
  * Check if a string is a valid DotNS name
16
75
  *
@@ -40,18 +99,15 @@ export function normalizeDotNsName(name: string): string {
40
99
  }
41
100
 
42
101
  /**
43
- * Resolve a DotNS name to an address
102
+ * Resolve a DotNS name to an address.
103
+ *
104
+ * @deprecated Not implemented — throws at runtime. Use
105
+ * `wallet.signMessageWithDotNsIdentity({ peopleChain, username })` (which
106
+ * internally calls {@link resolvePeopleUsernameOwner}) for the supported
107
+ * People-chain username flow.
44
108
  *
45
109
  * @param name - DotNS name (e.g., "alice.dot")
46
110
  * @returns Resolved record or null if not found
47
- *
48
- * @example
49
- * ```ts
50
- * const record = await resolveDotNs('alice.dot');
51
- * if (record) {
52
- * console.log('Address:', record.address);
53
- * }
54
- * ```
55
111
  */
56
112
  export async function resolveDotNs(name: string): Promise<DotNsRecord | null> {
57
113
  const normalized = normalizeDotNsName(name);
@@ -71,7 +127,11 @@ export async function resolveDotNs(name: string): Promise<DotNsRecord | null> {
71
127
  }
72
128
 
73
129
  /**
74
- * Reverse resolve an address to a DotNS name
130
+ * Reverse resolve an address to a DotNS name.
131
+ *
132
+ * @deprecated Not implemented — throws at runtime. Reverse lookup will land
133
+ * alongside future identity work; for now resolve forward via
134
+ * `wallet.signMessageWithDotNsIdentity`.
75
135
  *
76
136
  * @param address - SS58 address
77
137
  * @returns Primary name or null if none set
@@ -87,7 +147,9 @@ export async function reverseDotNs(address: string): Promise<string | null> {
87
147
  }
88
148
 
89
149
  /**
90
- * Check if a DotNS name is available for registration
150
+ * Check if a DotNS name is available for registration.
151
+ *
152
+ * @deprecated Not implemented — depends on {@link resolveDotNs} which throws.
91
153
  *
92
154
  * @param name - Name to check
93
155
  * @returns True if available
@@ -96,3 +158,51 @@ export async function isDotNsAvailable(name: string): Promise<boolean> {
96
158
  const record = await resolveDotNs(name).catch(() => null);
97
159
  return record === null;
98
160
  }
161
+
162
+ /**
163
+ * Resolve a People / People Lite username to its owning `AccountId32`.
164
+ *
165
+ * Queries `Resources.UsernameOwnerOf` on the caller-supplied typed-api fragment.
166
+ * The returned value is the raw 32-byte account id as a `0x`-prefixed hex
167
+ * string, or `null` when no owner is registered for that username.
168
+ *
169
+ * The `username` is UTF-8 encoded as-is — no normalization is applied. Pass
170
+ * the exact byte string the chain stores (typically with the `.dot` suffix).
171
+ *
172
+ * @internal Exposed for unit testing. Consumers should use
173
+ * `wallet.signMessageWithDotNsIdentity` instead, which orchestrates the
174
+ * chain-connection lifecycle.
175
+ */
176
+ export async function resolvePeopleUsernameOwner(
177
+ username: string,
178
+ peopleApi: PeopleUsernameQueryApi,
179
+ ): Promise<`0x${string}` | null> {
180
+ const owner = await peopleApi.query.Resources.UsernameOwnerOf.getValue(
181
+ new TextEncoder().encode(username),
182
+ );
183
+ if (!owner) return null;
184
+
185
+ return accountIdBytesToHex(accountIdBytes(owner));
186
+ }
187
+
188
+ function assertHex(value: string): `0x${string}` {
189
+ if (!/^0x[0-9a-fA-F]*$/.test(value)) {
190
+ throw new Error(`Expected 0x-prefixed hex string, got ${value}`);
191
+ }
192
+ return value as `0x${string}`;
193
+ }
194
+
195
+ export function accountIdHexToBytes(accountId: `0x${string}`): Uint8Array {
196
+ const bytes = hexToBytes(assertHex(accountId).slice(2));
197
+ if (bytes.length !== 32) {
198
+ throw new Error(`Expected 32-byte AccountId, got ${bytes.length} bytes`);
199
+ }
200
+ return bytes;
201
+ }
202
+
203
+ function accountIdBytesToHex(bytes: Uint8Array): `0x${string}` {
204
+ if (bytes.length !== 32) {
205
+ throw new Error(`Expected 32-byte AccountId, got ${bytes.length} bytes`);
206
+ }
207
+ return `0x${bytesToHex(bytes)}`;
208
+ }
@@ -14,7 +14,10 @@ export {
14
14
  resolveDotNs,
15
15
  reverseDotNs,
16
16
  isDotNsAvailable,
17
+ accountIdHexToBytes,
18
+ resolvePeopleUsernameOwner,
17
19
  } from "./dotns.js";
20
+ export type { PeopleUsernameChain, PeopleUsernameQueryApi } from "./dotns.js";
18
21
 
19
22
  // Context alias utilities
20
23
  export {
package/src/index.ts CHANGED
@@ -36,6 +36,8 @@ export type {
36
36
  ChainApi,
37
37
  CloudStorageApi,
38
38
  Account,
39
+ DotNsIdentitySignature,
40
+ SignMessageWithDotNsIdentityArgs,
39
41
  ChainClient,
40
42
  ChainDefinition,
41
43
  TypedApi,
@@ -6,7 +6,11 @@
6
6
 
7
7
  import { useState, useEffect, useCallback } from "react";
8
8
  import { useProductSDK } from "./context.js";
9
- import type { Account } from "../core/types.js";
9
+ import type {
10
+ Account,
11
+ DotNsIdentitySignature,
12
+ SignMessageWithDotNsIdentityArgs,
13
+ } from "../core/types.js";
10
14
 
11
15
  /** Wallet hook state */
12
16
  export interface UseWalletState {
@@ -32,6 +36,10 @@ export interface UseWalletActions {
32
36
  selectAccount: (address: string) => void;
33
37
  /** Sign a message */
34
38
  signMessage: (message: string | Uint8Array) => Promise<Uint8Array>;
39
+ /** Sign a message with the account that owns a DotNS / People username identity. */
40
+ signMessageWithDotNsIdentity: (
41
+ args: SignMessageWithDotNsIdentityArgs,
42
+ ) => Promise<DotNsIdentitySignature>;
35
43
  }
36
44
 
37
45
  /** Return type of useWallet */
@@ -122,6 +130,13 @@ export function useWallet(): UseWalletReturn {
122
130
  [app],
123
131
  );
124
132
 
133
+ const signMessageWithDotNsIdentity = useCallback(
134
+ async (args: SignMessageWithDotNsIdentityArgs) => {
135
+ return app.wallet.signMessageWithDotNsIdentity(args);
136
+ },
137
+ [app],
138
+ );
139
+
125
140
  // Subscribe to account changes
126
141
  useEffect(() => {
127
142
  const unsubscribe = app.wallet.onAccountChange((account) => {
@@ -136,5 +151,6 @@ export function useWallet(): UseWalletReturn {
136
151
  disconnect,
137
152
  selectAccount,
138
153
  signMessage,
154
+ signMessageWithDotNsIdentity,
139
155
  };
140
156
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/createApp.ts"],"names":[],"mappings":"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","file":"chunk-TRTQ6KGE.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * createApp - Main entry point for the Product SDK\n *\n * Creates an App instance with wallet, storage, chain, and cloud storage APIs.\n */\n\nimport type { ChainDefinition } from \"polkadot-api\";\nimport type {\n App,\n AppConfig,\n WalletApi,\n ChainApi,\n Account,\n CloudStorageApi,\n LocalStorageApi,\n} from \"./types.js\";\nimport { configure, createLogger } from \"@parity/product-sdk-logger\";\nimport { createLocalKvStore } from \"@parity/product-sdk-local-storage\";\nimport { SignerManager } from \"@parity/product-sdk-signer\";\nimport {\n CloudStorageClient,\n calculateCid,\n createLazySigner,\n} from \"@parity/product-sdk-cloud-storage\";\nimport {\n createChainClient,\n getClient,\n isConnected,\n destroyAll,\n} from \"@parity/product-sdk-chain-client\";\n\nconst log = createLogger(\"app\");\n\n/**\n * Create a new Product SDK app instance\n *\n * @param config - Application configuration\n * @returns App instance with all APIs\n *\n * @example\n * ```ts\n * import { createApp } from '@parity/product-sdk';\n *\n * // Default: cloud storage enabled with paseo environment\n * const app = await createApp({\n * name: 'my-app',\n * logLevel: 'info',\n * });\n *\n * // Custom cloud storage environment\n * const prodApp = await createApp({\n * name: 'my-app',\n * cloudStorage: { environment: 'polkadot' },\n * });\n *\n * // Disable cloud storage entirely\n * const noCloudStorageApp = await createApp({\n * name: 'my-app',\n * cloudStorage: false,\n * });\n *\n * // Connect wallet\n * const { accounts } = await app.wallet.connect();\n *\n * // Use storage\n * await app.localStorage.set('key', 'value');\n *\n * // Use cloud storage (check for null if it might be disabled)\n * if (app.cloudStorage) {\n * const cid = await app.cloudStorage.upload('hello world');\n * }\n * ```\n */\nexport async function createApp(config: AppConfig): Promise<App> {\n // Set log level if specified\n if (config.logLevel) {\n configure({ level: config.logLevel });\n }\n\n log.info(\"Creating Product SDK app\", { name: config.name });\n\n // Initialize storage (container-only - will throw if not in container)\n const localKvStore = await createLocalKvStore({ prefix: config.name });\n\n // Initialize signer manager\n const signerManager = new SignerManager({\n dappName: config.name,\n });\n\n // Initialize cloud storage client (configurable, defaults to paseo).\n //\n // The signer is wrapped lazily so the cloud storage client can be built before\n // an account is selected. Uploads will throw a clear error if no signer\n // is available at submission time. Reads (fetch / fetchJson) don't need\n // a signer, so they work regardless of whether an account is selected --\n // but they are container-only and throw CloudStorageHostUnavailableError\n // outside a host container (no IPFS-gateway fallback).\n const cloudStorageEnabled = config.cloudStorage !== false;\n const cloudStorageEnvironment =\n typeof config.cloudStorage === \"object\" ? config.cloudStorage.environment : \"paseo\";\n const cloudStorageClient = cloudStorageEnabled\n ? await CloudStorageClient.create({\n environment: cloudStorageEnvironment,\n signer: createLazySigner(() => signerManager.getSigner()),\n })\n : null;\n\n if (cloudStorageEnabled) {\n log.debug(\"Cloud Storage client (Bulletin) initialized\", {\n environment: cloudStorageEnvironment,\n });\n } else {\n log.debug(\"Cloud Storage client disabled\");\n }\n\n // Create storage API adapter\n const localStorageApi: LocalStorageApi = {\n get: (key) => localKvStore.get(key),\n set: (key, value) => localKvStore.set(key, value),\n getJSON: <T>(key: string) => localKvStore.getJSON<T>(key),\n setJSON: <T>(key: string, value: T) => localKvStore.setJSON(key, value),\n remove: (key) => localKvStore.remove(key),\n clear: async () => {\n // LocalKvStore doesn't have clear - this is a no-op\n log.debug(\"clear() is not supported in container storage mode\");\n },\n };\n\n // Create wallet API adapter\n const walletApi = createWalletApi(signerManager);\n\n // Create chain API\n const chainApi: ChainApi = {\n getClient(descriptor) {\n log.debug(\"getClient called\", { genesis: descriptor.genesis });\n const client = getClient(descriptor);\n return client.getTypedApi(descriptor);\n },\n\n getRawClient(descriptor) {\n log.debug(\"getRawClient called\", { genesis: descriptor.genesis });\n return getClient(descriptor);\n },\n\n async connect<T extends Record<string, ChainDefinition>>(chains: T) {\n log.debug(\"connect called\", { chains: Object.keys(chains) });\n return createChainClient({ chains });\n },\n\n isConnected(descriptor) {\n return isConnected(descriptor);\n },\n\n destroyAll() {\n log.debug(\"destroyAll called\");\n destroyAll();\n },\n };\n\n // Create Cloud Storage API adapter (null if disabled)\n const cloudStorageApi: CloudStorageApi | null = cloudStorageClient\n ? {\n upload: async (data) => {\n const bytes = typeof data === \"string\" ? new TextEncoder().encode(data) : data;\n // Explicitly request a DAG-PB manifest so chunked uploads always\n // resolve to a single root CID. Without this, AsyncBulletinClient\n // can return `result.cid: undefined` for chunked-without-manifest\n // uploads — but CloudStorageApi.upload promises a string return, and\n // app consumers expect a CID they can hand to `fetch(cid)`. Keep\n // the defensive null-check below as belt-and-braces in case the\n // upstream contract shifts.\n const result = await cloudStorageClient.store(bytes).withManifest(true).send();\n if (!result.cid) {\n throw new Error(\n \"Cloud storage upload returned no CID despite .withManifest(true). Upstream contract may have shifted — file an issue.\",\n );\n }\n return result.cid.toString();\n },\n fetch: (cid) => cloudStorageClient.fetchBytes(cid),\n computeCid: async (data) => {\n const bytes = typeof data === \"string\" ? new TextEncoder().encode(data) : data;\n const cid = await calculateCid(bytes);\n return cid.toString();\n },\n }\n : null;\n\n log.info(\"Product SDK app created\", {\n name: config.name,\n cloudStorage: cloudStorageEnabled ? cloudStorageEnvironment : \"disabled\",\n });\n\n return {\n wallet: walletApi,\n localStorage: localStorageApi,\n chain: chainApi,\n cloudStorage: cloudStorageApi,\n getAppInfo: () => ({ ...config }),\n };\n}\n\n/**\n * Create wallet API adapter using SignerManager from leaf package\n */\nfunction createWalletApi(signerManager: SignerManager): WalletApi {\n // Track account change subscribers\n const accountChangeSubscribers = new Set<(account: Account | null) => void>();\n\n // Subscribe to signer manager state changes\n signerManager.subscribe((state) => {\n const account = state.selectedAccount\n ? {\n address: state.selectedAccount.address,\n name: state.selectedAccount.name ?? undefined,\n source: state.selectedAccount.source,\n }\n : null;\n for (const callback of accountChangeSubscribers) {\n try {\n callback(account);\n } catch (e) {\n log.warn(\"Account change callback threw\", { error: e });\n }\n }\n });\n\n return {\n async connect(): Promise<{ accounts: Account[] }> {\n const result = await signerManager.connect();\n if (!result.ok) {\n throw new Error(result.error.message);\n }\n return {\n accounts: result.value.map((a) => ({\n address: a.address,\n name: a.name ?? undefined,\n source: a.source,\n })),\n };\n },\n\n async disconnect(): Promise<void> {\n signerManager.disconnect();\n },\n\n getAccounts(): Account[] {\n return signerManager.getState().accounts.map((a) => ({\n address: a.address,\n name: a.name ?? undefined,\n source: a.source,\n }));\n },\n\n getSelectedAccount(): Account | null {\n const selected = signerManager.getState().selectedAccount;\n if (!selected) return null;\n return {\n address: selected.address,\n name: selected.name ?? undefined,\n source: selected.source,\n };\n },\n\n selectAccount(address: string): void {\n signerManager.selectAccount(address);\n },\n\n async signMessage(message: string | Uint8Array): Promise<Uint8Array> {\n const bytes = typeof message === \"string\" ? new TextEncoder().encode(message) : message;\n const result = await signerManager.signRaw(bytes);\n if (!result.ok) {\n throw new Error(result.error.message);\n }\n return result.value;\n },\n\n onAccountChange(callback: (account: Account | null) => void): () => void {\n accountChangeSubscribers.add(callback);\n return () => accountChangeSubscribers.delete(callback);\n },\n\n getProductAccount(): Account | null {\n // Product accounts require async call - this sync API can't support it properly\n // Users should use SignerManager.getProductAccount() directly\n log.warn(\n \"getProductAccount() is deprecated - use SignerManager.getProductAccount() directly\",\n );\n return null;\n },\n\n getAnonymousAlias(): string | null {\n // Anonymous aliases require async call - this sync API can't support it properly\n // Users should use SignerManager.getProductAccountAlias() directly\n log.warn(\n \"getAnonymousAlias() is deprecated - use SignerManager.getProductAccountAlias() directly\",\n );\n return null;\n },\n\n async createProof(_message: Uint8Array): Promise<Uint8Array> {\n // Ring VRF proofs require SignerManager.createRingVRFProof() directly\n throw new Error(\n \"createProof() is not implemented in the App API. \" +\n \"Use SignerManager.createRingVRFProof() directly.\",\n );\n },\n };\n}\n"]}