@kaspacom/kcc20-tx-builder 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +54 -8
  3. package/RELEASING.md +93 -0
  4. package/SECURITY.md +16 -0
  5. package/artifacts/KCC20SnapshotBootstrap.placeholder.json +101677 -0
  6. package/artifacts/KCC20SnapshotController.placeholder.json +46547 -0
  7. package/artifacts/kcc20-snapshot-bootstrap/KCC20SnapshotBootstrap.placeholder.json +101677 -0
  8. package/artifacts/kcc20-snapshot-controller/KCC20SnapshotController.placeholder.json +46547 -0
  9. package/artifacts/snapshot-standards.lock.json +56 -0
  10. package/dist/artifacts.d.ts +31 -0
  11. package/dist/artifacts.d.ts.map +1 -1
  12. package/dist/artifacts.js +31 -0
  13. package/dist/artifacts.js.map +1 -1
  14. package/dist/cjs/artifacts.d.ts +31 -0
  15. package/dist/cjs/artifacts.js +32 -1
  16. package/dist/cjs/artifacts.js.map +1 -1
  17. package/dist/cjs/engine.js +58 -44
  18. package/dist/cjs/engine.js.map +1 -1
  19. package/dist/cjs/index.d.ts +7 -0
  20. package/dist/cjs/index.js +8 -1
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/operations.d.ts +1 -1
  23. package/dist/cjs/operations.js +4 -0
  24. package/dist/cjs/operations.js.map +1 -1
  25. package/dist/cjs/sale-common.d.ts +1 -1
  26. package/dist/cjs/sale-common.js +39 -4
  27. package/dist/cjs/sale-common.js.map +1 -1
  28. package/dist/cjs/snapshot-deployment.d.ts +82 -0
  29. package/dist/cjs/snapshot-deployment.js +82 -0
  30. package/dist/cjs/snapshot-deployment.js.map +1 -0
  31. package/dist/cjs/snapshot-operation.d.ts +27 -0
  32. package/dist/cjs/snapshot-operation.js +2433 -0
  33. package/dist/cjs/snapshot-operation.js.map +1 -0
  34. package/dist/cjs/snapshot-signerless-transactions.d.ts +132 -0
  35. package/dist/cjs/snapshot-signerless-transactions.js +746 -0
  36. package/dist/cjs/snapshot-signerless-transactions.js.map +1 -0
  37. package/dist/cjs/snapshot-signerless.d.ts +226 -0
  38. package/dist/cjs/snapshot-signerless.js +1022 -0
  39. package/dist/cjs/snapshot-signerless.js.map +1 -0
  40. package/dist/cjs/snapshot-verification.d.ts +57 -0
  41. package/dist/cjs/snapshot-verification.js +96 -0
  42. package/dist/cjs/snapshot-verification.js.map +1 -0
  43. package/dist/cjs/token-operation.js +3 -3
  44. package/dist/cjs/token-operation.js.map +1 -1
  45. package/dist/engine.d.ts.map +1 -1
  46. package/dist/engine.js +58 -44
  47. package/dist/engine.js.map +1 -1
  48. package/dist/index.d.ts +7 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +7 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/operations.d.ts +1 -1
  53. package/dist/operations.d.ts.map +1 -1
  54. package/dist/operations.js +4 -0
  55. package/dist/operations.js.map +1 -1
  56. package/dist/sale-common.d.ts +1 -1
  57. package/dist/sale-common.d.ts.map +1 -1
  58. package/dist/sale-common.js +39 -4
  59. package/dist/sale-common.js.map +1 -1
  60. package/dist/snapshot-deployment.d.ts +83 -0
  61. package/dist/snapshot-deployment.d.ts.map +1 -0
  62. package/dist/snapshot-deployment.js +78 -0
  63. package/dist/snapshot-deployment.js.map +1 -0
  64. package/dist/snapshot-operation.d.ts +28 -0
  65. package/dist/snapshot-operation.d.ts.map +1 -0
  66. package/dist/snapshot-operation.js +2428 -0
  67. package/dist/snapshot-operation.js.map +1 -0
  68. package/dist/snapshot-signerless-transactions.d.ts +133 -0
  69. package/dist/snapshot-signerless-transactions.d.ts.map +1 -0
  70. package/dist/snapshot-signerless-transactions.js +735 -0
  71. package/dist/snapshot-signerless-transactions.js.map +1 -0
  72. package/dist/snapshot-signerless.d.ts +227 -0
  73. package/dist/snapshot-signerless.d.ts.map +1 -0
  74. package/dist/snapshot-signerless.js +993 -0
  75. package/dist/snapshot-signerless.js.map +1 -0
  76. package/dist/snapshot-verification.d.ts +58 -0
  77. package/dist/snapshot-verification.d.ts.map +1 -0
  78. package/dist/snapshot-verification.js +92 -0
  79. package/dist/snapshot-verification.js.map +1 -0
  80. package/dist/token-operation.js +3 -3
  81. package/dist/token-operation.js.map +1 -1
  82. package/docs/API.md +67 -0
  83. package/docs/COMPATIBILITY.json +84 -0
  84. package/docs/INTEGRATION.md +215 -0
  85. package/docs/LIVE_TRANSFER.md +106 -0
  86. package/docs/RECIPES.md +175 -0
  87. package/docs/TRANSACTION_GUIDE.md +163 -0
  88. package/docs/snapshot-start.md +27 -0
  89. package/examples/README.md +81 -0
  90. package/examples/backend/cli.ts +55 -0
  91. package/examples/backend/recipe.ts +39 -0
  92. package/examples/backend/runtime.ts +13 -0
  93. package/examples/frontend/index.html +48 -0
  94. package/examples/frontend/main.ts +213 -0
  95. package/examples/frontend/runtime.ts +14 -0
  96. package/examples/package-lock.json +1287 -0
  97. package/examples/package.json +23 -0
  98. package/examples/shared/build.ts +179 -0
  99. package/examples/shared/fixture.ts +24 -0
  100. package/examples/shared/live-transfer.ts +155 -0
  101. package/examples/shared/recipe-fixtures.ts +543 -0
  102. package/examples/shared/recipes.ts +55 -0
  103. package/examples/shared/signing.ts +110 -0
  104. package/examples/shared/timeout.ts +8 -0
  105. package/examples/test/browser.ts +116 -0
  106. package/examples/test/build.test.ts +197 -0
  107. package/examples/test/recipes.test.ts +59 -0
  108. package/examples/test/sources.test.ts +73 -0
  109. package/examples/tsconfig.json +26 -0
  110. package/examples/vendor/NOTICE.md +15 -0
  111. package/examples/vendor/kaspa/LICENSE +15 -0
  112. package/examples/vendor/kaspa/kaspa.d.ts +8908 -0
  113. package/examples/vendor/kaspa/kaspa.js +15202 -0
  114. package/examples/vendor/kaspa/kaspa_bg.wasm +0 -0
  115. package/examples/vendor/kaspa/package.json +24 -0
  116. package/package.json +21 -4
  117. package/src/artifacts.ts +35 -0
  118. package/src/engine.ts +91 -97
  119. package/src/index.ts +8 -0
  120. package/src/operations.ts +4 -0
  121. package/src/sale-common.ts +44 -3
  122. package/src/snapshot-deployment.ts +108 -0
  123. package/src/snapshot-operation.ts +3592 -0
  124. package/src/snapshot-signerless-transactions.ts +1272 -0
  125. package/src/snapshot-signerless.ts +1525 -0
  126. package/src/snapshot-verification.ts +86 -0
  127. package/src/token-operation.ts +3 -3
