@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.
- package/CHANGELOG.md +12 -1
- package/README.md +2 -2
- package/RELEASING.md +19 -14
- package/artifacts/ArgentKCC20.reference.json +12428 -0
- package/artifacts/ArgentKCC20Wrapper.placeholder.json +12455 -0
- package/artifacts/KCC20.placeholder.json +1447 -1973
- package/artifacts/KCC20FeeTicket.placeholder.json +816 -1447
- package/artifacts/KCC20Orderbook.placeholder.json +1 -2
- package/artifacts/KCC20SnapshotBootstrap.placeholder.json +101 -100
- package/artifacts/KCC20SnapshotController.placeholder.json +101 -100
- package/artifacts/KCC20Vesting.placeholder.json +427 -429
- package/artifacts/KCC20Wrapper.placeholder.json +733 -734
- package/artifacts/kcc20-snapshot-bootstrap/KCC20SnapshotBootstrap.placeholder.json +101 -100
- package/artifacts/kcc20-snapshot-controller/KCC20SnapshotController.placeholder.json +101 -100
- package/artifacts/snapshot-standards.lock.json +6 -3
- package/artifacts/token-profiles.json +109 -0
- package/artifacts/token-profiles.sha256 +1 -0
- package/dist/action-source-resolver.d.ts.map +1 -1
- package/dist/action-source-resolver.js +11 -2
- package/dist/action-source-resolver.js.map +1 -1
- package/dist/artifacts.d.ts +10 -10
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +10 -10
- package/dist/artifacts.js.map +1 -1
- package/dist/cjs/action-source-resolver.js +11 -2
- package/dist/cjs/action-source-resolver.js.map +1 -1
- package/dist/cjs/artifacts.d.ts +10 -10
- package/dist/cjs/artifacts.js +10 -10
- package/dist/cjs/artifacts.js.map +1 -1
- package/dist/cjs/engine.d.ts +10 -0
- package/dist/cjs/engine.js +124 -77
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.js +5 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/profile-transfer.d.ts +60 -0
- package/dist/cjs/profile-transfer.js +248 -0
- package/dist/cjs/profile-transfer.js.map +1 -0
- package/dist/cjs/snapshot-operation.d.ts +0 -1
- package/dist/cjs/snapshot-operation.js +1 -1126
- package/dist/cjs/snapshot-operation.js.map +1 -1
- package/dist/cjs/snapshot-signerless-transactions.d.ts +1 -1
- package/dist/cjs/snapshot-signerless-transactions.js +3 -6
- package/dist/cjs/snapshot-signerless-transactions.js.map +1 -1
- package/dist/cjs/snapshot-signerless.js +5 -3
- package/dist/cjs/snapshot-signerless.js.map +1 -1
- package/dist/cjs/snapshot-verification.d.ts +1 -1
- package/dist/cjs/snapshot-verification.js +16 -12
- package/dist/cjs/snapshot-verification.js.map +1 -1
- package/dist/cjs/token-authorization.d.ts +42 -0
- package/dist/cjs/token-authorization.js +93 -0
- package/dist/cjs/token-authorization.js.map +1 -0
- package/dist/cjs/token-profiles.d.ts +82 -0
- package/dist/cjs/token-profiles.generated.d.ts +3 -0
- package/dist/cjs/token-profiles.generated.js +114 -0
- package/dist/cjs/token-profiles.generated.js.map +1 -0
- package/dist/cjs/token-profiles.js +257 -0
- package/dist/cjs/token-profiles.js.map +1 -0
- package/dist/cjs/transfer-payload.d.ts +9 -0
- package/dist/cjs/transfer-payload.js +37 -0
- package/dist/cjs/transfer-payload.js.map +1 -0
- package/dist/cjs/wrapper-operation.d.ts +1 -0
- package/dist/cjs/wrapper-operation.js +1 -0
- package/dist/cjs/wrapper-operation.js.map +1 -1
- package/dist/engine.d.ts +10 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +125 -78
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/profile-transfer.d.ts +61 -0
- package/dist/profile-transfer.d.ts.map +1 -0
- package/dist/profile-transfer.js +245 -0
- package/dist/profile-transfer.js.map +1 -0
- package/dist/snapshot-operation.d.ts +0 -1
- package/dist/snapshot-operation.d.ts.map +1 -1
- package/dist/snapshot-operation.js +0 -1125
- package/dist/snapshot-operation.js.map +1 -1
- package/dist/snapshot-signerless-transactions.d.ts +1 -1
- package/dist/snapshot-signerless-transactions.d.ts.map +1 -1
- package/dist/snapshot-signerless-transactions.js +3 -6
- package/dist/snapshot-signerless-transactions.js.map +1 -1
- package/dist/snapshot-signerless.d.ts.map +1 -1
- package/dist/snapshot-signerless.js +5 -3
- package/dist/snapshot-signerless.js.map +1 -1
- package/dist/snapshot-verification.d.ts +1 -1
- package/dist/snapshot-verification.d.ts.map +1 -1
- package/dist/snapshot-verification.js +16 -12
- package/dist/snapshot-verification.js.map +1 -1
- package/dist/token-authorization.d.ts +43 -0
- package/dist/token-authorization.d.ts.map +1 -0
- package/dist/token-authorization.js +88 -0
- package/dist/token-authorization.js.map +1 -0
- package/dist/token-profiles.d.ts +83 -0
- package/dist/token-profiles.d.ts.map +1 -0
- package/dist/token-profiles.generated.d.ts +4 -0
- package/dist/token-profiles.generated.d.ts.map +1 -0
- package/dist/token-profiles.generated.js +111 -0
- package/dist/token-profiles.generated.js.map +1 -0
- package/dist/token-profiles.js +245 -0
- package/dist/token-profiles.js.map +1 -0
- package/dist/transfer-payload.d.ts +10 -0
- package/dist/transfer-payload.d.ts.map +1 -0
- package/dist/transfer-payload.js +34 -0
- package/dist/transfer-payload.js.map +1 -0
- package/dist/wrapper-operation.d.ts +1 -0
- package/dist/wrapper-operation.d.ts.map +1 -1
- package/dist/wrapper-operation.js +1 -0
- package/dist/wrapper-operation.js.map +1 -1
- package/docs/API.md +1 -1
- package/docs/COMPATIBILITY.json +24 -33
- package/docs/INTEGRATION.md +2 -2
- package/docs/LAST_CALL.md +50 -0
- package/examples/README.md +5 -2
- package/examples/backend/cli.ts +2 -1
- package/examples/frontend/main.ts +2 -1
- package/examples/shared/build.ts +3 -0
- package/examples/shared/offline-profile.ts +27 -0
- package/examples/shared/recipe-fixtures.ts +3 -2
- package/examples/test/browser.ts +2 -2
- package/examples/test/build.test.ts +21 -8
- package/examples/test/sources.test.ts +3 -2
- package/package.json +2 -2
- package/src/action-source-resolver.ts +22 -4
- package/src/artifacts.ts +12 -10
- package/src/engine.ts +208 -105
- package/src/index.ts +7 -0
- package/src/profile-transfer.ts +425 -0
- package/src/snapshot-operation.ts +59 -1731
- package/src/snapshot-signerless-transactions.ts +8 -17
- package/src/snapshot-signerless.ts +5 -3
- package/src/snapshot-verification.ts +17 -13
- package/src/token-authorization.ts +135 -0
- package/src/token-profiles.generated.ts +112 -0
- package/src/token-profiles.ts +456 -0
- package/src/transfer-payload.ts +45 -0
- 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
|
+
}
|
package/src/wrapper-operation.ts
CHANGED
|
@@ -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(),
|