@kaspacom/kcc20-tx-builder 0.5.0 → 0.6.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 (119) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/RELEASING.md +19 -14
  4. package/artifacts/ArgentKCC20.reference.json +12428 -0
  5. package/artifacts/ArgentKCC20Wrapper.placeholder.json +12455 -0
  6. package/artifacts/KCC20.placeholder.json +1447 -1973
  7. package/artifacts/KCC20FeeTicket.placeholder.json +816 -1447
  8. package/artifacts/KCC20Orderbook.placeholder.json +1 -2
  9. package/artifacts/KCC20SnapshotBootstrap.placeholder.json +101 -100
  10. package/artifacts/KCC20SnapshotController.placeholder.json +101 -100
  11. package/artifacts/KCC20Vesting.placeholder.json +427 -429
  12. package/artifacts/KCC20Wrapper.placeholder.json +733 -734
  13. package/artifacts/kcc20-snapshot-bootstrap/KCC20SnapshotBootstrap.placeholder.json +101 -100
  14. package/artifacts/kcc20-snapshot-controller/KCC20SnapshotController.placeholder.json +101 -100
  15. package/artifacts/snapshot-standards.lock.json +6 -3
  16. package/artifacts/token-profiles.json +109 -0
  17. package/artifacts/token-profiles.sha256 +1 -0
  18. package/dist/action-source-resolver.d.ts.map +1 -1
  19. package/dist/action-source-resolver.js +11 -2
  20. package/dist/action-source-resolver.js.map +1 -1
  21. package/dist/artifacts.d.ts +10 -10
  22. package/dist/artifacts.d.ts.map +1 -1
  23. package/dist/artifacts.js +10 -10
  24. package/dist/artifacts.js.map +1 -1
  25. package/dist/cjs/action-source-resolver.js +11 -2
  26. package/dist/cjs/action-source-resolver.js.map +1 -1
  27. package/dist/cjs/artifacts.d.ts +10 -10
  28. package/dist/cjs/artifacts.js +10 -10
  29. package/dist/cjs/artifacts.js.map +1 -1
  30. package/dist/cjs/engine.d.ts +10 -0
  31. package/dist/cjs/engine.js +124 -77
  32. package/dist/cjs/engine.js.map +1 -1
  33. package/dist/cjs/index.d.ts +5 -0
  34. package/dist/cjs/index.js +5 -0
  35. package/dist/cjs/index.js.map +1 -1
  36. package/dist/cjs/profile-transfer.d.ts +60 -0
  37. package/dist/cjs/profile-transfer.js +248 -0
  38. package/dist/cjs/profile-transfer.js.map +1 -0
  39. package/dist/cjs/snapshot-signerless-transactions.js +2 -5
  40. package/dist/cjs/snapshot-signerless-transactions.js.map +1 -1
  41. package/dist/cjs/snapshot-signerless.js +5 -3
  42. package/dist/cjs/snapshot-signerless.js.map +1 -1
  43. package/dist/cjs/token-authorization.d.ts +42 -0
  44. package/dist/cjs/token-authorization.js +93 -0
  45. package/dist/cjs/token-authorization.js.map +1 -0
  46. package/dist/cjs/token-profiles.d.ts +82 -0
  47. package/dist/cjs/token-profiles.generated.d.ts +3 -0
  48. package/dist/cjs/token-profiles.generated.js +114 -0
  49. package/dist/cjs/token-profiles.generated.js.map +1 -0
  50. package/dist/cjs/token-profiles.js +257 -0
  51. package/dist/cjs/token-profiles.js.map +1 -0
  52. package/dist/cjs/transfer-payload.d.ts +9 -0
  53. package/dist/cjs/transfer-payload.js +37 -0
  54. package/dist/cjs/transfer-payload.js.map +1 -0
  55. package/dist/cjs/wrapper-operation.d.ts +1 -0
  56. package/dist/cjs/wrapper-operation.js +1 -0
  57. package/dist/cjs/wrapper-operation.js.map +1 -1
  58. package/dist/engine.d.ts +10 -0
  59. package/dist/engine.d.ts.map +1 -1
  60. package/dist/engine.js +125 -78
  61. package/dist/engine.js.map +1 -1
  62. package/dist/index.d.ts +5 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +5 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/profile-transfer.d.ts +61 -0
  67. package/dist/profile-transfer.d.ts.map +1 -0
  68. package/dist/profile-transfer.js +245 -0
  69. package/dist/profile-transfer.js.map +1 -0
  70. package/dist/snapshot-signerless-transactions.d.ts.map +1 -1
  71. package/dist/snapshot-signerless-transactions.js +2 -5
  72. package/dist/snapshot-signerless-transactions.js.map +1 -1
  73. package/dist/snapshot-signerless.d.ts.map +1 -1
  74. package/dist/snapshot-signerless.js +5 -3
  75. package/dist/snapshot-signerless.js.map +1 -1
  76. package/dist/token-authorization.d.ts +43 -0
  77. package/dist/token-authorization.d.ts.map +1 -0
  78. package/dist/token-authorization.js +88 -0
  79. package/dist/token-authorization.js.map +1 -0
  80. package/dist/token-profiles.d.ts +83 -0
  81. package/dist/token-profiles.d.ts.map +1 -0
  82. package/dist/token-profiles.generated.d.ts +4 -0
  83. package/dist/token-profiles.generated.d.ts.map +1 -0
  84. package/dist/token-profiles.generated.js +111 -0
  85. package/dist/token-profiles.generated.js.map +1 -0
  86. package/dist/token-profiles.js +245 -0
  87. package/dist/token-profiles.js.map +1 -0
  88. package/dist/transfer-payload.d.ts +10 -0
  89. package/dist/transfer-payload.d.ts.map +1 -0
  90. package/dist/transfer-payload.js +34 -0
  91. package/dist/transfer-payload.js.map +1 -0
  92. package/dist/wrapper-operation.d.ts +1 -0
  93. package/dist/wrapper-operation.d.ts.map +1 -1
  94. package/dist/wrapper-operation.js +1 -0
  95. package/dist/wrapper-operation.js.map +1 -1
  96. package/docs/COMPATIBILITY.json +24 -33
  97. package/docs/LAST_CALL.md +50 -0
  98. package/examples/README.md +3 -0
  99. package/examples/backend/cli.ts +2 -1
  100. package/examples/frontend/main.ts +2 -1
  101. package/examples/shared/build.ts +3 -0
  102. package/examples/shared/offline-profile.ts +27 -0
  103. package/examples/shared/recipe-fixtures.ts +3 -2
  104. package/examples/test/browser.ts +2 -2
  105. package/examples/test/build.test.ts +21 -8
  106. package/examples/test/sources.test.ts +3 -2
  107. package/package.json +2 -2
  108. package/src/action-source-resolver.ts +22 -4
  109. package/src/artifacts.ts +12 -10
  110. package/src/engine.ts +208 -105
  111. package/src/index.ts +7 -0
  112. package/src/profile-transfer.ts +425 -0
  113. package/src/snapshot-signerless-transactions.ts +7 -16
  114. package/src/snapshot-signerless.ts +5 -3
  115. package/src/token-authorization.ts +135 -0
  116. package/src/token-profiles.generated.ts +112 -0
  117. package/src/token-profiles.ts +456 -0
  118. package/src/transfer-payload.ts +45 -0
  119. package/src/wrapper-operation.ts +2 -0
