@parity/product-sdk 0.0.0-dev.312.0 → 0.0.0-dev.329.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.
Files changed (57) hide show
  1. package/dist/address/index.d.ts +3 -1
  2. package/dist/{chunk-BRZNHTLQ.js → chunk-7GM3HUZF.js} +2 -2
  3. package/dist/chunk-7GM3HUZF.js.map +1 -0
  4. package/dist/chunk-TUXU2VPS.js +14 -0
  5. package/dist/chunk-TUXU2VPS.js.map +1 -0
  6. package/dist/{chunk-DHRCWMY5.js → chunk-VWD6HK2C.js} +29 -20
  7. package/dist/chunk-VWD6HK2C.js.map +1 -0
  8. package/dist/core/index.d.ts +50 -3
  9. package/dist/core/index.js +3 -2
  10. package/dist/{dotns-bswWAmR2.d.ts → dotns-CtmOwVQW.d.ts} +2 -2
  11. package/dist/identity/index.d.ts +118 -89
  12. package/dist/identity/index.js +200 -78
  13. package/dist/identity/index.js.map +1 -1
  14. package/dist/index.d.ts +3 -53
  15. package/dist/index.js +3 -2
  16. package/dist/nfts/index.d.ts +1 -0
  17. package/dist/nfts/index.js +3 -0
  18. package/dist/nfts/index.js.map +1 -0
  19. package/dist/react/index.d.ts +5 -5
  20. package/dist/react/index.js +4 -3
  21. package/dist/react/index.js.map +1 -1
  22. package/dist/renderer/index.d.ts +1 -0
  23. package/dist/renderer/index.js +3 -0
  24. package/dist/renderer/index.js.map +1 -0
  25. package/dist/testing/index.d.ts +2 -2
  26. package/dist/testing/index.js +3 -5
  27. package/dist/testing/index.js.map +1 -1
  28. package/dist/{types-B5Zx4RNq.d.ts → types-BpWw-OJl.d.ts} +19 -9
  29. package/package.json +23 -12
  30. package/src/core/createApp.test.ts +194 -0
  31. package/src/core/createApp.ts +35 -22
  32. package/src/core/local-storage-api.ts +74 -0
  33. package/src/core/types.ts +18 -7
  34. package/src/identity/dotns-abis.ts +44 -9
  35. package/src/identity/dotns-addresses.test.ts +536 -0
  36. package/src/identity/dotns-addresses.ts +320 -0
  37. package/src/identity/dotns-errors.ts +2 -0
  38. package/src/identity/dotns-namehash.ts +2 -2
  39. package/src/identity/dotns-registry.test.ts +158 -3
  40. package/src/identity/dotns-registry.ts +71 -73
  41. package/src/identity/dotns.ts +2 -2
  42. package/src/identity/index.test.ts +43 -0
  43. package/src/identity/index.ts +10 -6
  44. package/src/identity/types.ts +0 -19
  45. package/src/index.ts +3 -2
  46. package/src/individuality/contract.test.ts +426 -2
  47. package/src/nfts/contract.test.ts +58 -0
  48. package/src/nfts/index.ts +8 -0
  49. package/src/react/context.ts +1 -1
  50. package/src/react/index.ts +1 -1
  51. package/src/react/provider.tsx +3 -3
  52. package/src/renderer/index.ts +10 -0
  53. package/src/testing.ts +14 -11
  54. package/dist/chunk-BRZNHTLQ.js.map +0 -1
  55. package/dist/chunk-DHRCWMY5.js.map +0 -1
  56. package/src/identity/product-account.test.ts +0 -130
  57. package/src/identity/product-account.ts +0 -115
@@ -31,10 +31,32 @@
31
31
  * `@ts-expect-error` would report as unused and this file would fail.
32
32
  */
33
33
  import type { getChainAPI } from "@parity/product-sdk-chain-client";
34
- import type { RingVRFProof as HostRingVRFProof } from "@parity/product-sdk-host";
35
34
  import type {
35
+ RingLocation as HostRingLocation,
36
+ RingVRFProof as HostRingVRFProof,
37
+ VrfSignature as HostVrfSignature,
38
+ VrfTranscriptItem as HostVrfTranscriptItem,
39
+ } from "@parity/product-sdk-host";
40
+ import type {
41
+ AirdropChain,
42
+ ClaimChain,
36
43
  ConsumersChain,
44
+ GameChain,
45
+ GamePlayersChain,
37
46
  IndividualityChain,
47
+ LegacySuffixChain,
48
+ LiteAliasBindChain,
49
+ LiteSignUpChain,
50
+ NetworkSuffixChain,
51
+ PapiIndividualityChain,
52
+ PrizeStatusChain,
53
+ RegisterChain,
54
+ RegistrationEligibilityChain,
55
+ RingLocation as SdkRingLocation,
56
+ ScoreContextChain,
57
+ SignUpChain,
58
+ AccountVrfSignature,
59
+ VrfTranscriptItem as IndividualityVrfTranscriptItem,
38
60
  RingVRFProof as AsPersonRingVRFProof,
39
61
  } from "@parity/product-sdk-individuality";
40
62
  import { expect, test } from "vitest";
@@ -45,17 +67,108 @@ type Assert<T extends true> = T;
45
67
 
