@parity/product-sdk 0.0.0-dev.312.0 → 0.0.0-dev.329.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/address/index.d.ts +3 -1
- package/dist/{chunk-BRZNHTLQ.js → chunk-7GM3HUZF.js} +2 -2
- package/dist/chunk-7GM3HUZF.js.map +1 -0
- package/dist/chunk-TUXU2VPS.js +14 -0
- package/dist/chunk-TUXU2VPS.js.map +1 -0
- package/dist/{chunk-DHRCWMY5.js → chunk-VWD6HK2C.js} +29 -20
- package/dist/chunk-VWD6HK2C.js.map +1 -0
- package/dist/core/index.d.ts +50 -3
- package/dist/core/index.js +3 -2
- package/dist/{dotns-bswWAmR2.d.ts → dotns-CtmOwVQW.d.ts} +2 -2
- package/dist/identity/index.d.ts +118 -89
- package/dist/identity/index.js +200 -78
- package/dist/identity/index.js.map +1 -1
- package/dist/index.d.ts +3 -53
- package/dist/index.js +3 -2
- package/dist/nfts/index.d.ts +1 -0
- package/dist/nfts/index.js +3 -0
- package/dist/nfts/index.js.map +1 -0
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +4 -3
- package/dist/react/index.js.map +1 -1
- package/dist/renderer/index.d.ts +1 -0
- package/dist/renderer/index.js +3 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +3 -5
- package/dist/testing/index.js.map +1 -1
- package/dist/{types-B5Zx4RNq.d.ts → types-BpWw-OJl.d.ts} +19 -9
- package/package.json +22 -11
- package/src/core/createApp.test.ts +194 -0
- package/src/core/createApp.ts +35 -22
- package/src/core/local-storage-api.ts +74 -0
- package/src/core/types.ts +18 -7
- package/src/identity/dotns-abis.ts +44 -9
- package/src/identity/dotns-addresses.test.ts +536 -0
- package/src/identity/dotns-addresses.ts +320 -0
- package/src/identity/dotns-errors.ts +2 -0
- package/src/identity/dotns-namehash.ts +2 -2
- package/src/identity/dotns-registry.test.ts +158 -3
- package/src/identity/dotns-registry.ts +71 -73
- package/src/identity/dotns.ts +2 -2
- package/src/identity/index.test.ts +43 -0
- package/src/identity/index.ts +10 -6
- package/src/identity/types.ts +0 -19
- package/src/index.ts +3 -2
- package/src/individuality/contract.test.ts +426 -2
- package/src/nfts/contract.test.ts +58 -0
- package/src/nfts/index.ts +8 -0
- package/src/react/context.ts +1 -1
- package/src/react/index.ts +1 -1
- package/src/react/provider.tsx +3 -3
- package/src/renderer/index.ts +10 -0
- package/src/testing.ts +14 -11
- package/dist/chunk-BRZNHTLQ.js.map +0 -1
- package/dist/chunk-DHRCWMY5.js.map +0 -1
- package/src/identity/product-account.test.ts +0 -130
- 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
|
|
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
|
+
});
|
package/src/react/context.ts
CHANGED
|
@@ -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
|
-
|
|
31
|
+
"Wrap your app with <ProductSDKProvider>.",
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
return app;
|
package/src/react/index.ts
CHANGED
package/src/react/provider.tsx
CHANGED
|
@@ -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
|
-
/**
|
|
15
|
-
name
|
|
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
|
|
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
|
-
//
|
|
123
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 () => {
|