@@ -0,0 +1,1525 @@
1
+ // @ts-nocheck
2
+ import { blake2b } from "@noble/hashes/blake2b";
3
+ import {
4
+ buildKcc20TransferSigScript,
5
+ dispatchTagFor,
6
+ holderExtensionCommitment,
7
+ templateParts,
8
+ } from "./abi.js";
9
+
10
+ export const SNAPSHOT_CODEC =
11
+ "kasmelt-snapshot-v2/blake2b256/sharded-full-claim";
12
+ export const SNAPSHOT_CONTROLLER_STATE_COMMITMENT_DOMAIN =
13
+ "kcc20-snapshot-controller-state/v2";
14
+ export const OWNER_P2PK_SCHNORR = 0;
15
+ export const OWNER_P2PKH_SCHNORR = 1;
16
+ export const OWNER_P2PKH_ECDSA = 2;
17
+ export const OWNER_P2SH = 3;
18
+ export const OWNER_COVENANT_ID = 4;
19
+ export const BORROW_DISABLED = 0;
20
+ export const MIN_KCC20_CELL_SOMPI = 50_000_000n;
21
+ export const MAX_I64 = 9_223_372_036_854_775_807n;
22
+ export const MAX_SEQUENCE = 18_446_744_073_709_551_615n;
23
+ export const LOCK_TIME_THRESHOLD = 500_000_000_000n;
24
+ export const MAX_SNAPSHOT_SHARDS = 8;
25
+ export const ZERO_32 = new Uint8Array(32);
26
+
27
+ const CLAIMABLE_SOURCE_OWNER_SCHEMES = [OWNER_P2PK_SCHNORR, OWNER_P2SH];
28
+
29
+ export function buildSnapshotTokenMetadata({
30
+ migrationId,
31
+ creator,
32
+ ticker,
33
+ name,
34
+ displayScale,
35
+ totalAllocation,
36
+ treasury = creator,
37
+ protocolFeeRecipient,
38
+ protocolFeeBps = 0n,
39
+ }) {
40
+ const amount = positiveI64(totalAllocation, "total allocation");
41
+ const extension = {
42
+ kind: 1,
43
+ creator: bytes32(creator, "creator"),
44
+ ticker: bytes32(ticker, "ticker"),
45
+ name: bytes32(name, "name"),
46
+ displayScale: positiveI64(displayScale, "display scale"),
47
+ maxSupply: amount,
48
+ mintLaneCount: 1,
49
+ mintPolicy: 0,
50
+ mintPriceSompi: 0n,
51
+ treasury: bytes32(treasury, "treasury"),
52
+ protocolFeeRecipient: bytes32(
53
+ protocolFeeRecipient,
54
+ "protocol fee recipient",
55
+ ),
56
+ protocolFeeBps: nonnegativeI64(protocolFeeBps, "protocol fee bps"),
57
+ };
58
+ if (extension.protocolFeeBps > 10_000n)
59
+ throw new Error("protocol fee bps exceeds 10000");
60
+ return operation("snapshot-token-metadata", migrationId, {
61
+ totalSupply: amount.toString(),
62
+ holderExtensionCommitment: hex(holderExtensionCommitment(extension)),
63
+ mintPolicy: "fixed_supply",
64
+ });
65
+ }
66
+
67
+ export function buildControllerGenesisPlan({
68
+ migrationId,
69
+ migrationCommitment,
70
+ controllerArtifact,
71
+ bootstrapArtifact,
72
+ kcc20Artifact,
73
+ holderExtensionCommitment: extensionCommitment,
74
+ manifestCommitment,
75
+ context,
76
+ shard,
77
+ shardCount,
78
+ deploymentGuardCommitment = ZERO_32,
79
+ controllerCellKas = MIN_KCC20_CELL_SOMPI,
80
+ reserveCellKas = MIN_KCC20_CELL_SOMPI,
81
+ recipientMinCellKas = MIN_KCC20_CELL_SOMPI,
82
+ claimNotBeforeDaaScore,
83
+ }) {
84
+ requireArtifacts({ controllerArtifact, bootstrapArtifact, kcc20Artifact });
85
+ const normalizedShard = normalizeShard(shard);
86
+ const count = boundedShardCount(shardCount);
87
+ if (normalizedShard.shardIndex >= count)
88
+ throw new Error("shard index must be below shard count");
89
+ const guard = bytes32(
90
+ deploymentGuardCommitment,
91
+ "deployment guard commitment",
92
+ );
93
+ if ((normalizedShard.shardIndex === 0) === equalBytes(guard, ZERO_32)) {
94
+ throw new Error(
95
+ "only shard zero must commit to the nonzero peer deployment plan",
96
+ );
97
+ }
98
+ const kcc20Template = templateParts(kcc20Artifact);
99
+ const bootstrapTemplate = templateParts(bootstrapArtifact);
100
+ const cells = validateCells({
101
+ controllerCellKas,
102
+ reserveCellKas,
103
+ recipientMinCellKas,
104
+ });
105
+ const state = normalizeControllerState(
106
+ {
107
+ migrationId: migrationCommitment,
108
+ tokenId: ZERO_32,
109
+ holderExtensionCommitment: extensionCommitment,
110
+ manifestCommitment,
111
+ snapshotContext: context,
112
+ kcc20TemplateHash: kcc20Template.hash,
113
+ bootstrapTemplateHash: bootstrapTemplate.hash,
114
+ deploymentGuardCommitment: guard,
115
+ claimNotBeforeDaaScore: claimActivation(claimNotBeforeDaaScore).lockTime,
116
+ shardIndex: normalizedShard.shardIndex,
117
+ globalIndexStart: normalizedShard.globalIndexStart,
118
+ holderCount: normalizedShard.holderCount,
119
+ treeDepth: normalizedShard.depth,
120
+ kcc20TemplatePrefixLen: kcc20Template.prefix.length,
121
+ kcc20TemplateSuffixLen: kcc20Template.suffix.length,
122
+ bootstrapTemplatePrefixLen: bootstrapTemplate.prefix.length,
123
+ bootstrapTemplateSuffixLen: bootstrapTemplate.suffix.length,
124
+ shardCount: count,
125
+ ...cells,
126
+ remainingRoot: normalizedShard.initialRoot,
127
+ remainingAmount: normalizedShard.allocationTotal,
128
+ claimsProcessed: 0n,
129
+ },
130
+ false,
131
+ );
132
+ return operation("snapshot-controller-genesis-plan", migrationId, {
133
+ contract: "KCC20SnapshotController",
134
+ entrypoint: "genesis",
135
+ controllerState: serializeState(state),
136
+ controllerStateCommitment: snapshotControllerStateCommitment(state),
137
+ shard: serializeShard(normalizedShard),
138
+ ...serializeCells(cells),
139
+ });
140
+ }
141
+
142
+ export function snapshotPeerPlanCommitment({
143
+ migrationCommitment,
144
+ manifestCommitment,
145
+ shards,
146
+ }) {
147
+ const normalized = normalizeStartShards(shards, false);
148
+ return hash256(
149
+ Uint8Array.of(0x21),
150
+ bytes32(migrationCommitment, "migration commitment"),
151
+ bytes32(manifestCommitment, "manifest commitment"),
152
+ leI64(normalized.length),
153
+ ...normalized.slice(1).flatMap(shardPlanParts),
154
+ );
155
+ }
156
+
157
+ export function snapshotStartPlanCommitment({
158
+ migrationCommitment,
159
+ manifestCommitment,
160
+ holderExtensionCommitment: extensionCommitment,
161
+ tokenId,
162
+ totalSupply,
163
+ claimNotBeforeDaaScore,
164
+ shards,
165
+ }) {
166
+ const normalized = normalizeStartShards(shards, false);
167
+ return hash256(
168
+ Uint8Array.of(0x22),
169
+ bytes32(migrationCommitment, "migration commitment"),
170
+ bytes32(manifestCommitment, "manifest commitment"),
171
+ bytes32(extensionCommitment, "holder extension commitment"),
172
+ nonzeroBytes32(tokenId, "token id"),
173
+ leI64(positiveI64(totalSupply, "total supply")),
174
+ leI64(claimActivation(claimNotBeforeDaaScore).lockTime),
175
+ leI64(normalized.length),
176
+ ...normalized.flatMap(shardPlanParts),
177
+ );
178
+ }
179
+
180
+ export function buildBootstrapGenesisPlan({
181
+ migrationId,
182
+ migrationCommitment,
183
+ bootstrapArtifact,
184
+ controllerArtifact,
185
+ kcc20Artifact,
186
+ manifestCommitment,
187
+ holderExtensionCommitment: extensionCommitment,
188
+ totalSupply,
189
+ claimNotBeforeDaaScore,
190
+ expectedTokenId,
191
+ shards,
192
+ reserveCellKas = MIN_KCC20_CELL_SOMPI,
193
+ controllerCellKas = MIN_KCC20_CELL_SOMPI,
194
+ bootstrapCellKas = MIN_KCC20_CELL_SOMPI,
195
+ }) {
196
+ requireArtifacts({ controllerArtifact, bootstrapArtifact, kcc20Artifact });
197
+ const normalized = normalizeStartShards(shards, false);
198
+ const migration = nonzeroBytes32(migrationCommitment, "migration commitment");
199
+ const manifest = nonzeroBytes32(manifestCommitment, "manifest commitment");
200
+ const extension = nonzeroBytes32(
201
+ extensionCommitment,
202
+ "holder extension commitment",
203
+ );
204
+ const token = nonzeroBytes32(expectedTokenId, "expected token id");
205
+ const supply = positiveI64(totalSupply, "total supply");
206
+ const activation = claimActivation(claimNotBeforeDaaScore).lockTime;
207
+ const bootstrapTemplate = templateParts(bootstrapArtifact);
208
+ validateStartShards(normalized, {
209
+ migrationId: migration,
210
+ manifestCommitment: manifest,
211
+ holderExtensionCommitment: extension,
212
+ totalSupply: supply,
213
+ claimNotBeforeDaaScore: activation,
214
+ bootstrapTemplateHash: bootstrapTemplate.hash,
215
+ bootstrapTemplatePrefixLen: bootstrapTemplate.prefix.length,
216
+ bootstrapTemplateSuffixLen: bootstrapTemplate.suffix.length,
217
+ controllerCellKas,
218
+ reserveCellKas,
219
+ });
220
+ const peerPlanCommitment = snapshotPeerPlanCommitment({
221
+ migrationCommitment: migration,
222
+ manifestCommitment: manifest,
223
+ shards: normalized,
224
+ });
225
+ if (
226
+ !equalBytes(
227
+ normalized[0].state.deploymentGuardCommitment,
228
+ peerPlanCommitment,
229
+ )
230
+ ) {
231
+ throw new Error(
232
+ "shard-zero controller does not commit to the exact peer controller plan",
233
+ );
234
+ }
235
+ const startPlanCommitment = snapshotStartPlanCommitment({
236
+ migrationCommitment: migration,
237
+ manifestCommitment: manifest,
238
+ holderExtensionCommitment: extension,
239
+ tokenId: token,
240
+ totalSupply: supply,
241
+ claimNotBeforeDaaScore: activation,
242
+ shards: normalized,
243
+ });
244
+ const kcc20Template = templateParts(kcc20Artifact);
245
+ const controllerTemplate = templateParts(controllerArtifact);
246
+ const state = normalizeBootstrapState({
247
+ migrationId: migration,
248
+ manifestCommitment: manifest,
249
+ holderExtensionCommitment: extension,
250
+ totalSupply: supply,
251
+ claimNotBeforeDaaScore: activation,
252
+ expectedTokenId: token,
253
+ kcc20TemplateHash: kcc20Template.hash,
254
+ kcc20TemplatePrefixLen: kcc20Template.prefix.length,
255
+ kcc20TemplateSuffixLen: kcc20Template.suffix.length,
256
+ controllerTemplateHash: controllerTemplate.hash,
257
+ controllerTemplatePrefixLen: controllerTemplate.prefix.length,
258
+ controllerTemplateSuffixLen: controllerTemplate.suffix.length,
259
+ bootstrapTemplateHash: bootstrapTemplate.hash,
260
+ bootstrapTemplatePrefixLen: bootstrapTemplate.prefix.length,
261
+ bootstrapTemplateSuffixLen: bootstrapTemplate.suffix.length,
262
+ reserveCellKas,
263
+ controllerCellKas,
264
+ bootstrapCellKas,
265
+ shardCount: normalized.length,
266
+ peerPlanCommitment,
267
+ startPlanCommitment,
268
+ });
269
+ return operation("snapshot-bootstrap-genesis-plan", migrationId, {
270
+ contract: "KCC20SnapshotBootstrap",
271
+ entrypoint: "genesis",
272
+ bootstrapState: serializeState(state),
273
+ bootstrapCellKas: state.bootstrapCellKas.toString(),
274
+ tokenId: hex(token),
275
+ });
276
+ }
277
+
278
+ export function buildSnapshotStartPlan({
279
+ migrationId,
280
+ bootstrapArtifact,
281
+ controllerArtifact,
282
+ kcc20Artifact,
283
+ bootstrapId,
284
+ bootstrapState,
285
+ shards,
286
+ }) {
287
+ requireArtifacts({ controllerArtifact, bootstrapArtifact, kcc20Artifact });
288
+ const state = normalizeBootstrapState(bootstrapState);
289
+ const normalized = normalizeStartShards(shards, false);
290
+ validateStartShards(normalized, state);
291
+ const peer = snapshotPeerPlanCommitment({
292
+ migrationCommitment: state.migrationId,
293
+ manifestCommitment: state.manifestCommitment,
294
+ shards: normalized,
295
+ });
296
+ const start = snapshotStartPlanCommitment({
297
+ migrationCommitment: state.migrationId,
298
+ manifestCommitment: state.manifestCommitment,
299
+ holderExtensionCommitment: state.holderExtensionCommitment,
300
+ tokenId: state.expectedTokenId,
301
+ totalSupply: state.totalSupply,
302
+ claimNotBeforeDaaScore: state.claimNotBeforeDaaScore,
303
+ shards: normalized,
304
+ });
305
+ if (
306
+ !equalBytes(peer, state.peerPlanCommitment) ||
307
+ !equalBytes(start, state.startPlanCommitment) ||
308
+ !equalBytes(normalized[0].state.deploymentGuardCommitment, peer)
309
+ ) {
310
+ throw new Error(
311
+ "controllers do not match the immutable bootstrap start commitments",
312
+ );
313
+ }
314
+ return operation("snapshot-start-plan", migrationId, {
315
+ contract: "KCC20SnapshotBootstrap",
316
+ entrypoint: "start",
317
+ approvalPolicy: "permissionless-exact-commitment",
318
+ bootstrapId: hex(nonzeroBytes32(bootstrapId, "bootstrap covenant id")),
319
+ bootstrapState: serializeState(state),
320
+ bootstrapCellKas: state.bootstrapCellKas.toString(),
321
+ controllerCellKas: state.controllerCellKas.toString(),
322
+ reserveCellKas: state.reserveCellKas.toString(),
323
+ tokenId: hex(state.expectedTokenId),
324
+ controllers: normalized.map((shard) => ({
325
+ controllerId: hex(shard.controllerId),
326
+ stateBefore: serializeState(shard.state),
327
+ stateAfter: serializeState({
328
+ ...shard.state,
329
+ tokenId: state.expectedTokenId,
330
+ }),
331
+ })),
332
+ reserves: normalized.map((shard) =>
333
+ serializeKcc20State(
334
+ kcc20State(
335
+ shard.state.remainingAmount,
336
+ shard.controllerId,
337
+ OWNER_COVENANT_ID,
338
+ state.holderExtensionCommitment,
339
+ ),
340
+ ),
341
+ ),
342
+ bootstrapSuccessor: null,
343
+ });
344
+ }
345
+
346
+ export function buildSnapshotStartSignatureScripts(
347
+ kw,
348
+ {
349
+ plan,
350
+ bootstrapArtifact,
351
+ controllerArtifact,
352
+ kcc20Artifact,
353
+ bootstrapInputIndex = 0,
354
+ controllerInputStart = 1,
355
+ },
356
+ ) {
357
+ requireArtifacts({ controllerArtifact, bootstrapArtifact, kcc20Artifact });
358
+ if (plan?.kind !== "snapshot-start-plan")
359
+ throw new Error("snapshot start plan is required");
360
+ const controllers = plan.controllers.map((entry) =>
361
+ normalizeControllerState(entry.stateAfter, true),
362
+ );
363
+ const reserves = plan.reserves.map(normalizeKcc20State);
364
+ const kcc20Template = templateParts(kcc20Artifact);
365
+ const controllerTemplate = templateParts(controllerArtifact);
366
+ const bootstrapBuilder = new kw.ScriptBuilder();
367
+ for (const field of encodeControllerStateRecordArrays(controllers))
368
+ bootstrapBuilder.addData(field);
369
+ for (const field of encodeKcc20StateRecordArrays(reserves))
370
+ bootstrapBuilder.addData(field);
371
+ bootstrapBuilder.addData(kcc20Template.prefix);
372
+ bootstrapBuilder.addData(kcc20Template.suffix);
373
+ bootstrapBuilder.addData(controllerTemplate.prefix);
374
+ bootstrapBuilder.addData(controllerTemplate.suffix);
375
+ bootstrapBuilder.addData(dispatchTagFor(bootstrapArtifact, "start"));
376
+ const controllerSignatureScripts = controllers.map((state, index) => {
377
+ const builder = new kw.ScriptBuilder();
378
+ addControllerState(builder, state);
379
+ builder.addI64(BigInt(bootstrapInputIndex));
380
+ builder.addI64(BigInt(index));
381
+ builder.addI64(BigInt(controllers.length + index));
382
+ addKcc20State(builder, reserves[index]);
383
+ builder.addData(kcc20Template.prefix);
384
+ builder.addData(kcc20Template.suffix);
385
+ builder.addData(dispatchTagFor(controllerArtifact, "start"));
386
+ return {
387
+ inputIndex: controllerInputStart + index,
388
+ signatureScript: builder.drain(),
389
+ };
390
+ });
391
+ return {
392
+ bootstrapSignatureScript: bootstrapBuilder.drain(),
393
+ controllerSignatureScripts,
394
+ };
395
+ }
396
+
397
+ export function buildClaimPlan({
398
+ migrationId,
399
+ migrationCommitment,
400
+ controllerArtifact,
401
+ bootstrapArtifact,
402
+ kcc20Artifact,
403
+ controllerId,
404
+ tokenId,
405
+ holderExtensionCommitment: extensionCommitment,
406
+ manifestCommitment,
407
+ context,
408
+ shard,
409
+ shardCount,
410
+ deploymentGuardCommitment = ZERO_32,
411
+ currentRoot,
412
+ currentReserveAmount,
413
+ currentClaimsProcessed,
414
+ claim,
415
+ controllerCellKas,
416
+ reserveCellKas,
417
+ recipientMinCellKas = MIN_KCC20_CELL_SOMPI,
418
+ reserveInputCellKas = reserveCellKas,
419
+ claimNotBeforeDaaScore,
420
+ lockTime = claimNotBeforeDaaScore,
421
+ controllerInputSequence = 0n,
422
+ }) {
423
+ requireArtifacts({ controllerArtifact, bootstrapArtifact, kcc20Artifact });
424
+ const normalizedShard = normalizeShard(shard);
425
+ const count = boundedShardCount(shardCount);
426
+ const cells = validateCells({
427
+ controllerCellKas,
428
+ reserveCellKas,
429
+ recipientMinCellKas,
430
+ });
431
+ const activation = claimActivation(
432
+ claimNotBeforeDaaScore,
433
+ lockTime,
434
+ controllerInputSequence,
435
+ );
436
+ if (
437
+ nonnegativeI64(reserveInputCellKas, "reserve input cell KAS") !==
438
+ cells.reserveCellKas
439
+ ) {
440
+ throw new Error(
441
+ "reserve input cell KAS does not match the controller binding",
442
+ );
443
+ }
444
+ const result = transitionClaim({
445
+ context,
446
+ shard: normalizedShard,
447
+ currentRoot,
448
+ currentReserveAmount,
449
+ claim,
450
+ });
451
+ const claimsProcessed = nonnegativeI64(
452
+ currentClaimsProcessed,
453
+ "current claims processed",
454
+ );
455
+ const nextClaimsProcessed = claimsProcessed + 1n;
456
+ if (
457
+ claimsProcessed >= BigInt(normalizedShard.holderCount) ||
458
+ result.terminal !==
459
+ (nextClaimsProcessed === BigInt(normalizedShard.holderCount))
460
+ ) {
461
+ throw new Error(
462
+ "remaining amount and processed claim count disagree on terminal state",
463
+ );
464
+ }
465
+ const controller = nonzeroBytes32(controllerId, "controller id");
466
+ const extension = nonzeroBytes32(
467
+ extensionCommitment,
468
+ "holder extension commitment",
469
+ );
470
+ const token = nonzeroBytes32(tokenId, "token id");
471
+ const kcc20Template = templateParts(kcc20Artifact);
472
+ const bootstrapTemplate = templateParts(bootstrapArtifact);
473
+ const before = normalizeControllerState(
474
+ {
475
+ migrationId: migrationCommitment,
476
+ tokenId: token,
477
+ holderExtensionCommitment: extension,
478
+ manifestCommitment,
479
+ snapshotContext: context,
480
+ kcc20TemplateHash: kcc20Template.hash,
481
+ bootstrapTemplateHash: bootstrapTemplate.hash,
482
+ deploymentGuardCommitment,
483
+ claimNotBeforeDaaScore: activation.lockTime,
484
+ shardIndex: normalizedShard.shardIndex,
485
+ globalIndexStart: normalizedShard.globalIndexStart,
486
+ holderCount: normalizedShard.holderCount,
487
+ treeDepth: normalizedShard.depth,
488
+ kcc20TemplatePrefixLen: kcc20Template.prefix.length,
489
+ kcc20TemplateSuffixLen: kcc20Template.suffix.length,
490
+ bootstrapTemplatePrefixLen: bootstrapTemplate.prefix.length,
491
+ bootstrapTemplateSuffixLen: bootstrapTemplate.suffix.length,
492
+ shardCount: count,
493
+ ...cells,
494
+ remainingRoot: currentRoot,
495
+ remainingAmount: currentReserveAmount,
496
+ claimsProcessed,
497
+ },
498
+ true,
499
+ );
500
+ const recipientState = kcc20State(
501
+ result.claimAmount,
502
+ result.owner,
503
+ result.ownerScheme,
504
+ extension,
505
+ );
506
+ const tokenOutputs = [];
507
+ if (!result.terminal) {
508
+ tokenOutputs.push({
509
+ role: "reserve",
510
+ cellKas: cells.reserveCellKas.toString(),
511
+ state: serializeKcc20State(
512
+ kcc20State(
513
+ result.remainingAmount,
514
+ controller,
515
+ OWNER_COVENANT_ID,
516
+ extension,
517
+ ),
518
+ ),
519
+ });
520
+ }
521
+ tokenOutputs.push({
522
+ role: "recipient",
523
+ cellKas: (result.terminal
524
+ ? cells.reserveCellKas + cells.controllerCellKas
525
+ : cells.recipientMinCellKas
526
+ ).toString(),
527
+ state: serializeKcc20State(recipientState),
528
+ });
529
+ return operation("snapshot-claim-plan", migrationId, {
530
+ transaction: {
531
+ version: 1,
532
+ lockTime: activation.lockTime.toString(),
533
+ controllerInputSequence: activation.controllerInputSequence.toString(),
534
+ sequenceSetBeforeSigning: true,
535
+ },
536
+ contract: "KCC20SnapshotController",
537
+ entrypoint: "claim",
538
+ kcc20Entrypoint: "transfer",
539
+ controllerId: hex(controller),
540
+ tokenId: hex(token),
541
+ shardIndex: normalizedShard.shardIndex,
542
+ globalIndex: result.globalIndex,
543
+ shardLeafIndex: result.shardLeafIndex,
544
+ proofSiblings: normalizeSiblings(claim.siblings, normalizedShard.depth).map(
545
+ hex,
546
+ ),
547
+ previousRoot: hex(bytes32(currentRoot, "current root")),
548
+ nextRoot: hex(result.nextRoot),
549
+ claimAmount: result.claimAmount.toString(),
550
+ claimOwner: hex(result.owner),
551
+ claimOwnerScheme: result.ownerScheme,
552
+ authorizationRequired:
553
+ result.ownerScheme === OWNER_P2PK_SCHNORR
554
+ ? "kcc2-p2pk-schnorr-signature"
555
+ : "kcc2-p2sh-participating-input",
556
+ terminal: result.terminal,
557
+ tokenOutputs,
558
+ controllerSuccessor: result.terminal
559
+ ? null
560
+ : {
561
+ cellKas: cells.controllerCellKas.toString(),
562
+ remainingRoot: hex(result.nextRoot),
563
+ remainingAmount: result.remainingAmount.toString(),
564
+ claimsProcessed: nextClaimsProcessed.toString(),
565
+ },
566
+ controllerStateBefore: serializeState(before),
567
+ controllerStateAfter: result.terminal
568
+ ? null
569
+ : serializeState({
570
+ ...before,
571
+ remainingRoot: result.nextRoot,
572
+ remainingAmount: result.remainingAmount,
573
+ claimsProcessed: nextClaimsProcessed,
574
+ }),
575
+ });
576
+ }
577
+
578
+ export function buildClaimSignatureScripts(
579
+ kw,
580
+ {
581
+ controllerArtifact,
582
+ kcc20Artifact,
583
+ claim,
584
+ reserveNext,
585
+ recipient,
586
+ kcc20TemplatePrefix,
587
+ kcc20TemplateSuffix,
588
+ nextTokenStates,
589
+ },
590
+ ) {
591
+ requireControllerArtifact(controllerArtifact);
592
+ requireKcc20Artifact(kcc20Artifact);
593
+ requireOwnerWitness(claim.ownerScheme, claim.ownerWitness);
594
+ const builder = new kw.ScriptBuilder();
595
+ builder.addI64(BigInt(claim.globalIndex));
596
+ builder.addI64(BigInt(claim.shardLeafIndex));
597
+ addByte(builder, claim.ownerScheme);
598
+ builder.addData(bytes32(claim.owner, "claim owner"));
599
+ builder.addData(Uint8Array.from(claim.ownerWitness ?? []));
600
+ builder.addI64(positiveI64(claim.amount, "claim amount"));
601
+ builder.addData(concatSiblings(claim.siblings));
602
+ addKcc20State(builder, reserveNext);
603
+ addKcc20State(builder, recipient);
604
+ builder.addData(Uint8Array.from(kcc20TemplatePrefix));
605
+ builder.addData(Uint8Array.from(kcc20TemplateSuffix));
606
+ builder.addData(dispatchTagFor(controllerArtifact, "claim"));
607
+ return {
608
+ controllerSignatureScript: builder.drain(),
609
+ kcc20SignatureScript: buildKcc20TransferSigScript(kw, kcc20Artifact, {
610
+ nextStates: nextTokenStates,
611
+ witness: Uint8Array.of(0),
612
+ }),
613
+ };
614
+ }
615
+
616
+ export function snapshotTradingSetupIdentity({
617
+ tokenId,
618
+ wrapperId,
619
+ orderbookId,
620
+ }) {
621
+ const token = bytes32(tokenId, "token covenant id");
622
+ const wrapper = bytes32(wrapperId, "wrapper covenant id");
623
+ const orderbook = bytes32(orderbookId, "orderbook covenant id");
624
+ if (equalBytes(token, wrapper) || equalBytes(token, orderbook)) {
625
+ throw new Error(
626
+ "snapshot trading setup wrapper market id must differ from the canonical token id",
627
+ );
628
+ }
629
+ return {
630
+ tokenId: hex(token),
631
+ wrapperId: hex(wrapper),
632
+ orderbookId: hex(orderbook),
633
+ };
634
+ }
635
+
636
+ export function transitionClaim({
637
+ context,
638
+ shard,
639
+ currentRoot,
640
+ currentReserveAmount,
641
+ claim,
642
+ }) {
643
+ const normalizedShard = normalizeShard(shard);
644
+ const globalIndex = integer(claim.globalIndex, "global index");
645
+ const shardLeafIndex = integer(claim.shardLeafIndex, "shard leaf index");
646
+ if (globalIndex !== normalizedShard.globalIndexStart + shardLeafIndex) {
647
+ throw new Error("global index does not match shard-local position");
648
+ }
649
+ if (shardLeafIndex < 0 || shardLeafIndex >= normalizedShard.holderCount) {
650
+ throw new Error("shard leaf index is outside the real holder set");
651
+ }
652
+ const ownerScheme = Number(claim.ownerScheme);
653
+ if (!CLAIMABLE_SOURCE_OWNER_SCHEMES.includes(ownerScheme))
654
+ throw new Error("unsupported snapshot owner scheme");
655
+ const owner = bytes32(claim.owner, "claim owner");
656
+ const claimAmount = positiveI64(claim.amount, "claim amount");
657
+ const reserve = positiveI64(currentReserveAmount, "current reserve amount");
658
+ if (claimAmount > reserve) throw new Error("claim exceeds current reserve");
659
+ const siblings = normalizeSiblings(claim.siblings, normalizedShard.depth);
660
+ const ctx = bytes32(context, "snapshot context");
661
+ const oldLeaf = entitlementLeaf(ctx, {
662
+ globalIndex,
663
+ ownerScheme,
664
+ owner,
665
+ amount: claimAmount,
666
+ });
667
+ if (
668
+ !equalBytes(
669
+ foldProof(oldLeaf, siblings, shardLeafIndex),
670
+ bytes32(currentRoot, "current root"),
671
+ )
672
+ ) {
673
+ throw new Error("claim proof does not match current shard root");
674
+ }
675
+ const nextRoot = foldProof(
676
+ claimedLeaf(ctx, globalIndex),
677
+ siblings,
678
+ shardLeafIndex,
679
+ );
680
+ const remainingAmount = reserve - claimAmount;
681
+ return {
682
+ globalIndex,
683
+ shardLeafIndex,
684
+ ownerScheme,
685
+ owner,
686
+ claimAmount,
687
+ nextRoot,
688
+ remainingAmount,
689
+ terminal: remainingAmount === 0n,
690
+ };
691
+ }
692
+
693
+ export function entitlementLeafPreimage(
694
+ context,
695
+ { globalIndex, ownerScheme, owner, amount },
696
+ ) {
697
+ return concat(
698
+ Uint8Array.of(0x10),
699
+ bytes32(context, "snapshot context"),
700
+ leI64(globalIndex),
701
+ Uint8Array.of(ownerScheme),
702
+ bytes32(owner, "owner"),
703
+ leI64(amount),
704
+ );
705
+ }
706
+
707
+ export const entitlementLeaf = (context, holder) =>
708
+ hash256(entitlementLeafPreimage(context, holder));
709
+ export const claimedLeafPreimage = (context, globalIndex) =>
710
+ concat(
711
+ Uint8Array.of(0x12),
712
+ bytes32(context, "snapshot context"),
713
+ leI64(globalIndex),
714
+ );
715
+ export const claimedLeaf = (context, globalIndex) =>
716
+ hash256(claimedLeafPreimage(context, globalIndex));
717
+ export const paddingLeafPreimage = (context, shardIndex, shardLeafIndex) =>
718
+ concat(
719
+ Uint8Array.of(0x13),
720
+ bytes32(context, "snapshot context"),
721
+ leI64(shardIndex),
722
+ leI64(shardLeafIndex),
723
+ );
724
+ export const paddingLeaf = (context, shardIndex, shardLeafIndex) =>
725
+ hash256(paddingLeafPreimage(context, shardIndex, shardLeafIndex));
726
+ export const branchNodePreimage = (left, right) =>
727
+ concat(
728
+ Uint8Array.of(0x11),
729
+ bytes32(left, "left node"),
730
+ bytes32(right, "right node"),
731
+ );
732
+ export const branchNode = (left, right) =>
733
+ hash256(branchNodePreimage(left, right));
734
+
735
+ export function foldProof(leaf, siblings, shardLeafIndex) {
736
+ let node = bytes32(leaf, "leaf");
737
+ let cursor = integer(shardLeafIndex, "shard leaf index");
738
+ for (const sibling of siblings) {
739
+ node =
740
+ cursor % 2 === 0 ? branchNode(node, sibling) : branchNode(sibling, node);
741
+ cursor = Math.floor(cursor / 2);
742
+ }
743
+ return node;
744
+ }
745
+
746
+ export function claimActivation(
747
+ claimNotBeforeDaaScore,
748
+ lockTime = claimNotBeforeDaaScore,
749
+ controllerInputSequence = 0n,
750
+ ) {
751
+ const notBefore = positiveI64(
752
+ claimNotBeforeDaaScore,
753
+ "claim-not-before DAA score",
754
+ );
755
+ if (notBefore >= LOCK_TIME_THRESHOLD)
756
+ throw new Error("claim-not-before must use DAA-score locktime mode");
757
+ const actualLockTime = nonnegativeI64(lockTime, "claim locktime");
758
+ if (actualLockTime !== notBefore)
759
+ throw new Error(
760
+ "claim locktime must equal the controller claim-not-before DAA score",
761
+ );
762
+ const sequence = BigInt(controllerInputSequence);
763
+ if (sequence !== 0n) {
764
+ if (sequence === MAX_SEQUENCE)
765
+ throw new Error("all-final input sequence bypass is forbidden");
766
+ throw new Error("controller input sequence must be zero before signing");
767
+ }
768
+ return { lockTime: actualLockTime, controllerInputSequence: sequence };
769
+ }
770
+
771
+ export function snapshotControllerStateCommitment(state) {
772
+ const normalized = normalizeControllerState(state, null);
773
+ return hex(
774
+ hash256(
775
+ new TextEncoder().encode(SNAPSHOT_CONTROLLER_STATE_COMMITMENT_DOMAIN),
776
+ encodeSnapshotControllerState(normalized),
777
+ ),
778
+ );
779
+ }
780
+
781
+ export function encodeSnapshotControllerState(state) {
782
+ const value = normalizeControllerState(state, null);
783
+ return concat(
784
+ push(value.migrationId),
785
+ push(value.tokenId),
786
+ push(value.holderExtensionCommitment),
787
+ push(value.manifestCommitment),
788
+ push(value.snapshotContext),
789
+ push(value.kcc20TemplateHash),
790
+ push(value.bootstrapTemplateHash),
791
+ push(value.deploymentGuardCommitment),
792
+ push(leI64(value.claimNotBeforeDaaScore)),
793
+ push(leI64(value.shardIndex)),
794
+ push(leI64(value.globalIndexStart)),
795
+ push(leI64(value.holderCount)),
796
+ push(leI64(value.treeDepth)),
797
+ push(leI64(value.kcc20TemplatePrefixLen)),
798
+ push(leI64(value.kcc20TemplateSuffixLen)),
799
+ push(leI64(value.bootstrapTemplatePrefixLen)),
800
+ push(leI64(value.bootstrapTemplateSuffixLen)),
801
+ push(leI64(value.shardCount)),
802
+ push(leI64(value.controllerCellKas)),
803
+ push(leI64(value.reserveCellKas)),
804
+ push(leI64(value.recipientMinCellKas)),
805
+ push(value.remainingRoot),
806
+ push(leI64(value.remainingAmount)),
807
+ push(leI64(value.claimsProcessed)),
808
+ );
809
+ }
810
+
811
+ export function encodeSnapshotBootstrapState(state) {
812
+ const value = normalizeBootstrapState(state);
813
+ return concat(
814
+ push(value.migrationId),
815
+ push(value.manifestCommitment),
816
+ push(value.holderExtensionCommitment),
817
+ push(leI64(value.totalSupply)),
818
+ push(leI64(value.claimNotBeforeDaaScore)),
819
+ push(value.expectedTokenId),
820
+ push(value.kcc20TemplateHash),
821
+ push(leI64(value.kcc20TemplatePrefixLen)),
822
+ push(leI64(value.kcc20TemplateSuffixLen)),
823
+ push(value.controllerTemplateHash),
824
+ push(leI64(value.controllerTemplatePrefixLen)),
825
+ push(leI64(value.controllerTemplateSuffixLen)),
826
+ push(value.bootstrapTemplateHash),
827
+ push(leI64(value.bootstrapTemplatePrefixLen)),
828
+ push(leI64(value.bootstrapTemplateSuffixLen)),
829
+ push(leI64(value.reserveCellKas)),
830
+ push(leI64(value.controllerCellKas)),
831
+ push(leI64(value.bootstrapCellKas)),
832
+ push(leI64(value.shardCount)),
833
+ push(value.peerPlanCommitment),
834
+ push(value.startPlanCommitment),
835
+ );
836
+ }
837
+
838
+ export const parseSnapshotControllerState = (state) =>
839
+ normalizeControllerState(state, null);
840
+ export const parseSnapshotBootstrapState = (state) =>
841
+ normalizeBootstrapState(state);
842
+
843
+ function operation(kind, migrationId, fields) {
844
+ if (typeof migrationId !== "string" || migrationId.length === 0)
845
+ throw new Error("migration id is required");
846
+ return {
847
+ schema: "kaspacom-snapshot-wallet-operation/v2",
848
+ kind,
849
+ migrationId,
850
+ planOnly: true,
851
+ transaction: { version: 1, lockTime: "0", inputSequence: "0" },
852
+ ...fields,
853
+ };
854
+ }
855
+
856
+ function requireArtifacts({
857
+ controllerArtifact,
858
+ bootstrapArtifact,
859
+ kcc20Artifact,
860
+ }) {
861
+ requireControllerArtifact(controllerArtifact);
862
+ requireBootstrapArtifact(bootstrapArtifact);
863
+ requireKcc20Artifact(kcc20Artifact);
864
+ }
865
+
866
+ function requireKcc20Artifact(artifact) {
867
+ if (
868
+ artifact?.contract_name !== "KCC20" ||
869
+ artifact?.program_information?.convention !== "KCC20"
870
+ ) {
871
+ throw new Error("canonical KCC20 artifact is required");
872
+ }
873
+ dispatchTagFor(artifact, "transfer");
874
+ }
875
+
876
+ function requireControllerArtifact(artifact) {
877
+ if (
878
+ artifact?.contract_name !== "KCC20SnapshotController" ||
879
+ artifact?.program_information?.start_entrypoint !== "start" ||
880
+ artifact?.program_information?.claim_entrypoint !== "claim"
881
+ ) {
882
+ throw new Error(
883
+ "signerless KCC20 snapshot controller artifact is required",
884
+ );
885
+ }
886
+ dispatchTagFor(artifact, "start");
887
+ dispatchTagFor(artifact, "claim");
888
+ }
889
+
890
+ function requireBootstrapArtifact(artifact) {
891
+ const entrypoints = Object.keys(
892
+ artifact?.contracts?.KCC20SnapshotBootstrap?.entries ?? {},
893
+ );
894
+ if (
895
+ artifact?.contract_name !== "KCC20SnapshotBootstrap" ||
896
+ artifact?.program_information?.start_entrypoint !== "start" ||
897
+ entrypoints.length !== 1 ||
898
+ entrypoints[0] !== "start"
899
+ ) {
900
+ throw new Error(
901
+ "immutable signerless KCC20 snapshot bootstrap artifact is required",
902
+ );
903
+ }
904
+ dispatchTagFor(artifact, "start");
905
+ }
906
+
907
+ function requireOwnerWitness(ownerScheme, witness) {
908
+ const scheme = Number(ownerScheme);
909
+ const value = Uint8Array.from(witness ?? []);
910
+ if (scheme === OWNER_P2PK_SCHNORR && value.length !== 65) {
911
+ throw new Error("P2PK Schnorr entitlement witness must be 65 bytes");
912
+ }
913
+ if (scheme === OWNER_P2SH && value.length !== 1) {
914
+ throw new Error(
915
+ "P2SH entitlement witness must identify one participating input",
916
+ );
917
+ }
918
+ if (!CLAIMABLE_SOURCE_OWNER_SCHEMES.includes(scheme))
919
+ throw new Error("unsupported snapshot owner scheme");
920
+ }
921
+
922
+ function normalizeShard(shard) {
923
+ if (!shard || typeof shard !== "object") throw new Error("shard is required");
924
+ const value = {
925
+ shardIndex: integer(shard.shardIndex, "shard index"),
926
+ globalIndexStart: integer(shard.globalIndexStart, "global index start"),
927
+ holderCount: integer(shard.holderCount, "holder count"),
928
+ depth: integer(shard.depth ?? shard.treeDepth, "tree depth"),
929
+ allocationTotal: positiveI64(
930
+ shard.allocationTotal ?? shard.remainingAmount,
931
+ "shard allocation total",
932
+ ),
933
+ initialRoot: nonzeroBytes32(
934
+ shard.initialRoot ?? shard.remainingRoot,
935
+ "initial root",
936
+ ),
937
+ };
938
+ if (
939
+ value.shardIndex < 0 ||
940
+ value.globalIndexStart < 0 ||
941
+ value.holderCount < 1
942
+ ) {
943
+ throw new Error(
944
+ "shard indexes must be nonnegative and holder count positive",
945
+ );
946
+ }
947
+ if (
948
+ value.depth < 1 ||
949
+ value.depth > 32 ||
950
+ value.holderCount > 2 ** value.depth
951
+ ) {
952
+ throw new Error("shard tree configuration is invalid");
953
+ }
954
+ return value;
955
+ }
956
+
957
+ function normalizeStartShards(shards, initialized) {
958
+ if (
959
+ !Array.isArray(shards) ||
960
+ shards.length < 1 ||
961
+ shards.length > MAX_SNAPSHOT_SHARDS
962
+ ) {
963
+ throw new Error("snapshot start requires 1..8 shards");
964
+ }
965
+ const seen = new Set();
966
+ return shards.map((entry, index) => {
967
+ const controllerId = nonzeroBytes32(entry?.controllerId, "controller id");
968
+ const key = hex(controllerId);
969
+ if (seen.has(key)) throw new Error("controller ids must be unique");
970
+ seen.add(key);
971
+ const state = normalizeControllerState(
972
+ entry?.state ?? entry?.controllerState ?? entry?.stateBefore,
973
+ initialized,
974
+ );
975
+ if (Number(state.shardIndex) !== index)
976
+ throw new Error("snapshot shards must be ordered contiguously from zero");
977
+ return { controllerId, state };
978
+ });
979
+ }
980
+
981
+ function validateStartShards(shards, expected) {
982
+ let total = 0n;
983
+ for (const [index, shard] of shards.entries()) {
984
+ const state = shard.state;
985
+ if (
986
+ !equalBytes(state.migrationId, expected.migrationId) ||
987
+ !equalBytes(state.manifestCommitment, expected.manifestCommitment) ||
988
+ !equalBytes(
989
+ state.holderExtensionCommitment,
990
+ expected.holderExtensionCommitment,
991
+ ) ||
992
+ !equalBytes(
993
+ state.bootstrapTemplateHash,
994
+ expected.bootstrapTemplateHash,
995
+ ) ||
996
+ state.bootstrapTemplatePrefixLen !==
997
+ BigInt(expected.bootstrapTemplatePrefixLen) ||
998
+ state.bootstrapTemplateSuffixLen !==
999
+ BigInt(expected.bootstrapTemplateSuffixLen) ||
1000
+ !equalBytes(state.tokenId, ZERO_32) ||
1001
+ state.claimNotBeforeDaaScore !==
1002
+ BigInt(expected.claimNotBeforeDaaScore) ||
1003
+ state.shardCount !== BigInt(shards.length) ||
1004
+ state.controllerCellKas !== BigInt(expected.controllerCellKas) ||
1005
+ state.reserveCellKas !== BigInt(expected.reserveCellKas) ||
1006
+ state.claimsProcessed !== 0n
1007
+ ) {
1008
+ throw new Error(`shard ${index} does not match immutable start policy`);
1009
+ }
1010
+ if (index > 0 && !equalBytes(state.deploymentGuardCommitment, ZERO_32)) {
1011
+ throw new Error("peer controllers must not contain a deployment guard");
1012
+ }
1013
+ total += state.remainingAmount;
1014
+ }
1015
+ if (total !== BigInt(expected.totalSupply))
1016
+ throw new Error("shard allocations do not equal fixed token supply");
1017
+ }
1018
+
1019
+ function shardPlanParts(shard) {
1020
+ const state = shard.state;
1021
+ return [
1022
+ leI64(state.shardIndex),
1023
+ shard.controllerId,
1024
+ state.remainingRoot,
1025
+ leI64(state.remainingAmount),
1026
+ leI64(state.globalIndexStart),
1027
+ leI64(state.holderCount),
1028
+ leI64(state.treeDepth),
1029
+ state.snapshotContext,
1030
+ ];
1031
+ }
1032
+
1033
+ function validateCells({
1034
+ controllerCellKas,
1035
+ reserveCellKas,
1036
+ recipientMinCellKas,
1037
+ }) {
1038
+ const controller = positiveI64(controllerCellKas, "controller cell KAS");
1039
+ const reserve = positiveI64(reserveCellKas, "reserve cell KAS");
1040
+ const recipient = positiveI64(
1041
+ recipientMinCellKas,
1042
+ "recipient minimum cell KAS",
1043
+ );
1044
+ if (
1045
+ controller < MIN_KCC20_CELL_SOMPI ||
1046
+ reserve < MIN_KCC20_CELL_SOMPI ||
1047
+ recipient < MIN_KCC20_CELL_SOMPI
1048
+ ) {
1049
+ throw new Error(
1050
+ "controller and KCC20 cells must be at least 50000000 sompi",
1051
+ );
1052
+ }
1053
+ if (controller > MAX_I64 - reserve || controller + reserve < recipient) {
1054
+ throw new Error("terminal KAS disposition is invalid");
1055
+ }
1056
+ return {
1057
+ controllerCellKas: controller,
1058
+ reserveCellKas: reserve,
1059
+ recipientMinCellKas: recipient,
1060
+ };
1061
+ }
1062
+
1063
+ function normalizeControllerState(state, initialized) {
1064
+ if (!state || typeof state !== "object")
1065
+ throw new Error("controller state is required");
1066
+ const value = {
1067
+ migrationId: nonzeroBytes32(state.migrationId, "migration id"),
1068
+ tokenId: bytes32(state.tokenId, "token id"),
1069
+ holderExtensionCommitment: nonzeroBytes32(
1070
+ state.holderExtensionCommitment,
1071
+ "holder extension commitment",
1072
+ ),
1073
+ manifestCommitment: nonzeroBytes32(
1074
+ state.manifestCommitment,
1075
+ "manifest commitment",
1076
+ ),
1077
+ snapshotContext: nonzeroBytes32(state.snapshotContext, "snapshot context"),
1078
+ kcc20TemplateHash: nonzeroBytes32(
1079
+ state.kcc20TemplateHash,
1080
+ "KCC20 template hash",
1081
+ ),
1082
+ bootstrapTemplateHash: nonzeroBytes32(
1083
+ state.bootstrapTemplateHash,
1084
+ "bootstrap template hash",
1085
+ ),
1086
+ deploymentGuardCommitment: bytes32(
1087
+ state.deploymentGuardCommitment,
1088
+ "deployment guard commitment",
1089
+ ),
1090
+ claimNotBeforeDaaScore: positiveI64(
1091
+ state.claimNotBeforeDaaScore,
1092
+ "claim-not-before DAA score",
1093
+ ),
1094
+ shardIndex: nonnegativeI64(state.shardIndex, "shard index"),
1095
+ globalIndexStart: nonnegativeI64(
1096
+ state.globalIndexStart,
1097
+ "global index start",
1098
+ ),
1099
+ holderCount: positiveI64(state.holderCount, "holder count"),
1100
+ treeDepth: positiveI64(state.treeDepth, "tree depth"),
1101
+ kcc20TemplatePrefixLen: positiveI64(
1102
+ state.kcc20TemplatePrefixLen,
1103
+ "KCC20 template prefix length",
1104
+ ),
1105
+ kcc20TemplateSuffixLen: positiveI64(
1106
+ state.kcc20TemplateSuffixLen,
1107
+ "KCC20 template suffix length",
1108
+ ),
1109
+ bootstrapTemplatePrefixLen: positiveI64(
1110
+ state.bootstrapTemplatePrefixLen,
1111
+ "bootstrap template prefix length",
1112
+ ),
1113
+ bootstrapTemplateSuffixLen: positiveI64(
1114
+ state.bootstrapTemplateSuffixLen,
1115
+ "bootstrap template suffix length",
1116
+ ),
1117
+ shardCount: positiveI64(state.shardCount, "shard count"),
1118
+ controllerCellKas: positiveI64(
1119
+ state.controllerCellKas,
1120
+ "controller cell KAS",
1121
+ ),
1122
+ reserveCellKas: positiveI64(state.reserveCellKas, "reserve cell KAS"),
1123
+ recipientMinCellKas: positiveI64(
1124
+ state.recipientMinCellKas,
1125
+ "recipient minimum cell KAS",
1126
+ ),
1127
+ remainingRoot: nonzeroBytes32(state.remainingRoot, "remaining root"),
1128
+ remainingAmount: positiveI64(state.remainingAmount, "remaining amount"),
1129
+ claimsProcessed: nonnegativeI64(state.claimsProcessed, "claims processed"),
1130
+ };
1131
+ if (
1132
+ value.claimNotBeforeDaaScore >= LOCK_TIME_THRESHOLD ||
1133
+ value.treeDepth > 32n ||
1134
+ value.shardCount > BigInt(MAX_SNAPSHOT_SHARDS) ||
1135
+ value.shardIndex >= value.shardCount ||
1136
+ value.claimsProcessed >= value.holderCount
1137
+ ) {
1138
+ throw new Error("controller state configuration is invalid");
1139
+ }
1140
+ if (initialized === true && equalBytes(value.tokenId, ZERO_32))
1141
+ throw new Error("initialized controller token id is zero");
1142
+ if (initialized === false && !equalBytes(value.tokenId, ZERO_32))
1143
+ throw new Error("uninitialized controller token id is nonzero");
1144
+ return value;
1145
+ }
1146
+
1147
+ function normalizeBootstrapState(state) {
1148
+ if (!state || typeof state !== "object")
1149
+ throw new Error("bootstrap state is required");
1150
+ const value = {
1151
+ migrationId: nonzeroBytes32(state.migrationId, "migration id"),
1152
+ manifestCommitment: nonzeroBytes32(
1153
+ state.manifestCommitment,
1154
+ "manifest commitment",
1155
+ ),
1156
+ holderExtensionCommitment: nonzeroBytes32(
1157
+ state.holderExtensionCommitment,
1158
+ "holder extension commitment",
1159
+ ),
1160
+ totalSupply: positiveI64(state.totalSupply, "total supply"),
1161
+ claimNotBeforeDaaScore: positiveI64(
1162
+ state.claimNotBeforeDaaScore,
1163
+ "claim-not-before DAA score",
1164
+ ),
1165
+ expectedTokenId: nonzeroBytes32(state.expectedTokenId, "expected token id"),
1166
+ kcc20TemplateHash: nonzeroBytes32(
1167
+ state.kcc20TemplateHash,
1168
+ "KCC20 template hash",
1169
+ ),
1170
+ kcc20TemplatePrefixLen: positiveI64(
1171
+ state.kcc20TemplatePrefixLen,
1172
+ "KCC20 template prefix length",
1173
+ ),
1174
+ kcc20TemplateSuffixLen: positiveI64(
1175
+ state.kcc20TemplateSuffixLen,
1176
+ "KCC20 template suffix length",
1177
+ ),
1178
+ controllerTemplateHash: nonzeroBytes32(
1179
+ state.controllerTemplateHash,
1180
+ "controller template hash",
1181
+ ),
1182
+ controllerTemplatePrefixLen: positiveI64(
1183
+ state.controllerTemplatePrefixLen,
1184
+ "controller template prefix length",
1185
+ ),
1186
+ controllerTemplateSuffixLen: positiveI64(
1187
+ state.controllerTemplateSuffixLen,
1188
+ "controller template suffix length",
1189
+ ),
1190
+ bootstrapTemplateHash: nonzeroBytes32(
1191
+ state.bootstrapTemplateHash,
1192
+ "bootstrap template hash",
1193
+ ),
1194
+ bootstrapTemplatePrefixLen: positiveI64(
1195
+ state.bootstrapTemplatePrefixLen,
1196
+ "bootstrap template prefix length",
1197
+ ),
1198
+ bootstrapTemplateSuffixLen: positiveI64(
1199
+ state.bootstrapTemplateSuffixLen,
1200
+ "bootstrap template suffix length",
1201
+ ),
1202
+ reserveCellKas: positiveI64(state.reserveCellKas, "reserve cell KAS"),
1203
+ controllerCellKas: positiveI64(
1204
+ state.controllerCellKas,
1205
+ "controller cell KAS",
1206
+ ),
1207
+ bootstrapCellKas: positiveI64(state.bootstrapCellKas, "bootstrap cell KAS"),
1208
+ shardCount: positiveI64(state.shardCount, "shard count"),
1209
+ peerPlanCommitment: nonzeroBytes32(
1210
+ state.peerPlanCommitment,
1211
+ "peer plan commitment",
1212
+ ),
1213
+ startPlanCommitment: nonzeroBytes32(
1214
+ state.startPlanCommitment,
1215
+ "start plan commitment",
1216
+ ),
1217
+ };
1218
+ if (
1219
+ value.claimNotBeforeDaaScore >= LOCK_TIME_THRESHOLD ||
1220
+ value.shardCount > BigInt(MAX_SNAPSHOT_SHARDS) ||
1221
+ value.reserveCellKas < MIN_KCC20_CELL_SOMPI ||
1222
+ value.controllerCellKas < MIN_KCC20_CELL_SOMPI ||
1223
+ value.bootstrapCellKas < MIN_KCC20_CELL_SOMPI
1224
+ ) {
1225
+ throw new Error("bootstrap state configuration is invalid");
1226
+ }
1227
+ return value;
1228
+ }
1229
+
1230
+ function normalizeKcc20State(state) {
1231
+ if (!state || typeof state !== "object")
1232
+ throw new Error("KCC20 state is required");
1233
+ return {
1234
+ amount: positiveI64(state.amount, "KCC20 amount"),
1235
+ owner: bytes32(state.owner, "KCC20 owner"),
1236
+ ownerScheme: Number(state.ownerScheme),
1237
+ borrowScheme: Number(state.borrowScheme),
1238
+ borrowGuard: bytes32(state.borrowGuard, "KCC20 borrow guard"),
1239
+ extensionCommitment: bytes32(
1240
+ state.extensionCommitment,
1241
+ "KCC20 extension commitment",
1242
+ ),
1243
+ };
1244
+ }
1245
+
1246
+ function kcc20State(amount, owner, ownerScheme, extensionCommitment) {
1247
+ return normalizeKcc20State({
1248
+ amount,
1249
+ owner,
1250
+ ownerScheme,
1251
+ borrowScheme: BORROW_DISABLED,
1252
+ borrowGuard: ZERO_32,
1253
+ extensionCommitment,
1254
+ });
1255
+ }
1256
+
1257
+ function encodeControllerStateRecordArrays(states) {
1258
+ const fields = [
1259
+ ["migrationId", "bytes"],
1260
+ ["tokenId", "bytes"],
1261
+ ["holderExtensionCommitment", "bytes"],
1262
+ ["manifestCommitment", "bytes"],
1263
+ ["snapshotContext", "bytes"],
1264
+ ["kcc20TemplateHash", "bytes"],
1265
+ ["bootstrapTemplateHash", "bytes"],
1266
+ ["deploymentGuardCommitment", "bytes"],
1267
+ ["claimNotBeforeDaaScore", "int"],
1268
+ ["shardIndex", "int"],
1269
+ ["globalIndexStart", "int"],
1270
+ ["holderCount", "int"],
1271
+ ["treeDepth", "int"],
1272
+ ["kcc20TemplatePrefixLen", "int"],
1273
+ ["kcc20TemplateSuffixLen", "int"],
1274
+ ["bootstrapTemplatePrefixLen", "int"],
1275
+ ["bootstrapTemplateSuffixLen", "int"],
1276
+ ["shardCount", "int"],
1277
+ ["controllerCellKas", "int"],
1278
+ ["reserveCellKas", "int"],
1279
+ ["recipientMinCellKas", "int"],
1280
+ ["remainingRoot", "bytes"],
1281
+ ["remainingAmount", "int"],
1282
+ ["claimsProcessed", "int"],
1283
+ ];
1284
+ return fields.map(([field, kind]) =>
1285
+ concat(
1286
+ ...states.map((state) =>
1287
+ kind === "int" ? leI64(state[field]) : state[field],
1288
+ ),
1289
+ ),
1290
+ );
1291
+ }
1292
+
1293
+ function encodeKcc20StateRecordArrays(states) {
1294
+ return [
1295
+ concat(...states.map((state) => leI64(state.amount))),
1296
+ concat(...states.map((state) => state.owner)),
1297
+ Uint8Array.from(states.map((state) => state.ownerScheme)),
1298
+ Uint8Array.from(states.map((state) => state.borrowScheme)),
1299
+ concat(...states.map((state) => state.borrowGuard)),
1300
+ concat(...states.map((state) => state.extensionCommitment)),
1301
+ ];
1302
+ }
1303
+
1304
+ function addControllerState(builder, state) {
1305
+ const byteFields = new Set([
1306
+ "migrationId",
1307
+ "tokenId",
1308
+ "holderExtensionCommitment",
1309
+ "manifestCommitment",
1310
+ "snapshotContext",
1311
+ "kcc20TemplateHash",
1312
+ "bootstrapTemplateHash",
1313
+ "deploymentGuardCommitment",
1314
+ "remainingRoot",
1315
+ ]);
1316
+ for (const field of [
1317
+ "migrationId",
1318
+ "tokenId",
1319
+ "holderExtensionCommitment",
1320
+ "manifestCommitment",
1321
+ "snapshotContext",
1322
+ "kcc20TemplateHash",
1323
+ "bootstrapTemplateHash",
1324
+ "deploymentGuardCommitment",
1325
+ "claimNotBeforeDaaScore",
1326
+ "shardIndex",
1327
+ "globalIndexStart",
1328
+ "holderCount",
1329
+ "treeDepth",
1330
+ "kcc20TemplatePrefixLen",
1331
+ "kcc20TemplateSuffixLen",
1332
+ "bootstrapTemplatePrefixLen",
1333
+ "bootstrapTemplateSuffixLen",
1334
+ "shardCount",
1335
+ "controllerCellKas",
1336
+ "reserveCellKas",
1337
+ "recipientMinCellKas",
1338
+ "remainingRoot",
1339
+ "remainingAmount",
1340
+ "claimsProcessed",
1341
+ ]) {
1342
+ if (byteFields.has(field)) builder.addData(state[field]);
1343
+ else builder.addI64(state[field]);
1344
+ }
1345
+ }
1346
+
1347
+ function addKcc20State(builder, state) {
1348
+ const value = state?.amount === 0n ? state : normalizeKcc20State(state);
1349
+ builder.addI64(BigInt(value?.amount ?? 0));
1350
+ builder.addData(bytes32(value?.owner ?? ZERO_32, "KCC20 owner"));
1351
+ addByte(builder, value?.ownerScheme ?? 0);
1352
+ addByte(builder, value?.borrowScheme ?? BORROW_DISABLED);
1353
+ builder.addData(bytes32(value?.borrowGuard ?? ZERO_32, "KCC20 borrow guard"));
1354
+ builder.addData(
1355
+ bytes32(
1356
+ value?.extensionCommitment ?? ZERO_32,
1357
+ "KCC20 extension commitment",
1358
+ ),
1359
+ );
1360
+ }
1361
+
1362
+ function addByte(builder, value) {
1363
+ const byte = Number(value);
1364
+ if (!Number.isInteger(byte) || byte < 0 || byte > 255)
1365
+ throw new Error("value must be a byte");
1366
+ builder.addOps([0x01, byte]);
1367
+ }
1368
+
1369
+ function normalizeSiblings(siblings, depth) {
1370
+ if (!Array.isArray(siblings) || siblings.length !== depth) {
1371
+ throw new Error(`proof must contain exactly ${depth} siblings`);
1372
+ }
1373
+ return siblings.map((sibling) => bytes32(sibling, "proof sibling"));
1374
+ }
1375
+
1376
+ function concatSiblings(siblings) {
1377
+ return concat(
1378
+ ...siblings.map((sibling) => bytes32(sibling, "proof sibling")),
1379
+ );
1380
+ }
1381
+
1382
+ function serializeState(state) {
1383
+ return Object.fromEntries(
1384
+ Object.entries(state).map(([key, value]) => [
1385
+ key,
1386
+ value instanceof Uint8Array ? hex(value) : value.toString(),
1387
+ ]),
1388
+ );
1389
+ }
1390
+
1391
+ function serializeKcc20State(state) {
1392
+ return {
1393
+ amount: state.amount.toString(),
1394
+ owner: hex(state.owner),
1395
+ ownerScheme: state.ownerScheme,
1396
+ borrowScheme: state.borrowScheme,
1397
+ borrowGuard: hex(state.borrowGuard),
1398
+ extensionCommitment: hex(state.extensionCommitment),
1399
+ };
1400
+ }
1401
+
1402
+ function serializeShard(shard) {
1403
+ return {
1404
+ shardIndex: shard.shardIndex,
1405
+ globalIndexStart: shard.globalIndexStart,
1406
+ holderCount: shard.holderCount,
1407
+ depth: shard.depth,
1408
+ allocationTotal: shard.allocationTotal.toString(),
1409
+ initialRoot: hex(shard.initialRoot),
1410
+ };
1411
+ }
1412
+
1413
+ function serializeCells(cells) {
1414
+ return {
1415
+ controllerCellKas: cells.controllerCellKas.toString(),
1416
+ reserveCellKas: cells.reserveCellKas.toString(),
1417
+ recipientMinCellKas: cells.recipientMinCellKas.toString(),
1418
+ };
1419
+ }
1420
+
1421
+ function boundedShardCount(value) {
1422
+ const count = integer(value, "shard count");
1423
+ if (count < 1 || count > MAX_SNAPSHOT_SHARDS)
1424
+ throw new Error("shard count must be 1..8");
1425
+ return count;
1426
+ }
1427
+
1428
+ function hash256(...parts) {
1429
+ return Uint8Array.from(blake2b(concat(...parts), { dkLen: 32 }));
1430
+ }
1431
+
1432
+ function concat(...parts) {
1433
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
1434
+ let offset = 0;
1435
+ for (const part of parts) {
1436
+ out.set(part, offset);
1437
+ offset += part.length;
1438
+ }
1439
+ return out;
1440
+ }
1441
+
1442
+ function leI64(value) {
1443
+ const amount = BigInt(value);
1444
+ if (amount < 0n || amount > MAX_I64)
1445
+ throw new Error("integer is outside nonnegative signed 64-bit range");
1446
+ const bytes = new Uint8Array(8);
1447
+ let cursor = amount;
1448
+ for (let index = 0; index < 8; index += 1) {
1449
+ bytes[index] = Number(cursor & 0xffn);
1450
+ cursor >>= 8n;
1451
+ }
1452
+ return bytes;
1453
+ }
1454
+
1455
+ function push(value) {
1456
+ const bytes = Uint8Array.from(value);
1457
+ if (bytes.length > 75) throw new Error("snapshot state push is too large");
1458
+ return concat(Uint8Array.of(bytes.length), bytes);
1459
+ }
1460
+
1461
+ function bytes32(value, label) {
1462
+ let bytes;
1463
+ if (typeof value === "string") {
1464
+ const normalized = value.replace(/^0x/, "");
1465
+ if (!/^(?:[0-9a-fA-F]{2}){32}$/.test(normalized))
1466
+ throw new Error(`${label} must be 32 bytes`);
1467
+ bytes = hexBytes(normalized);
1468
+ } else {
1469
+ bytes = Uint8Array.from(value ?? []);
1470
+ }
1471
+ if (bytes.length !== 32) throw new Error(`${label} must be 32 bytes`);
1472
+ return bytes;
1473
+ }
1474
+
1475
+ function nonzeroBytes32(value, label) {
1476
+ const bytes = bytes32(value, label);
1477
+ if (equalBytes(bytes, ZERO_32)) throw new Error(`${label} must be nonzero`);
1478
+ return bytes;
1479
+ }
1480
+
1481
+ function integer(value, label) {
1482
+ const number = Number(value);
1483
+ if (!Number.isSafeInteger(number))
1484
+ throw new Error(`${label} must be a safe integer`);
1485
+ return number;
1486
+ }
1487
+
1488
+ function nonnegativeI64(value, label) {
1489
+ const number = BigInt(value);
1490
+ if (number < 0n || number > MAX_I64) {
1491
+ throw new Error(`${label} must be a nonnegative signed 64-bit integer`);
1492
+ }
1493
+ return number;
1494
+ }
1495
+
1496
+ function positiveI64(value, label) {
1497
+ const number = BigInt(value);
1498
+ if (number <= 0n || number > MAX_I64) {
1499
+ throw new Error(`${label} must be a positive signed 64-bit integer`);
1500
+ }
1501
+ return number;
1502
+ }
1503
+
1504
+ function equalBytes(a, b) {
1505
+ const left = Uint8Array.from(a ?? []);
1506
+ const right = Uint8Array.from(b ?? []);
1507
+ return (
1508
+ left.length === right.length &&
1509
+ left.every((byte, index) => byte === right[index])
1510
+ );
1511
+ }
1512
+
1513
+ function hex(value) {
1514
+ return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join(
1515
+ "",
1516
+ );
1517
+ }
1518
+
1519
+ function hexBytes(value) {
1520
+ const bytes = new Uint8Array(value.length / 2);
1521
+ for (let index = 0; index < bytes.length; index += 1) {
1522
+ bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
1523
+ }
1524
+ return bytes;
1525
+ }