46
68
  type PaseoClient = Awaited<ReturnType<typeof getChainAPI<"paseo">>>;
47
69
  type DevnetClient = Awaited<ReturnType<typeof getChainAPI<"devnet">>>;
70
+ // Previewnet runs a Paseo runtime kept a step ahead of paseo-next-v2, so it
71
+ // satisfies every individuality contract paseo does — including the game
72
+ // surface devnet predates. These assertions are a compile-time guard on the
73
+ // re-added descriptor. The only shape drift is `PeopleLite.LitePeople`'s method
74
+ // enum gaining a `Fee` variant, which the contracts type as `unknown` and absorb.
75
+ type PreviewnetClient = Awaited<ReturnType<typeof getChainAPI<"previewnet">>>;
48
76
 
49
- // These four aliases are the test. Each fails to typecheck if its condition
77
+ // These aliases are the test. Each fails to typecheck if its condition
50
78
  // breaks, so they need no export and no runtime reference.
51
79
  type PaseoSatisfiesContract = Assert<PaseoClient extends IndividualityChain ? true : false>;
52
80
  type DevnetSatisfiesContract = Assert<DevnetClient extends IndividualityChain ? true : false>;
81
+ type PreviewnetSatisfiesContract = Assert<
82
+ PreviewnetClient extends IndividualityChain ? true : false
83
+ >;
53
84
 
54
85
  // `ConsumersChain` needs this more than the batch contract does: its value shape
55
86
  // was derived from the pallet and the codegen rather than read off the emitted
56
87
  // descriptor, so these two lines are what make that derivation a checked fact.
57
88
  type PaseoSatisfiesConsumers = Assert<PaseoClient extends ConsumersChain ? true : false>;
58
89
  type DevnetSatisfiesConsumers = Assert<DevnetClient extends ConsumersChain ? true : false>;
90
+ type PreviewnetSatisfiesConsumers = Assert<PreviewnetClient extends ConsumersChain ? true : false>;
91
+
92
+ // Separate from the personhood contract: reading draws needs neither `Resources`
93
+ // nor `PeopleLite`. Both chains satisfy it, which is weaker than it looks — devnet's
94
+ // event-id base is 28 bytes to paseo's 27, and `SizedHex<N>` erases `N`, so only the
95
+ // length check in `airdrop-ids.ts` catches that.
96
+ type PaseoSatisfiesAirdropContract = Assert<PaseoClient extends AirdropChain ? true : false>;
97
+ type DevnetSatisfiesAirdropContract = Assert<DevnetClient extends AirdropChain ? true : false>;
98
+ type PreviewnetSatisfiesAirdropContract = Assert<
99
+ PreviewnetClient extends AirdropChain ? true : false
100
+ >;
101
+
102
+ // Same again for the current-game contract, which shares no entry with either of
103
+ // the other two — but paseo only, and that asymmetry is the point.
104
+ type PaseoSatisfiesGameContract = Assert<PaseoClient extends GameChain ? true : false>;
105
+ // Previewnet is ahead of paseo, so it carries the game surface too (unlike devnet).
106
+ type PreviewnetSatisfiesGameContract = Assert<PreviewnetClient extends GameChain ? true : false>;
107
+
108
+ // Devnet's metadata predates the multi-airdrop game work — the 28-byte base above is
109
+ // one symptom, `game-types.ts` lists the rest — so the game surface is paseo-only.
110
+ // Asserted negatively on purpose: a devnet re-pin breaks this line, which is the
111
+ // prompt to flip it positive and check the read against it.
112
+ type DevnetPredatesTheGameContract = Assert<DevnetClient extends GameChain ? false : true>;
113
+
114
+ // `GamePlayersChain` is the opt-in half of the game read (`players`), typed
115
+ // separately so a caller who never asks about a player keeps the narrower
116
+ // contract. Both game-capable chains carry `Game.Players`.
117
+ type PaseoSatisfiesPlayersContract = Assert<PaseoClient extends GamePlayersChain ? true : false>;
118
+ type PreviewnetSatisfiesPlayersContract = Assert<
119
+ PreviewnetClient extends GamePlayersChain ? true : false
120
+ >;
121
+
122
+ // `fromPapi` hands a caller's own typed API to the reads; it must land exactly
123
+ // where the chain-client shape puts it, or every contract above would be
124
+ // satisfied by `getChainAPI` and none by a self-connected product.
125
+ type FromPapiSatisfiesContracts = Assert<
126
+ PapiIndividualityChain<PaseoClient["individuality"]> extends IndividualityChain &
127
+ AirdropChain &
128
+ GameChain &
129
+ GamePlayersChain &
130
+ ScoreContextChain &
131
+ SignUpChain &
132
+ LiteSignUpChain
133
+ ? true
134
+ : false
135
+ >;
136
+
137
+ // Storage is the only way either chain resolves a suffix since the re-pin, so the
138
+ // bring-your-own-client path needs it asserted too.
139
+ type FromPapiSatisfiesTheSuffixStorage = Assert<
140
+ PapiIndividualityChain<
141
+ PaseoClient["individuality"],
142
+ PaseoClient["raw"]["individuality"]
143
+ > extends NetworkSuffixChain
144
+ ? true
145
+ : false
146
+ >;
147
+
148
+ type PaseoSatisfiesScoreContext = Assert<PaseoClient extends ScoreContextChain ? true : false>;
149
+ type DevnetSatisfiesScoreContext = Assert<DevnetClient extends ScoreContextChain ? true : false>;
150
+ type PreviewnetSatisfiesScoreContext = Assert<
151
+ PreviewnetClient extends ScoreContextChain ? true : false
152
+ >;
153
+
154
+ // The suffix moved from a constant to storage upstream, and the 2026-09-03 re-pin
155
+ // brought that to both live chains. Devnet still predates it, so it holds the old
156
+ // shape and is the one asserted negatively now.
157
+ type PreviewnetPredatesTheLegacySuffix = Assert<
158
+ PreviewnetClient extends LegacySuffixChain ? false : true
159
+ >;
160
+ type PaseoPredatesTheLegacySuffix = Assert<PaseoClient extends LegacySuffixChain ? false : true>;
161
+ type DevnetPredatesTheLegacySuffix = Assert<DevnetClient extends LegacySuffixChain ? false : true>;
162
+ type PreviewnetHasSuffixStorage = Assert<
163
+ PreviewnetClient extends NetworkSuffixChain ? true : false
164
+ >;
165
+ type PaseoHasSuffixStorage = Assert<PaseoClient extends NetworkSuffixChain ? true : false>;
166
+ type DevnetHasNoSuffixStorage = Assert<DevnetClient extends NetworkSuffixChain ? false : true>;
167
+
168
+ // The two are declared separately, so a rename on either side would go
169
+ // unnoticed anywhere but here.
170
+ type LocalRingLocationFitsHost = Assert<SdkRingLocation extends HostRingLocation ? true : false>;
171
+ type HostRingLocationFitsLocal = Assert<HostRingLocation extends SdkRingLocation ? true : false>;
59
172
 
