@kaspacom/kcc20-tx-builder 0.4.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 (139) hide show
  1. package/CHANGELOG.md +12 -1
  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-operation.d.ts +0 -1
  40. package/dist/cjs/snapshot-operation.js +1 -1126
  41. package/dist/cjs/snapshot-operation.js.map +1 -1
  42. package/dist/cjs/snapshot-signerless-transactions.d.ts +1 -1
  43. package/dist/cjs/snapshot-signerless-transactions.js +3 -6
  44. package/dist/cjs/snapshot-signerless-transactions.js.map +1 -1
  45. package/dist/cjs/snapshot-signerless.js +5 -3
  46. package/dist/cjs/snapshot-signerless.js.map +1 -1
  47. package/dist/cjs/snapshot-verification.d.ts +1 -1
  48. package/dist/cjs/snapshot-verification.js +16 -12
  49. package/dist/cjs/snapshot-verification.js.map +1 -1
  50. package/dist/cjs/token-authorization.d.ts +42 -0
  51. package/dist/cjs/token-authorization.js +93 -0
  52. package/dist/cjs/token-authorization.js.map +1 -0
  53. package/dist/cjs/token-profiles.d.ts +82 -0
  54. package/dist/cjs/token-profiles.generated.d.ts +3 -0
  55. package/dist/cjs/token-profiles.generated.js +114 -0
  56. package/dist/cjs/token-profiles.generated.js.map +1 -0
  57. package/dist/cjs/token-profiles.js +257 -0
  58. package/dist/cjs/token-profiles.js.map +1 -0
  59. package/dist/cjs/transfer-payload.d.ts +9 -0
  60. package/dist/cjs/transfer-payload.js +37 -0
  61. package/dist/cjs/transfer-payload.js.map +1 -0
  62. package/dist/cjs/wrapper-operation.d.ts +1 -0
  63. package/dist/cjs/wrapper-operation.js +1 -0
  64. package/dist/cjs/wrapper-operation.js.map +1 -1
  65. package/dist/engine.d.ts +10 -0
  66. package/dist/engine.d.ts.map +1 -1
  67. package/dist/engine.js +125 -78
  68. package/dist/engine.js.map +1 -1
  69. package/dist/index.d.ts +5 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +5 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/profile-transfer.d.ts +61 -0
  74. package/dist/profile-transfer.d.ts.map +1 -0
  75. package/dist/profile-transfer.js +245 -0
  76. package/dist/profile-transfer.js.map +1 -0
  77. package/dist/snapshot-operation.d.ts +0 -1
  78. package/dist/snapshot-operation.d.ts.map +1 -1
  79. package/dist/snapshot-operation.js +0 -1125
  80. package/dist/snapshot-operation.js.map +1 -1
  81. package/dist/snapshot-signerless-transactions.d.ts +1 -1
  82. package/dist/snapshot-signerless-transactions.d.ts.map +1 -1
  83. package/dist/snapshot-signerless-transactions.js +3 -6
  84. package/dist/snapshot-signerless-transactions.js.map +1 -1
  85. package/dist/snapshot-signerless.d.ts.map +1 -1
  86. package/dist/snapshot-signerless.js +5 -3
  87. package/dist/snapshot-signerless.js.map +1 -1
  88. package/dist/snapshot-verification.d.ts +1 -1
  89. package/dist/snapshot-verification.d.ts.map +1 -1
  90. package/dist/snapshot-verification.js +16 -12
  91. package/dist/snapshot-verification.js.map +1 -1
  92. package/dist/token-authorization.d.ts +43 -0
  93. package/dist/token-authorization.d.ts.map +1 -0
  94. package/dist/token-authorization.js +88 -0
  95. package/dist/token-authorization.js.map +1 -0
  96. package/dist/token-profiles.d.ts +83 -0
  97. package/dist/token-profiles.d.ts.map +1 -0
  98. package/dist/token-profiles.generated.d.ts +4 -0
  99. package/dist/token-profiles.generated.d.ts.map +1 -0
  100. package/dist/token-profiles.generated.js +111 -0
  101. package/dist/token-profiles.generated.js.map +1 -0
  102. package/dist/token-profiles.js +245 -0
  103. package/dist/token-profiles.js.map +1 -0
  104. package/dist/transfer-payload.d.ts +10 -0
  105. package/dist/transfer-payload.d.ts.map +1 -0
  106. package/dist/transfer-payload.js +34 -0
  107. package/dist/transfer-payload.js.map +1 -0
  108. package/dist/wrapper-operation.d.ts +1 -0
  109. package/dist/wrapper-operation.d.ts.map +1 -1
  110. package/dist/wrapper-operation.js +1 -0
  111. package/dist/wrapper-operation.js.map +1 -1
  112. package/docs/API.md +1 -1
  113. package/docs/COMPATIBILITY.json +24 -33
  114. package/docs/INTEGRATION.md +2 -2
  115. package/docs/LAST_CALL.md +50 -0
  116. package/examples/README.md +5 -2
  117. package/examples/backend/cli.ts +2 -1
  118. package/examples/frontend/main.ts +2 -1
  119. package/examples/shared/build.ts +3 -0
  120. package/examples/shared/offline-profile.ts +27 -0
  121. package/examples/shared/recipe-fixtures.ts +3 -2
  122. package/examples/test/browser.ts +2 -2
  123. package/examples/test/build.test.ts +21 -8
  124. package/examples/test/sources.test.ts +3 -2
  125. package/package.json +2 -2
  126. package/src/action-source-resolver.ts +22 -4
  127. package/src/artifacts.ts +12 -10
  128. package/src/engine.ts +208 -105
  129. package/src/index.ts +7 -0
  130. package/src/profile-transfer.ts +425 -0
  131. package/src/snapshot-operation.ts +59 -1731
  132. package/src/snapshot-signerless-transactions.ts +8 -17
  133. package/src/snapshot-signerless.ts +5 -3
  134. package/src/snapshot-verification.ts +17 -13
  135. package/src/token-authorization.ts +135 -0
  136. package/src/token-profiles.generated.ts +112 -0
  137. package/src/token-profiles.ts +456 -0
  138. package/src/transfer-payload.ts +45 -0
  139. package/src/wrapper-operation.ts +2 -0
