@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.
- package/CHANGELOG.md +6 -0
- 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-signerless-transactions.js +2 -5
- 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/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-signerless-transactions.d.ts.map +1 -1
- package/dist/snapshot-signerless-transactions.js +2 -5
- 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/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/COMPATIBILITY.json +24 -33
- package/docs/LAST_CALL.md +50 -0
- package/examples/README.md +3 -0
- 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-signerless-transactions.ts +7 -16
- package/src/snapshot-signerless.ts +5 -3
- 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,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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
+
}
|