60
173
  // Negative control, kept type-only so it emits no runtime code. If
61
174
  // `IndividualityChain` ever stopped constraining, this flips to `false` and the
@@ -68,6 +181,72 @@ type ClientWithoutIndividuality = {
68
181
  type RejectsBogusClient = Assert<
69
182
  ClientWithoutIndividuality extends IndividualityChain ? false : true
70
183
  >;
184
+ type RejectsBogusAirdropClient = Assert<
185
+ ClientWithoutIndividuality extends AirdropChain ? false : true
186
+ >;
187
+ type RejectsBogusGameClient = Assert<ClientWithoutIndividuality extends GameChain ? false : true>;
188
+ type RejectsBogusPlayersClient = Assert<
189
+ ClientWithoutIndividuality extends GamePlayersChain ? false : true
190
+ >;
191
+
192
+ // `Game.Game` being optional is what makes "between games" representable at all,
193
+ // and `GameIndex` / `GameSchedules` being `ValueQuery` is what lets the read treat
194
+ // them as always answering. The structural contract pins the latter two by typing
195
+ // them non-optional; this pins the first, which no assignability check would
196
+ // catch — a suddenly-required `Game` would still satisfy an optional contract.
197
+ type GameStorage = PaseoClient["individuality"]["query"]["Game"];
198
+ type GameValue = Awaited<ReturnType<GameStorage["Game"]["getValue"]>>;
199
+ type GameIsOptional = Assert<undefined extends GameValue ? true : false>;
200
+ // Negative control: `GameIndex` is `ValueQuery`, so it must *not* admit undefined.
201
+ type GameIndexValue = Awaited<ReturnType<GameStorage["GameIndex"]["getValue"]>>;
202
+ type GameIndexIsNotOptional = Assert<undefined extends GameIndexValue ? false : true>;
203
+
204
+ // The event id is derived from a runtime constant rather than a storage entry, so
205
+ // the constant has to exist and still be a plain string. Its *width* cannot be
206
+ // asserted here: PAPI's `SizedHex<N>` is `string & { __hexString?: N | unknown }`,
207
+ // and the `| unknown` collapses every width to the same type, so `SizedHex<32>`
208
+ // and `SizedHex<27>` are mutually assignable and any such assertion would pass
209
+ // vacuously. The 27-byte expectation is enforced at runtime instead, by the
210
+ // length check in `airdrop-ids.ts` and the pinned vectors beside it.
211
+ type GameConstants = PaseoClient["individuality"]["constants"]["Game"];
212
+ type EventIdBaseExists = Assert<"airdrop_event_id_base" extends keyof GameConstants ? true : false>;
213
+ type AirdropEventIdBase = Awaited<ReturnType<GameConstants["airdrop_event_id_base"]>>;
214
+ type EventIdBaseIsAString = Assert<AirdropEventIdBase extends string ? true : false>;
215
+
216
+ // The composed prize-status read spans both pallets, so it is the intersection
217
+ // that has to hold — and on paseo only, since it inherits `GameChain`.
218
+ type PaseoSatisfiesPrizeStatusContract = Assert<
219
+ PaseoClient extends PrizeStatusChain ? true : false
220
+ >;
221
+ type PreviewnetSatisfiesPrizeStatusContract = Assert<
222
+ PreviewnetClient extends PrizeStatusChain ? true : false
223
+ >;
224
+ type DevnetPredatesThePrizeStatusContract = Assert<
225
+ DevnetClient extends PrizeStatusChain ? false : true
226
+ >;
227
+
228
+ // `Registrations` is keyed by the entropy slot with the entry as its *value*, so
229
+ // `readDrawRegistration` can only work by scanning the event's prefix. Pinning the
230
+ // arity here is what would catch a descriptor regeneration that added a reverse
231
+ // index and made the scan unnecessary.
232
+ type RegistrationsEntries = Awaited<
233
+ ReturnType<PaseoClient["individuality"]["query"]["Airdrop"]["Registrations"]["getEntries"]>
234
+ >;
235
+ type RegistrationsKeyIsEventIdAndSlot = Assert<
236
+ RegistrationsEntries[number]["keyArgs"] extends [unknown, unknown] ? true : false
237
+ >;
238
+
239
+ // `Winners` is a double map keyed by the registration entry, which is what makes
240
+ // "did I win" a point lookup instead of a scan of every winner. If a descriptor
241
+ // regeneration ever collapsed it to a single key, the read layer would silently
242
+ // need rewriting, so the arity is pinned.
243
+ type WinnersKey = Parameters<
244
+ PaseoClient["individuality"]["query"]["Airdrop"]["Winners"]["getValue"]
245
+ >;
246
+ type WinnersTakesTwoKeys = Assert<
247
+ WinnersKey extends [unknown, unknown, ...unknown[]] ? true : false
248
+ >;
249
+
71
250
  type RejectsBogusConsumersClient = Assert<
72
251
  ClientWithoutIndividuality extends ConsumersChain ? false : true
73
252
  >;
@@ -128,6 +307,251 @@ type SignUpWithAliasTakesTheDocumentedArgs = Assert<
128
307
  type SignUpArgsWithoutSig = Pick<SignUpWithAliasArgs, "identifier_key" | "statement_account">;
129
308
  type RejectsSignUpArgsMissingSig = Assert<"sig" extends keyof SignUpArgsWithoutSig ? false : true>;
130
309
 
310
+ // The claim is the one contract here that touches `tx` as well as storage. Paseo
311
+ // only, like the game surface it composes with.
312
+ type PaseoSatisfiesClaimContract = Assert<PaseoClient extends ClaimChain ? true : false>;
313
+ type PreviewnetSatisfiesClaimContract = Assert<PreviewnetClient extends ClaimChain ? true : false>;
314
+ type RejectsBogusClaimClient = Assert<ClientWithoutIndividuality extends ClaimChain ? false : true>;
315
+
316
+ // `claim_airdrop`'s three arguments, pinned by name. `ClaimChain` types them
317
+ // structurally, and PAPI encodes whatever object it is handed — so a renamed field
318
+ // would encode as `undefined` and fail on chain with nothing local to point at it.
319
+ type ClaimAirdropArgs = Parameters<GameTx["claim_airdrop"]>[0];
320
+ type ClaimAirdropTakesTheDocumentedArgs = Assert<
321
+ "game_index" extends keyof ClaimAirdropArgs
322
+ ? "airdrop_index" extends keyof ClaimAirdropArgs
323
+ ? "beneficiary" extends keyof ClaimAirdropArgs
324
+ ? true
325
+ : false
326
+ : false
327
+ : false
328
+ >;
329
+
330
+ // Game sign-up.
331
+ type PaseoSatisfiesSignUpContract = Assert<PaseoClient extends SignUpChain ? true : false>;
332
+ type RejectsBogusSignUpClient = Assert<
333
+ ClientWithoutIndividuality extends SignUpChain ? false : true
334
+ >;
335
+
336
+ // `SignUpChain` alone does not reject devnet and cannot: a `tx` argument is
337
+ // checked contravariantly and excess-property checking does not apply between
338
+ // named types, so an interface naming `airdrops` is satisfied by devnet's call,
339
+ // which has only `airdrop`. `identifier_key` does not separate them either, since
340
+ // `SizedHex<N>`'s brand is optional. Assert on the intersection the read takes,
341
+ // where `GameChain` is the half that rejects devnet.
342
+ type PaseoSatisfiesSignUpRead = Assert<PaseoClient extends GameChain & SignUpChain ? true : false>;
343
+ type PreviewnetSatisfiesSignUpRead = Assert<
344
+ PreviewnetClient extends GameChain & SignUpChain ? true : false
345
+ >;
346
+ type DevnetPredatesTheSignUpRead = Assert<
347
+ DevnetClient extends GameChain & SignUpChain ? false : true
348
+ >;
349
+
350
+ // The divergence itself: devnet's argument is `airdrop`, so `signUpWithAccountTx`
351
+ // emitting `airdrops` would encode as `undefined` and enter no draw at all.
352
+ type DevnetGameTx = DevnetClient["individuality"]["tx"]["Game"];
353
+ type DevnetSignUpArgs = Parameters<DevnetGameTx["sign_up_with_account"]>[0];
354
+ type DevnetHasNoAirdropsArg = Assert<"airdrops" extends keyof DevnetSignUpArgs ? false : true>;
355
+ type DevnetHasTheSingularArg = Assert<"airdrop" extends keyof DevnetSignUpArgs ? true : false>;
356
+
357
+ // Pinned by name for the same reason `claim_airdrop`'s are: PAPI encodes the
358
+ // object it is handed, so a renamed field silently encodes as `undefined`.
359
+ type SignUpWithAccountArgs = Parameters<GameTx["sign_up_with_account"]>[0];
360
+ type SignUpWithAccountTakesTheDocumentedArgs = Assert<
361
+ "identifier_key" extends keyof SignUpWithAccountArgs
362
+ ? "airdrops" extends keyof SignUpWithAccountArgs
363
+ ? true
364
+ : false
365
+ : false
366
+ >;
367
+
368
+ // `None` is how a player enters no draw, and the only form a recognized player
369
+ // can use at all.
370
+ type AirdropsArg = SignUpWithAccountArgs["airdrops"];
371
+ type AirdropsIsOptional = Assert<undefined extends AirdropsArg ? true : false>;
372
+
373
+ // Extracted from the enum rather than restated, so a renamed field fails here
374
+ // instead of on chain.
375
+ type AccountVrfs = Extract<NonNullable<AirdropsArg>, { type: "Account" }>["value"][number];
376
+ type AccountVrfHasPreOutputAndProof = Assert<
377
+ "pre_output" extends keyof AccountVrfs
378
+ ? "proof" extends keyof AccountVrfs
379
+ ? true
380
+ : false
381
+ : false
382
+ >;
383
+
384
+ // The VRF types are local copies of the host's, declared here for the same reason
385
+ // `AsPersonRingVRFProof` is: so the package keeps no host dependency. Those host
386
+ // types are mapped straight off truapi's wire types, so a rename there changes them
387
+ // with no line of this repo touched, and the break would land as a runtime
388
+ // TypeError rather than a failed typecheck.
389
+ type HostVrfSignatureSatisfiesLocal = Assert<
390
+ HostVrfSignature extends AccountVrfSignature ? true : false
391
+ >;
392
+ type HostVrfItemSatisfiesLocal = Assert<
393
+ HostVrfTranscriptItem extends IndividualityVrfTranscriptItem ? true : false
394
+ >;
395
+
396
+ // Negative controls, so the two above cannot pass vacuously.
397
+ type RejectsSignatureMissingProof = Assert<
398
+ { preOutput: Uint8Array } extends AccountVrfSignature ? false : true
399
+ >;
400
+ type RejectsItemMissingValue = Assert<
401
+ { label: Uint8Array } extends IndividualityVrfTranscriptItem ? false : true
402
+ >;
403
+
404
+ // The lite sign-up surface: the alias binding, the lite-person marker, the
405
+ // invite pin, the ring membership entry and the free sign-up call. Paseo and
406
+ // previewnet carry all five; devnet predates `Game.LiteInvites` and the call,
407
+ // so it is asserted negatively, like the game surface it composes with.
408
+ type PaseoSatisfiesLiteSignUpContract = Assert<PaseoClient extends LiteSignUpChain ? true : false>;
409
+ type PreviewnetSatisfiesLiteSignUpContract = Assert<
410
+ PreviewnetClient extends LiteSignUpChain ? true : false
411
+ >;
412
+ type DevnetPredatesTheLiteSignUpContract = Assert<
413
+ DevnetClient extends LiteSignUpChain ? false : true
414
+ >;
415
+ type RejectsBogusLiteSignUpClient = Assert<
416
+ ClientWithoutIndividuality extends LiteSignUpChain ? false : true
417
+ >;
418
+
419
+ // `LiteSignUpChain` alone proves nothing about what the read accepts: it takes
420
+ // an intersection and resolves a suffix.
421
+ type LiteSignUpReadBase = GameChain & SignUpChain & ScoreContextChain & LiteSignUpChain;
422
+ type PaseoSatisfiesTheLiteReadWithATld = Assert<
423
+ PaseoClient extends LiteSignUpReadBase ? true : false
424
+ >;
425
+ // Both chains resolve the suffix from storage since the 2026-09-03 re-pin, so the
426
+ // storage overload is the live path and neither can use the constant one.
427
+ type PaseoSatisfiesTheLiteReadFromStorage = Assert<
428
+ PaseoClient extends LiteSignUpReadBase & NetworkSuffixChain ? true : false
429
+ >;
430
+ type PreviewnetSatisfiesTheLiteReadFromStorage = Assert<
431
+ PreviewnetClient extends LiteSignUpReadBase & NetworkSuffixChain ? true : false
432
+ >;
433
+ type NeitherChainHasTheLegacyConstant = Assert<
434
+ PaseoClient | PreviewnetClient extends LiteSignUpReadBase & LegacySuffixChain ? false : true
435
+ >;
436
+ type DevnetPredatesTheLiteRead = Assert<DevnetClient extends LiteSignUpReadBase ? false : true>;
437
+
438
+ // Wider than the reads above: the builder assembles the extrinsic itself, so it
439
+ // needs the chain spec and the metadata blob, not just the call.
440
+ type PaseoSatisfiesBindContract = Assert<PaseoClient extends LiteAliasBindChain ? true : false>;
441
+ type PreviewnetSatisfiesBindContract = Assert<
442
+ PreviewnetClient extends LiteAliasBindChain ? true : false
443
+ >;
444
+ type DevnetSatisfiesBindContract = Assert<DevnetClient extends LiteAliasBindChain ? true : false>;
445
+ type FromPapiSatisfiesBindContract = Assert<
446
+ PapiIndividualityChain<
447
+ PaseoClient["individuality"],
448
+ PaseoClient["raw"]["individuality"]
449
+ > extends LiteAliasBindChain
450
+ ? true
451
+ : false
452
+ >;
453
+ type RejectsBogusBindClient = Assert<
454
+ ClientWithoutIndividuality extends LiteAliasBindChain ? false : true
455
+ >;
456
+
457
+ // Pinned by name for the same reason `sign_up_with_account`'s are: PAPI encodes
458
+ // the object it is handed, so a renamed field silently encodes as `undefined` —
459
+ // and `account` here is a call argument, not implied by the signer.
460
+ type LiteInviteArgs = Parameters<GameTx["sign_up_with_account_lite_invite"]>[0];
461
+ type LiteInviteTakesTheDocumentedArgs = Assert<
462
+ "account" extends keyof LiteInviteArgs
463
+ ? "identifier_key" extends keyof LiteInviteArgs
464
+ ? "airdrops" extends keyof LiteInviteArgs
465
+ ? true
466
+ : false
467
+ : false
468
+ : false
469
+ >;
470
+ type LiteInviteAirdropsIsOptional = Assert<
471
+ undefined extends LiteInviteArgs["airdrops"] ? true : false
472
+ >;
473
+
474
+ // `PeopleLite.AccountToAlias`: the three fields the lite requirement read uses.
475
+ // The context matters as much as the alias — a binding outside the score
476
+ // context does not satisfy the game's origin check — so it is pinned by name.
477
+ type LiteBindingValue = NonNullable<
478
+ Awaited<
479
+ ReturnType<
480
+ PaseoClient["individuality"]["query"]["PeopleLite"]["AccountToAlias"]["getValue"]
481
+ >
482
+ >
483
+ >;
484
+ type LiteBindingHasTheDocumentedFields = Assert<
485
+ "revision" extends keyof LiteBindingValue
486
+ ? "ring" extends keyof LiteBindingValue
487
+ ? LiteBindingValue["ca"] extends { alias: string; context: string }
488
+ ? true
489
+ : false
490
+ : false
491
+ : false
492
+ >;
493
+
494
+ // `Members.Members` is a double map (collection id, member key), which is what
495
+ // lets the membership check be a point read. A collapsed key would silently
496
+ // turn the varargs call into a mistyped one, so the arity is pinned.
497
+ type MembersKey = Parameters<
498
+ PaseoClient["individuality"]["query"]["Members"]["Members"]["getValue"]
499
+ >;
500
+ type MembersTakesTwoKeys = Assert<
501
+ MembersKey extends [unknown, unknown, ...unknown[]] ? true : false
502
+ >;
503
+
504
+ // `Game.Players`: the variant spelling the read keys by, and the one field it uses.
505
+ // Sibling reads got exactly these pins.
506
+ type PlayersEntry = PaseoClient["individuality"]["query"]["Game"]["Players"];
507
+ type PlayersKey = Parameters<PlayersEntry["getValue"]>[0];
508
+ type PlayersKeyIsAccountOrPerson = Assert<
509
+ PlayersKey extends { type: "Account" | "Person" } ? true : false
510
+ >;
511
+ type RejectsAliasSpelling = Assert<
512
+ { type: "Alias"; value: string } extends PlayersKey ? false : true
513
+ >;
514
+ type PlayersValue = NonNullable<Awaited<ReturnType<PlayersEntry["getValue"]>>>;
515
+ type PlayersHasRegistered = Assert<"registered" extends keyof PlayersValue ? true : false>;
516
+
517
+ type PaseoSatisfiesRegisterContract = Assert<PaseoClient extends RegisterChain ? true : false>;
518
+ type PreviewnetSatisfiesRegisterContract = Assert<
519
+ PreviewnetClient extends RegisterChain ? true : false
520
+ >;
521
+ type DevnetSatisfiesRegisterContract = Assert<DevnetClient extends RegisterChain ? true : false>;
522
+ type RejectsBogusRegisterClient = Assert<
523
+ ClientWithoutIndividuality extends RegisterChain ? false : true
524
+ >;
525
+
526
+ type PaseoSatisfiesEligibilityContract = Assert<
527
+ PaseoClient extends RegistrationEligibilityChain ? true : false
528
+ >;
529
+ type PreviewnetSatisfiesEligibilityContract = Assert<
530
+ PreviewnetClient extends RegistrationEligibilityChain ? true : false
531
+ >;
532
+ type DevnetSatisfiesEligibilityContract = Assert<
533
+ DevnetClient extends RegistrationEligibilityChain ? true : false
534
+ >;
535
+ type RejectsBogusEligibilityClient = Assert<
536
+ ClientWithoutIndividuality extends RegistrationEligibilityChain ? false : true
537
+ >;
538
+
539
+ // The same trap as `claim_airdrop`'s pins, and worse: `key` is an `Option`, so a
540
+ // renamed argument encodes as `None` — the resume arm this builder never means.
541
+ type ScoreTx = PaseoClient["individuality"]["tx"]["Score"];
542
+ type RegisterExists = Assert<"register" extends keyof ScoreTx ? true : false>;
543
+ type RegisterArgs = Parameters<ScoreTx["register"]>[0];
544
+ type RegisterTakesKey = Assert<"key" extends keyof RegisterArgs ? true : false>;
545
+ type RegisterKeyIsOptional = Assert<undefined extends RegisterArgs["key"] ? true : false>;
546
+
547
+ // `ValueQuery` is what lets the read type the threshold as a plain number, and
548
+ // `Participants` being optional is what makes "never seen" a null, not a throw.
549
+ type ScoreStorage = PaseoClient["individuality"]["query"]["Score"];
550
+ type ThresholdValue = Awaited<ReturnType<ScoreStorage["PersonhoodThreshold"]["getValue"]>>;
551
+ type ThresholdIsNotOptional = Assert<undefined extends ThresholdValue ? false : true>;
552
+ type ParticipantsValue = Awaited<ReturnType<ScoreStorage["Participants"]["getValue"]>>;
553
+ type ParticipantsIsOptional = Assert<undefined extends ParticipantsValue ? true : false>;
554
+
131
555
  test("the individuality chain contract is asserted at compile time", () => {
132
556
  // The type assertions above are the test. This keeps vitest from reporting
133
557
  // the file as an empty suite.
@@ -0,0 +1,58 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Compile-time guard for the chain contract in `@parity/product-sdk-nfts`.
5
+ *
6
+ * That package types its chain parameter structurally, listing the storage
7
+ * entries it needs rather than naming a descriptor — same trade as
8
+ * `IndividualityChain`, and the same gap: a hand-written contract can drift from
9
+ * the real chain silently, because the package's own test doubles satisfy it by
10
+ * construction. See `../individuality/contract.test.ts` for why this assertion
11
+ * has to live in `@parity/product-sdk` to mean anything, and why it checks the
12
+ * sibling's built `dist` rather than its `src`.
13
+ *
14
+ * The `ItemDefs` line below is the one that earned this file. PAPI generates its
15
+ * key as `FixedSizeArray<2, number>` rather than the `[number, number]` tuple it
16
+ * gives `CollectionMetadata` and `ItemMetadata`, because the runtime's metadata
17
+ * declares the key type as an array. The raw storage layout is nonetheless two
18
+ * Twox64Concat segments — a genuine two-key map, verified against live state
19
+ * with `rpc.state_getKeys` — so a prefix scan by collection is sound at the
20
+ * protocol level. Whether PAPI's *typing* admits the one-arg `getEntries` that
21
+ * `NftsChain` asks for is a different question, and this is what answers it.
22
+ *
23
+ * **Paseo only, and that asymmetry is the point.** `devnet-asset-hub` carries
24
+ * neither `Scarcity` nor `NftClaims`, so the negative control below is not a
25
+ * hypothetical: it is the live state of the other supported network.
26
+ */
27
+ import type { getChainAPI } from "@parity/product-sdk-chain-client";
28
+ import type { NftsChain, NftsCreditsChain } from "@parity/product-sdk-nfts";
29
+ import { expect, test } from "vitest";
30
+
31
+ // The false branch must be `false`, not `never`. `never` is assignable to
32
+ // `true`, so a `never` branch would make the assertion unfalsifiable.
33
+ type Assert<T extends true> = T;
34
+
35
+ type PaseoClient = Awaited<ReturnType<typeof getChainAPI<"paseo">>>;
36
+ type DevnetClient = Awaited<ReturnType<typeof getChainAPI<"devnet">>>;
37
+
38
+ // The test: this alias fails to typecheck if the contract drifts from the
39
+ // descriptor, so it needs no export and no runtime reference.
40
+ type PaseoSatisfiesContract = Assert<PaseoClient extends NftsChain ? true : false>;
41
+
42
+ // The credits read spans both chains the preset carries, so the same client
43
+ // has to satisfy the second contract too.
44
+ type PaseoSatisfiesCredits = Assert<PaseoClient extends NftsCreditsChain ? true : false>;
45
+
46
+ // The negative control, and a real fact about the network rather than a
47
+ // hypothetical: devnet Asset Hub has no `Scarcity` and no `NftClaims`. If
48
+ // `NftsChain` ever stopped constraining, this `@ts-expect-error` would report
49
+ // as unused and the file would fail.
50
+ // @ts-expect-error devnet Asset Hub carries neither pallet
51
+ type DevnetLacksPallets = Assert<DevnetClient extends NftsChain ? true : false>;
52
+
53
+ test("nfts chain contract holds against the paseo descriptor", () => {
54
+ // The assertions above are compile-time. This keeps vitest from reporting
55
+ // the file as having no tests, and documents that a green run means tsc
56
+ // accepted them.
57
+ expect(true).toBe(true);
58
+ });
@@ -0,0 +1,8 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @parity/product-sdk/nfts
5
+ *
6
+ * Re-exports from @parity/product-sdk-nfts.
7
+ */
8
+ export * from "@parity/product-sdk-nfts";
@@ -28,7 +28,7 @@ export function useProductSDK(): App {
28
28
  if (!app) {
29
29
  throw new Error(
30
30
  "useProductSDK must be used within a ProductSDKProvider. " +
31
- 'Wrap your app with <ProductSDKProvider name="your-app">.',
31
+ "Wrap your app with <ProductSDKProvider>.",
32
32
  );
33
33
  }
34
34
  return app;
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * function App() {
14
14
  * return (
15
- * <ProductSDKProvider name="my-app">
15
+ * <ProductSDKProvider>
16
16
  * <MyApp />
17
17
  * </ProductSDKProvider>
18
18
  * );
@@ -11,8 +11,8 @@ import type { App, LogLevel } from "../core/types.js";
11
11
 
12
12
  /** Props for ProductSDKProvider */
13
13
  export interface ProductSDKProviderProps {
14
- /** Application name - used for storage namespacing and product account derivation */
15
- name: string;
14
+ /** @deprecated Ignored with a warning. The host product ID supplies the app name. */
15
+ name?: string;
16
16
  /** Log level (default: 'info') */
17
17
  logLevel?: LogLevel;
18
18
  /** Child components */
@@ -30,7 +30,7 @@ export interface ProductSDKProviderProps {
30
30
  *
31
31
  * function App() {
32
32
  * return (
33
- * <ProductSDKProvider name="my-app" fallback={<Loading />}>
33
+ * <ProductSDKProvider fallback={<Loading />}>
34
34
  * <MyApp />
35
35
  * </ProductSDKProvider>
36
36
  * );
@@ -0,0 +1,10 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Renderer trees — Pocket card faces and rendered chat bodies.
5
+ *
6
+ * Re-exports `@parity/product-sdk-renderer`.
7
+ *
8
+ * @module
9
+ */
10
+ export * from "@parity/product-sdk-renderer";
package/src/testing.ts CHANGED
@@ -11,7 +11,9 @@
11
11
  * @packageDocumentation
12
12
  */
13
13
  import { ProductCloudStorageError } from "@parity/product-sdk-cloud-storage";
14
+ import type { LocalKvStore } from "@parity/product-sdk-local-storage";
14
15
  import { createFakeHostLocalStorage } from "@parity/product-sdk-local-storage/testing";
16
+ import { createLocalStorageApi } from "./core/local-storage-api.js";
15
17
  import { err, ok, unwrapErr, unwrapOk } from "@parity/result";
16
18
 
17
19
  import type {
@@ -119,10 +121,13 @@ function makeFakeWallet(options?: FakeWalletOptions): WalletApi {
119
121
  }
120
122
 
121
123
  function makeFakeLocalStorage(): LocalStorageApi {
122
- // Compose the local-storage fake; mirror createApp's adapter semantics
123
- // (get normalizes "" -> null, getJSON normalizes undefined -> null).
124
+ // Route through the *same* `createLocalStorageApi` factory `createApp` uses,
125
+ // so the fake and the shipped adapter cannot drift (#344). The fake's host
126
+ // is adapted to a `LocalKvStore` here rather than via the async
127
+ // `createLocalKvStore`, so `createFakeApp` stays synchronous; the semantics
128
+ // (get "" -> null, getJSON undefined -> null) match `createHostBackend`.
124
129
  const host = createFakeHostLocalStorage();
125
- return {
130
+ const kv: LocalKvStore = {
126
131
  async get(key) {
127
132
  return (await host.readString(key)) || null;
128
133
  },
@@ -132,18 +137,14 @@ function makeFakeLocalStorage(): LocalStorageApi {
132
137
  async getJSON<T>(key: string) {
133
138
  return ((await host.readJSON(key)) ?? null) as T | null;
134
139
  },
135
- async setJSON<T>(key: string, value: T) {
140
+ async setJSON(key, value) {
136
141
  await host.writeJSON(key, value);
137
142
  },
138
143
  async remove(key) {
139
144
  await host.clear(key);
140
145
  },
141
- async clear() {
142
- // Unlike the production adapter (a no-op in container mode), the fake
143
- // actually empties, so tests get real isolation.
144
- host.reset();
145
- },
146
146
  };
147
+ return createLocalStorageApi(kv);
147
148
  }
148
149
 
149
150
  function makeFakeCloudStorage(): CloudStorageApi {
@@ -257,7 +258,7 @@ if (import.meta.vitest) {
257
258
  expect(accounts).toHaveLength(2);
258
259
  });
259
260
 
260
- test("localStorage round-trips and clear empties it", async () => {
261
+ test("localStorage round-trips and remove deletes a key", async () => {
261
262
  const app = createFakeApp();
262
263
  await app.localStorage.set("k", "v");
263
264
  await app.localStorage.setJSON("o", { n: 1 });
@@ -265,8 +266,10 @@ if (import.meta.vitest) {
265
266
  expect(await app.localStorage.getJSON("o")).toEqual({ n: 1 });
266
267
  expect(await app.localStorage.get("missing")).toBeNull();
267
268
 
268
- await app.localStorage.clear();
269
+ await app.localStorage.remove("k");
269
270
  expect(await app.localStorage.get("k")).toBeNull();
271
+ // Untouched keys stay — there is no clear-all.
272
+ expect(await app.localStorage.getJSON("o")).toEqual({ n: 1 });
270
273
  });
271
274
 
272
275
  test("cloudStorage round-trips upload -> fetch; computeCid matches upload", async () => {