@@ -0,0 +1,456 @@
1
+ import { sha256 } from "@noble/hashes/sha256";
2
+ import {
3
+ buildKcc20ScriptForState,
4
+ normalizeSilverScriptArtifact,
5
+ templateHash,
6
+ dispatchTagFor,
7
+ } from "./abi.js";
8
+
9
+ export const KCC20_STANDARD_REVISION =
10
+ "3fbec524abfbc20e87652eb938db218f8c17db17";
11
+ export type TokenOperation =
12
+ | "transfer"
13
+ | "borrow"
14
+ | "mint"
15
+ | "burn"
16
+ | "wrap"
17
+ | "unwrap"
18
+ | "trade";
19
+ export interface TokenWrapperProfile {
20
+ fingerprint: string;
21
+ artifactKey: string;
22
+ artifactSha256: string;
23
+ artifactCanonicalSha256: string;
24
+ scriptSha256: string;
25
+ scriptLength: number;
26
+ templateHash: string;
27
+ stateLayout: { start: number; len: number };
28
+ tokenTemplateHash: string;
29
+ }
30
+ export interface TokenProfile {
31
+ id: string;
32
+ artifactKey: string;
33
+ artifactSha256: string;
34
+ artifactCanonicalSha256: string;
35
+ scriptSha256: string;
36
+ templateHash: string;
37
+ scriptLength: number;
38
+ stateLayout: { start: number; len: number };
39
+ standardRevision: string;
40
+ transferTag: string;
41
+ delegatorTag: string;
42
+ transferEntrypoint: string;
43
+ delegatorEntrypoint: string;
44
+ ownerSchemes: number[];
45
+ maxTokenInputs: number;
46
+ maxTokenOutputs: number;
47
+ operations: TokenOperation[];
48
+ fungibility: "single-holder-class" | "unverified";
49
+ nativeExtensions: boolean;
50
+ wrapper?: TokenWrapperProfile;
51
+ /** Reviewed fixed display convention for programs without token metadata. */
52
+ displayDecimals?: number;
53
+ }
54
+ export interface TokenProfileRegistry {
55
+ schema: "kcc20-token-profiles/v2";
56
+ standardRevision: string;
57
+ profiles: TokenProfile[];
58
+ }
59
+
60
+ export interface AuthenticatedTokenProfileRegistry {
61
+ readonly manifest: TokenProfileRegistry;
62
+ readonly sha256: string;
63
+ }
64
+ const hex = (bytes: Uint8Array) =>
65
+ Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
66
+ const hashPattern = /^[0-9a-f]{64}$/;
67
+
68
+ /** JSON canonicalization is shared with the registry generator and Rust decoder. */
69
+ export function canonicalRegistryJson(value: unknown): string {
70
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
71
+ if (Array.isArray(value))
72
+ return `[${value.map(canonicalRegistryJson).join(",")}]`;
73
+ const object = value as Record<string, unknown>;
74
+ return `{${Object.keys(object)
75
+ .sort()
76
+ .map((k) => `${JSON.stringify(k)}:${canonicalRegistryJson(object[k])}`)
77
+ .join(",")}}`;
78
+ }
79
+
80
+ export function validateTokenProfileRegistry(
81
+ value: unknown,
82
+ expectedDigest: string,
83
+ ): TokenProfileRegistry {
84
+ if (
85
+ !hashPattern.test(expectedDigest) ||
86
+ hex(sha256(new TextEncoder().encode(canonicalRegistryJson(value)))) !==
87
+ expectedDigest
88
+ ) {
89
+ throw new Error("KCC20 profile registry digest mismatch");
90
+ }
91
+ const registry = value as TokenProfileRegistry;
92
+ if (
93
+ registry?.schema !== "kcc20-token-profiles/v2" ||
94
+ registry.standardRevision !== KCC20_STANDARD_REVISION ||
95
+ !Array.isArray(registry.profiles)
96
+ ) {
97
+ throw new Error("unsupported KCC20 profile registry");
98
+ }
99
+ const ids = new Set<string>(),
100
+ templates = new Set<string>();
101
+ for (const p of registry.profiles) {
102
+ if (
103
+ !p ||
104
+ !/^[a-z0-9][a-z0-9-]{1,95}$/.test(p.id) ||
105
+ ids.has(p.id) ||
106
+ templates.has(p.templateHash) ||
107
+ !/^[A-Za-z0-9.-]+\.json$/.test(p.artifactKey) ||
108
+ ![
109
+ p.artifactSha256,
110
+ p.artifactCanonicalSha256,
111
+ p.scriptSha256,
112
+ p.templateHash,
113
+ ].every((h) => hashPattern.test(h)) ||
114
+ p.standardRevision !== registry.standardRevision ||
115
+ !Number.isSafeInteger(p.scriptLength) ||
116
+ p.scriptLength <= 112 ||
117
+ !Number.isSafeInteger(p.stateLayout?.start) ||
118
+ p.stateLayout.start < 0 ||
119
+ p.stateLayout.len !== 112 ||
120
+ p.stateLayout.start + 112 > p.scriptLength ||
121
+ !/^[0-9a-f]{8}$/.test(p.transferTag) ||
122
+ !/^[0-9a-f]{8}$/.test(p.delegatorTag) ||
123
+ !/^[A-Za-z_][A-Za-z0-9_]{0,95}$/.test(p.transferEntrypoint) ||
124
+ !/^[A-Za-z_][A-Za-z0-9_]{0,95}$/.test(p.delegatorEntrypoint) ||
125
+ !Array.isArray(p.ownerSchemes) ||
126
+ !p.ownerSchemes.length ||
127
+ p.ownerSchemes.some((s) => !Number.isInteger(s) || s < 0 || s > 4) ||
128
+ new Set(p.ownerSchemes).size !== p.ownerSchemes.length ||
129
+ ![p.maxTokenInputs, p.maxTokenOutputs].every(
130
+ (n) => Number.isSafeInteger(n) && n > 0 && n <= 255,
131
+ ) ||
132
+ !Array.isArray(p.operations) ||
133
+ p.operations.some(
134
+ (op) =>
135
+ ![
136
+ "transfer",
137
+ "borrow",
138
+ "mint",
139
+ "burn",
140
+ "wrap",
141
+ "unwrap",
142
+ "trade",
143
+ ].includes(op),
144
+ ) ||
145
+ !["single-holder-class", "unverified"].includes(p.fungibility) ||
146
+ typeof p.nativeExtensions !== "boolean"
147
+ ) {
148
+ throw new Error("invalid or duplicate KCC20 token profile");
149
+ }
150
+ if (
151
+ p.displayDecimals !== undefined &&
152
+ (!Number.isInteger(p.displayDecimals) ||
153
+ p.displayDecimals < 0 ||
154
+ p.displayDecimals > 8)
155
+ )
156
+ throw new Error("unsupported profile display precision");
157
+ if (p.wrapper) {
158
+ const w = p.wrapper;
159
+ if (
160
+ !/^[A-Za-z0-9.-]+\.json$/.test(w.artifactKey) ||
161
+ w.tokenTemplateHash !== p.templateHash ||
162
+ ![
163
+ w.fingerprint,
164
+ w.artifactSha256,
165
+ w.artifactCanonicalSha256,
166
+ w.scriptSha256,
167
+ w.templateHash,
168
+ ].every((h) => hashPattern.test(h)) ||
169
+ !Number.isSafeInteger(w.scriptLength) ||
170
+ w.stateLayout?.start !== 1 ||
171
+ w.stateLayout.len !== 44 ||
172
+ w.scriptLength <= 45
173
+ )
174
+ throw new Error("invalid token wrapper binding");
175
+ }
176
+ ids.add(p.id);
177
+ templates.add(p.templateHash);
178
+ }
179
+ // Snapshot caller-owned data so changing an upload cannot change a bundled profile.
180
+ return deepFreeze(JSON.parse(JSON.stringify(registry)));
181
+ }
182
+
183
+ /** Resolve a profile only after authenticating the complete registry manifest. */
184
+ export function resolveAuthenticatedTokenProfile(
185
+ registry: AuthenticatedTokenProfileRegistry,
186
+ profileId: string,
187
+ ): TokenProfile {
188
+ const manifest = validateTokenProfileRegistry(
189
+ registry?.manifest,
190
+ registry?.sha256,
191
+ );
192
+ const profile = manifest.profiles.find((candidate) => candidate.id === profileId);
193
+ if (!profile) throw new Error("KCC20 token profile is unavailable");
194
+ return profile;
195
+ }
196
+ function deepFreeze<T>(value: T): T {
197
+ if (value && typeof value === "object") {
198
+ Object.values(value).forEach(deepFreeze);
199
+ Object.freeze(value);
200
+ }
201
+ return value;
202
+ }
203
+
204
+ export function authenticateTokenArtifact(
205
+ profile: TokenProfile,
206
+ raw: unknown,
207
+ ): ReturnType<typeof normalizeSilverScriptArtifact> {
208
+ if (
209
+ hex(sha256(new TextEncoder().encode(canonicalRegistryJson(raw)))) !==
210
+ profile.artifactCanonicalSha256
211
+ ) {
212
+ throw new Error(
213
+ "KCC20 artifact ABI or metadata does not match the bundled profile",
214
+ );
215
+ }
216
+ const a = normalizeSilverScriptArtifact(raw);
217
+ if (
218
+ !Array.isArray(a.script) ||
219
+ a.script.some((b: number) => !Number.isInteger(b) || b < 0 || b > 255) ||
220
+ a.script.length !== profile.scriptLength ||
221
+ a.state_layout?.start !== profile.stateLayout.start ||
222
+ a.state_layout?.len !== 112
223
+ ) {
224
+ throw new Error("KCC20 artifact layout does not match the bundled profile");
225
+ }
226
+ const bytes = Uint8Array.from(a.script);
227
+ if (
228
+ hex(sha256(bytes)) !== profile.scriptSha256 ||
229
+ hex(
230
+ templateHash(
231
+ bytes.slice(0, profile.stateLayout.start),
232
+ bytes.slice(profile.stateLayout.start + 112),
233
+ ),
234
+ ) !== profile.templateHash
235
+ ) {
236
+ throw new Error("KCC20 artifact script does not match the bundled profile");
237
+ }
238
+ for (const name of [
239
+ profile.transferEntrypoint,
240
+ profile.delegatorEntrypoint,
241
+ ]) {
242
+ if (!a.abi.some((entry: { name: string }) => entry.name === name))
243
+ throw new Error("KCC20 profile entrypoint missing from artifact");
244
+ }
245
+ if (
246
+ hex(dispatchTagFor(a, profile.transferEntrypoint)) !==
247
+ profile.transferTag ||
248
+ hex(dispatchTagFor(a, profile.delegatorEntrypoint)) !== profile.delegatorTag
249
+ )
250
+ throw new Error("profile dispatch tags do not match artifact");
251
+ const config = (raw as any).program_information;
252
+ if (
253
+ !config ||
254
+ config.max_token_inputs !== profile.maxTokenInputs ||
255
+ config.max_token_outputs !== profile.maxTokenOutputs ||
256
+ JSON.stringify(config.owner_schemes) !==
257
+ JSON.stringify(profile.ownerSchemes)
258
+ )
259
+ throw new Error("profile token configuration does not match artifact");
260
+ return a;
261
+ }
262
+
263
+ export function authenticateTokenWrapper(
264
+ profile: TokenProfile,
265
+ raw: unknown,
266
+ ): ReturnType<typeof normalizeSilverScriptArtifact> {
267
+ const w = profile.wrapper;
268
+ if (!w || w.tokenTemplateHash !== profile.templateHash)
269
+ throw new Error("token has no bundled wrapper binding");
270
+ if (
271
+ hex(sha256(new TextEncoder().encode(canonicalRegistryJson(raw)))) !==
272
+ w.artifactCanonicalSha256
273
+ )
274
+ throw new Error("wrapper artifact does not match the bundled profile");
275
+ const a = normalizeSilverScriptArtifact(raw),
276
+ bytes = Uint8Array.from(a.script);
277
+ if (
278
+ bytes.length !== w.scriptLength ||
279
+ a.state_layout.start !== w.stateLayout.start ||
280
+ a.state_layout.len !== w.stateLayout.len ||
281
+ hex(sha256(bytes)) !== w.scriptSha256 ||
282
+ hex(
283
+ templateHash(
284
+ bytes.slice(0, w.stateLayout.start),
285
+ bytes.slice(w.stateLayout.start + w.stateLayout.len),
286
+ ),
287
+ ) !== w.templateHash
288
+ )
289
+ throw new Error("wrapper template does not match the bundled profile");
290
+ return a;
291
+ }
292
+
293
+ export function requireTokenCapability(
294
+ profile: TokenProfile,
295
+ operation: TokenOperation,
296
+ ): void {
297
+ if (!profile.operations.includes(operation))
298
+ throw new Error(`KCC20 profile does not support ${operation}`);
299
+ if (
300
+ ["wrap", "unwrap", "trade"].includes(operation) &&
301
+ (profile.fungibility !== "single-holder-class" ||
302
+ !profile.ownerSchemes.includes(4) ||
303
+ !profile.wrapper)
304
+ ) {
305
+ throw new Error("KCC20 profile does not support this trading operation");
306
+ }
307
+ if (["mint", "burn"].includes(operation) && !profile.nativeExtensions)
308
+ throw new Error("external mint/burn adapter is unavailable");
309
+ }
310
+
311
+ /** Resolve the trading scale, enforcing profiles with a fixed precision. */
312
+ export function resolveTokenProfilePriceScale(
313
+ profile: TokenProfile,
314
+ requested: bigint | undefined,
315
+ fallback: bigint,
316
+ ): bigint {
317
+ const expected =
318
+ profile.displayDecimals === undefined
319
+ ? undefined
320
+ : 10n ** BigInt(profile.displayDecimals);
321
+ if (expected !== undefined) {
322
+ if (requested !== undefined && requested !== expected)
323
+ throw new Error(
324
+ "priceScale does not match token profile display precision",
325
+ );
326
+ return expected;
327
+ }
328
+ return requested ?? fallback;
329
+ }
330
+
331
+ /** Caller must authenticate the returned script against the source UTXO on chain. */
332
+ export function rebuildProfileToken(
333
+ profile: TokenProfile,
334
+ artifact: unknown,
335
+ state: unknown,
336
+ ): Uint8Array {
337
+ return buildKcc20ScriptForState(
338
+ authenticateTokenArtifact(profile, artifact),
339
+ state,
340
+ );
341
+ }
342
+
343
+ export interface TokenReadiness {
344
+ status:
345
+ | "needs_verification"
346
+ | "blocked"
347
+ | "queued"
348
+ | "wrapper_indexing"
349
+ | "imported";
350
+ reasonCode: string | null;
351
+ reason: string | null;
352
+ profileId: string | null;
353
+ standardRevision: string;
354
+ capabilities: Partial<Record<TokenOperation, boolean>>;
355
+ }
356
+ /** One evaluator for import, token pages and wrapper scheduling; never authorizes signing. */
357
+ export function evaluateTokenReadiness(input: {
358
+ profile?: TokenProfile | null;
359
+ authenticated: boolean;
360
+ tokenVerified: boolean;
361
+ decimals?: number | null;
362
+ singleHolderClass: boolean;
363
+ wrapper: "missing" | "indexing" | "ready" | "blocked";
364
+ }): TokenReadiness {
365
+ const p = input.profile;
366
+ const result: TokenReadiness = {
367
+ status: "blocked",
368
+ reasonCode: null,
369
+ reason: null,
370
+ profileId: p?.id ?? null,
371
+ standardRevision: KCC20_STANDARD_REVISION,
372
+ capabilities: {},
373
+ };
374
+ const stop = (
375
+ status: TokenReadiness["status"],
376
+ code: string,
377
+ reason: string,
378
+ ) => ({ ...result, status, reasonCode: code, reason });
379
+ if (!p)
380
+ return stop(
381
+ "blocked",
382
+ "TOKEN_PROFILE_UNAVAILABLE",
383
+ "Token contract template is unavailable.",
384
+ );
385
+ if (!input.authenticated)
386
+ return stop(
387
+ "blocked",
388
+ "TOKEN_EVIDENCE_UNAVAILABLE",
389
+ "Current token outputs have not been authenticated against the token template.",
390
+ );
391
+ if (!input.tokenVerified)
392
+ return stop(
393
+ "needs_verification",
394
+ "TOKEN_VERIFICATION_REQUIRED",
395
+ "Token identity has not been verified.",
396
+ );
397
+ for (const op of p.operations) {
398
+ try {
399
+ requireTokenCapability(p, op);
400
+ result.capabilities[op] = true;
401
+ } catch {
402
+ result.capabilities[op] = false;
403
+ }
404
+ }
405
+ if (!input.singleHolderClass || p.fungibility !== "single-holder-class")
406
+ return stop(
407
+ "blocked",
408
+ "HOLDER_CLASS_UNSUPPORTED",
409
+ "Token holder variants cannot share a trading market.",
410
+ );
411
+ if (
412
+ !Number.isInteger(input.decimals) ||
413
+ input.decimals! < 0 ||
414
+ input.decimals! > 8
415
+ )
416
+ return stop(
417
+ "blocked",
418
+ "TRADING_PRECISION_UNSUPPORTED",
419
+ "Trading requires verified token decimals between 0 and 8.",
420
+ );
421
+ if (p.displayDecimals !== undefined && input.decimals !== p.displayDecimals)
422
+ return stop(
423
+ "blocked",
424
+ "TRADING_PRECISION_MISMATCH",
425
+ "Trading precision does not match the token profile.",
426
+ );
427
+ if (
428
+ !result.capabilities.wrap ||
429
+ !result.capabilities.unwrap ||
430
+ !result.capabilities.trade
431
+ )
432
+ return stop(
433
+ "blocked",
434
+ "TRADING_PROFILE_UNSUPPORTED",
435
+ "Token template does not support wrap, trade and unwrap.",
436
+ );
437
+ if (input.wrapper === "blocked")
438
+ return stop(
439
+ "blocked",
440
+ "WRAPPER_UNAVAILABLE",
441
+ "The trading wrapper is unavailable.",
442
+ );
443
+ if (input.wrapper === "missing")
444
+ return stop(
445
+ "queued",
446
+ "WRAPPER_REQUIRED",
447
+ "Trading wrapper creation is required.",
448
+ );
449
+ if (input.wrapper === "indexing")
450
+ return stop(
451
+ "wrapper_indexing",
452
+ "WRAPPER_INDEXING",
453
+ "Trading wrapper is being indexed.",
454
+ );
455
+ return { ...result, status: "imported" };
456
+ }
@@ -0,0 +1,45 @@
1
+ export interface Kcc20TransferPayloadFields {
2
+ covenantId: string;
3
+ fromOwnerIdentifier: string;
4
+ recipientOwner: string;
5
+ recipientOwnerScheme: number;
6
+ tokenAmount: bigint;
7
+ }
8
+
9
+ /** Indexer payload for a transfer; source identity is the covenant state owner. */
10
+ export function buildAuthenticatedKcc20TransferPayload(
11
+ fields: Kcc20TransferPayloadFields,
12
+ ): Uint8Array {
13
+ return new TextEncoder().encode(
14
+ JSON.stringify({
15
+ tn12: {
16
+ v: 1,
17
+ tmpl: "KCC20",
18
+ op: "transfer",
19
+ args: [
20
+ { name: "covenantId", type: "byte[32]", value: fields.covenantId },
21
+ {
22
+ name: "fromOwner",
23
+ type: "byte[32]",
24
+ value: fields.fromOwnerIdentifier,
25
+ },
26
+ {
27
+ name: "recipientOwner",
28
+ type: "byte[32]",
29
+ value: fields.recipientOwner,
30
+ },
31
+ {
32
+ name: "recipientOwnerScheme",
33
+ type: "u64",
34
+ value: String(fields.recipientOwnerScheme),
35
+ },
36
+ {
37
+ name: "tokenAmount",
38
+ type: "u64",
39
+ value: fields.tokenAmount.toString(),
40
+ },
41
+ ],
42
+ },
43
+ }),
44
+ );
45
+ }
@@ -370,6 +370,7 @@ export function buildKcc20WrapperMarketDeployOperation(
370
370
  input: {
371
371
  canonicalCovenantId: string;
372
372
  contractCanonicalCovenantId?: string | null;
373
+ tokenUtxo: Kcc20IndexedCovenantUtxo;
373
374
  tokenDecimals?: number | null;
374
375
  wrappedRootAmount?: string;
375
376
  feeTicketId?: string | null;
@@ -403,6 +404,7 @@ export function buildKcc20WrapperMarketDeployOperation(
403
404
  params: {
404
405
  canonicalCovenantId: appCanonicalId,
405
406
  contractCanonicalCovenantId: contractCanonicalId,
407
+ tokenUtxo: input.tokenUtxo,
406
408
  feeTicketId,
407
409
  wrappedRootAmount: rootAmount,
408
410
  priceScale: kcc20DisplayScaleForDecimals(decimals).toString(),