@@ -0,0 +1,425 @@
1
+ import {
2
+ authenticateTokenArtifact,
3
+ requireTokenCapability,
4
+ resolveAuthenticatedTokenProfile,
5
+ type AuthenticatedTokenProfileRegistry,
6
+ } from "./token-profiles.js";
7
+ import {
8
+ buildKcc20ScriptForState,
9
+ encodeKcc20StateRecordArrays,
10
+ } from "./abi.js";
11
+ import {
12
+ planTokenOwnerWitness,
13
+ buildTokenBorrowWitness,
14
+ type TokenSigningCapabilities,
15
+ } from "./token-authorization.js";
16
+
17
+ export interface ProfileTokenState {
18
+ amount: bigint;
19
+ owner: Uint8Array;
20
+ ownerScheme: number;
21
+ borrowScheme: number;
22
+ borrowGuard: Uint8Array;
23
+ extensionCommitment: Uint8Array;
24
+ }
25
+ export interface ProfileTransferInput {
26
+ /** A current, authenticated RPC UTXO. This API does not fetch chain state. */
27
+ entry: any;
28
+ state: ProfileTokenState;
29
+ publicKey?: string;
30
+ authorityInputIndex?: number;
31
+ }
32
+ export interface ProfileAuthorityInput {
33
+ entry: any;
34
+ revealScript: Uint8Array;
35
+ /** Exact authority witness, supplied by its reviewed authority adapter. */
36
+ arguments: Array<
37
+ | { type: "data"; hex: string }
38
+ | { type: "i64"; value: string }
39
+ | { type: "signature"; prefixHex?: string }
40
+ >;
41
+ signatureAlgorithm?: "schnorr" | "ecdsa";
42
+ }
43
+ export interface ProfileTransferPlan {
44
+ /** Host-authenticated registry input; never populate this from transaction request data. */
45
+ tokenProfileRegistry: AuthenticatedTokenProfileRegistry;
46
+ profileId: string;
47
+ artifact: unknown;
48
+ tokenInputs: ProfileTransferInput[];
49
+ nextStates: ProfileTokenState[];
50
+ authorities?: ProfileAuthorityInput[];
51
+ fundingEntry: any;
52
+ fundingAddress: string;
53
+ feeSompi: bigint;
54
+ network: string;
55
+ tokenOutputSompi?: bigint;
56
+ capabilities: TokenSigningCapabilities;
57
+ mode: "owner" | "borrow";
58
+ borrow?: {
59
+ nextGuard?: string;
60
+ oneTimePublicKey?: string;
61
+ signingPublicKey?: string;
62
+ };
63
+ }
64
+ const hex = (v: Uint8Array) =>
65
+ Array.from(v, (b) => b.toString(16).padStart(2, "0")).join("");
66
+ const amount = (e: any) =>
67
+ BigInt(e.amount ?? e.entry?.amount ?? e.utxoEntry?.amount);
68
+ const spk = (e: any) =>
69
+ e.scriptPublicKey ?? e.entry?.scriptPublicKey ?? e.utxoEntry?.scriptPublicKey;
70
+ const cid = (e: any): string | undefined => {
71
+ const c = e.covenantId ?? e.entry?.covenantId ?? e.utxoEntry?.covenantId;
72
+ return c == null ? undefined : String(c).toLowerCase();
73
+ };
74
+ const outpoint = (e: any) => e.outpoint ?? e.entry?.outpoint;
75
+ const assert = (ok: unknown, message: string) => {
76
+ if (!ok) throw Error(message);
77
+ };
78
+
79
+ /**
80
+ * Deterministic standard transfer construction for explicit authority/borrow flows.
81
+ * Callers authenticate RPC freshness and canonical acceptance before signing.
82
+ * Authority continuations preserve their script, value and covenant identity.
83
+ */
84
+ export function buildProfileTransferTransaction(
85
+ kw: any,
86
+ plan: ProfileTransferPlan,
87
+ ): any {
88
+ const profile = resolveAuthenticatedTokenProfile(
89
+ plan.tokenProfileRegistry,
90
+ plan.profileId,
91
+ );
92
+ const { tokenInputs, nextStates } = plan;
93
+ requireTokenCapability(
94
+ profile,
95
+ plan.mode === "borrow" ? "borrow" : "transfer",
96
+ );
97
+ assert(
98
+ plan.mode === "owner" || plan.mode === "borrow",
99
+ "explicit owner or borrow mode is required",
100
+ );
101
+ const artifact = authenticateTokenArtifact(profile, plan.artifact);
102
+ assert(
103
+ tokenInputs.length > 0 && tokenInputs.length <= profile.maxTokenInputs,
104
+ "token input bound exceeded",
105
+ );
106
+ assert(
107
+ nextStates.length > 0 && nextStates.length <= profile.maxTokenOutputs,
108
+ "token output bound exceeded",
109
+ );
110
+ assert(
111
+ plan.feeSompi >= 0n && plan.feeSompi <= 50_000_000n,
112
+ "fee exceeds transaction limit",
113
+ );
114
+ const tokenId = cid(tokenInputs[0].entry);
115
+ assert(
116
+ tokenId && /^[0-9a-f]{64}$/.test(tokenId),
117
+ "native token identity is required",
118
+ );
119
+ const extension = hex(tokenInputs[0].state.extensionCommitment);
120
+ let total = 0n;
121
+ for (const input of tokenInputs) {
122
+ assert(cid(input.entry) === tokenId, "mixed native token identities");
123
+ assert(
124
+ input.state.amount >= 0n &&
125
+ hex(input.state.extensionCommitment) === extension,
126
+ "invalid input amount or extension",
127
+ );
128
+ total += input.state.amount;
129
+ const expected = kw.payToScriptHashScript(
130
+ buildKcc20ScriptForState(artifact, input.state),
131
+ );
132
+ assert(
133
+ kw.addressFromScriptPublicKey(expected, plan.network).toString() ===
134
+ kw
135
+ .addressFromScriptPublicKey(spk(input.entry), plan.network)
136
+ .toString(),
137
+ "token input script does not authenticate its state",
138
+ );
139
+ }
140
+ assert(total <= 0x7fffffffffffffffn, "transfer total exceeds integer limit");
141
+ for (const state of nextStates) {
142
+ assert(
143
+ state.amount >= 0n && hex(state.extensionCommitment) === extension,
144
+ "invalid output amount or extension",
145
+ );
146
+ assert(
147
+ profile.ownerSchemes.includes(state.ownerScheme) &&
148
+ [0, 1, 2, 3].includes(state.borrowScheme),
149
+ "unsupported output owner or borrow scheme",
150
+ );
151
+ }
152
+ assert(
153
+ nextStates.reduce((sum, s) => sum + s.amount, 0n) === total,
154
+ "token conservation failed",
155
+ );
156
+ const authorities = plan.authorities ?? [];
157
+ assert(
158
+ authorities.length <= 255 - tokenInputs.length,
159
+ "too many authority inputs",
160
+ );
161
+ const authorityEvidence = authorities.map((a, i) => {
162
+ assert(
163
+ a.revealScript.length > 0 && a.revealScript.length <= 100_000,
164
+ "invalid authority reveal size",
165
+ );
166
+ const expected = kw.payToScriptHashScript(a.revealScript);
167
+ assert(
168
+ kw.addressFromScriptPublicKey(expected, plan.network).toString() ===
169
+ kw.addressFromScriptPublicKey(spk(a.entry), plan.network).toString(),
170
+ "authority reveal does not match its RPC output",
171
+ );
172
+ assert(
173
+ cid(a.entry) !== tokenId,
174
+ "token family inputs must be listed as token inputs",
175
+ );
176
+ assert(a.arguments.length <= 128, "authority witness exceeds limit");
177
+ for (const arg of a.arguments) {
178
+ assert(
179
+ ["data", "i64", "signature"].includes(arg.type),
180
+ "unsupported authority witness argument",
181
+ );
182
+ if (arg.type === "data")
183
+ assert(
184
+ /^(?:[a-f0-9]{2})*$/i.test(arg.hex) && arg.hex.length <= 200_000,
185
+ "invalid authority witness data",
186
+ );
187
+ if (arg.type === "i64")
188
+ assert(
189
+ /^-?\d+$/.test(arg.value) &&
190
+ BigInt(arg.value) >= -0x7fffffffffffffffn &&
191
+ BigInt(arg.value) <= 0x7fffffffffffffffn,
192
+ "invalid authority integer",
193
+ );
194
+ if (arg.type === "signature") {
195
+ assert(
196
+ a.signatureAlgorithm && plan.capabilities[a.signatureAlgorithm],
197
+ "wallet cannot sign the authority scheme",
198
+ );
199
+ assert(
200
+ /^(?:[a-f0-9]{2})*$/i.test(arg.prefixHex ?? ""),
201
+ "invalid authority signature prefix",
202
+ );
203
+ }
204
+ }
205
+ return {
206
+ index: tokenInputs.length + i,
207
+ scriptPublicKeyHex: String(expected.script),
208
+ covenantId: cid(a.entry),
209
+ };
210
+ });
211
+ const entries = [
212
+ ...tokenInputs.map((i) => i.entry),
213
+ ...authorities.map((i) => i.entry),
214
+ plan.fundingEntry,
215
+ ];
216
+ const keys = entries.map((e) => {
217
+ const o = outpoint(e);
218
+ assert(
219
+ o &&
220
+ /^[0-9a-f]{64}$/i.test(String(o.transactionId)) &&
221
+ Number.isInteger(o.index) &&
222
+ o.index >= 0,
223
+ "invalid source outpoint",
224
+ );
225
+ return `${String(o.transactionId).toLowerCase()}:${o.index}`;
226
+ });
227
+ assert(new Set(keys).size === keys.length, "duplicate transaction input");
228
+ assert(!cid(plan.fundingEntry), "funding input must not spend a covenant");
229
+ assert(
230
+ kw
231
+ .addressFromScriptPublicKey(spk(plan.fundingEntry), plan.network)
232
+ .toString() === plan.fundingAddress,
233
+ "funding input does not belong to wallet",
234
+ );
235
+ const scripts: any[] = [];
236
+ for (let index = 0; index < tokenInputs.length; index++) {
237
+ const input = tokenInputs[index];
238
+ let authorization: any;
239
+ if (index === 0 && plan.mode === "borrow") {
240
+ const previous = input.state,
241
+ next = nextStates[0],
242
+ borrow = plan.borrow ?? {};
243
+ assert(previous.borrowScheme !== 0, "borrowing is disabled");
244
+ assert(
245
+ hex(previous.owner) === hex(next.owner) &&
246
+ previous.ownerScheme === next.ownerScheme &&
247
+ previous.borrowScheme === next.borrowScheme,
248
+ "borrow successor changes ownership or scheme",
249
+ );
250
+ const delta = next.amount - previous.amount;
251
+ let threshold = 0n;
252
+ if (previous.borrowScheme === 1) {
253
+ for (let i = 7; i >= 0; i--)
254
+ threshold = (threshold << 8n) | BigInt(previous.borrowGuard[i]);
255
+ assert(
256
+ (threshold & (1n << 63n)) === 0n || threshold === 1n << 63n,
257
+ "negative thresholds are not supported by this adapter",
258
+ );
259
+ if (threshold === 1n << 63n) threshold = 0n;
260
+ }
261
+ assert(delta > threshold, "borrow increase does not exceed threshold");
262
+ if (previous.borrowScheme !== 3)
263
+ assert(
264
+ hex(previous.borrowGuard) === hex(next.borrowGuard),
265
+ "borrow guard must be preserved",
266
+ );
267
+ else
268
+ assert(
269
+ hex(next.borrowGuard) === borrow.nextGuard,
270
+ "borrow successor guard differs from committed next link",
271
+ );
272
+ if (previous.borrowScheme !== 1) {
273
+ assert(
274
+ plan.capabilities.schnorr,
275
+ "wallet cannot sign borrowed receive",
276
+ );
277
+ assert(
278
+ borrow.signingPublicKey ===
279
+ (previous.borrowScheme === 2
280
+ ? hex(previous.borrowGuard)
281
+ : borrow.oneTimePublicKey),
282
+ "borrow signing key does not match authorization",
283
+ );
284
+ }
285
+ // Validate the complete witness shape/hash link before leaving a signature slot.
286
+ const witness = buildTokenBorrowWitness({
287
+ profile,
288
+ borrowScheme: previous.borrowScheme,
289
+ currentGuard: hex(previous.borrowGuard),
290
+ ...(previous.borrowScheme === 1
291
+ ? {}
292
+ : {
293
+ signature: "00".repeat(64) + "01",
294
+ nextGuard: borrow.nextGuard,
295
+ oneTimePublicKey: borrow.oneTimePublicKey,
296
+ }),
297
+ });
298
+ authorization =
299
+ previous.borrowScheme === 1
300
+ ? { argument: { type: "data", hex: hex(witness) } }
301
+ : {
302
+ argument: {
303
+ type: "signature",
304
+ prefixHex: hex(witness.slice(0, -65)),
305
+ },
306
+ signatureAlgorithm: "schnorr",
307
+ };
308
+ } else {
309
+ authorization = planTokenOwnerWitness({
310
+ profile,
311
+ owner: hex(input.state.owner),
312
+ ownerScheme: input.state.ownerScheme,
313
+ publicKey: input.publicKey,
314
+ capabilities: plan.capabilities,
315
+ role: index === 0 ? "leader" : "delegator",
316
+ authorityInputIndex: input.authorityInputIndex,
317
+ authorityInputs: authorityEvidence,
318
+ });
319
+ }
320
+ scripts.push({
321
+ inputIndex: index,
322
+ scriptHex: hex(buildKcc20ScriptForState(artifact, input.state)),
323
+ signType: 1,
324
+ ...(authorization.signatureAlgorithm
325
+ ? { signatureAlgorithm: authorization.signatureAlgorithm }
326
+ : {}),
327
+ signatureScript: {
328
+ mode: "ordered-args",
329
+ args: [
330
+ ...(index === 0
331
+ ? encodeKcc20StateRecordArrays(nextStates).map((v: Uint8Array) => ({
332
+ type: "data",
333
+ hex: hex(v),
334
+ }))
335
+ : []),
336
+ authorization.argument,
337
+ {
338
+ type: "data",
339
+ hex: index === 0 ? profile.transferTag : profile.delegatorTag,
340
+ },
341
+ ],
342
+ },
343
+ });
344
+ }
345
+ for (let i = 0; i < authorities.length; i++)
346
+ scripts.push({
347
+ inputIndex: tokenInputs.length + i,
348
+ scriptHex: hex(authorities[i].revealScript),
349
+ signType: 1,
350
+ signatureAlgorithm: authorities[i].signatureAlgorithm,
351
+ signatureScript: { mode: "ordered-args", args: authorities[i].arguments },
352
+ });
353
+ const tokenValue = plan.tokenOutputSompi ?? 50_000_000n;
354
+ assert(tokenValue > 0n, "token output KAS must be positive");
355
+ if (plan.mode === "borrow")
356
+ assert(
357
+ tokenValue >= amount(tokenInputs[0].entry),
358
+ "borrow successor must preserve its KAS value",
359
+ );
360
+ const outputs = nextStates.map(
361
+ (state) =>
362
+ new kw.TransactionOutput(
363
+ tokenValue,
364
+ kw.payToScriptHashScript(buildKcc20ScriptForState(artifact, state)),
365
+ new kw.CovenantBinding(0, new kw.Hash(tokenId)),
366
+ ),
367
+ );
368
+ for (let i = 0; i < authorities.length; i++) {
369
+ const entry = authorities[i].entry,
370
+ identity = cid(entry);
371
+ outputs.push(
372
+ new kw.TransactionOutput(
373
+ amount(entry),
374
+ spk(entry),
375
+ identity
376
+ ? new kw.CovenantBinding(
377
+ tokenInputs.length + i,
378
+ new kw.Hash(identity),
379
+ )
380
+ : undefined,
381
+ ),
382
+ );
383
+ }
384
+ const change =
385
+ entries.reduce((sum, e) => sum + amount(e), 0n) -
386
+ BigInt(nextStates.length) * tokenValue -
387
+ authorities.reduce((sum, a) => sum + amount(a.entry), 0n) -
388
+ plan.feeSompi;
389
+ assert(change > 10_000n, "insufficient funding or change");
390
+ outputs.push(
391
+ new kw.TransactionOutput(
392
+ change,
393
+ kw.payToAddressScript(plan.fundingAddress),
394
+ ),
395
+ );
396
+ const transaction = new kw.Transaction({
397
+ version: 1,
398
+ lockTime: 0n,
399
+ subnetworkId: "00".repeat(20),
400
+ gas: 0n,
401
+ payload: "",
402
+ inputs: entries.map((e) => ({
403
+ previousOutpoint: outpoint(e),
404
+ utxo: e,
405
+ sequence: 0n,
406
+ sigOpCount: 0,
407
+ computeBudget: 1000,
408
+ })),
409
+ outputs,
410
+ });
411
+ transaction.inputs[entries.length - 1].computeBudget = 30;
412
+ return {
413
+ schema: "kcc20-in-process-pskt-builder-output/v1",
414
+ psktTransactionJson: transaction.serializeToSafeJSON(),
415
+ scripts,
416
+ signInputs: [{ index: entries.length - 1, sighashType: 1 }],
417
+ submitTransactionSupported: true,
418
+ metadata: {
419
+ tokenProfileId: profile.id,
420
+ covenantId: tokenId,
421
+ mode: plan.mode,
422
+ fundingChangeOutputIndex: outputs.length - 1,
423
+ },
424
+ };
425
+ }
@@ -61,10 +61,7 @@ export function buildSnapshotKcc20DeploymentPayload({
61
61
  if (normalizedProtocolFeeBps > 10_000n) {
62
62
  throw new Error("snapshot token protocol fee bps exceeds 10000");
63
63
  }
64
- const normalizedMigrationId = hex32(
65
- migrationId,
66
- "snapshot migration id",
67
- );
64
+ const normalizedMigrationId = hex32(migrationId, "snapshot migration id");
68
65
  const normalizedManifestCommitment = hex32(
69
66
  manifestCommitment,
70
67
  "snapshot manifest commitment",
@@ -132,11 +129,7 @@ export function buildSnapshotKcc20DeploymentPayload({
132
129
  claimArg("displayScale", "u64", normalizedDisplayScale.toString()),
133
130
  claimArg("mintPriceSompi", "u64", "0"),
134
131
  claimArg("treasury", "byte[32]", normalizedTreasury),
135
- claimArg(
136
- "protocolFeeRecipient",
137
- "byte[32]",
138
- normalizedFeeRecipient,
139
- ),
132
+ claimArg("protocolFeeRecipient", "byte[32]", normalizedFeeRecipient),
140
133
  claimArg("protocolFeeBps", "u64", normalizedProtocolFeeBps.toString()),
141
134
  claimArg("ticker", "string", normalizedTicker),
142
135
  claimArg("name", "string", normalizedName),
@@ -780,9 +773,7 @@ function fixedText32(value, label) {
780
773
  }
781
774
 
782
775
  function bytesHex(value) {
783
- return [...value]
784
- .map((byte) => byte.toString(16).padStart(2, "0"))
785
- .join("");
776
+ return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
786
777
  }
787
778
 
788
779
  function transactionOf(kw, inputs, outputs, lockTime, payload) {
@@ -968,10 +959,10 @@ function setP2shSignatureScript(
968
959
  revealScript,
969
960
  prefix,
970
961
  ) {
971
- transaction.inputs[inputIndex].signatureScript =
972
- kw.ScriptBuilder.fromScript(
973
- revealScript,
974
- ).encodePayToScriptHashSignatureScript(prefix);
962
+ transaction.inputs[inputIndex].signatureScript = kw.ScriptBuilder.fromScript(
963
+ revealScript,
964
+ { flags: { covenantsEnabled: true } },
965
+ ).encodePayToScriptHashSignatureScript(prefix);
975
966
  }
976
967
 
977
968
  function unsignedEnvelope(kw, transaction, intent, signInputs) {
@@ -363,7 +363,9 @@ export function buildSnapshotStartSignatureScripts(
363
363
  const reserves = plan.reserves.map(normalizeKcc20State);
364
364
  const kcc20Template = templateParts(kcc20Artifact);
365
365
  const controllerTemplate = templateParts(controllerArtifact);
366
- const bootstrapBuilder = new kw.ScriptBuilder();
366
+ const bootstrapBuilder = new kw.ScriptBuilder({
367
+ flags: { covenantsEnabled: true },
368
+ });
367
369
  for (const field of encodeControllerStateRecordArrays(controllers))
368
370
  bootstrapBuilder.addData(field);
369
371
  for (const field of encodeKcc20StateRecordArrays(reserves))
@@ -374,7 +376,7 @@ export function buildSnapshotStartSignatureScripts(
374
376
  bootstrapBuilder.addData(controllerTemplate.suffix);
375
377
  bootstrapBuilder.addData(dispatchTagFor(bootstrapArtifact, "start"));
376
378
  const controllerSignatureScripts = controllers.map((state, index) => {
377
- const builder = new kw.ScriptBuilder();
379
+ const builder = new kw.ScriptBuilder({ flags: { covenantsEnabled: true } });
378
380
  addControllerState(builder, state);
379
381
  builder.addI64(BigInt(bootstrapInputIndex));
380
382
  builder.addI64(BigInt(index));
@@ -591,7 +593,7 @@ export function buildClaimSignatureScripts(
591
593
  requireControllerArtifact(controllerArtifact);
592
594
  requireKcc20Artifact(kcc20Artifact);
593
595
  requireOwnerWitness(claim.ownerScheme, claim.ownerWitness);
594
- const builder = new kw.ScriptBuilder();
596
+ const builder = new kw.ScriptBuilder({ flags: { covenantsEnabled: true } });
595
597
  builder.addI64(BigInt(claim.globalIndex));
596
598
  builder.addI64(BigInt(claim.shardLeafIndex));
597
599
  addByte(builder, claim.ownerScheme);
@@ -0,0 +1,135 @@
1
+ import { blake3 } from "@noble/hashes/blake3";
2
+ import { requireTokenCapability, type TokenProfile } from "./token-profiles.js";
3
+
4
+ export interface TokenSigningCapabilities {
5
+ schnorr: boolean;
6
+ ecdsa: boolean;
7
+ }
8
+ export type TokenWitnessArgument =
9
+ | { type: "data"; hex: string }
10
+ | { type: "signature"; prefixHex: string };
11
+ export interface TokenAuthorityInput {
12
+ index: number;
13
+ scriptPublicKeyHex: string;
14
+ covenantId?: string;
15
+ }
16
+ const hex = (v: Uint8Array) =>
17
+ Array.from(v, (b) => b.toString(16).padStart(2, "0")).join("");
18
+ const bytes = (v: string, size: number, label: string) => {
19
+ if (!new RegExp(`^[0-9a-f]{${size * 2}}$`, "i").test(v))
20
+ throw new Error(`invalid ${label}`);
21
+ return Uint8Array.from(v.match(/../g)!, (b) => parseInt(b, 16));
22
+ };
23
+ export function tokenPublicKeyHash(publicKey: Uint8Array): Uint8Array {
24
+ if (![32, 33].includes(publicKey.length))
25
+ throw new Error("invalid token public key length");
26
+ return blake3(publicKey);
27
+ }
28
+ /** Authority inputs must come from the authenticated transaction UTXOs, never an uploaded artifact. */
29
+ export function planTokenOwnerWitness(input: {
30
+ profile: TokenProfile;
31
+ owner: string;
32
+ ownerScheme: number;
33
+ publicKey?: string;
34
+ capabilities: TokenSigningCapabilities;
35
+ authorityInputIndex?: number;
36
+ authorityInputs?: readonly TokenAuthorityInput[];
37
+ role: "leader" | "delegator";
38
+ }): {
39
+ argument: TokenWitnessArgument;
40
+ signatureAlgorithm?: "schnorr" | "ecdsa";
41
+ authorityInputIndex?: number;
42
+ } {
43
+ const { profile, ownerScheme } = input;
44
+ requireTokenCapability(profile, "transfer");
45
+ bytes(input.owner, 32, "token owner");
46
+ if (!profile.ownerSchemes.includes(ownerScheme))
47
+ throw new Error("token profile does not support this owner scheme");
48
+ const prefix = input.role === "leader" ? "00" : "";
49
+ if (ownerScheme <= 2) {
50
+ const algorithm = ownerScheme === 2 ? "ecdsa" : "schnorr";
51
+ if (!input.capabilities[algorithm])
52
+ throw new Error(`wallet does not support ${algorithm} token signatures`);
53
+ const key = bytes(
54
+ input.publicKey ?? "",
55
+ ownerScheme === 2 ? 33 : 32,
56
+ "wallet public key",
57
+ );
58
+ if (ownerScheme === 2 && key[0] !== 2 && key[0] !== 3)
59
+ throw new Error("ECDSA key must be compressed");
60
+ const expected =
61
+ ownerScheme === 0 ? hex(key) : hex(tokenPublicKeyHash(key));
62
+ if (expected !== input.owner.toLowerCase())
63
+ throw new Error("wallet public key does not own the token output");
64
+ return {
65
+ argument: {
66
+ type: "signature",
67
+ prefixHex: prefix + (ownerScheme === 0 ? "" : hex(key)),
68
+ },
69
+ signatureAlgorithm: algorithm,
70
+ };
71
+ }
72
+ const index = input.authorityInputIndex;
73
+ if (!Number.isSafeInteger(index) || index! < 0 || index! > 255)
74
+ throw new Error("explicit authority input index is required");
75
+ const matches = (input.authorityInputs ?? []).filter(
76
+ (a) => a.index === index,
77
+ );
78
+ if (matches.length !== 1)
79
+ throw new Error("authority input is missing or ambiguous");
80
+ const authority = matches[0];
81
+ if (
82
+ ownerScheme === 3 &&
83
+ authority.scriptPublicKeyHex.toLowerCase() !==
84
+ `aa20${input.owner.toLowerCase()}87`
85
+ )
86
+ throw new Error("authority input does not match token P2SH owner");
87
+ if (
88
+ ownerScheme === 4 &&
89
+ authority.covenantId?.toLowerCase() !== input.owner.toLowerCase()
90
+ )
91
+ throw new Error("authority input does not match token covenant owner");
92
+ return {
93
+ argument: {
94
+ type: "data",
95
+ hex:
96
+ prefix +
97
+ (ownerScheme === 3 ? index!.toString(16).padStart(2, "0") : ""),
98
+ },
99
+ authorityInputIndex: index,
100
+ };
101
+ }
102
+ /** Borrowing is an explicit mode. This function never converts an ordinary send into a borrow. */
103
+ export function buildTokenBorrowWitness(input: {
104
+ profile: TokenProfile;
105
+ borrowScheme: number;
106
+ currentGuard: string;
107
+ signature?: string;
108
+ nextGuard?: string;
109
+ oneTimePublicKey?: string;
110
+ }): Uint8Array {
111
+ requireTokenCapability(input.profile, "borrow");
112
+ const current = bytes(input.currentGuard, 32, "borrow guard");
113
+ if (input.borrowScheme === 1) {
114
+ if (
115
+ input.signature !== undefined ||
116
+ input.nextGuard !== undefined ||
117
+ input.oneTimePublicKey !== undefined
118
+ )
119
+ throw new Error("threshold borrowing requires an empty authorization");
120
+ return Uint8Array.of(1);
121
+ }
122
+ const sig = bytes(input.signature ?? "", 65, "borrow signature");
123
+ if (input.borrowScheme === 2) {
124
+ if (input.nextGuard !== undefined || input.oneTimePublicKey !== undefined)
125
+ throw new Error("Schnorr borrowing does not rotate its guard");
126
+ return Uint8Array.from([1, ...sig]);
127
+ }
128
+ if (input.borrowScheme !== 3)
129
+ throw new Error("unsupported or disabled borrow scheme");
130
+ const guard = bytes(input.nextGuard ?? "", 32, "next borrow guard");
131
+ const key = bytes(input.oneTimePublicKey ?? "", 32, "one-time public key");
132
+ if (hex(blake3(Uint8Array.from([...guard, ...key]))) !== hex(current))
133
+ throw new Error("hash-chain link does not match current guard");
134
+ return Uint8Array.from([1, ...guard, ...key, ...sig]);
135
+ }