@kaspacom/kcc20-tx-builder 0.2.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +54 -8
- package/RELEASING.md +93 -0
- package/SECURITY.md +16 -0
- package/artifacts/KCC20SnapshotBootstrap.placeholder.json +101677 -0
- package/artifacts/KCC20SnapshotController.placeholder.json +46547 -0
- package/artifacts/kcc20-snapshot-bootstrap/KCC20SnapshotBootstrap.placeholder.json +101677 -0
- package/artifacts/kcc20-snapshot-controller/KCC20SnapshotController.placeholder.json +46547 -0
- package/artifacts/snapshot-standards.lock.json +56 -0
- package/dist/artifacts.d.ts +31 -0
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +31 -0
- package/dist/artifacts.js.map +1 -1
- package/dist/cjs/artifacts.d.ts +31 -0
- package/dist/cjs/artifacts.js +32 -1
- package/dist/cjs/artifacts.js.map +1 -1
- package/dist/cjs/engine.js +58 -44
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/operations.d.ts +1 -1
- package/dist/cjs/operations.js +4 -0
- package/dist/cjs/operations.js.map +1 -1
- package/dist/cjs/sale-common.d.ts +1 -1
- package/dist/cjs/sale-common.js +39 -4
- package/dist/cjs/sale-common.js.map +1 -1
- package/dist/cjs/snapshot-deployment.d.ts +82 -0
- package/dist/cjs/snapshot-deployment.js +82 -0
- package/dist/cjs/snapshot-deployment.js.map +1 -0
- package/dist/cjs/snapshot-operation.d.ts +27 -0
- package/dist/cjs/snapshot-operation.js +2433 -0
- package/dist/cjs/snapshot-operation.js.map +1 -0
- package/dist/cjs/snapshot-signerless-transactions.d.ts +132 -0
- package/dist/cjs/snapshot-signerless-transactions.js +746 -0
- package/dist/cjs/snapshot-signerless-transactions.js.map +1 -0
- package/dist/cjs/snapshot-signerless.d.ts +226 -0
- package/dist/cjs/snapshot-signerless.js +1022 -0
- package/dist/cjs/snapshot-signerless.js.map +1 -0
- package/dist/cjs/snapshot-verification.d.ts +57 -0
- package/dist/cjs/snapshot-verification.js +96 -0
- package/dist/cjs/snapshot-verification.js.map +1 -0
- package/dist/cjs/token-operation.js +3 -3
- package/dist/cjs/token-operation.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +58 -44
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +4 -0
- package/dist/operations.js.map +1 -1
- package/dist/sale-common.d.ts +1 -1
- package/dist/sale-common.d.ts.map +1 -1
- package/dist/sale-common.js +39 -4
- package/dist/sale-common.js.map +1 -1
- package/dist/snapshot-deployment.d.ts +83 -0
- package/dist/snapshot-deployment.d.ts.map +1 -0
- package/dist/snapshot-deployment.js +78 -0
- package/dist/snapshot-deployment.js.map +1 -0
- package/dist/snapshot-operation.d.ts +28 -0
- package/dist/snapshot-operation.d.ts.map +1 -0
- package/dist/snapshot-operation.js +2428 -0
- package/dist/snapshot-operation.js.map +1 -0
- package/dist/snapshot-signerless-transactions.d.ts +133 -0
- package/dist/snapshot-signerless-transactions.d.ts.map +1 -0
- package/dist/snapshot-signerless-transactions.js +735 -0
- package/dist/snapshot-signerless-transactions.js.map +1 -0
- package/dist/snapshot-signerless.d.ts +227 -0
- package/dist/snapshot-signerless.d.ts.map +1 -0
- package/dist/snapshot-signerless.js +993 -0
- package/dist/snapshot-signerless.js.map +1 -0
- package/dist/snapshot-verification.d.ts +58 -0
- package/dist/snapshot-verification.d.ts.map +1 -0
- package/dist/snapshot-verification.js +92 -0
- package/dist/snapshot-verification.js.map +1 -0
- package/dist/token-operation.js +3 -3
- package/dist/token-operation.js.map +1 -1
- package/docs/API.md +67 -0
- package/docs/COMPATIBILITY.json +84 -0
- package/docs/INTEGRATION.md +215 -0
- package/docs/LIVE_TRANSFER.md +106 -0
- package/docs/RECIPES.md +175 -0
- package/docs/TRANSACTION_GUIDE.md +163 -0
- package/docs/snapshot-start.md +27 -0
- package/examples/README.md +81 -0
- package/examples/backend/cli.ts +55 -0
- package/examples/backend/recipe.ts +39 -0
- package/examples/backend/runtime.ts +13 -0
- package/examples/frontend/index.html +48 -0
- package/examples/frontend/main.ts +213 -0
- package/examples/frontend/runtime.ts +14 -0
- package/examples/package-lock.json +1287 -0
- package/examples/package.json +23 -0
- package/examples/shared/build.ts +179 -0
- package/examples/shared/fixture.ts +24 -0
- package/examples/shared/live-transfer.ts +155 -0
- package/examples/shared/recipe-fixtures.ts +543 -0
- package/examples/shared/recipes.ts +55 -0
- package/examples/shared/signing.ts +110 -0
- package/examples/shared/timeout.ts +8 -0
- package/examples/test/browser.ts +116 -0
- package/examples/test/build.test.ts +197 -0
- package/examples/test/recipes.test.ts +59 -0
- package/examples/test/sources.test.ts +73 -0
- package/examples/tsconfig.json +26 -0
- package/examples/vendor/NOTICE.md +15 -0
- package/examples/vendor/kaspa/LICENSE +15 -0
- package/examples/vendor/kaspa/kaspa.d.ts +8908 -0
- package/examples/vendor/kaspa/kaspa.js +15202 -0
- package/examples/vendor/kaspa/kaspa_bg.wasm +0 -0
- package/examples/vendor/kaspa/package.json +24 -0
- package/package.json +21 -4
- package/src/artifacts.ts +35 -0
- package/src/engine.ts +91 -97
- package/src/index.ts +8 -0
- package/src/operations.ts +4 -0
- package/src/sale-common.ts +44 -3
- package/src/snapshot-deployment.ts +108 -0
- package/src/snapshot-operation.ts +3592 -0
- package/src/snapshot-signerless-transactions.ts +1272 -0
- package/src/snapshot-signerless.ts +1525 -0
- package/src/snapshot-verification.ts +86 -0
- package/src/token-operation.ts +3 -3
|
@@ -0,0 +1,3592 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import { blake2b } from "@noble/hashes/blake2b";
|
|
4
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
5
|
+
import type { KaspaWasmRuntime } from "./runtime.js";
|
|
6
|
+
import {
|
|
7
|
+
bytesToHex,
|
|
8
|
+
hexToBytes,
|
|
9
|
+
requireHex32,
|
|
10
|
+
selectFundingEntry,
|
|
11
|
+
setVersionOneInputMassFields,
|
|
12
|
+
utxoAmountSompi,
|
|
13
|
+
ZERO_HASH,
|
|
14
|
+
} from "./sale-common.js";
|
|
15
|
+
import {
|
|
16
|
+
buildKcc20ScriptForState,
|
|
17
|
+
buildKcc20TransferSigScript,
|
|
18
|
+
dispatchTagFor,
|
|
19
|
+
encodeKcc20StateRecordArrays,
|
|
20
|
+
normalizeSilverScriptArtifact,
|
|
21
|
+
templateParts,
|
|
22
|
+
} from "./abi.js";
|
|
23
|
+
import {
|
|
24
|
+
buildBootstrapGenesisPlan as buildSignerlessBootstrapPlan,
|
|
25
|
+
buildControllerGenesisPlan as createSignerlessControllerPlan,
|
|
26
|
+
buildClaimPlan as buildSignerlessClaimPlan,
|
|
27
|
+
buildSnapshotStartPlan,
|
|
28
|
+
snapshotControllerStateCommitment as signerlessControllerStateCommitment,
|
|
29
|
+
snapshotPeerPlanCommitment,
|
|
30
|
+
} from "./snapshot-signerless.js";
|
|
31
|
+
import {
|
|
32
|
+
buildBootstrapGenesisUnsignedTransaction,
|
|
33
|
+
buildControllerGenesisUnsignedTransaction,
|
|
34
|
+
buildSnapshotBootstrapScriptForState as buildSignerlessBootstrapScript,
|
|
35
|
+
buildSnapshotControllerScriptForState as buildSignerlessControllerScript,
|
|
36
|
+
buildSnapshotKcc20DeploymentPayload,
|
|
37
|
+
buildSnapshotStartUnsignedTransaction,
|
|
38
|
+
deriveSnapshotTokenId,
|
|
39
|
+
} from "./snapshot-signerless-transactions.js";
|
|
40
|
+
|
|
41
|
+
export const KCC20_SNAPSHOT_BUILDER_KEYS = [
|
|
42
|
+
"kcc20-snapshot.deploy-bootstrap",
|
|
43
|
+
"kcc20-snapshot.deploy-controller",
|
|
44
|
+
"kcc20-snapshot.start",
|
|
45
|
+
"kcc20-snapshot.claim",
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* KAS the public funding input must add to an immutable snapshot start.
|
|
50
|
+
* The consumed bootstrap cell is recycled while every reserve output must be
|
|
51
|
+
* funded. Accept the start plan itself so callers cannot accidentally read
|
|
52
|
+
* reserve fields from the narrower bootstrap-genesis plan.
|
|
53
|
+
*/
|
|
54
|
+
export function calculateSnapshotStartRequiredFunding(
|
|
55
|
+
startPlan: {
|
|
56
|
+
reserveCellKas: string | number | bigint;
|
|
57
|
+
bootstrapCellKas: string | number | bigint;
|
|
58
|
+
},
|
|
59
|
+
shardCount: number,
|
|
60
|
+
priorityFeeSompi: string | number | bigint,
|
|
61
|
+
safetyMarginSompi: string | number | bigint = 10_000n,
|
|
62
|
+
): bigint {
|
|
63
|
+
if (!Number.isSafeInteger(shardCount) || shardCount < 1) {
|
|
64
|
+
throw new Error("start shard count must be a positive safe integer");
|
|
65
|
+
}
|
|
66
|
+
const reserveCellKas = snapshotFundingI64(
|
|
67
|
+
startPlan?.reserveCellKas,
|
|
68
|
+
"start reserve cell KAS",
|
|
69
|
+
false,
|
|
70
|
+
);
|
|
71
|
+
const bootstrapCellKas = snapshotFundingI64(
|
|
72
|
+
startPlan?.bootstrapCellKas,
|
|
73
|
+
"start bootstrap cell KAS",
|
|
74
|
+
false,
|
|
75
|
+
);
|
|
76
|
+
const priorityFee = snapshotFundingI64(
|
|
77
|
+
priorityFeeSompi,
|
|
78
|
+
"snapshot priority fee",
|
|
79
|
+
true,
|
|
80
|
+
);
|
|
81
|
+
const safetyMargin = snapshotFundingI64(
|
|
82
|
+
safetyMarginSompi,
|
|
83
|
+
"snapshot start safety margin",
|
|
84
|
+
true,
|
|
85
|
+
);
|
|
86
|
+
return (
|
|
87
|
+
reserveCellKas * BigInt(shardCount) -
|
|
88
|
+
bootstrapCellKas +
|
|
89
|
+
priorityFee +
|
|
90
|
+
safetyMargin
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function snapshotFundingI64(
|
|
95
|
+
value: unknown,
|
|
96
|
+
label: string,
|
|
97
|
+
allowZero: boolean,
|
|
98
|
+
): bigint {
|
|
99
|
+
let amount: bigint;
|
|
100
|
+
try {
|
|
101
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
102
|
+
throw new Error("unsafe integer");
|
|
103
|
+
}
|
|
104
|
+
amount = BigInt(value as string | number | bigint);
|
|
105
|
+
} catch {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`${label} must be a ${allowZero ? "nonnegative" : "positive"} signed 64-bit integer`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
amount > 9_223_372_036_854_775_807n ||
|
|
112
|
+
(allowZero ? amount < 0n : amount <= 0n)
|
|
113
|
+
) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${label} must be a ${allowZero ? "nonnegative" : "positive"} signed 64-bit integer`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return amount;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface Kcc20SnapshotBuilderOptions {
|
|
122
|
+
wasm: KaspaWasmRuntime;
|
|
123
|
+
artifacts?: Record<string, unknown>;
|
|
124
|
+
artifactProvider?: (key: string) => Promise<unknown>;
|
|
125
|
+
config?: Record<string, any>;
|
|
126
|
+
sourceProvider?: {
|
|
127
|
+
getUtxosByAddresses(args: { addresses: string[] }): Promise<unknown>;
|
|
128
|
+
};
|
|
129
|
+
rpcClientFactory?: (config: unknown) => any;
|
|
130
|
+
transactionWithCalculatedFeeChange: (...args: any[]) => any;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const SNAPSHOT_CANCEL_SINK_P2SH =
|
|
134
|
+
"d379e50f3e2203f9a92584916335bb7cd014b237ceaa8b36f1d3d80d0cc88b32";
|
|
135
|
+
|
|
136
|
+
export function createKcc20SnapshotPsktBuilder(
|
|
137
|
+
options: Kcc20SnapshotBuilderOptions,
|
|
138
|
+
): (input: unknown) => Promise<Record<string, unknown>> {
|
|
139
|
+
const config = options.config ?? {};
|
|
140
|
+
const transactionWithCalculatedFeeChange =
|
|
141
|
+
options.transactionWithCalculatedFeeChange;
|
|
142
|
+
class InjectedRpcClient {
|
|
143
|
+
private readonly inner: any;
|
|
144
|
+
constructor(rpcConfig: unknown) {
|
|
145
|
+
this.inner = options.sourceProvider
|
|
146
|
+
? undefined
|
|
147
|
+
: options.rpcClientFactory?.(rpcConfig);
|
|
148
|
+
}
|
|
149
|
+
connect() {
|
|
150
|
+
if (options.sourceProvider) return Promise.resolve();
|
|
151
|
+
return this.inner?.connect?.() ?? Promise.resolve();
|
|
152
|
+
}
|
|
153
|
+
disconnect() {
|
|
154
|
+
if (options.sourceProvider) return Promise.resolve();
|
|
155
|
+
return this.inner?.disconnect?.() ?? Promise.resolve();
|
|
156
|
+
}
|
|
157
|
+
getUtxosByAddresses(args: { addresses: string[] }) {
|
|
158
|
+
if (options.sourceProvider)
|
|
159
|
+
return options.sourceProvider.getUtxosByAddresses(args);
|
|
160
|
+
if (this.inner?.getUtxosByAddresses)
|
|
161
|
+
return this.inner.getUtxosByAddresses(args);
|
|
162
|
+
throw new Error(
|
|
163
|
+
"No source provider or RPC client was supplied to the snapshot builder",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const kaspaWasm = new Proxy(options.wasm, {
|
|
168
|
+
get(target, property, receiver) {
|
|
169
|
+
if (property === "RpcClient") return InjectedRpcClient;
|
|
170
|
+
return Reflect.get(target, property, receiver);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const INPUT_SCHEMA = "kcc20-in-process-pskt-builder-input/v1";
|
|
174
|
+
const OUTPUT_SCHEMA = "kcc20-in-process-pskt-builder-output/v1";
|
|
175
|
+
const NATIVE_SUBNETWORK_ID = "00".repeat(20);
|
|
176
|
+
const DEFAULT_WRPC_URL =
|
|
177
|
+
"wss://tn10-node.kaspa.com/kaspa/testnet-10/wrpc/borsh";
|
|
178
|
+
const DEFAULT_PRIORITY_FEE = 10_000n;
|
|
179
|
+
const DEFAULT_CELL_SOMPI = 50_000_000n;
|
|
180
|
+
const MAX_I64 = 9_223_372_036_854_775_807n;
|
|
181
|
+
const MAX_U32 = 0xffffffff;
|
|
182
|
+
const BOOTSTRAP_COMPUTE_BUDGET = 2_000;
|
|
183
|
+
const CONTROLLER_COMPUTE_BUDGET = 2_000;
|
|
184
|
+
const KCC20_COMPUTE_BUDGET = 200;
|
|
185
|
+
const PUBLIC_INPUT_COMPUTE_BUDGET = 30;
|
|
186
|
+
const KCC20_ARTIFACT_PATH = "KCC20.placeholder.json";
|
|
187
|
+
const BOOTSTRAP_ARTIFACT_PATH = "KCC20SnapshotBootstrap.placeholder.json";
|
|
188
|
+
const CONTROLLER_ARTIFACT_PATH = "KCC20SnapshotController.placeholder.json";
|
|
189
|
+
const CONTROLLER_STATE_COMMITMENT_DOMAIN = new TextEncoder().encode(
|
|
190
|
+
"kcc20-snapshot-controller-state/v1",
|
|
191
|
+
);
|
|
192
|
+
const artifactCache = new Map();
|
|
193
|
+
|
|
194
|
+
async function buildSnapshotMigrationPskt(input) {
|
|
195
|
+
if (input?.schema !== INPUT_SCHEMA) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`unsupported in-process PSKT builder input schema: ${input?.schema || "missing"}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
switch (input.request?.builderKey) {
|
|
201
|
+
case "kcc20-snapshot.deploy-bootstrap":
|
|
202
|
+
return buildSignerlessBootstrapGenesis(input);
|
|
203
|
+
case "kcc20-snapshot.deploy-controller":
|
|
204
|
+
return buildSignerlessControllerGenesis(input);
|
|
205
|
+
case "kcc20-snapshot.start":
|
|
206
|
+
return buildSignerlessStart(input);
|
|
207
|
+
case "kcc20-snapshot.claim":
|
|
208
|
+
return buildClaim(input);
|
|
209
|
+
default:
|
|
210
|
+
throw new Error(
|
|
211
|
+
`unsupported snapshot builderKey: ${input.request?.builderKey || "missing"}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function buildSignerlessControllerGenesis(input) {
|
|
217
|
+
const request = input.request || {};
|
|
218
|
+
const params = request.params || {};
|
|
219
|
+
const network = networkOf(request);
|
|
220
|
+
const walletAddress = walletAddressOf(request);
|
|
221
|
+
const artifacts = await readPinnedArtifacts(
|
|
222
|
+
params.artifactPins,
|
|
223
|
+
true,
|
|
224
|
+
true,
|
|
225
|
+
);
|
|
226
|
+
const shard = snapshotShard(params.shard);
|
|
227
|
+
const allShards = normalizedSignerlessShards(
|
|
228
|
+
params.allShards ?? [params.shard],
|
|
229
|
+
);
|
|
230
|
+
const plan = signerlessControllerPlan(params, artifacts, shard, allShards);
|
|
231
|
+
const rpc = rpcFor(network);
|
|
232
|
+
await rpc.connect();
|
|
233
|
+
try {
|
|
234
|
+
const walletUtxos = await rpc.getUtxosByAddresses({
|
|
235
|
+
addresses: [walletAddress],
|
|
236
|
+
});
|
|
237
|
+
const cellKas = BigInt(plan.controllerCellKas);
|
|
238
|
+
const fundingEntry = selectFundingEntry(
|
|
239
|
+
walletUtxos.entries,
|
|
240
|
+
cellKas + priorityFee() + 10_000n,
|
|
241
|
+
[],
|
|
242
|
+
params.fundingOutpoint,
|
|
243
|
+
);
|
|
244
|
+
const built = buildControllerGenesisUnsignedTransaction(kaspaWasm, {
|
|
245
|
+
plan,
|
|
246
|
+
controllerArtifact: artifacts.controller,
|
|
247
|
+
fundingInput: portableInput(fundingEntry, network),
|
|
248
|
+
changeAddress: walletAddress,
|
|
249
|
+
feeSompi: priorityFee(),
|
|
250
|
+
});
|
|
251
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
252
|
+
kaspaWasm,
|
|
253
|
+
built.transaction,
|
|
254
|
+
built.unsignedTransaction.outputs.length - 1,
|
|
255
|
+
network,
|
|
256
|
+
"signerless snapshot controller genesis",
|
|
257
|
+
);
|
|
258
|
+
const json = JSON.parse(adjusted.serializeToSafeJSON());
|
|
259
|
+
const controllerId = requireHex32(
|
|
260
|
+
json.outputs?.[0]?.covenant?.covenantId,
|
|
261
|
+
"controller covenant id",
|
|
262
|
+
);
|
|
263
|
+
const revealScript = buildSignerlessControllerScript(
|
|
264
|
+
artifacts.controller,
|
|
265
|
+
plan.controllerState,
|
|
266
|
+
);
|
|
267
|
+
return outputEnvelope(adjusted, [{ index: 0 }], [], {
|
|
268
|
+
migrationId: requireHex32(params.migrationId, "migration id"),
|
|
269
|
+
stage: "controller-deploy",
|
|
270
|
+
controllerId,
|
|
271
|
+
shard: shard.shardIndex,
|
|
272
|
+
controllerState: plan.controllerState,
|
|
273
|
+
controllerStateCommitment: signerlessControllerStateCommitment(
|
|
274
|
+
plan.controllerState,
|
|
275
|
+
),
|
|
276
|
+
holderExtensionCommitment: requireHex32(
|
|
277
|
+
params.holderExtensionCommitment,
|
|
278
|
+
"holder extension commitment",
|
|
279
|
+
),
|
|
280
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
281
|
+
intentInputs: [{ role: "creator-funding", index: 0 }],
|
|
282
|
+
intentOutputs: [
|
|
283
|
+
{
|
|
284
|
+
role: "snapshot-controller",
|
|
285
|
+
index: 0,
|
|
286
|
+
shard: shard.shardIndex,
|
|
287
|
+
covenantId: controllerId,
|
|
288
|
+
revealScriptHex: bytesToHex(revealScript),
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
} finally {
|
|
293
|
+
await rpc.disconnect();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function buildSignerlessBootstrapGenesis(input) {
|
|
298
|
+
const request = input.request || {};
|
|
299
|
+
const params = request.params || {};
|
|
300
|
+
const network = networkOf(request);
|
|
301
|
+
const walletAddress = walletAddressOf(request);
|
|
302
|
+
const artifacts = await readPinnedArtifacts(
|
|
303
|
+
params.artifactPins,
|
|
304
|
+
true,
|
|
305
|
+
true,
|
|
306
|
+
);
|
|
307
|
+
const prepared = signerlessDeploymentPlans(params, artifacts);
|
|
308
|
+
const rpc = rpcFor(network);
|
|
309
|
+
await rpc.connect();
|
|
310
|
+
try {
|
|
311
|
+
const walletUtxos = await rpc.getUtxosByAddresses({
|
|
312
|
+
addresses: [walletAddress],
|
|
313
|
+
});
|
|
314
|
+
const cellKas = BigInt(prepared.bootstrapPlan.bootstrapCellKas);
|
|
315
|
+
const fundingEntry = selectFundingEntry(
|
|
316
|
+
walletUtxos.entries,
|
|
317
|
+
cellKas + priorityFee() + 10_000n,
|
|
318
|
+
[],
|
|
319
|
+
params.fundingOutpoint,
|
|
320
|
+
);
|
|
321
|
+
const built = buildBootstrapGenesisUnsignedTransaction(kaspaWasm, {
|
|
322
|
+
plan: prepared.bootstrapPlan,
|
|
323
|
+
bootstrapArtifact: artifacts.bootstrap,
|
|
324
|
+
fundingInput: portableInput(fundingEntry, network),
|
|
325
|
+
changeAddress: walletAddress,
|
|
326
|
+
feeSompi: priorityFee(),
|
|
327
|
+
});
|
|
328
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
329
|
+
kaspaWasm,
|
|
330
|
+
built.transaction,
|
|
331
|
+
built.unsignedTransaction.outputs.length - 1,
|
|
332
|
+
network,
|
|
333
|
+
"signerless snapshot bootstrap genesis",
|
|
334
|
+
);
|
|
335
|
+
const json = JSON.parse(adjusted.serializeToSafeJSON());
|
|
336
|
+
const covenantId = requireHex32(
|
|
337
|
+
json.outputs?.[0]?.covenant?.covenantId,
|
|
338
|
+
"bootstrap covenant id",
|
|
339
|
+
);
|
|
340
|
+
const revealScript = buildSignerlessBootstrapScript(
|
|
341
|
+
artifacts.bootstrap,
|
|
342
|
+
prepared.bootstrapPlan.bootstrapState,
|
|
343
|
+
);
|
|
344
|
+
return outputEnvelope(adjusted, [{ index: 0 }], [], {
|
|
345
|
+
migrationId: requireHex32(params.migrationId, "migration id"),
|
|
346
|
+
stage: "bootstrap-deploy",
|
|
347
|
+
covenantId,
|
|
348
|
+
tokenId: prepared.tokenId,
|
|
349
|
+
bootstrapState: prepared.bootstrapPlan.bootstrapState,
|
|
350
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
351
|
+
intentInputs: [{ role: "creator-funding", index: 0 }],
|
|
352
|
+
intentOutputs: [
|
|
353
|
+
{
|
|
354
|
+
role: "snapshot-bootstrap",
|
|
355
|
+
index: 0,
|
|
356
|
+
covenantId,
|
|
357
|
+
revealScriptHex: bytesToHex(revealScript),
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
} finally {
|
|
362
|
+
await rpc.disconnect();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function buildSignerlessStart(input) {
|
|
367
|
+
const request = input.request || {};
|
|
368
|
+
const params = request.params || {};
|
|
369
|
+
const network = networkOf(request);
|
|
370
|
+
const walletAddress = walletAddressOf(request);
|
|
371
|
+
const artifacts = await readPinnedArtifacts(
|
|
372
|
+
params.artifactPins,
|
|
373
|
+
true,
|
|
374
|
+
true,
|
|
375
|
+
);
|
|
376
|
+
const prepared = signerlessDeploymentPlans(params, artifacts);
|
|
377
|
+
const bootstrapId = requireHex32(
|
|
378
|
+
params.bootstrapId,
|
|
379
|
+
"bootstrap covenant id",
|
|
380
|
+
);
|
|
381
|
+
const startPlan = buildSnapshotStartPlan({
|
|
382
|
+
migrationId: requireHex32(params.migrationId, "migration id"),
|
|
383
|
+
bootstrapArtifact: artifacts.bootstrap,
|
|
384
|
+
controllerArtifact: artifacts.controller,
|
|
385
|
+
kcc20Artifact: artifacts.kcc20,
|
|
386
|
+
bootstrapId,
|
|
387
|
+
bootstrapState: prepared.bootstrapPlan.bootstrapState,
|
|
388
|
+
shards: prepared.startShards,
|
|
389
|
+
});
|
|
390
|
+
const bootstrapScript = buildSignerlessBootstrapScript(
|
|
391
|
+
artifacts.bootstrap,
|
|
392
|
+
prepared.bootstrapPlan.bootstrapState,
|
|
393
|
+
);
|
|
394
|
+
const controllerScripts = prepared.controllerPlans.map((plan) =>
|
|
395
|
+
buildSignerlessControllerScript(
|
|
396
|
+
artifacts.controller,
|
|
397
|
+
plan.controllerState,
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
const bootstrapSource = sourceDescriptor(
|
|
401
|
+
params.bootstrapSource,
|
|
402
|
+
"bootstrap source",
|
|
403
|
+
scriptAddress(bootstrapScript, network),
|
|
404
|
+
);
|
|
405
|
+
const controllerSources = prepared.shards.map((shard, index) =>
|
|
406
|
+
sourceDescriptor(
|
|
407
|
+
shard.controllerSource,
|
|
408
|
+
`controller ${index} source`,
|
|
409
|
+
scriptAddress(controllerScripts[index], network),
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
const rpc = rpcFor(network);
|
|
413
|
+
await rpc.connect();
|
|
414
|
+
try {
|
|
415
|
+
const addresses = [
|
|
416
|
+
bootstrapSource.address,
|
|
417
|
+
...controllerSources.map((source) => source.address),
|
|
418
|
+
];
|
|
419
|
+
const covenantUtxos = await rpc.getUtxosByAddresses({
|
|
420
|
+
addresses: [...new Set(addresses)],
|
|
421
|
+
});
|
|
422
|
+
const bootstrapEntry = findExactEntry(
|
|
423
|
+
covenantUtxos.entries,
|
|
424
|
+
bootstrapSource,
|
|
425
|
+
);
|
|
426
|
+
assertCovenantSource(bootstrapEntry, {
|
|
427
|
+
covenantId: bootstrapId,
|
|
428
|
+
script: bootstrapScript,
|
|
429
|
+
amount: prepared.bootstrapPlan.bootstrapCellKas,
|
|
430
|
+
label: "snapshot bootstrap source",
|
|
431
|
+
});
|
|
432
|
+
const controllerEntries = controllerSources.map((source, index) => {
|
|
433
|
+
const entry = findExactEntry(covenantUtxos.entries, source);
|
|
434
|
+
assertCovenantSource(entry, {
|
|
435
|
+
covenantId: prepared.shards[index].controllerId,
|
|
436
|
+
script: controllerScripts[index],
|
|
437
|
+
amount: prepared.controllerPlans[index].controllerCellKas,
|
|
438
|
+
label: `snapshot controller ${index} source`,
|
|
439
|
+
});
|
|
440
|
+
return entry;
|
|
441
|
+
});
|
|
442
|
+
const walletUtxos = await rpc.getUtxosByAddresses({
|
|
443
|
+
addresses: [walletAddress],
|
|
444
|
+
});
|
|
445
|
+
const requiredFunding = calculateSnapshotStartRequiredFunding(
|
|
446
|
+
startPlan,
|
|
447
|
+
prepared.shards.length,
|
|
448
|
+
priorityFee(),
|
|
449
|
+
);
|
|
450
|
+
const fundingEntry = selectFundingEntry(
|
|
451
|
+
walletUtxos.entries,
|
|
452
|
+
requiredFunding > 0n ? requiredFunding : priorityFee() + 10_000n,
|
|
453
|
+
[bootstrapEntry, ...controllerEntries],
|
|
454
|
+
params.fundingOutpoint,
|
|
455
|
+
);
|
|
456
|
+
const tokenMetadata = requiredRecord(
|
|
457
|
+
params.tokenMetadata,
|
|
458
|
+
"snapshot token metadata",
|
|
459
|
+
);
|
|
460
|
+
const payload = buildSnapshotKcc20DeploymentPayload({
|
|
461
|
+
creator: tokenMetadata.creator,
|
|
462
|
+
ticker: tokenMetadata.ticker,
|
|
463
|
+
name: tokenMetadata.name,
|
|
464
|
+
displayScale: tokenMetadata.displayScale,
|
|
465
|
+
totalSupply: params.totalSupply,
|
|
466
|
+
holderExtensionCommitment: params.holderExtensionCommitment,
|
|
467
|
+
migrationId: params.migrationCommitment ?? params.migrationId,
|
|
468
|
+
manifestCommitment: params.manifestCommitment,
|
|
469
|
+
reserves: startPlan.reserves,
|
|
470
|
+
treasury: tokenMetadata.treasury ?? tokenMetadata.creator,
|
|
471
|
+
protocolFeeRecipient:
|
|
472
|
+
tokenMetadata.protocolFeeRecipient ?? tokenMetadata.creator,
|
|
473
|
+
protocolFeeBps: tokenMetadata.protocolFeeBps ?? 0,
|
|
474
|
+
});
|
|
475
|
+
const built = buildSnapshotStartUnsignedTransaction(kaspaWasm, {
|
|
476
|
+
plan: startPlan,
|
|
477
|
+
bootstrapArtifact: artifacts.bootstrap,
|
|
478
|
+
controllerArtifact: artifacts.controller,
|
|
479
|
+
kcc20Artifact: artifacts.kcc20,
|
|
480
|
+
bootstrapInput: portableInput(bootstrapEntry, network, bootstrapScript),
|
|
481
|
+
controllerInputs: controllerEntries.map((entry, index) =>
|
|
482
|
+
portableInput(entry, network, controllerScripts[index]),
|
|
483
|
+
),
|
|
484
|
+
fundingInput: portableInput(fundingEntry, network),
|
|
485
|
+
changeAddress: walletAddress,
|
|
486
|
+
feeSompi: priorityFee(),
|
|
487
|
+
payload,
|
|
488
|
+
});
|
|
489
|
+
const changeIndex = built.unsignedTransaction.outputs.length - 1;
|
|
490
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
491
|
+
kaspaWasm,
|
|
492
|
+
built.transaction,
|
|
493
|
+
changeIndex,
|
|
494
|
+
network,
|
|
495
|
+
"signerless snapshot start",
|
|
496
|
+
);
|
|
497
|
+
return outputEnvelope(
|
|
498
|
+
adjusted,
|
|
499
|
+
[{ index: 1 + prepared.shards.length }],
|
|
500
|
+
[],
|
|
501
|
+
{
|
|
502
|
+
migrationId: requireHex32(params.migrationId, "migration id"),
|
|
503
|
+
stage: "snapshot-start",
|
|
504
|
+
tokenId: prepared.tokenId,
|
|
505
|
+
covenantId: prepared.tokenId,
|
|
506
|
+
bootstrapId,
|
|
507
|
+
bootstrapState: prepared.bootstrapPlan.bootstrapState,
|
|
508
|
+
bootstrapConsumed: true,
|
|
509
|
+
shards: prepared.shards.map((shard) => shard.shardIndex),
|
|
510
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
511
|
+
intentInputs: [
|
|
512
|
+
{
|
|
513
|
+
role: "snapshot-bootstrap",
|
|
514
|
+
index: 0,
|
|
515
|
+
covenantId: bootstrapId,
|
|
516
|
+
revealScriptHex: bytesToHex(bootstrapScript),
|
|
517
|
+
staticSignatureScriptHex: signatureScriptHex(adjusted, 0),
|
|
518
|
+
},
|
|
519
|
+
...prepared.shards.map((shard, index) => ({
|
|
520
|
+
role: "snapshot-controller",
|
|
521
|
+
index: index + 1,
|
|
522
|
+
shard: shard.shardIndex,
|
|
523
|
+
covenantId: shard.controllerId,
|
|
524
|
+
revealScriptHex: bytesToHex(controllerScripts[index]),
|
|
525
|
+
staticSignatureScriptHex: signatureScriptHex(adjusted, index + 1),
|
|
526
|
+
})),
|
|
527
|
+
{ role: "creator-funding", index: 1 + prepared.shards.length },
|
|
528
|
+
],
|
|
529
|
+
intentOutputs: [
|
|
530
|
+
...prepared.shards.map((shard, index) => ({
|
|
531
|
+
role: "shard-reserve",
|
|
532
|
+
index,
|
|
533
|
+
shard: shard.shardIndex,
|
|
534
|
+
covenantId: prepared.tokenId,
|
|
535
|
+
tokenAmount: shard.allocationTotal.toString(),
|
|
536
|
+
owner: shard.controllerId,
|
|
537
|
+
ownerScheme: 4,
|
|
538
|
+
borrowScheme: 0,
|
|
539
|
+
borrowGuard: ZERO_HASH,
|
|
540
|
+
extensionCommitment: requireHex32(
|
|
541
|
+
params.holderExtensionCommitment,
|
|
542
|
+
"holder extension commitment",
|
|
543
|
+
),
|
|
544
|
+
revealScriptHex: bytesToHex(
|
|
545
|
+
buildKcc20ScriptForState(
|
|
546
|
+
artifacts.kcc20,
|
|
547
|
+
kcc20State(startPlan.reserves[index]),
|
|
548
|
+
),
|
|
549
|
+
),
|
|
550
|
+
})),
|
|
551
|
+
...prepared.shards.map((shard, index) => ({
|
|
552
|
+
role: "snapshot-controller",
|
|
553
|
+
index: prepared.shards.length + index,
|
|
554
|
+
shard: shard.shardIndex,
|
|
555
|
+
covenantId: shard.controllerId,
|
|
556
|
+
revealScriptHex: bytesToHex(
|
|
557
|
+
buildSignerlessControllerScript(
|
|
558
|
+
artifacts.controller,
|
|
559
|
+
startPlan.controllers[index].stateAfter,
|
|
560
|
+
),
|
|
561
|
+
),
|
|
562
|
+
})),
|
|
563
|
+
],
|
|
564
|
+
},
|
|
565
|
+
);
|
|
566
|
+
} finally {
|
|
567
|
+
await rpc.disconnect();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function signerlessDeploymentPlans(params, artifacts) {
|
|
572
|
+
const shards = normalizedSignerlessShards(params.allShards);
|
|
573
|
+
if (
|
|
574
|
+
shards.some((shard) => !shard.controllerId || !shard.controllerSource)
|
|
575
|
+
) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
"signerless bootstrap/start requires every indexed controller identity and source",
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
const controllerPlans = shards.map((shard) =>
|
|
581
|
+
signerlessControllerPlan(params, artifacts, shard, shards),
|
|
582
|
+
);
|
|
583
|
+
const startShards = shards.map((shard, index) => ({
|
|
584
|
+
controllerId: shard.controllerId,
|
|
585
|
+
state: controllerPlans[index].controllerState,
|
|
586
|
+
}));
|
|
587
|
+
const reserves = shards.map((shard) => ({
|
|
588
|
+
amount: shard.allocationTotal.toString(),
|
|
589
|
+
owner: shard.controllerId,
|
|
590
|
+
ownerScheme: 4,
|
|
591
|
+
borrowScheme: 0,
|
|
592
|
+
borrowGuard: ZERO_HASH,
|
|
593
|
+
extensionCommitment: requireHex32(
|
|
594
|
+
params.holderExtensionCommitment,
|
|
595
|
+
"holder extension commitment",
|
|
596
|
+
),
|
|
597
|
+
}));
|
|
598
|
+
const tokenId = deriveSnapshotTokenId(kaspaWasm, {
|
|
599
|
+
controllerGenesisOutpoint: outpointOfSource(
|
|
600
|
+
shards[0].controllerSource,
|
|
601
|
+
"shard-zero controller source",
|
|
602
|
+
),
|
|
603
|
+
reserves,
|
|
604
|
+
kcc20Artifact: artifacts.kcc20,
|
|
605
|
+
reserveCellKas: minimumCell(params.reserveCellKas, "reserve cell KAS"),
|
|
606
|
+
});
|
|
607
|
+
const bootstrapPlan = buildSignerlessBootstrapPlan({
|
|
608
|
+
migrationId: requireHex32(params.migrationId, "migration id"),
|
|
609
|
+
migrationCommitment: requireHex32(
|
|
610
|
+
params.migrationCommitment ?? params.migrationId,
|
|
611
|
+
"migration commitment",
|
|
612
|
+
),
|
|
613
|
+
bootstrapArtifact: artifacts.bootstrap,
|
|
614
|
+
controllerArtifact: artifacts.controller,
|
|
615
|
+
kcc20Artifact: artifacts.kcc20,
|
|
616
|
+
manifestCommitment: requireHex32(
|
|
617
|
+
params.manifestCommitment,
|
|
618
|
+
"manifest commitment",
|
|
619
|
+
),
|
|
620
|
+
holderExtensionCommitment: requireHex32(
|
|
621
|
+
params.holderExtensionCommitment,
|
|
622
|
+
"holder extension commitment",
|
|
623
|
+
),
|
|
624
|
+
totalSupply: positiveI64(params.totalSupply, "total supply"),
|
|
625
|
+
claimNotBeforeDaaScore: positiveI64(
|
|
626
|
+
params.claimNotBeforeDaaScore,
|
|
627
|
+
"claim-not-before DAA score",
|
|
628
|
+
),
|
|
629
|
+
expectedTokenId: tokenId,
|
|
630
|
+
shards: startShards,
|
|
631
|
+
reserveCellKas: minimumCell(params.reserveCellKas, "reserve cell KAS"),
|
|
632
|
+
controllerCellKas: minimumCell(
|
|
633
|
+
params.controllerCellKas,
|
|
634
|
+
"controller cell KAS",
|
|
635
|
+
),
|
|
636
|
+
bootstrapCellKas: minimumCell(
|
|
637
|
+
params.bootstrapCellKas,
|
|
638
|
+
"bootstrap cell KAS",
|
|
639
|
+
),
|
|
640
|
+
});
|
|
641
|
+
return { shards, controllerPlans, startShards, tokenId, bootstrapPlan };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function signerlessControllerPlan(params, artifacts, shard, allShards) {
|
|
645
|
+
const common = {
|
|
646
|
+
migrationId: requireHex32(params.migrationId, "migration id"),
|
|
647
|
+
migrationCommitment: requireHex32(
|
|
648
|
+
params.migrationCommitment ?? params.migrationId,
|
|
649
|
+
"migration commitment",
|
|
650
|
+
),
|
|
651
|
+
controllerArtifact: artifacts.controller,
|
|
652
|
+
bootstrapArtifact: artifacts.bootstrap,
|
|
653
|
+
kcc20Artifact: artifacts.kcc20,
|
|
654
|
+
holderExtensionCommitment: requireHex32(
|
|
655
|
+
params.holderExtensionCommitment,
|
|
656
|
+
"holder extension commitment",
|
|
657
|
+
),
|
|
658
|
+
manifestCommitment: requireHex32(
|
|
659
|
+
params.manifestCommitment,
|
|
660
|
+
"manifest commitment",
|
|
661
|
+
),
|
|
662
|
+
context: requireHex32(params.snapshotContext, "snapshot context"),
|
|
663
|
+
shardCount: allShards.length,
|
|
664
|
+
controllerCellKas: minimumCell(
|
|
665
|
+
params.controllerCellKas,
|
|
666
|
+
"controller cell KAS",
|
|
667
|
+
),
|
|
668
|
+
reserveCellKas: minimumCell(params.reserveCellKas, "reserve cell KAS"),
|
|
669
|
+
recipientMinCellKas: minimumCell(
|
|
670
|
+
params.recipientMinCellKas,
|
|
671
|
+
"recipient minimum cell KAS",
|
|
672
|
+
),
|
|
673
|
+
claimNotBeforeDaaScore: positiveI64(
|
|
674
|
+
params.claimNotBeforeDaaScore,
|
|
675
|
+
"claim-not-before DAA score",
|
|
676
|
+
),
|
|
677
|
+
};
|
|
678
|
+
if (shard.shardIndex !== 0) {
|
|
679
|
+
return createSignerlessControllerPlan({
|
|
680
|
+
...common,
|
|
681
|
+
shard,
|
|
682
|
+
deploymentGuardCommitment: ZERO_HASH,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const provisional = createSignerlessControllerPlan({
|
|
686
|
+
...common,
|
|
687
|
+
shard,
|
|
688
|
+
deploymentGuardCommitment: "01".repeat(32),
|
|
689
|
+
});
|
|
690
|
+
const peerPlans = allShards.slice(1).map((peer) => {
|
|
691
|
+
if (!peer.controllerId)
|
|
692
|
+
throw new Error(
|
|
693
|
+
"deploy shard zero only after every peer controller is indexed",
|
|
694
|
+
);
|
|
695
|
+
const plan = createSignerlessControllerPlan({
|
|
696
|
+
...common,
|
|
697
|
+
shard: peer,
|
|
698
|
+
deploymentGuardCommitment: ZERO_HASH,
|
|
699
|
+
});
|
|
700
|
+
return { controllerId: peer.controllerId, state: plan.controllerState };
|
|
701
|
+
});
|
|
702
|
+
const guard = snapshotPeerPlanCommitment({
|
|
703
|
+
migrationCommitment: common.migrationCommitment,
|
|
704
|
+
manifestCommitment: common.manifestCommitment,
|
|
705
|
+
shards: [
|
|
706
|
+
{ controllerId: "01".repeat(32), state: provisional.controllerState },
|
|
707
|
+
...peerPlans,
|
|
708
|
+
],
|
|
709
|
+
});
|
|
710
|
+
return createSignerlessControllerPlan({
|
|
711
|
+
...common,
|
|
712
|
+
shard,
|
|
713
|
+
deploymentGuardCommitment: guard,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function normalizedSignerlessShards(values) {
|
|
718
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > 8) {
|
|
719
|
+
throw new Error("signerless snapshot requires 1..8 ordered shards");
|
|
720
|
+
}
|
|
721
|
+
const shards = values.map((value) => ({
|
|
722
|
+
...snapshotShard(value),
|
|
723
|
+
controllerId: value?.controllerId
|
|
724
|
+
? requireHex32(value.controllerId, "controller id")
|
|
725
|
+
: undefined,
|
|
726
|
+
controllerSource: value?.controllerSource,
|
|
727
|
+
}));
|
|
728
|
+
shards.sort((left, right) => left.shardIndex - right.shardIndex);
|
|
729
|
+
if (shards.some((shard, index) => shard.shardIndex !== index)) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
"signerless snapshot shards must be contiguous from zero",
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return shards;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function portableInput(entry, network, script) {
|
|
738
|
+
return {
|
|
739
|
+
previousOutpoint: entry.outpoint,
|
|
740
|
+
utxo: entry,
|
|
741
|
+
networkId: network,
|
|
742
|
+
...(script ? { script } : {}),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function outpointOfSource(source, label) {
|
|
747
|
+
return {
|
|
748
|
+
transactionId: requireHex32(source?.txidHex, `${label} txid`),
|
|
749
|
+
index: unsignedInteger(source?.vout, `${label} vout`),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function buildBootstrapCancellation(input) {
|
|
754
|
+
const request = input.request || {};
|
|
755
|
+
const params = request.params || {};
|
|
756
|
+
const network = networkOf(request);
|
|
757
|
+
const walletAddress = walletAddressOf(request);
|
|
758
|
+
const artifacts = await readPinnedArtifacts(
|
|
759
|
+
params.artifactPins,
|
|
760
|
+
false,
|
|
761
|
+
true,
|
|
762
|
+
);
|
|
763
|
+
const template = templateParts(artifacts.kcc20);
|
|
764
|
+
const base = bootstrapBaseState(params, template);
|
|
765
|
+
const migrationId = requireHex32(params.migrationId, "migration ID");
|
|
766
|
+
const bootstrapId = requireHex32(params.bootstrapId, "bootstrap ID");
|
|
767
|
+
const phase = String(params.bootstrapPhase || "").toUpperCase();
|
|
768
|
+
if (!["UNBOUND", "TOKEN_BOUND", "SEALED"].includes(phase)) {
|
|
769
|
+
throw new Error(
|
|
770
|
+
"bootstrap cancellation phase is not terminally cancellable",
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
const bound = phase !== "UNBOUND";
|
|
774
|
+
const tokenId = bound
|
|
775
|
+
? requireHex32(params.tokenId, "token ID")
|
|
776
|
+
: ZERO_HASH;
|
|
777
|
+
const beforeState = {
|
|
778
|
+
...base,
|
|
779
|
+
phase: phase === "UNBOUND" ? 0n : phase === "TOKEN_BOUND" ? 1n : 2n,
|
|
780
|
+
tokenId: hexToBytes(tokenId),
|
|
781
|
+
shardPlanCommitment:
|
|
782
|
+
phase === "SEALED"
|
|
783
|
+
? hexToBytes(
|
|
784
|
+
requireHex32(
|
|
785
|
+
params.bootstrapShardPlanCommitment,
|
|
786
|
+
"bootstrap shard plan commitment",
|
|
787
|
+
),
|
|
788
|
+
)
|
|
789
|
+
: hexToBytes(ZERO_HASH),
|
|
790
|
+
activationDigest:
|
|
791
|
+
phase === "SEALED"
|
|
792
|
+
? hexToBytes(
|
|
793
|
+
requireHex32(
|
|
794
|
+
params.bootstrapActivationDigest,
|
|
795
|
+
"bootstrap activation digest",
|
|
796
|
+
),
|
|
797
|
+
)
|
|
798
|
+
: hexToBytes(ZERO_HASH),
|
|
799
|
+
shardCount:
|
|
800
|
+
phase === "SEALED"
|
|
801
|
+
? positiveI64(params.bootstrapShardCount, "bootstrap shard count")
|
|
802
|
+
: 0n,
|
|
803
|
+
};
|
|
804
|
+
if (beforeState.shardCount > 8n) {
|
|
805
|
+
throw new Error("bootstrap cancellation shard count exceeds policy");
|
|
806
|
+
}
|
|
807
|
+
const bootstrapScript = buildSnapshotBootstrapScriptForState(
|
|
808
|
+
artifacts.bootstrap,
|
|
809
|
+
beforeState,
|
|
810
|
+
);
|
|
811
|
+
const bootstrapSource = sourceDescriptor(
|
|
812
|
+
params.bootstrapSource,
|
|
813
|
+
"bootstrap source",
|
|
814
|
+
scriptAddress(bootstrapScript, network),
|
|
815
|
+
);
|
|
816
|
+
const tokenState = bound
|
|
817
|
+
? kcc20State(params.tokenSource?.state)
|
|
818
|
+
: undefined;
|
|
819
|
+
if (
|
|
820
|
+
tokenState &&
|
|
821
|
+
(tokenState.amount !== base.totalSupply ||
|
|
822
|
+
tokenState.ownerScheme !== 4 ||
|
|
823
|
+
bytesToHex(tokenState.owner) !== bootstrapId ||
|
|
824
|
+
bytesToHex(tokenState.extensionCommitment) !==
|
|
825
|
+
bytesToHex(base.holderExtensionCommitment))
|
|
826
|
+
) {
|
|
827
|
+
throw new Error("bootstrap cancellation token source does not match");
|
|
828
|
+
}
|
|
829
|
+
const tokenScript = tokenState
|
|
830
|
+
? buildKcc20ScriptForState(artifacts.kcc20, tokenState)
|
|
831
|
+
: undefined;
|
|
832
|
+
const tokenSource = tokenScript
|
|
833
|
+
? sourceDescriptor(
|
|
834
|
+
params.tokenSource,
|
|
835
|
+
"token source",
|
|
836
|
+
scriptAddress(tokenScript, network),
|
|
837
|
+
)
|
|
838
|
+
: undefined;
|
|
839
|
+
if (!bound && params.tokenSource != null) {
|
|
840
|
+
throw new Error(
|
|
841
|
+
"unbound bootstrap cancellation must not include a token",
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const sinkTokenState = tokenState
|
|
845
|
+
? {
|
|
846
|
+
amount: base.totalSupply,
|
|
847
|
+
owner: hexToBytes(SNAPSHOT_CANCEL_SINK_P2SH),
|
|
848
|
+
ownerScheme: 3,
|
|
849
|
+
borrowScheme: 0,
|
|
850
|
+
borrowGuard: hexToBytes(ZERO_HASH),
|
|
851
|
+
extensionCommitment: base.holderExtensionCommitment,
|
|
852
|
+
}
|
|
853
|
+
: undefined;
|
|
854
|
+
const sinkTokenScript = sinkTokenState
|
|
855
|
+
? buildKcc20ScriptForState(artifacts.kcc20, sinkTokenState)
|
|
856
|
+
: undefined;
|
|
857
|
+
const rpc = rpcFor(network);
|
|
858
|
+
await rpc.connect();
|
|
859
|
+
try {
|
|
860
|
+
const bootstrapPromise = rpc.getUtxosByAddresses({
|
|
861
|
+
addresses: [bootstrapSource.address],
|
|
862
|
+
});
|
|
863
|
+
const tokenPromise = tokenSource
|
|
864
|
+
? rpc.getUtxosByAddresses({ addresses: [tokenSource.address] })
|
|
865
|
+
: Promise.resolve({ entries: [] });
|
|
866
|
+
const [bootstrapUtxos, tokenUtxos, walletUtxos] = await Promise.all([
|
|
867
|
+
bootstrapPromise,
|
|
868
|
+
tokenPromise,
|
|
869
|
+
rpc.getUtxosByAddresses({ addresses: [walletAddress] }),
|
|
870
|
+
]);
|
|
871
|
+
const bootstrapEntry = findExactEntry(
|
|
872
|
+
bootstrapUtxos.entries,
|
|
873
|
+
bootstrapSource,
|
|
874
|
+
);
|
|
875
|
+
assertCovenantSource(bootstrapEntry, {
|
|
876
|
+
covenantId: bootstrapId,
|
|
877
|
+
script: bootstrapScript,
|
|
878
|
+
amount: base.bootstrapCellKas,
|
|
879
|
+
label: "bootstrap cancellation source",
|
|
880
|
+
});
|
|
881
|
+
const tokenEntry = tokenSource
|
|
882
|
+
? findExactEntry(tokenUtxos.entries, tokenSource)
|
|
883
|
+
: undefined;
|
|
884
|
+
if (tokenEntry) {
|
|
885
|
+
assertCovenantSource(tokenEntry, {
|
|
886
|
+
covenantId: tokenId,
|
|
887
|
+
script: tokenScript,
|
|
888
|
+
amount: base.tokenCellKas,
|
|
889
|
+
label: "bootstrap cancellation token",
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const consumed = [bootstrapEntry, ...(tokenEntry ? [tokenEntry] : [])];
|
|
893
|
+
const fundingEntry = selectFundingEntry(
|
|
894
|
+
withoutOutpoints(
|
|
895
|
+
walletUtxos.entries,
|
|
896
|
+
consumed.map(({ outpoint }) => outpoint),
|
|
897
|
+
),
|
|
898
|
+
priorityFee() + 10_000n,
|
|
899
|
+
[],
|
|
900
|
+
params.fundingOutpoint,
|
|
901
|
+
);
|
|
902
|
+
const outputs = [];
|
|
903
|
+
if (sinkTokenScript) {
|
|
904
|
+
outputs.push(
|
|
905
|
+
new kaspaWasm.TransactionOutput(
|
|
906
|
+
base.tokenCellKas,
|
|
907
|
+
kaspaWasm.payToScriptHashScript(sinkTokenScript),
|
|
908
|
+
new kaspaWasm.CovenantBinding(1, new kaspaWasm.Hash(tokenId)),
|
|
909
|
+
),
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
const sinkOutputIndex = outputs.length;
|
|
913
|
+
outputs.push(
|
|
914
|
+
new kaspaWasm.TransactionOutput(
|
|
915
|
+
base.bootstrapCellKas,
|
|
916
|
+
kaspaWasm.payToScriptHashScript(Uint8Array.of(0x6a)),
|
|
917
|
+
),
|
|
918
|
+
);
|
|
919
|
+
const fundingChangeOutputIndex = appendFundingChange(
|
|
920
|
+
outputs,
|
|
921
|
+
[...consumed, fundingEntry],
|
|
922
|
+
walletAddress,
|
|
923
|
+
base.bootstrapCellKas + (bound ? base.tokenCellKas : 0n),
|
|
924
|
+
priorityFee(),
|
|
925
|
+
);
|
|
926
|
+
const inputs = [
|
|
927
|
+
covenantInput(bootstrapEntry, BOOTSTRAP_COMPUTE_BUDGET),
|
|
928
|
+
...(tokenEntry
|
|
929
|
+
? [covenantInput(tokenEntry, KCC20_COMPUTE_BUDGET)]
|
|
930
|
+
: []),
|
|
931
|
+
publicInput(fundingEntry, PUBLIC_INPUT_COMPUTE_BUDGET),
|
|
932
|
+
];
|
|
933
|
+
const transaction = new kaspaWasm.Transaction({
|
|
934
|
+
version: 1,
|
|
935
|
+
lockTime: 0n,
|
|
936
|
+
inputs,
|
|
937
|
+
outputs,
|
|
938
|
+
subnetworkId: NATIVE_SUBNETWORK_ID,
|
|
939
|
+
gas: 0n,
|
|
940
|
+
payload: "",
|
|
941
|
+
});
|
|
942
|
+
setVersionOneInputMassFields(transaction, PUBLIC_INPUT_COMPUTE_BUDGET);
|
|
943
|
+
transaction.inputs[0].computeBudget = BOOTSTRAP_COMPUTE_BUDGET;
|
|
944
|
+
if (tokenEntry) {
|
|
945
|
+
transaction.inputs[1].computeBudget = KCC20_COMPUTE_BUDGET;
|
|
946
|
+
}
|
|
947
|
+
const recipes = bootstrapApprovalRecipes(
|
|
948
|
+
bytesToHex(bootstrapScript),
|
|
949
|
+
bootstrapCancellationArgs(
|
|
950
|
+
artifacts,
|
|
951
|
+
bound,
|
|
952
|
+
sinkOutputIndex,
|
|
953
|
+
sinkTokenState,
|
|
954
|
+
),
|
|
955
|
+
);
|
|
956
|
+
transaction.inputs[0].signatureScript = encodeP2sh(
|
|
957
|
+
signatureScriptArgsBytes(recipes.placeholder.args),
|
|
958
|
+
bootstrapScript,
|
|
959
|
+
);
|
|
960
|
+
if (tokenEntry && tokenScript && sinkTokenState) {
|
|
961
|
+
transaction.inputs[1].signatureScript = encodeP2sh(
|
|
962
|
+
buildKcc20TransferSigScript(kaspaWasm, artifacts.kcc20, {
|
|
963
|
+
nextStates: [sinkTokenState],
|
|
964
|
+
witness: Uint8Array.of(0),
|
|
965
|
+
}),
|
|
966
|
+
tokenScript,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
const fundingInputIndex = inputs.length - 1;
|
|
970
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
971
|
+
kaspaWasm,
|
|
972
|
+
transaction,
|
|
973
|
+
fundingChangeOutputIndex,
|
|
974
|
+
network,
|
|
975
|
+
"snapshot bootstrap cancellation",
|
|
976
|
+
[recipes.approval],
|
|
977
|
+
);
|
|
978
|
+
return outputEnvelope(
|
|
979
|
+
adjusted,
|
|
980
|
+
[{ index: fundingInputIndex }],
|
|
981
|
+
[recipes.approval],
|
|
982
|
+
{
|
|
983
|
+
migrationId,
|
|
984
|
+
stage: "bootstrap-cancel",
|
|
985
|
+
bootstrapId,
|
|
986
|
+
...(bound ? { tokenId } : {}),
|
|
987
|
+
cancellationMode: bound ? "BOUND" : "UNBOUND",
|
|
988
|
+
cancellationSinkP2sh: SNAPSHOT_CANCEL_SINK_P2SH,
|
|
989
|
+
bootstrapState: {
|
|
990
|
+
...bootstrapStateReceipt(beforeState),
|
|
991
|
+
phase,
|
|
992
|
+
},
|
|
993
|
+
bootstrapOutcome: "CANCELLED",
|
|
994
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
995
|
+
intentInputs: [
|
|
996
|
+
{
|
|
997
|
+
role: "snapshot-bootstrap",
|
|
998
|
+
index: 0,
|
|
999
|
+
covenantId: bootstrapId,
|
|
1000
|
+
revealScriptHex: bytesToHex(bootstrapScript),
|
|
1001
|
+
},
|
|
1002
|
+
...(tokenEntry && tokenScript
|
|
1003
|
+
? [
|
|
1004
|
+
{
|
|
1005
|
+
role: "fixed-supply-token",
|
|
1006
|
+
index: 1,
|
|
1007
|
+
covenantId: tokenId,
|
|
1008
|
+
revealScriptHex: bytesToHex(tokenScript),
|
|
1009
|
+
staticSignatureScriptHex: signatureScriptHex(adjusted, 1),
|
|
1010
|
+
},
|
|
1011
|
+
]
|
|
1012
|
+
: []),
|
|
1013
|
+
{ role: "creator-funding", index: fundingInputIndex },
|
|
1014
|
+
],
|
|
1015
|
+
intentOutputs: [
|
|
1016
|
+
...(sinkTokenScript && sinkTokenState
|
|
1017
|
+
? [
|
|
1018
|
+
{
|
|
1019
|
+
role: "cancelled-token",
|
|
1020
|
+
index: 0,
|
|
1021
|
+
covenantId: tokenId,
|
|
1022
|
+
revealScriptHex: bytesToHex(sinkTokenScript),
|
|
1023
|
+
owner: SNAPSHOT_CANCEL_SINK_P2SH,
|
|
1024
|
+
ownerScheme: 3,
|
|
1025
|
+
borrowScheme: 0,
|
|
1026
|
+
borrowGuard: ZERO_HASH,
|
|
1027
|
+
extensionCommitment: bytesToHex(
|
|
1028
|
+
sinkTokenState.extensionCommitment,
|
|
1029
|
+
),
|
|
1030
|
+
tokenAmount: sinkTokenState.amount.toString(),
|
|
1031
|
+
},
|
|
1032
|
+
]
|
|
1033
|
+
: []),
|
|
1034
|
+
{
|
|
1035
|
+
role: "cancellation-sink",
|
|
1036
|
+
index: sinkOutputIndex,
|
|
1037
|
+
revealScriptHex: "6a",
|
|
1038
|
+
},
|
|
1039
|
+
],
|
|
1040
|
+
},
|
|
1041
|
+
);
|
|
1042
|
+
} finally {
|
|
1043
|
+
await rpc.disconnect();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function buildBootstrapBind(input) {
|
|
1048
|
+
const request = input.request || {};
|
|
1049
|
+
const params = request.params || {};
|
|
1050
|
+
const network = networkOf(request);
|
|
1051
|
+
const walletAddress = walletAddressOf(request);
|
|
1052
|
+
const artifacts = await readPinnedArtifacts(
|
|
1053
|
+
params.artifactPins,
|
|
1054
|
+
false,
|
|
1055
|
+
true,
|
|
1056
|
+
);
|
|
1057
|
+
const template = templateParts(artifacts.kcc20);
|
|
1058
|
+
const base = bootstrapBaseState(params, template);
|
|
1059
|
+
const bootstrapId = requireHex32(params.bootstrapId, "bootstrap ID");
|
|
1060
|
+
const tokenId = requireHex32(params.tokenId, "token ID");
|
|
1061
|
+
const tokenState = kcc20State(params.tokenSource?.state);
|
|
1062
|
+
if (
|
|
1063
|
+
tokenState.amount !== base.totalSupply ||
|
|
1064
|
+
tokenState.ownerScheme !== 4 ||
|
|
1065
|
+
bytesToHex(tokenState.owner) !== bootstrapId ||
|
|
1066
|
+
bytesToHex(tokenState.extensionCommitment) !==
|
|
1067
|
+
bytesToHex(base.holderExtensionCommitment)
|
|
1068
|
+
) {
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
"bootstrap bind token source does not match the migration",
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
const beforeState = {
|
|
1074
|
+
...base,
|
|
1075
|
+
phase: 0n,
|
|
1076
|
+
tokenId: hexToBytes(ZERO_HASH),
|
|
1077
|
+
shardPlanCommitment: hexToBytes(ZERO_HASH),
|
|
1078
|
+
activationDigest: hexToBytes(ZERO_HASH),
|
|
1079
|
+
shardCount: 0n,
|
|
1080
|
+
};
|
|
1081
|
+
const afterState = {
|
|
1082
|
+
...base,
|
|
1083
|
+
phase: 1n,
|
|
1084
|
+
tokenId: hexToBytes(tokenId),
|
|
1085
|
+
shardPlanCommitment: hexToBytes(ZERO_HASH),
|
|
1086
|
+
activationDigest: hexToBytes(ZERO_HASH),
|
|
1087
|
+
shardCount: 0n,
|
|
1088
|
+
};
|
|
1089
|
+
const bootstrapScript = buildSnapshotBootstrapScriptForState(
|
|
1090
|
+
artifacts.bootstrap,
|
|
1091
|
+
beforeState,
|
|
1092
|
+
);
|
|
1093
|
+
const nextBootstrapScript = buildSnapshotBootstrapScriptForState(
|
|
1094
|
+
artifacts.bootstrap,
|
|
1095
|
+
afterState,
|
|
1096
|
+
);
|
|
1097
|
+
const tokenScript = buildKcc20ScriptForState(artifacts.kcc20, tokenState);
|
|
1098
|
+
const bootstrapSource = sourceDescriptor(
|
|
1099
|
+
params.bootstrapSource,
|
|
1100
|
+
"bootstrap source",
|
|
1101
|
+
scriptAddress(bootstrapScript, network),
|
|
1102
|
+
);
|
|
1103
|
+
const tokenSource = sourceDescriptor(
|
|
1104
|
+
params.tokenSource,
|
|
1105
|
+
"token source",
|
|
1106
|
+
scriptAddress(tokenScript, network),
|
|
1107
|
+
);
|
|
1108
|
+
const rpc = rpcFor(network);
|
|
1109
|
+
await rpc.connect();
|
|
1110
|
+
try {
|
|
1111
|
+
const [bootstrapUtxos, tokenUtxos, walletUtxos] = await Promise.all([
|
|
1112
|
+
rpc.getUtxosByAddresses({ addresses: [bootstrapSource.address] }),
|
|
1113
|
+
rpc.getUtxosByAddresses({ addresses: [tokenSource.address] }),
|
|
1114
|
+
rpc.getUtxosByAddresses({ addresses: [walletAddress] }),
|
|
1115
|
+
]);
|
|
1116
|
+
const bootstrapEntry = findExactEntry(
|
|
1117
|
+
bootstrapUtxos.entries,
|
|
1118
|
+
bootstrapSource,
|
|
1119
|
+
);
|
|
1120
|
+
assertCovenantSource(bootstrapEntry, {
|
|
1121
|
+
covenantId: bootstrapId,
|
|
1122
|
+
script: bootstrapScript,
|
|
1123
|
+
amount: base.bootstrapCellKas,
|
|
1124
|
+
label: "bootstrap bind source",
|
|
1125
|
+
});
|
|
1126
|
+
const tokenEntry = findExactEntry(tokenUtxos.entries, tokenSource);
|
|
1127
|
+
assertCovenantSource(tokenEntry, {
|
|
1128
|
+
covenantId: tokenId,
|
|
1129
|
+
script: tokenScript,
|
|
1130
|
+
amount: base.tokenCellKas,
|
|
1131
|
+
label: "bootstrap bind token source",
|
|
1132
|
+
});
|
|
1133
|
+
const fundingEntry = selectFundingEntry(
|
|
1134
|
+
withoutOutpoints(walletUtxos.entries, [
|
|
1135
|
+
bootstrapEntry.outpoint,
|
|
1136
|
+
tokenEntry.outpoint,
|
|
1137
|
+
]),
|
|
1138
|
+
priorityFee() + 10_000n,
|
|
1139
|
+
[],
|
|
1140
|
+
params.fundingOutpoint,
|
|
1141
|
+
);
|
|
1142
|
+
const outputs = [
|
|
1143
|
+
new kaspaWasm.TransactionOutput(
|
|
1144
|
+
base.tokenCellKas,
|
|
1145
|
+
kaspaWasm.payToScriptHashScript(tokenScript),
|
|
1146
|
+
new kaspaWasm.CovenantBinding(1, new kaspaWasm.Hash(tokenId)),
|
|
1147
|
+
),
|
|
1148
|
+
new kaspaWasm.TransactionOutput(
|
|
1149
|
+
base.bootstrapCellKas,
|
|
1150
|
+
kaspaWasm.payToScriptHashScript(nextBootstrapScript),
|
|
1151
|
+
new kaspaWasm.CovenantBinding(0, new kaspaWasm.Hash(bootstrapId)),
|
|
1152
|
+
),
|
|
1153
|
+
];
|
|
1154
|
+
const fundingChangeOutputIndex = appendFundingChange(
|
|
1155
|
+
outputs,
|
|
1156
|
+
[bootstrapEntry, tokenEntry, fundingEntry],
|
|
1157
|
+
walletAddress,
|
|
1158
|
+
base.tokenCellKas + base.bootstrapCellKas,
|
|
1159
|
+
priorityFee(),
|
|
1160
|
+
);
|
|
1161
|
+
const transaction = new kaspaWasm.Transaction({
|
|
1162
|
+
version: 1,
|
|
1163
|
+
lockTime: 0n,
|
|
1164
|
+
inputs: [
|
|
1165
|
+
covenantInput(bootstrapEntry, BOOTSTRAP_COMPUTE_BUDGET),
|
|
1166
|
+
covenantInput(tokenEntry, KCC20_COMPUTE_BUDGET),
|
|
1167
|
+
publicInput(fundingEntry, PUBLIC_INPUT_COMPUTE_BUDGET),
|
|
1168
|
+
],
|
|
1169
|
+
outputs,
|
|
1170
|
+
subnetworkId: NATIVE_SUBNETWORK_ID,
|
|
1171
|
+
gas: 0n,
|
|
1172
|
+
payload: "",
|
|
1173
|
+
});
|
|
1174
|
+
setVersionOneInputMassFields(transaction, PUBLIC_INPUT_COMPUTE_BUDGET);
|
|
1175
|
+
transaction.inputs[0].computeBudget = BOOTSTRAP_COMPUTE_BUDGET;
|
|
1176
|
+
transaction.inputs[1].computeBudget = KCC20_COMPUTE_BUDGET;
|
|
1177
|
+
const recipes = bootstrapApprovalRecipes(
|
|
1178
|
+
bytesToHex(bootstrapScript),
|
|
1179
|
+
bootstrapBindArgs(artifacts, afterState, tokenState),
|
|
1180
|
+
);
|
|
1181
|
+
transaction.inputs[0].signatureScript = encodeP2sh(
|
|
1182
|
+
signatureScriptArgsBytes(recipes.placeholder.args),
|
|
1183
|
+
bootstrapScript,
|
|
1184
|
+
);
|
|
1185
|
+
transaction.inputs[1].signatureScript = encodeP2sh(
|
|
1186
|
+
buildKcc20TransferSigScript(kaspaWasm, artifacts.kcc20, {
|
|
1187
|
+
nextStates: [tokenState],
|
|
1188
|
+
witness: Uint8Array.of(0),
|
|
1189
|
+
}),
|
|
1190
|
+
tokenScript,
|
|
1191
|
+
);
|
|
1192
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
1193
|
+
kaspaWasm,
|
|
1194
|
+
transaction,
|
|
1195
|
+
fundingChangeOutputIndex,
|
|
1196
|
+
network,
|
|
1197
|
+
"snapshot bootstrap bind",
|
|
1198
|
+
[recipes.approval],
|
|
1199
|
+
);
|
|
1200
|
+
return outputEnvelope(adjusted, [{ index: 2 }], [recipes.approval], {
|
|
1201
|
+
migrationId: requireHex32(params.migrationId, "migration ID"),
|
|
1202
|
+
stage: "bootstrap-bind",
|
|
1203
|
+
tokenId,
|
|
1204
|
+
bootstrapId,
|
|
1205
|
+
bootstrapState: {
|
|
1206
|
+
...bootstrapStateReceipt(afterState),
|
|
1207
|
+
phase: "TOKEN_BOUND",
|
|
1208
|
+
},
|
|
1209
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
1210
|
+
intentInputs: [
|
|
1211
|
+
{
|
|
1212
|
+
role: "snapshot-bootstrap",
|
|
1213
|
+
index: 0,
|
|
1214
|
+
covenantId: bootstrapId,
|
|
1215
|
+
revealScriptHex: bytesToHex(bootstrapScript),
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
role: "fixed-supply-token",
|
|
1219
|
+
index: 1,
|
|
1220
|
+
covenantId: tokenId,
|
|
1221
|
+
revealScriptHex: bytesToHex(tokenScript),
|
|
1222
|
+
staticSignatureScriptHex: signatureScriptHex(adjusted, 1),
|
|
1223
|
+
},
|
|
1224
|
+
{ role: "creator-funding", index: 2 },
|
|
1225
|
+
],
|
|
1226
|
+
intentOutputs: [
|
|
1227
|
+
{
|
|
1228
|
+
role: "fixed-supply-token",
|
|
1229
|
+
index: 0,
|
|
1230
|
+
covenantId: tokenId,
|
|
1231
|
+
revealScriptHex: bytesToHex(tokenScript),
|
|
1232
|
+
owner: bootstrapId,
|
|
1233
|
+
ownerScheme: 4,
|
|
1234
|
+
borrowScheme: 0,
|
|
1235
|
+
borrowGuard: bytesToHex(tokenState.borrowGuard),
|
|
1236
|
+
extensionCommitment: bytesToHex(tokenState.extensionCommitment),
|
|
1237
|
+
tokenAmount: tokenState.amount.toString(),
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
role: "snapshot-bootstrap",
|
|
1241
|
+
index: 1,
|
|
1242
|
+
covenantId: bootstrapId,
|
|
1243
|
+
revealScriptHex: bytesToHex(nextBootstrapScript),
|
|
1244
|
+
},
|
|
1245
|
+
],
|
|
1246
|
+
});
|
|
1247
|
+
} finally {
|
|
1248
|
+
await rpc.disconnect();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
async function buildBootstrapSeal(input) {
|
|
1253
|
+
const request = input.request || {};
|
|
1254
|
+
const params = request.params || {};
|
|
1255
|
+
const network = networkOf(request);
|
|
1256
|
+
const walletAddress = walletAddressOf(request);
|
|
1257
|
+
const artifacts = await readPinnedArtifacts(
|
|
1258
|
+
params.artifactPins,
|
|
1259
|
+
false,
|
|
1260
|
+
true,
|
|
1261
|
+
);
|
|
1262
|
+
const template = templateParts(artifacts.kcc20);
|
|
1263
|
+
const base = bootstrapBaseState(params, template);
|
|
1264
|
+
const migrationId = requireHex32(params.migrationId, "migration ID");
|
|
1265
|
+
const bootstrapId = requireHex32(params.bootstrapId, "bootstrap ID");
|
|
1266
|
+
const tokenId = requireHex32(params.tokenId, "token ID");
|
|
1267
|
+
const shards = reserveShards(params.shards, base.totalSupply);
|
|
1268
|
+
const shardPlanCommitment = snapshotShardPlanCommitment({
|
|
1269
|
+
migrationCommitment: migrationId,
|
|
1270
|
+
manifestCommitment: bytesToHex(base.manifestCommitment),
|
|
1271
|
+
tokenId,
|
|
1272
|
+
totalSupply: base.totalSupply,
|
|
1273
|
+
shards,
|
|
1274
|
+
});
|
|
1275
|
+
const beforeState = {
|
|
1276
|
+
...base,
|
|
1277
|
+
phase: 1n,
|
|
1278
|
+
tokenId: hexToBytes(tokenId),
|
|
1279
|
+
shardPlanCommitment: hexToBytes(ZERO_HASH),
|
|
1280
|
+
activationDigest: hexToBytes(ZERO_HASH),
|
|
1281
|
+
shardCount: 0n,
|
|
1282
|
+
};
|
|
1283
|
+
const afterState = {
|
|
1284
|
+
...base,
|
|
1285
|
+
phase: 2n,
|
|
1286
|
+
tokenId: hexToBytes(tokenId),
|
|
1287
|
+
shardPlanCommitment: hexToBytes(shardPlanCommitment),
|
|
1288
|
+
activationDigest: hexToBytes(
|
|
1289
|
+
requireHex32(params.activationDigest, "activation digest"),
|
|
1290
|
+
),
|
|
1291
|
+
shardCount: BigInt(shards.length),
|
|
1292
|
+
};
|
|
1293
|
+
const bootstrapScript = buildSnapshotBootstrapScriptForState(
|
|
1294
|
+
artifacts.bootstrap,
|
|
1295
|
+
beforeState,
|
|
1296
|
+
);
|
|
1297
|
+
const nextBootstrapScript = buildSnapshotBootstrapScriptForState(
|
|
1298
|
+
artifacts.bootstrap,
|
|
1299
|
+
afterState,
|
|
1300
|
+
);
|
|
1301
|
+
const bootstrapSource = sourceDescriptor(
|
|
1302
|
+
params.bootstrapSource,
|
|
1303
|
+
"bootstrap source",
|
|
1304
|
+
scriptAddress(bootstrapScript, network),
|
|
1305
|
+
);
|
|
1306
|
+
const rpc = rpcFor(network);
|
|
1307
|
+
await rpc.connect();
|
|
1308
|
+
try {
|
|
1309
|
+
const [bootstrapUtxos, walletUtxos] = await Promise.all([
|
|
1310
|
+
rpc.getUtxosByAddresses({ addresses: [bootstrapSource.address] }),
|
|
1311
|
+
rpc.getUtxosByAddresses({ addresses: [walletAddress] }),
|
|
1312
|
+
]);
|
|
1313
|
+
const bootstrapEntry = findExactEntry(
|
|
1314
|
+
bootstrapUtxos.entries,
|
|
1315
|
+
bootstrapSource,
|
|
1316
|
+
);
|
|
1317
|
+
assertCovenantSource(bootstrapEntry, {
|
|
1318
|
+
covenantId: bootstrapId,
|
|
1319
|
+
script: bootstrapScript,
|
|
1320
|
+
amount: base.bootstrapCellKas,
|
|
1321
|
+
label: "bootstrap seal source",
|
|
1322
|
+
});
|
|
1323
|
+
const fundingEntry = selectFundingEntry(
|
|
1324
|
+
withoutOutpoints(walletUtxos.entries, [bootstrapEntry.outpoint]),
|
|
1325
|
+
priorityFee() + 10_000n,
|
|
1326
|
+
[],
|
|
1327
|
+
params.fundingOutpoint,
|
|
1328
|
+
);
|
|
1329
|
+
const outputs = [
|
|
1330
|
+
new kaspaWasm.TransactionOutput(
|
|
1331
|
+
base.bootstrapCellKas,
|
|
1332
|
+
kaspaWasm.payToScriptHashScript(nextBootstrapScript),
|
|
1333
|
+
new kaspaWasm.CovenantBinding(0, new kaspaWasm.Hash(bootstrapId)),
|
|
1334
|
+
),
|
|
1335
|
+
];
|
|
1336
|
+
const fundingChangeOutputIndex = appendFundingChange(
|
|
1337
|
+
outputs,
|
|
1338
|
+
[bootstrapEntry, fundingEntry],
|
|
1339
|
+
walletAddress,
|
|
1340
|
+
base.bootstrapCellKas,
|
|
1341
|
+
priorityFee(),
|
|
1342
|
+
);
|
|
1343
|
+
const transaction = new kaspaWasm.Transaction({
|
|
1344
|
+
version: 1,
|
|
1345
|
+
lockTime: 0n,
|
|
1346
|
+
inputs: [
|
|
1347
|
+
covenantInput(bootstrapEntry, BOOTSTRAP_COMPUTE_BUDGET),
|
|
1348
|
+
publicInput(fundingEntry, PUBLIC_INPUT_COMPUTE_BUDGET),
|
|
1349
|
+
],
|
|
1350
|
+
outputs,
|
|
1351
|
+
subnetworkId: NATIVE_SUBNETWORK_ID,
|
|
1352
|
+
gas: 0n,
|
|
1353
|
+
payload: "",
|
|
1354
|
+
});
|
|
1355
|
+
setVersionOneInputMassFields(transaction, PUBLIC_INPUT_COMPUTE_BUDGET);
|
|
1356
|
+
transaction.inputs[0].computeBudget = BOOTSTRAP_COMPUTE_BUDGET;
|
|
1357
|
+
const recipes = bootstrapApprovalRecipes(
|
|
1358
|
+
bytesToHex(bootstrapScript),
|
|
1359
|
+
bootstrapSealArgs(artifacts, afterState),
|
|
1360
|
+
);
|
|
1361
|
+
transaction.inputs[0].signatureScript = encodeP2sh(
|
|
1362
|
+
signatureScriptArgsBytes(recipes.placeholder.args),
|
|
1363
|
+
bootstrapScript,
|
|
1364
|
+
);
|
|
1365
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
1366
|
+
kaspaWasm,
|
|
1367
|
+
transaction,
|
|
1368
|
+
fundingChangeOutputIndex,
|
|
1369
|
+
network,
|
|
1370
|
+
"snapshot bootstrap seal",
|
|
1371
|
+
[recipes.approval],
|
|
1372
|
+
);
|
|
1373
|
+
return outputEnvelope(adjusted, [{ index: 1 }], [recipes.approval], {
|
|
1374
|
+
migrationId,
|
|
1375
|
+
stage: "bootstrap-seal",
|
|
1376
|
+
tokenId,
|
|
1377
|
+
bootstrapId,
|
|
1378
|
+
bootstrapState: {
|
|
1379
|
+
...bootstrapStateReceipt(afterState),
|
|
1380
|
+
phase: "SEALED",
|
|
1381
|
+
},
|
|
1382
|
+
shardPlanCommitment,
|
|
1383
|
+
activationDigest: bytesToHex(afterState.activationDigest),
|
|
1384
|
+
shards: shards.map((shard) => shard.shardIndex),
|
|
1385
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
1386
|
+
intentInputs: [
|
|
1387
|
+
{
|
|
1388
|
+
role: "snapshot-bootstrap",
|
|
1389
|
+
index: 0,
|
|
1390
|
+
covenantId: bootstrapId,
|
|
1391
|
+
revealScriptHex: bytesToHex(bootstrapScript),
|
|
1392
|
+
},
|
|
1393
|
+
{ role: "creator-funding", index: 1 },
|
|
1394
|
+
],
|
|
1395
|
+
intentOutputs: [
|
|
1396
|
+
{
|
|
1397
|
+
role: "snapshot-bootstrap",
|
|
1398
|
+
index: 0,
|
|
1399
|
+
covenantId: bootstrapId,
|
|
1400
|
+
revealScriptHex: bytesToHex(nextBootstrapScript),
|
|
1401
|
+
},
|
|
1402
|
+
],
|
|
1403
|
+
});
|
|
1404
|
+
} finally {
|
|
1405
|
+
await rpc.disconnect();
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
async function buildBootstrapGenesis(input) {
|
|
1410
|
+
const request = input.request || {};
|
|
1411
|
+
const params = request.params || {};
|
|
1412
|
+
const network = networkOf(request);
|
|
1413
|
+
const walletAddress = walletAddressOf(request);
|
|
1414
|
+
const [kcc20, bootstrap] = await Promise.all([
|
|
1415
|
+
readPinnedArtifact(
|
|
1416
|
+
KCC20_ARTIFACT_PATH,
|
|
1417
|
+
params.artifactPins?.kcc20ArtifactSha256,
|
|
1418
|
+
"KCC20",
|
|
1419
|
+
),
|
|
1420
|
+
readPinnedArtifact(
|
|
1421
|
+
BOOTSTRAP_ARTIFACT_PATH,
|
|
1422
|
+
params.artifactPins?.bootstrapArtifactSha256,
|
|
1423
|
+
"KCC20SnapshotBootstrap",
|
|
1424
|
+
),
|
|
1425
|
+
]);
|
|
1426
|
+
const template = templateParts(kcc20);
|
|
1427
|
+
const expectedTemplateHash = requireHex32(
|
|
1428
|
+
params.expectedKcc20TemplateHash,
|
|
1429
|
+
"KCC20 template hash",
|
|
1430
|
+
);
|
|
1431
|
+
if (bytesToHex(template.hash) !== expectedTemplateHash) {
|
|
1432
|
+
throw new Error("KCC20 template hash does not match the migration pin");
|
|
1433
|
+
}
|
|
1434
|
+
const approvalKey = requireHex32(
|
|
1435
|
+
params.approvalKey ?? params.approvalKeyA,
|
|
1436
|
+
"approval key",
|
|
1437
|
+
);
|
|
1438
|
+
if (approvalKey === ZERO_HASH) {
|
|
1439
|
+
throw new Error("bootstrap approval key must be nonzero");
|
|
1440
|
+
}
|
|
1441
|
+
const state = {
|
|
1442
|
+
migrationCommitment: hexToBytes(
|
|
1443
|
+
requireHex32(params.migrationCommitment, "migration commitment"),
|
|
1444
|
+
),
|
|
1445
|
+
manifestCommitment: hexToBytes(
|
|
1446
|
+
requireHex32(params.manifestCommitment, "manifest commitment"),
|
|
1447
|
+
),
|
|
1448
|
+
holderExtensionCommitment: hexToBytes(
|
|
1449
|
+
requireHex32(
|
|
1450
|
+
params.holderExtensionCommitment,
|
|
1451
|
+
"holder extension commitment",
|
|
1452
|
+
),
|
|
1453
|
+
),
|
|
1454
|
+
totalSupply: positiveI64(params.totalSupply, "total supply"),
|
|
1455
|
+
claimNotBeforeDaaScore: positiveI64(
|
|
1456
|
+
params.claimNotBeforeDaaScore,
|
|
1457
|
+
"claim-not-before DAA score",
|
|
1458
|
+
),
|
|
1459
|
+
kcc20TemplateHash: hexToBytes(expectedTemplateHash),
|
|
1460
|
+
kcc20TemplatePrefixLen: BigInt(template.prefix.length),
|
|
1461
|
+
kcc20TemplateSuffixLen: BigInt(template.suffix.length),
|
|
1462
|
+
tokenCellKas: minimumCell(params.tokenCellKas, "token cell KAS"),
|
|
1463
|
+
reserveCellKas: minimumCell(params.reserveCellKas, "reserve cell KAS"),
|
|
1464
|
+
bootstrapCellKas: minimumCell(
|
|
1465
|
+
params.bootstrapCellKas,
|
|
1466
|
+
"bootstrap cell KAS",
|
|
1467
|
+
),
|
|
1468
|
+
approvalKey: hexToBytes(approvalKey),
|
|
1469
|
+
phase: 0n,
|
|
1470
|
+
tokenId: hexToBytes(ZERO_HASH),
|
|
1471
|
+
shardPlanCommitment: hexToBytes(ZERO_HASH),
|
|
1472
|
+
activationDigest: hexToBytes(ZERO_HASH),
|
|
1473
|
+
shardCount: 0n,
|
|
1474
|
+
};
|
|
1475
|
+
if (state.claimNotBeforeDaaScore >= 500_000_000_000n) {
|
|
1476
|
+
throw new Error("claim-not-before must use DAA-score locktime mode");
|
|
1477
|
+
}
|
|
1478
|
+
const bootstrapScript = buildSnapshotBootstrapScriptForState(
|
|
1479
|
+
bootstrap,
|
|
1480
|
+
state,
|
|
1481
|
+
);
|
|
1482
|
+
const rpc = rpcFor(network);
|
|
1483
|
+
await rpc.connect();
|
|
1484
|
+
try {
|
|
1485
|
+
const walletUtxos = await rpc.getUtxosByAddresses({
|
|
1486
|
+
addresses: [walletAddress],
|
|
1487
|
+
});
|
|
1488
|
+
const fundingEntry = selectFundingEntry(
|
|
1489
|
+
walletUtxos.entries,
|
|
1490
|
+
state.bootstrapCellKas + priorityFee() + 10_000n,
|
|
1491
|
+
[],
|
|
1492
|
+
params.fundingOutpoint,
|
|
1493
|
+
);
|
|
1494
|
+
const outputs = [
|
|
1495
|
+
new kaspaWasm.TransactionOutput(
|
|
1496
|
+
state.bootstrapCellKas,
|
|
1497
|
+
kaspaWasm.payToScriptHashScript(bootstrapScript),
|
|
1498
|
+
),
|
|
1499
|
+
];
|
|
1500
|
+
const changeIndex = appendFundingChange(
|
|
1501
|
+
outputs,
|
|
1502
|
+
[fundingEntry],
|
|
1503
|
+
walletAddress,
|
|
1504
|
+
state.bootstrapCellKas,
|
|
1505
|
+
priorityFee(),
|
|
1506
|
+
);
|
|
1507
|
+
const stateReceipt = bootstrapStateReceipt(state);
|
|
1508
|
+
const transaction = new kaspaWasm.Transaction({
|
|
1509
|
+
version: 1,
|
|
1510
|
+
lockTime: 0n,
|
|
1511
|
+
inputs: [publicInput(fundingEntry, PUBLIC_INPUT_COMPUTE_BUDGET)],
|
|
1512
|
+
outputs,
|
|
1513
|
+
subnetworkId: NATIVE_SUBNETWORK_ID,
|
|
1514
|
+
gas: 0n,
|
|
1515
|
+
payload: bytesToHex(
|
|
1516
|
+
new TextEncoder().encode(
|
|
1517
|
+
JSON.stringify({
|
|
1518
|
+
tn10: {
|
|
1519
|
+
v: 1,
|
|
1520
|
+
tmpl: "KCC20SnapshotBootstrap",
|
|
1521
|
+
args: [
|
|
1522
|
+
{
|
|
1523
|
+
name: "bootstrapState",
|
|
1524
|
+
type: "object",
|
|
1525
|
+
value: stateReceipt,
|
|
1526
|
+
},
|
|
1527
|
+
],
|
|
1528
|
+
},
|
|
1529
|
+
}),
|
|
1530
|
+
),
|
|
1531
|
+
),
|
|
1532
|
+
});
|
|
1533
|
+
setVersionOneInputMassFields(transaction, PUBLIC_INPUT_COMPUTE_BUDGET);
|
|
1534
|
+
transaction.populateGenesisCovenants([
|
|
1535
|
+
new kaspaWasm.GenesisCovenantGroup(0, [0]),
|
|
1536
|
+
]);
|
|
1537
|
+
transaction.finalize();
|
|
1538
|
+
const covenantId = requireHex32(
|
|
1539
|
+
transaction.outputs[0]?.covenant?.covenantId?.toString(),
|
|
1540
|
+
"bootstrap covenant id",
|
|
1541
|
+
);
|
|
1542
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
1543
|
+
kaspaWasm,
|
|
1544
|
+
transaction,
|
|
1545
|
+
changeIndex,
|
|
1546
|
+
network,
|
|
1547
|
+
"snapshot bootstrap genesis",
|
|
1548
|
+
);
|
|
1549
|
+
return outputEnvelope(adjusted, [{ index: 0 }], [], {
|
|
1550
|
+
migrationId: requireHex32(params.migrationId, "migration id"),
|
|
1551
|
+
stage: "bootstrap-deploy",
|
|
1552
|
+
covenantId,
|
|
1553
|
+
bootstrapState: { ...stateReceipt, phase: "UNBOUND" },
|
|
1554
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
1555
|
+
intentInputs: [{ role: "creator-funding", index: 0 }],
|
|
1556
|
+
intentOutputs: [
|
|
1557
|
+
{
|
|
1558
|
+
role: "snapshot-bootstrap",
|
|
1559
|
+
index: 0,
|
|
1560
|
+
covenantId,
|
|
1561
|
+
revealScriptHex: bytesToHex(bootstrapScript),
|
|
1562
|
+
},
|
|
1563
|
+
],
|
|
1564
|
+
});
|
|
1565
|
+
} finally {
|
|
1566
|
+
await rpc.disconnect();
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
async function buildControllerGenesis(input) {
|
|
1571
|
+
const request = input.request || {};
|
|
1572
|
+
const params = request.params || {};
|
|
1573
|
+
const network = networkOf(request);
|
|
1574
|
+
const walletAddress = walletAddressOf(request);
|
|
1575
|
+
const artifacts = await readPinnedArtifacts(params.artifactPins, true);
|
|
1576
|
+
const definition = snapshotDefinition(params, artifacts);
|
|
1577
|
+
const controllerState = initialControllerState(definition);
|
|
1578
|
+
const controllerScript = buildSnapshotControllerScriptForState(
|
|
1579
|
+
artifacts.controller,
|
|
1580
|
+
controllerState,
|
|
1581
|
+
);
|
|
1582
|
+
const outputSompi = definition.controllerCellKas;
|
|
1583
|
+
const rpc = rpcFor(network);
|
|
1584
|
+
await rpc.connect();
|
|
1585
|
+
try {
|
|
1586
|
+
const walletUtxos = await rpc.getUtxosByAddresses({
|
|
1587
|
+
addresses: [walletAddress],
|
|
1588
|
+
});
|
|
1589
|
+
const fundingEntry = selectFundingEntry(
|
|
1590
|
+
walletUtxos.entries,
|
|
1591
|
+
outputSompi + priorityFee() + 10_000n,
|
|
1592
|
+
[],
|
|
1593
|
+
params.fundingOutpoint,
|
|
1594
|
+
);
|
|
1595
|
+
const outputs = [
|
|
1596
|
+
new kaspaWasm.TransactionOutput(
|
|
1597
|
+
outputSompi,
|
|
1598
|
+
kaspaWasm.payToScriptHashScript(controllerScript),
|
|
1599
|
+
),
|
|
1600
|
+
];
|
|
1601
|
+
const fundingChangeOutputIndex = appendFundingChange(
|
|
1602
|
+
outputs,
|
|
1603
|
+
[fundingEntry],
|
|
1604
|
+
walletAddress,
|
|
1605
|
+
outputSompi,
|
|
1606
|
+
priorityFee(),
|
|
1607
|
+
);
|
|
1608
|
+
const transaction = new kaspaWasm.Transaction({
|
|
1609
|
+
version: 1,
|
|
1610
|
+
lockTime: 0n,
|
|
1611
|
+
inputs: [publicInput(fundingEntry, PUBLIC_INPUT_COMPUTE_BUDGET)],
|
|
1612
|
+
outputs,
|
|
1613
|
+
subnetworkId: NATIVE_SUBNETWORK_ID,
|
|
1614
|
+
gas: 0n,
|
|
1615
|
+
payload: controllerDeploymentPayload(definition, controllerState),
|
|
1616
|
+
});
|
|
1617
|
+
setVersionOneInputMassFields(transaction, PUBLIC_INPUT_COMPUTE_BUDGET);
|
|
1618
|
+
transaction.populateGenesisCovenants([
|
|
1619
|
+
new kaspaWasm.GenesisCovenantGroup(0, [0]),
|
|
1620
|
+
]);
|
|
1621
|
+
transaction.finalize();
|
|
1622
|
+
const controllerId = requireHex32(
|
|
1623
|
+
transaction.outputs[0]?.covenant?.covenantId?.toString(),
|
|
1624
|
+
"controller covenant id",
|
|
1625
|
+
);
|
|
1626
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
1627
|
+
kaspaWasm,
|
|
1628
|
+
transaction,
|
|
1629
|
+
fundingChangeOutputIndex,
|
|
1630
|
+
network,
|
|
1631
|
+
"snapshot controller genesis",
|
|
1632
|
+
);
|
|
1633
|
+
return outputEnvelope(adjusted, [{ index: 0 }], [], {
|
|
1634
|
+
migrationId: definition.migrationId,
|
|
1635
|
+
stage: "controller-deploy",
|
|
1636
|
+
controllerId,
|
|
1637
|
+
shard: definition.shard.shardIndex,
|
|
1638
|
+
controllerStateCommitment:
|
|
1639
|
+
snapshotControllerStateCommitment(controllerState),
|
|
1640
|
+
holderExtensionCommitment: definition.holderExtensionCommitment,
|
|
1641
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
1642
|
+
intentInputs: [{ role: "creator-funding", index: 0 }],
|
|
1643
|
+
intentOutputs: [
|
|
1644
|
+
{
|
|
1645
|
+
role: "snapshot-controller",
|
|
1646
|
+
index: 0,
|
|
1647
|
+
shard: definition.shard.shardIndex,
|
|
1648
|
+
covenantId: controllerId,
|
|
1649
|
+
revealScriptHex: bytesToHex(controllerScript),
|
|
1650
|
+
},
|
|
1651
|
+
],
|
|
1652
|
+
});
|
|
1653
|
+
} finally {
|
|
1654
|
+
await rpc.disconnect();
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
async function buildReserveHandoff(input) {
|
|
1659
|
+
const request = input.request || {};
|
|
1660
|
+
const params = request.params || {};
|
|
1661
|
+
const network = networkOf(request);
|
|
1662
|
+
const walletAddress = walletAddressOf(request);
|
|
1663
|
+
const migrationId = requireHex32(params.migrationId, "migration id");
|
|
1664
|
+
const artifacts = await readPinnedArtifacts(
|
|
1665
|
+
params.artifactPins,
|
|
1666
|
+
false,
|
|
1667
|
+
true,
|
|
1668
|
+
);
|
|
1669
|
+
const bootstrapId = requireHex32(
|
|
1670
|
+
params.bootstrapId,
|
|
1671
|
+
"bootstrap covenant id",
|
|
1672
|
+
);
|
|
1673
|
+
const tokenId = requireHex32(params.tokenId, "token covenant id");
|
|
1674
|
+
const extensionCommitment = requireHex32(
|
|
1675
|
+
params.holderExtensionCommitment,
|
|
1676
|
+
"holder extension commitment",
|
|
1677
|
+
);
|
|
1678
|
+
const totalSupply = positiveI64(params.totalSupply, "total supply");
|
|
1679
|
+
const tokenState = kcc20State(params.tokenSource?.state);
|
|
1680
|
+
if (
|
|
1681
|
+
tokenState.amount !== totalSupply ||
|
|
1682
|
+
tokenState.ownerScheme !== 4 ||
|
|
1683
|
+
bytesToHex(tokenState.owner) !== bootstrapId ||
|
|
1684
|
+
bytesToHex(tokenState.extensionCommitment) !== extensionCommitment
|
|
1685
|
+
) {
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
"reserve handoff token source does not match the migration",
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
const shards = reserveShards(params.shards, totalSupply);
|
|
1691
|
+
const template = templateParts(artifacts.kcc20);
|
|
1692
|
+
const expectedTemplateHash = requireHex32(
|
|
1693
|
+
params.kcc20TemplateHash,
|
|
1694
|
+
"KCC20 template hash",
|
|
1695
|
+
);
|
|
1696
|
+
if (bytesToHex(template.hash) !== expectedTemplateHash) {
|
|
1697
|
+
throw new Error("KCC20 template hash does not match the migration pin");
|
|
1698
|
+
}
|
|
1699
|
+
const shardPlanCommitment = snapshotShardPlanCommitment({
|
|
1700
|
+
migrationCommitment: migrationId,
|
|
1701
|
+
manifestCommitment: requireHex32(
|
|
1702
|
+
params.manifestCommitment,
|
|
1703
|
+
"manifest commitment",
|
|
1704
|
+
),
|
|
1705
|
+
tokenId,
|
|
1706
|
+
totalSupply,
|
|
1707
|
+
shards,
|
|
1708
|
+
});
|
|
1709
|
+
if (
|
|
1710
|
+
shardPlanCommitment !==
|
|
1711
|
+
requireHex32(
|
|
1712
|
+
params.bootstrapShardPlanCommitment,
|
|
1713
|
+
"bootstrap shard plan commitment",
|
|
1714
|
+
)
|
|
1715
|
+
) {
|
|
1716
|
+
throw new Error("activation shards do not match the sealed bootstrap");
|
|
1717
|
+
}
|
|
1718
|
+
const bootstrapState = {
|
|
1719
|
+
migrationCommitment: hexToBytes(migrationId),
|
|
1720
|
+
manifestCommitment: hexToBytes(
|
|
1721
|
+
requireHex32(params.manifestCommitment, "manifest commitment"),
|
|
1722
|
+
),
|
|
1723
|
+
holderExtensionCommitment: hexToBytes(extensionCommitment),
|
|
1724
|
+
totalSupply,
|
|
1725
|
+
claimNotBeforeDaaScore: positiveI64(
|
|
1726
|
+
params.claimNotBeforeDaaScore,
|
|
1727
|
+
"claim-not-before DAA score",
|
|
1728
|
+
),
|
|
1729
|
+
kcc20TemplateHash: hexToBytes(expectedTemplateHash),
|
|
1730
|
+
kcc20TemplatePrefixLen: BigInt(template.prefix.length),
|
|
1731
|
+
kcc20TemplateSuffixLen: BigInt(template.suffix.length),
|
|
1732
|
+
tokenCellKas: minimumCell(params.tokenCellKas, "token cell KAS"),
|
|
1733
|
+
reserveCellKas: minimumCell(params.reserveCellKas, "reserve cell KAS"),
|
|
1734
|
+
bootstrapCellKas: minimumCell(
|
|
1735
|
+
params.bootstrapCellKas,
|
|
1736
|
+
"bootstrap cell KAS",
|
|
1737
|
+
),
|
|
1738
|
+
approvalKey: hexToBytes(
|
|
1739
|
+
requireHex32(params.approvalKey ?? params.approvalKeyA, "approval key"),
|
|
1740
|
+
),
|
|
1741
|
+
phase: 2n,
|
|
1742
|
+
tokenId: hexToBytes(tokenId),
|
|
1743
|
+
shardPlanCommitment: hexToBytes(shardPlanCommitment),
|
|
1744
|
+
activationDigest: hexToBytes(
|
|
1745
|
+
requireHex32(
|
|
1746
|
+
params.bootstrapActivationDigest,
|
|
1747
|
+
"bootstrap activation digest",
|
|
1748
|
+
),
|
|
1749
|
+
),
|
|
1750
|
+
shardCount: BigInt(shards.length),
|
|
1751
|
+
};
|
|
1752
|
+
const bootstrapScript = buildSnapshotBootstrapScriptForState(
|
|
1753
|
+
artifacts.bootstrap,
|
|
1754
|
+
bootstrapState,
|
|
1755
|
+
);
|
|
1756
|
+
const bootstrapAddress = scriptAddress(bootstrapScript, network);
|
|
1757
|
+
const bootstrapSource = sourceDescriptor(
|
|
1758
|
+
params.bootstrapSource,
|
|
1759
|
+
"bootstrap source",
|
|
1760
|
+
bootstrapAddress,
|
|
1761
|
+
);
|
|
1762
|
+
const tokenScript = buildKcc20ScriptForState(artifacts.kcc20, tokenState);
|
|
1763
|
+
const tokenAddress = scriptAddress(tokenScript, network);
|
|
1764
|
+
const tokenSource = sourceDescriptor(
|
|
1765
|
+
params.tokenSource,
|
|
1766
|
+
"token source",
|
|
1767
|
+
tokenAddress,
|
|
1768
|
+
);
|
|
1769
|
+
const nextStates = shards.map((shard) => ({
|
|
1770
|
+
amount: shard.allocationTotal,
|
|
1771
|
+
owner: hexToBytes(shard.controllerId),
|
|
1772
|
+
ownerScheme: 4,
|
|
1773
|
+
borrowScheme: 0,
|
|
1774
|
+
borrowGuard: hexToBytes(ZERO_HASH),
|
|
1775
|
+
extensionCommitment: hexToBytes(extensionCommitment),
|
|
1776
|
+
}));
|
|
1777
|
+
const rpc = rpcFor(network);
|
|
1778
|
+
await rpc.connect();
|
|
1779
|
+
try {
|
|
1780
|
+
const [bootstrapUtxos, tokenUtxos, walletUtxos] = await Promise.all([
|
|
1781
|
+
rpc.getUtxosByAddresses({ addresses: [bootstrapAddress] }),
|
|
1782
|
+
rpc.getUtxosByAddresses({ addresses: [tokenAddress] }),
|
|
1783
|
+
rpc.getUtxosByAddresses({ addresses: [walletAddress] }),
|
|
1784
|
+
]);
|
|
1785
|
+
const bootstrapEntry = findExactEntry(
|
|
1786
|
+
bootstrapUtxos.entries,
|
|
1787
|
+
bootstrapSource,
|
|
1788
|
+
);
|
|
1789
|
+
assertCovenantSource(bootstrapEntry, {
|
|
1790
|
+
covenantId: bootstrapId,
|
|
1791
|
+
script: bootstrapScript,
|
|
1792
|
+
amount: bootstrapState.bootstrapCellKas,
|
|
1793
|
+
label: "reserve activation bootstrap source",
|
|
1794
|
+
});
|
|
1795
|
+
const tokenEntry = findExactEntry(tokenUtxos.entries, tokenSource);
|
|
1796
|
+
assertCovenantSource(tokenEntry, {
|
|
1797
|
+
covenantId: tokenId,
|
|
1798
|
+
script: tokenScript,
|
|
1799
|
+
amount: bootstrapState.tokenCellKas,
|
|
1800
|
+
label: "reserve handoff token source",
|
|
1801
|
+
});
|
|
1802
|
+
const fundingEntry = selectFundingEntry(
|
|
1803
|
+
withoutOutpoints(walletUtxos.entries, [
|
|
1804
|
+
bootstrapEntry.outpoint,
|
|
1805
|
+
tokenEntry.outpoint,
|
|
1806
|
+
]),
|
|
1807
|
+
priorityFee() + DEFAULT_CELL_SOMPI * BigInt(shards.length) + 10_000n,
|
|
1808
|
+
[],
|
|
1809
|
+
params.fundingOutpoint,
|
|
1810
|
+
);
|
|
1811
|
+
const outputs = nextStates.map(
|
|
1812
|
+
(state) =>
|
|
1813
|
+
new kaspaWasm.TransactionOutput(
|
|
1814
|
+
positiveI64(
|
|
1815
|
+
params.reserveCellKas ?? DEFAULT_CELL_SOMPI,
|
|
1816
|
+
"reserve cell KAS",
|
|
1817
|
+
),
|
|
1818
|
+
kaspaWasm.payToScriptHashScript(
|
|
1819
|
+
buildKcc20ScriptForState(artifacts.kcc20, state),
|
|
1820
|
+
),
|
|
1821
|
+
new kaspaWasm.CovenantBinding(1, new kaspaWasm.Hash(tokenId)),
|
|
1822
|
+
),
|
|
1823
|
+
);
|
|
1824
|
+
const outputSompi = outputs.reduce(
|
|
1825
|
+
(sum, output) => sum + BigInt(output.value),
|
|
1826
|
+
0n,
|
|
1827
|
+
);
|
|
1828
|
+
const fundingChangeOutputIndex = appendFundingChange(
|
|
1829
|
+
outputs,
|
|
1830
|
+
[bootstrapEntry, tokenEntry, fundingEntry],
|
|
1831
|
+
walletAddress,
|
|
1832
|
+
outputSompi,
|
|
1833
|
+
priorityFee(),
|
|
1834
|
+
);
|
|
1835
|
+
const transaction = new kaspaWasm.Transaction({
|
|
1836
|
+
version: 1,
|
|
1837
|
+
lockTime: 0n,
|
|
1838
|
+
inputs: [
|
|
1839
|
+
covenantInput(bootstrapEntry, BOOTSTRAP_COMPUTE_BUDGET),
|
|
1840
|
+
covenantInput(tokenEntry, KCC20_COMPUTE_BUDGET),
|
|
1841
|
+
publicInput(fundingEntry, PUBLIC_INPUT_COMPUTE_BUDGET),
|
|
1842
|
+
],
|
|
1843
|
+
outputs,
|
|
1844
|
+
subnetworkId: NATIVE_SUBNETWORK_ID,
|
|
1845
|
+
gas: 0n,
|
|
1846
|
+
payload: "",
|
|
1847
|
+
});
|
|
1848
|
+
setVersionOneInputMassFields(transaction, PUBLIC_INPUT_COMPUTE_BUDGET);
|
|
1849
|
+
transaction.inputs[0].computeBudget = BOOTSTRAP_COMPUTE_BUDGET;
|
|
1850
|
+
transaction.inputs[1].computeBudget = KCC20_COMPUTE_BUDGET;
|
|
1851
|
+
const bootstrapPrefix = new kaspaWasm.ScriptBuilder();
|
|
1852
|
+
for (const field of encodeKcc20StateRecordArrays(nextStates)) {
|
|
1853
|
+
bootstrapPrefix.addData(field);
|
|
1854
|
+
}
|
|
1855
|
+
bootstrapPrefix.addData(template.prefix);
|
|
1856
|
+
bootstrapPrefix.addData(template.suffix);
|
|
1857
|
+
bootstrapPrefix.addData(dispatchTagFor(artifacts.bootstrap, "activate"));
|
|
1858
|
+
transaction.inputs[0].signatureScript = encodeP2sh(
|
|
1859
|
+
bootstrapPrefix.drain(),
|
|
1860
|
+
bootstrapScript,
|
|
1861
|
+
);
|
|
1862
|
+
transaction.inputs[1].signatureScript = encodeP2sh(
|
|
1863
|
+
buildKcc20TransferSigScript(kaspaWasm, artifacts.kcc20, {
|
|
1864
|
+
nextStates,
|
|
1865
|
+
witness: Uint8Array.of(0),
|
|
1866
|
+
}),
|
|
1867
|
+
tokenScript,
|
|
1868
|
+
);
|
|
1869
|
+
const scripts = [];
|
|
1870
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
1871
|
+
kaspaWasm,
|
|
1872
|
+
transaction,
|
|
1873
|
+
fundingChangeOutputIndex,
|
|
1874
|
+
network,
|
|
1875
|
+
"snapshot reserve handoff",
|
|
1876
|
+
scripts,
|
|
1877
|
+
);
|
|
1878
|
+
return outputEnvelope(adjusted, [{ index: 2 }], scripts, {
|
|
1879
|
+
migrationId,
|
|
1880
|
+
stage: "reserve-handoff",
|
|
1881
|
+
tokenId,
|
|
1882
|
+
bootstrapId,
|
|
1883
|
+
bootstrapState: {
|
|
1884
|
+
...bootstrapStateReceipt(bootstrapState),
|
|
1885
|
+
phase: "ACTIVATED",
|
|
1886
|
+
},
|
|
1887
|
+
shardPlanCommitment,
|
|
1888
|
+
activationDigest: bytesToHex(bootstrapState.activationDigest),
|
|
1889
|
+
shards: shards.map((shard) => shard.shardIndex),
|
|
1890
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
1891
|
+
intentInputs: [
|
|
1892
|
+
{
|
|
1893
|
+
role: "snapshot-bootstrap",
|
|
1894
|
+
index: 0,
|
|
1895
|
+
covenantId: bootstrapId,
|
|
1896
|
+
revealScriptHex: bytesToHex(bootstrapScript),
|
|
1897
|
+
staticSignatureScriptHex: signatureScriptHex(adjusted, 0),
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
role: "fixed-supply-token",
|
|
1901
|
+
index: 1,
|
|
1902
|
+
covenantId: tokenId,
|
|
1903
|
+
revealScriptHex: bytesToHex(tokenScript),
|
|
1904
|
+
staticSignatureScriptHex: signatureScriptHex(adjusted, 1),
|
|
1905
|
+
},
|
|
1906
|
+
{ role: "creator-funding", index: 2 },
|
|
1907
|
+
],
|
|
1908
|
+
intentOutputs: shards.map((shard, index) => ({
|
|
1909
|
+
role: "shard-reserve",
|
|
1910
|
+
index,
|
|
1911
|
+
shard: shard.shardIndex,
|
|
1912
|
+
covenantId: tokenId,
|
|
1913
|
+
revealScriptHex: bytesToHex(
|
|
1914
|
+
buildKcc20ScriptForState(artifacts.kcc20, nextStates[index]),
|
|
1915
|
+
),
|
|
1916
|
+
owner: shard.controllerId,
|
|
1917
|
+
ownerScheme: 4,
|
|
1918
|
+
borrowScheme: 0,
|
|
1919
|
+
borrowGuard: ZERO_HASH,
|
|
1920
|
+
extensionCommitment: bytesToHex(
|
|
1921
|
+
bootstrapState.holderExtensionCommitment,
|
|
1922
|
+
),
|
|
1923
|
+
tokenAmount: shard.allocationTotal.toString(),
|
|
1924
|
+
})),
|
|
1925
|
+
});
|
|
1926
|
+
} finally {
|
|
1927
|
+
await rpc.disconnect();
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
async function buildClaim(input) {
|
|
1932
|
+
const request = input.request || {};
|
|
1933
|
+
const params = request.params || {};
|
|
1934
|
+
const network = networkOf(request);
|
|
1935
|
+
const walletAddress = walletAddressOf(request);
|
|
1936
|
+
const owner = requireHex32(request.owner?.kcc20Owner, "claim owner");
|
|
1937
|
+
const artifacts = await readPinnedArtifacts(
|
|
1938
|
+
params.artifactPins,
|
|
1939
|
+
true,
|
|
1940
|
+
true,
|
|
1941
|
+
);
|
|
1942
|
+
const definition = snapshotDefinition(params, artifacts);
|
|
1943
|
+
const tokenId = definition.tokenId;
|
|
1944
|
+
const controllerId = requireHex32(params.controllerId, "controller id");
|
|
1945
|
+
const proof = claimProof(params.proof, definition.shard);
|
|
1946
|
+
if (proof.ownerScheme !== 0 && proof.ownerScheme !== 3) {
|
|
1947
|
+
throw new Error(
|
|
1948
|
+
"snapshot claim builder supports only P2PK Schnorr and P2SH entitlements",
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
if (proof.ownerScheme === 0 && proof.owner !== owner) {
|
|
1952
|
+
throw new Error("P2PK claim proof owner does not match the wallet owner");
|
|
1953
|
+
}
|
|
1954
|
+
const p2shAuthority =
|
|
1955
|
+
proof.ownerScheme === 3
|
|
1956
|
+
? p2shAuthorityDescriptor(
|
|
1957
|
+
params.p2shAuthority,
|
|
1958
|
+
proof.owner,
|
|
1959
|
+
network,
|
|
1960
|
+
"P2SH claim authority",
|
|
1961
|
+
)
|
|
1962
|
+
: null;
|
|
1963
|
+
const currentRoot = requireHex32(params.currentRoot, "current root");
|
|
1964
|
+
if (proof.previousRoot !== currentRoot) {
|
|
1965
|
+
throw new Error("claim proof is stale for the current shard root");
|
|
1966
|
+
}
|
|
1967
|
+
assertClaimProofRoots(proof, definition.snapshotContext, currentRoot);
|
|
1968
|
+
const currentReserveAmount = positiveI64(
|
|
1969
|
+
params.currentReserveAmount,
|
|
1970
|
+
"current reserve amount",
|
|
1971
|
+
);
|
|
1972
|
+
const currentClaimsProcessed = unsignedInteger(
|
|
1973
|
+
params.currentClaimsProcessed,
|
|
1974
|
+
"current claims processed",
|
|
1975
|
+
);
|
|
1976
|
+
const allShards = normalizedSignerlessShards(
|
|
1977
|
+
params.allShards ?? [params.shard],
|
|
1978
|
+
);
|
|
1979
|
+
const controllerGenesisPlan = signerlessControllerPlan(
|
|
1980
|
+
params,
|
|
1981
|
+
artifacts,
|
|
1982
|
+
definition.shard,
|
|
1983
|
+
allShards,
|
|
1984
|
+
);
|
|
1985
|
+
const claimPlan = buildSignerlessClaimPlan({
|
|
1986
|
+
migrationId: definition.migrationId,
|
|
1987
|
+
migrationCommitment: requireHex32(
|
|
1988
|
+
params.migrationCommitment ?? params.migrationId,
|
|
1989
|
+
"migration commitment",
|
|
1990
|
+
),
|
|
1991
|
+
controllerArtifact: artifacts.controller,
|
|
1992
|
+
bootstrapArtifact: artifacts.bootstrap,
|
|
1993
|
+
kcc20Artifact: artifacts.kcc20,
|
|
1994
|
+
controllerId,
|
|
1995
|
+
tokenId,
|
|
1996
|
+
holderExtensionCommitment: definition.holderExtensionCommitment,
|
|
1997
|
+
manifestCommitment: definition.manifestCommitment,
|
|
1998
|
+
context: definition.snapshotContext,
|
|
1999
|
+
shard: definition.shard,
|
|
2000
|
+
shardCount: allShards.length,
|
|
2001
|
+
deploymentGuardCommitment:
|
|
2002
|
+
controllerGenesisPlan.controllerState.deploymentGuardCommitment,
|
|
2003
|
+
currentRoot,
|
|
2004
|
+
currentReserveAmount,
|
|
2005
|
+
currentClaimsProcessed,
|
|
2006
|
+
claim: {
|
|
2007
|
+
globalIndex: proof.globalIndex,
|
|
2008
|
+
shardLeafIndex: proof.shardLeafIndex,
|
|
2009
|
+
ownerScheme: proof.ownerScheme,
|
|
2010
|
+
owner: proof.owner,
|
|
2011
|
+
amount: proof.amount,
|
|
2012
|
+
siblings: proof.siblings,
|
|
2013
|
+
},
|
|
2014
|
+
controllerCellKas: definition.controllerCellKas,
|
|
2015
|
+
reserveCellKas: definition.reserveCellKas,
|
|
2016
|
+
recipientMinCellKas: definition.recipientMinCellKas,
|
|
2017
|
+
reserveInputCellKas: definition.reserveCellKas,
|
|
2018
|
+
claimNotBeforeDaaScore: definition.claimNotBeforeDaaScore,
|
|
2019
|
+
});
|
|
2020
|
+
const nextReserveAmount = currentReserveAmount - proof.amount;
|
|
2021
|
+
const terminal = claimPlan.terminal === true;
|
|
2022
|
+
const controllerState = claimPlan.controllerStateBefore;
|
|
2023
|
+
const nextControllerState = claimPlan.controllerStateAfter;
|
|
2024
|
+
const reserveState = {
|
|
2025
|
+
amount: currentReserveAmount,
|
|
2026
|
+
owner: hexToBytes(controllerId),
|
|
2027
|
+
ownerScheme: 4,
|
|
2028
|
+
borrowScheme: 0,
|
|
2029
|
+
borrowGuard: hexToBytes(ZERO_HASH),
|
|
2030
|
+
extensionCommitment: hexToBytes(definition.holderExtensionCommitment),
|
|
2031
|
+
};
|
|
2032
|
+
const nextReserveState = terminal
|
|
2033
|
+
? null
|
|
2034
|
+
: { ...reserveState, amount: nextReserveAmount };
|
|
2035
|
+
const recipientState = {
|
|
2036
|
+
amount: proof.amount,
|
|
2037
|
+
owner: hexToBytes(proof.owner),
|
|
2038
|
+
ownerScheme: proof.ownerScheme,
|
|
2039
|
+
borrowScheme: 0,
|
|
2040
|
+
borrowGuard: hexToBytes(ZERO_HASH),
|
|
2041
|
+
extensionCommitment: hexToBytes(definition.holderExtensionCommitment),
|
|
2042
|
+
};
|
|
2043
|
+
const controllerScript = buildSignerlessControllerScript(
|
|
2044
|
+
artifacts.controller,
|
|
2045
|
+
controllerState,
|
|
2046
|
+
);
|
|
2047
|
+
const reserveScript = buildKcc20ScriptForState(
|
|
2048
|
+
artifacts.kcc20,
|
|
2049
|
+
reserveState,
|
|
2050
|
+
);
|
|
2051
|
+
const controllerSource = sourceDescriptor(
|
|
2052
|
+
params.controllerSource,
|
|
2053
|
+
"controller source",
|
|
2054
|
+
scriptAddress(controllerScript, network),
|
|
2055
|
+
);
|
|
2056
|
+
const reserveSource = sourceDescriptor(
|
|
2057
|
+
params.reserveSource,
|
|
2058
|
+
"reserve source",
|
|
2059
|
+
scriptAddress(reserveScript, network),
|
|
2060
|
+
);
|
|
2061
|
+
const rpc = rpcFor(network);
|
|
2062
|
+
await rpc.connect();
|
|
2063
|
+
try {
|
|
2064
|
+
const [controllerUtxos, reserveUtxos, walletUtxos, authorityUtxos] =
|
|
2065
|
+
await Promise.all([
|
|
2066
|
+
rpc.getUtxosByAddresses({ addresses: [controllerSource.address] }),
|
|
2067
|
+
rpc.getUtxosByAddresses({ addresses: [reserveSource.address] }),
|
|
2068
|
+
rpc.getUtxosByAddresses({ addresses: [walletAddress] }),
|
|
2069
|
+
...(p2shAuthority
|
|
2070
|
+
? [
|
|
2071
|
+
rpc.getUtxosByAddresses({
|
|
2072
|
+
addresses: [p2shAuthority.source.address],
|
|
2073
|
+
}),
|
|
2074
|
+
]
|
|
2075
|
+
: []),
|
|
2076
|
+
]);
|
|
2077
|
+
const controllerEntry = findExactEntry(
|
|
2078
|
+
controllerUtxos.entries,
|
|
2079
|
+
controllerSource,
|
|
2080
|
+
);
|
|
2081
|
+
const reserveEntry = findExactEntry(reserveUtxos.entries, reserveSource);
|
|
2082
|
+
const authorityEntry = p2shAuthority
|
|
2083
|
+
? findExactEntry(authorityUtxos.entries, p2shAuthority.source)
|
|
2084
|
+
: null;
|
|
2085
|
+
assertCovenantSource(controllerEntry, {
|
|
2086
|
+
covenantId: controllerId,
|
|
2087
|
+
script: controllerScript,
|
|
2088
|
+
amount: definition.controllerCellKas,
|
|
2089
|
+
label: "claim controller source",
|
|
2090
|
+
});
|
|
2091
|
+
assertCovenantSource(reserveEntry, {
|
|
2092
|
+
covenantId: tokenId,
|
|
2093
|
+
script: reserveScript,
|
|
2094
|
+
amount: definition.reserveCellKas,
|
|
2095
|
+
label: "claim reserve source",
|
|
2096
|
+
});
|
|
2097
|
+
if (p2shAuthority && authorityEntry) {
|
|
2098
|
+
assertScriptPublicKey(
|
|
2099
|
+
authorityEntry,
|
|
2100
|
+
p2shAuthority.scriptPublicKeyHex,
|
|
2101
|
+
"P2SH claim authority source",
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
const fundingEntry = selectFundingEntry(
|
|
2105
|
+
withoutOutpoints(walletUtxos.entries, [
|
|
2106
|
+
controllerEntry.outpoint,
|
|
2107
|
+
reserveEntry.outpoint,
|
|
2108
|
+
...(authorityEntry ? [authorityEntry.outpoint] : []),
|
|
2109
|
+
]),
|
|
2110
|
+
priorityFee() + definition.recipientMinCellKas + 10_000n,
|
|
2111
|
+
[],
|
|
2112
|
+
params.fundingOutpoint,
|
|
2113
|
+
);
|
|
2114
|
+
const tokenStates = [
|
|
2115
|
+
...(nextReserveState ? [nextReserveState] : []),
|
|
2116
|
+
recipientState,
|
|
2117
|
+
];
|
|
2118
|
+
const outputs = tokenStates.map((state, index) => {
|
|
2119
|
+
const recipientIndex = tokenStates.length - 1;
|
|
2120
|
+
const cellKas =
|
|
2121
|
+
index === recipientIndex && terminal
|
|
2122
|
+
? definition.reserveCellKas + definition.controllerCellKas
|
|
2123
|
+
: index === recipientIndex
|
|
2124
|
+
? definition.recipientMinCellKas
|
|
2125
|
+
: definition.reserveCellKas;
|
|
2126
|
+
return new kaspaWasm.TransactionOutput(
|
|
2127
|
+
cellKas,
|
|
2128
|
+
kaspaWasm.payToScriptHashScript(
|
|
2129
|
+
buildKcc20ScriptForState(artifacts.kcc20, state),
|
|
2130
|
+
),
|
|
2131
|
+
new kaspaWasm.CovenantBinding(1, new kaspaWasm.Hash(tokenId)),
|
|
2132
|
+
);
|
|
2133
|
+
});
|
|
2134
|
+
if (nextControllerState) {
|
|
2135
|
+
outputs.push(
|
|
2136
|
+
new kaspaWasm.TransactionOutput(
|
|
2137
|
+
definition.controllerCellKas,
|
|
2138
|
+
kaspaWasm.payToScriptHashScript(
|
|
2139
|
+
buildSignerlessControllerScript(
|
|
2140
|
+
artifacts.controller,
|
|
2141
|
+
nextControllerState,
|
|
2142
|
+
),
|
|
2143
|
+
),
|
|
2144
|
+
new kaspaWasm.CovenantBinding(0, new kaspaWasm.Hash(controllerId)),
|
|
2145
|
+
),
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
const authorityReturnIndex = authorityEntry ? outputs.length : null;
|
|
2149
|
+
if (authorityEntry) {
|
|
2150
|
+
outputs.push(
|
|
2151
|
+
new kaspaWasm.TransactionOutput(
|
|
2152
|
+
utxoAmountSompi(authorityEntry),
|
|
2153
|
+
authorityEntry.scriptPublicKey,
|
|
2154
|
+
),
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
const outputSompi = outputs.reduce(
|
|
2158
|
+
(sum, output) => sum + BigInt(output.value),
|
|
2159
|
+
0n,
|
|
2160
|
+
);
|
|
2161
|
+
const fundingChangeOutputIndex = appendFundingChange(
|
|
2162
|
+
outputs,
|
|
2163
|
+
[
|
|
2164
|
+
controllerEntry,
|
|
2165
|
+
reserveEntry,
|
|
2166
|
+
...(authorityEntry ? [authorityEntry] : []),
|
|
2167
|
+
fundingEntry,
|
|
2168
|
+
],
|
|
2169
|
+
walletAddress,
|
|
2170
|
+
outputSompi,
|
|
2171
|
+
priorityFee(),
|
|
2172
|
+
);
|
|
2173
|
+
const transaction = new kaspaWasm.Transaction({
|
|
2174
|
+
version: 1,
|
|
2175
|
+
lockTime: definition.claimNotBeforeDaaScore,
|
|
2176
|
+
inputs: [
|
|
2177
|
+
covenantInput(controllerEntry, CONTROLLER_COMPUTE_BUDGET),
|
|
2178
|
+
covenantInput(reserveEntry, KCC20_COMPUTE_BUDGET),
|
|
2179
|
+
...(authorityEntry
|
|
2180
|
+
? [publicInput(authorityEntry, PUBLIC_INPUT_COMPUTE_BUDGET)]
|
|
2181
|
+
: []),
|
|
2182
|
+
publicInput(fundingEntry, PUBLIC_INPUT_COMPUTE_BUDGET),
|
|
2183
|
+
],
|
|
2184
|
+
outputs,
|
|
2185
|
+
subnetworkId: NATIVE_SUBNETWORK_ID,
|
|
2186
|
+
gas: 0n,
|
|
2187
|
+
payload: "",
|
|
2188
|
+
});
|
|
2189
|
+
setVersionOneInputMassFields(transaction, PUBLIC_INPUT_COMPUTE_BUDGET);
|
|
2190
|
+
transaction.inputs[0].computeBudget = CONTROLLER_COMPUTE_BUDGET;
|
|
2191
|
+
transaction.inputs[1].computeBudget = KCC20_COMPUTE_BUDGET;
|
|
2192
|
+
for (let index = 2; index < transaction.inputs.length; index += 1) {
|
|
2193
|
+
transaction.inputs[index].computeBudget = PUBLIC_INPUT_COMPUTE_BUDGET;
|
|
2194
|
+
}
|
|
2195
|
+
transaction.inputs[0].sequence = 0n;
|
|
2196
|
+
const template = templateParts(artifacts.kcc20);
|
|
2197
|
+
const controllerArgs = controllerClaimOrderedArgs({
|
|
2198
|
+
controllerArtifact: artifacts.controller,
|
|
2199
|
+
proof,
|
|
2200
|
+
nextReserveState,
|
|
2201
|
+
recipientState,
|
|
2202
|
+
template,
|
|
2203
|
+
ownerWitness:
|
|
2204
|
+
proof.ownerScheme === 3
|
|
2205
|
+
? dataArg(Uint8Array.of(2))
|
|
2206
|
+
: { type: "signature" },
|
|
2207
|
+
});
|
|
2208
|
+
const controllerScriptHint = {
|
|
2209
|
+
inputIndex: 0,
|
|
2210
|
+
scriptHex: bytesToHex(controllerScript),
|
|
2211
|
+
signatureScript: { mode: "ordered-args", args: controllerArgs },
|
|
2212
|
+
};
|
|
2213
|
+
const reservePrefix = buildKcc20TransferSigScript(
|
|
2214
|
+
kaspaWasm,
|
|
2215
|
+
artifacts.kcc20,
|
|
2216
|
+
{
|
|
2217
|
+
nextStates: tokenStates,
|
|
2218
|
+
witness: Uint8Array.of(0),
|
|
2219
|
+
},
|
|
2220
|
+
);
|
|
2221
|
+
transaction.inputs[1].signatureScript = encodeP2sh(
|
|
2222
|
+
reservePrefix,
|
|
2223
|
+
reserveScript,
|
|
2224
|
+
);
|
|
2225
|
+
if (p2shAuthority && authorityEntry) {
|
|
2226
|
+
transaction.inputs[2].signatureScript = encodeP2sh(
|
|
2227
|
+
p2shAuthority.unlockPrefix,
|
|
2228
|
+
p2shAuthority.revealScript,
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
const adjusted = transactionWithCalculatedFeeChange(
|
|
2232
|
+
kaspaWasm,
|
|
2233
|
+
transaction,
|
|
2234
|
+
fundingChangeOutputIndex,
|
|
2235
|
+
network,
|
|
2236
|
+
"snapshot claim",
|
|
2237
|
+
[controllerScriptHint],
|
|
2238
|
+
);
|
|
2239
|
+
const recipientIndex = tokenStates.length - 1;
|
|
2240
|
+
const reserveOutput = nextReserveState
|
|
2241
|
+
? {
|
|
2242
|
+
role: "reserve-successor",
|
|
2243
|
+
index: 0,
|
|
2244
|
+
covenantId: tokenId,
|
|
2245
|
+
revealScriptHex: bytesToHex(
|
|
2246
|
+
buildKcc20ScriptForState(artifacts.kcc20, nextReserveState),
|
|
2247
|
+
),
|
|
2248
|
+
owner: controllerId,
|
|
2249
|
+
ownerScheme: 4,
|
|
2250
|
+
borrowScheme: 0,
|
|
2251
|
+
borrowGuard: ZERO_HASH,
|
|
2252
|
+
extensionCommitment: definition.holderExtensionCommitment,
|
|
2253
|
+
tokenAmount: nextReserveAmount.toString(),
|
|
2254
|
+
}
|
|
2255
|
+
: null;
|
|
2256
|
+
const recipientOutput = {
|
|
2257
|
+
role: "recipient",
|
|
2258
|
+
index: recipientIndex,
|
|
2259
|
+
covenantId: tokenId,
|
|
2260
|
+
revealScriptHex: bytesToHex(
|
|
2261
|
+
buildKcc20ScriptForState(artifacts.kcc20, recipientState),
|
|
2262
|
+
),
|
|
2263
|
+
owner: proof.owner,
|
|
2264
|
+
ownerScheme: proof.ownerScheme,
|
|
2265
|
+
borrowScheme: 0,
|
|
2266
|
+
borrowGuard: ZERO_HASH,
|
|
2267
|
+
extensionCommitment: definition.holderExtensionCommitment,
|
|
2268
|
+
tokenAmount: proof.amount.toString(),
|
|
2269
|
+
};
|
|
2270
|
+
const controllerOutput = nextControllerState
|
|
2271
|
+
? {
|
|
2272
|
+
role: "controller-successor",
|
|
2273
|
+
index: tokenStates.length,
|
|
2274
|
+
covenantId: controllerId,
|
|
2275
|
+
revealScriptHex: bytesToHex(
|
|
2276
|
+
buildSignerlessControllerScript(
|
|
2277
|
+
artifacts.controller,
|
|
2278
|
+
nextControllerState,
|
|
2279
|
+
),
|
|
2280
|
+
),
|
|
2281
|
+
}
|
|
2282
|
+
: null;
|
|
2283
|
+
const authorityReturnOutput =
|
|
2284
|
+
authorityEntry && p2shAuthority && authorityReturnIndex !== null
|
|
2285
|
+
? {
|
|
2286
|
+
role: "p2sh-authority-return",
|
|
2287
|
+
index: authorityReturnIndex,
|
|
2288
|
+
scriptPublicKey: p2shAuthority.scriptPublicKeyHex,
|
|
2289
|
+
valueSompi: utxoAmountSompi(authorityEntry).toString(),
|
|
2290
|
+
}
|
|
2291
|
+
: null;
|
|
2292
|
+
const fundingInputIndex = authorityEntry ? 3 : 2;
|
|
2293
|
+
return outputEnvelope(
|
|
2294
|
+
adjusted,
|
|
2295
|
+
[{ index: fundingInputIndex }],
|
|
2296
|
+
[controllerScriptHint],
|
|
2297
|
+
{
|
|
2298
|
+
migrationId: definition.migrationId,
|
|
2299
|
+
stage: "claim",
|
|
2300
|
+
tokenId,
|
|
2301
|
+
controllerId,
|
|
2302
|
+
shard: definition.shard.shardIndex,
|
|
2303
|
+
globalIndex: proof.globalIndex,
|
|
2304
|
+
terminal,
|
|
2305
|
+
recipient: proof.owner,
|
|
2306
|
+
recipientOwnerScheme: proof.ownerScheme,
|
|
2307
|
+
amount: proof.amount.toString(),
|
|
2308
|
+
previousRoot: currentRoot,
|
|
2309
|
+
nextRoot: proof.nextRoot,
|
|
2310
|
+
maximumFeeSompi: feeOf(adjusted).toString(),
|
|
2311
|
+
intentInputs: [
|
|
2312
|
+
{
|
|
2313
|
+
role: "controller",
|
|
2314
|
+
index: 0,
|
|
2315
|
+
covenantId: controllerId,
|
|
2316
|
+
revealScriptHex: bytesToHex(controllerScript),
|
|
2317
|
+
},
|
|
2318
|
+
{
|
|
2319
|
+
role: "reserve",
|
|
2320
|
+
index: 1,
|
|
2321
|
+
covenantId: tokenId,
|
|
2322
|
+
revealScriptHex: bytesToHex(reserveScript),
|
|
2323
|
+
staticSignatureScriptHex: signatureScriptHex(adjusted, 1),
|
|
2324
|
+
},
|
|
2325
|
+
...(authorityEntry && p2shAuthority
|
|
2326
|
+
? [
|
|
2327
|
+
{
|
|
2328
|
+
role: "p2sh-authority",
|
|
2329
|
+
index: 2,
|
|
2330
|
+
owner: proof.owner,
|
|
2331
|
+
revealScriptHex: bytesToHex(p2shAuthority.revealScript),
|
|
2332
|
+
scriptPublicKey: p2shAuthority.scriptPublicKeyHex,
|
|
2333
|
+
staticSignatureScriptHex: signatureScriptHex(adjusted, 2),
|
|
2334
|
+
},
|
|
2335
|
+
]
|
|
2336
|
+
: []),
|
|
2337
|
+
],
|
|
2338
|
+
intentOutputs: [
|
|
2339
|
+
...(reserveOutput ? [reserveOutput] : []),
|
|
2340
|
+
recipientOutput,
|
|
2341
|
+
...(controllerOutput ? [controllerOutput] : []),
|
|
2342
|
+
...(authorityReturnOutput ? [authorityReturnOutput] : []),
|
|
2343
|
+
],
|
|
2344
|
+
},
|
|
2345
|
+
);
|
|
2346
|
+
} finally {
|
|
2347
|
+
await rpc.disconnect();
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function snapshotDefinition(params, artifacts) {
|
|
2352
|
+
const migrationId = requireHex32(params.migrationId, "migration id");
|
|
2353
|
+
const tokenId = requireHex32(params.tokenId, "token covenant id");
|
|
2354
|
+
const holderExtensionCommitment = requireHex32(
|
|
2355
|
+
params.holderExtensionCommitment,
|
|
2356
|
+
"holder extension commitment",
|
|
2357
|
+
);
|
|
2358
|
+
const manifestCommitment = requireHex32(
|
|
2359
|
+
params.manifestCommitment,
|
|
2360
|
+
"manifest commitment",
|
|
2361
|
+
);
|
|
2362
|
+
const snapshotContext = requireHex32(
|
|
2363
|
+
params.snapshotContext,
|
|
2364
|
+
"snapshot context",
|
|
2365
|
+
);
|
|
2366
|
+
const claimNotBeforeDaaScore = positiveI64(
|
|
2367
|
+
params.claimNotBeforeDaaScore,
|
|
2368
|
+
"claim-not-before DAA score",
|
|
2369
|
+
);
|
|
2370
|
+
if (claimNotBeforeDaaScore >= 500_000_000_000n) {
|
|
2371
|
+
throw new Error("claim-not-before must use DAA-score locktime mode");
|
|
2372
|
+
}
|
|
2373
|
+
const shard = snapshotShard(params.shard);
|
|
2374
|
+
const controllerCellKas = minimumCell(
|
|
2375
|
+
params.controllerCellKas,
|
|
2376
|
+
"controller cell KAS",
|
|
2377
|
+
);
|
|
2378
|
+
const reserveCellKas = minimumCell(
|
|
2379
|
+
params.reserveCellKas,
|
|
2380
|
+
"reserve cell KAS",
|
|
2381
|
+
);
|
|
2382
|
+
const recipientMinCellKas = minimumCell(
|
|
2383
|
+
params.recipientMinCellKas,
|
|
2384
|
+
"recipient minimum cell KAS",
|
|
2385
|
+
);
|
|
2386
|
+
if (controllerCellKas + reserveCellKas < recipientMinCellKas) {
|
|
2387
|
+
throw new Error(
|
|
2388
|
+
"terminal cell disposition is below the recipient minimum",
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
const template = templateParts(artifacts.kcc20);
|
|
2392
|
+
const expectedTemplateHash = requireHex32(
|
|
2393
|
+
params.kcc20TemplateHash,
|
|
2394
|
+
"KCC20 template hash",
|
|
2395
|
+
);
|
|
2396
|
+
if (bytesToHex(template.hash) !== expectedTemplateHash) {
|
|
2397
|
+
throw new Error("KCC20 template hash does not match the migration pin");
|
|
2398
|
+
}
|
|
2399
|
+
return {
|
|
2400
|
+
migrationId,
|
|
2401
|
+
tokenId,
|
|
2402
|
+
holderExtensionCommitment,
|
|
2403
|
+
manifestCommitment,
|
|
2404
|
+
snapshotContext,
|
|
2405
|
+
claimNotBeforeDaaScore,
|
|
2406
|
+
shard,
|
|
2407
|
+
controllerCellKas,
|
|
2408
|
+
reserveCellKas,
|
|
2409
|
+
recipientMinCellKas,
|
|
2410
|
+
kcc20TemplateHash: expectedTemplateHash,
|
|
2411
|
+
kcc20TemplatePrefixLen: template.prefix.length,
|
|
2412
|
+
kcc20TemplateSuffixLen: template.suffix.length,
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function initialControllerState(definition) {
|
|
2417
|
+
return {
|
|
2418
|
+
tokenId: hexToBytes(definition.tokenId),
|
|
2419
|
+
holderExtensionCommitment: hexToBytes(
|
|
2420
|
+
definition.holderExtensionCommitment,
|
|
2421
|
+
),
|
|
2422
|
+
manifestCommitment: hexToBytes(definition.manifestCommitment),
|
|
2423
|
+
snapshotContext: hexToBytes(definition.snapshotContext),
|
|
2424
|
+
kcc20TemplateHash: hexToBytes(definition.kcc20TemplateHash),
|
|
2425
|
+
claimNotBeforeDaaScore: definition.claimNotBeforeDaaScore,
|
|
2426
|
+
shardIndex: BigInt(definition.shard.shardIndex),
|
|
2427
|
+
globalIndexStart: BigInt(definition.shard.globalIndexStart),
|
|
2428
|
+
holderCount: BigInt(definition.shard.holderCount),
|
|
2429
|
+
treeDepth: BigInt(definition.shard.depth),
|
|
2430
|
+
kcc20TemplatePrefixLen: BigInt(definition.kcc20TemplatePrefixLen),
|
|
2431
|
+
kcc20TemplateSuffixLen: BigInt(definition.kcc20TemplateSuffixLen),
|
|
2432
|
+
controllerCellKas: definition.controllerCellKas,
|
|
2433
|
+
reserveCellKas: definition.reserveCellKas,
|
|
2434
|
+
recipientMinCellKas: definition.recipientMinCellKas,
|
|
2435
|
+
remainingRoot: hexToBytes(definition.shard.initialRoot),
|
|
2436
|
+
remainingAmount: definition.shard.allocationTotal,
|
|
2437
|
+
claimsProcessed: 0n,
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function buildSnapshotControllerScriptForState(artifact, state) {
|
|
2442
|
+
const normalized = normalizeSilverScriptArtifact(artifact);
|
|
2443
|
+
const encoded = encodeControllerState(state);
|
|
2444
|
+
const start = normalized.state_layout?.start;
|
|
2445
|
+
const length = normalized.state_layout?.len;
|
|
2446
|
+
if (
|
|
2447
|
+
!Number.isInteger(start) ||
|
|
2448
|
+
!Number.isInteger(length) ||
|
|
2449
|
+
encoded.length !== length
|
|
2450
|
+
) {
|
|
2451
|
+
throw new Error("snapshot controller artifact state layout is invalid");
|
|
2452
|
+
}
|
|
2453
|
+
return concatBytes(
|
|
2454
|
+
Uint8Array.from(normalized.script).slice(0, start),
|
|
2455
|
+
encoded,
|
|
2456
|
+
Uint8Array.from(normalized.script).slice(start + length),
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function buildSnapshotBootstrapScriptForState(artifact, state) {
|
|
2461
|
+
const normalized = normalizeSilverScriptArtifact(artifact);
|
|
2462
|
+
const encoded = encodeBootstrapState(state);
|
|
2463
|
+
const start = normalized.state_layout?.start;
|
|
2464
|
+
const length = normalized.state_layout?.len;
|
|
2465
|
+
if (
|
|
2466
|
+
normalized.contract_name !== "KCC20SnapshotBootstrap" ||
|
|
2467
|
+
!Number.isInteger(start) ||
|
|
2468
|
+
!Number.isInteger(length) ||
|
|
2469
|
+
encoded.length !== length
|
|
2470
|
+
) {
|
|
2471
|
+
throw new Error("snapshot bootstrap artifact state layout is invalid");
|
|
2472
|
+
}
|
|
2473
|
+
return concatBytes(
|
|
2474
|
+
Uint8Array.from(normalized.script).slice(0, start),
|
|
2475
|
+
encoded,
|
|
2476
|
+
Uint8Array.from(normalized.script).slice(start + length),
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function encodeBootstrapState(state) {
|
|
2481
|
+
return concatBytes(
|
|
2482
|
+
pushData(state.migrationCommitment),
|
|
2483
|
+
pushData(state.manifestCommitment),
|
|
2484
|
+
pushData(state.holderExtensionCommitment),
|
|
2485
|
+
pushData(i64(state.totalSupply)),
|
|
2486
|
+
pushData(i64(state.claimNotBeforeDaaScore)),
|
|
2487
|
+
pushData(state.kcc20TemplateHash),
|
|
2488
|
+
pushData(i64(state.kcc20TemplatePrefixLen)),
|
|
2489
|
+
pushData(i64(state.kcc20TemplateSuffixLen)),
|
|
2490
|
+
pushData(i64(state.tokenCellKas)),
|
|
2491
|
+
pushData(i64(state.reserveCellKas)),
|
|
2492
|
+
pushData(i64(state.bootstrapCellKas)),
|
|
2493
|
+
pushData(state.approvalKey),
|
|
2494
|
+
pushData(i64(state.phase)),
|
|
2495
|
+
pushData(state.tokenId),
|
|
2496
|
+
pushData(state.shardPlanCommitment),
|
|
2497
|
+
pushData(state.activationDigest),
|
|
2498
|
+
pushData(i64(state.shardCount)),
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function bootstrapStateReceipt(state) {
|
|
2503
|
+
return {
|
|
2504
|
+
migrationCommitment: bytesToHex(state.migrationCommitment),
|
|
2505
|
+
manifestCommitment: bytesToHex(state.manifestCommitment),
|
|
2506
|
+
holderExtensionCommitment: bytesToHex(state.holderExtensionCommitment),
|
|
2507
|
+
totalSupply: state.totalSupply.toString(),
|
|
2508
|
+
claimNotBeforeDaaScore: state.claimNotBeforeDaaScore.toString(),
|
|
2509
|
+
kcc20TemplateHash: bytesToHex(state.kcc20TemplateHash),
|
|
2510
|
+
kcc20TemplatePrefixLen: state.kcc20TemplatePrefixLen.toString(),
|
|
2511
|
+
kcc20TemplateSuffixLen: state.kcc20TemplateSuffixLen.toString(),
|
|
2512
|
+
tokenCellKas: state.tokenCellKas.toString(),
|
|
2513
|
+
reserveCellKas: state.reserveCellKas.toString(),
|
|
2514
|
+
bootstrapCellKas: state.bootstrapCellKas.toString(),
|
|
2515
|
+
approvalKey: bytesToHex(state.approvalKey),
|
|
2516
|
+
phase: state.phase.toString(),
|
|
2517
|
+
tokenId: bytesToHex(state.tokenId),
|
|
2518
|
+
shardPlanCommitment: bytesToHex(state.shardPlanCommitment),
|
|
2519
|
+
activationDigest: bytesToHex(state.activationDigest),
|
|
2520
|
+
shardCount: state.shardCount.toString(),
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
function bootstrapBaseState(params, template) {
|
|
2525
|
+
const approvalKey = requireHex32(
|
|
2526
|
+
params.approvalKey ?? params.approvalKeyA,
|
|
2527
|
+
"approval key",
|
|
2528
|
+
);
|
|
2529
|
+
if (approvalKey === ZERO_HASH) {
|
|
2530
|
+
throw new Error("bootstrap approval key must be nonzero");
|
|
2531
|
+
}
|
|
2532
|
+
const expectedTemplateHash = requireHex32(
|
|
2533
|
+
params.kcc20TemplateHash,
|
|
2534
|
+
"KCC20 template hash",
|
|
2535
|
+
);
|
|
2536
|
+
if (bytesToHex(template.hash) !== expectedTemplateHash) {
|
|
2537
|
+
throw new Error("KCC20 template hash does not match the migration pin");
|
|
2538
|
+
}
|
|
2539
|
+
const claimNotBeforeDaaScore = positiveI64(
|
|
2540
|
+
params.claimNotBeforeDaaScore,
|
|
2541
|
+
"claim-not-before DAA score",
|
|
2542
|
+
);
|
|
2543
|
+
if (claimNotBeforeDaaScore >= 500_000_000_000n) {
|
|
2544
|
+
throw new Error("claim-not-before must use DAA-score locktime mode");
|
|
2545
|
+
}
|
|
2546
|
+
return {
|
|
2547
|
+
migrationCommitment: hexToBytes(
|
|
2548
|
+
requireHex32(params.migrationId, "migration ID"),
|
|
2549
|
+
),
|
|
2550
|
+
manifestCommitment: hexToBytes(
|
|
2551
|
+
requireHex32(params.manifestCommitment, "manifest commitment"),
|
|
2552
|
+
),
|
|
2553
|
+
holderExtensionCommitment: hexToBytes(
|
|
2554
|
+
requireHex32(
|
|
2555
|
+
params.holderExtensionCommitment,
|
|
2556
|
+
"holder extension commitment",
|
|
2557
|
+
),
|
|
2558
|
+
),
|
|
2559
|
+
totalSupply: positiveI64(params.totalSupply, "total supply"),
|
|
2560
|
+
claimNotBeforeDaaScore,
|
|
2561
|
+
kcc20TemplateHash: hexToBytes(expectedTemplateHash),
|
|
2562
|
+
kcc20TemplatePrefixLen: BigInt(template.prefix.length),
|
|
2563
|
+
kcc20TemplateSuffixLen: BigInt(template.suffix.length),
|
|
2564
|
+
tokenCellKas: minimumCell(params.tokenCellKas, "token cell KAS"),
|
|
2565
|
+
reserveCellKas: minimumCell(params.reserveCellKas, "reserve cell KAS"),
|
|
2566
|
+
bootstrapCellKas: minimumCell(
|
|
2567
|
+
params.bootstrapCellKas,
|
|
2568
|
+
"bootstrap cell KAS",
|
|
2569
|
+
),
|
|
2570
|
+
approvalKey: hexToBytes(approvalKey),
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function bootstrapStateArgs(state) {
|
|
2575
|
+
return [
|
|
2576
|
+
dataArg(state.migrationCommitment),
|
|
2577
|
+
dataArg(state.manifestCommitment),
|
|
2578
|
+
dataArg(state.holderExtensionCommitment),
|
|
2579
|
+
i64Arg(state.totalSupply),
|
|
2580
|
+
i64Arg(state.claimNotBeforeDaaScore),
|
|
2581
|
+
dataArg(state.kcc20TemplateHash),
|
|
2582
|
+
i64Arg(state.kcc20TemplatePrefixLen),
|
|
2583
|
+
i64Arg(state.kcc20TemplateSuffixLen),
|
|
2584
|
+
i64Arg(state.tokenCellKas),
|
|
2585
|
+
i64Arg(state.reserveCellKas),
|
|
2586
|
+
i64Arg(state.bootstrapCellKas),
|
|
2587
|
+
dataArg(state.approvalKey),
|
|
2588
|
+
i64Arg(state.phase),
|
|
2589
|
+
dataArg(state.tokenId),
|
|
2590
|
+
dataArg(state.shardPlanCommitment),
|
|
2591
|
+
dataArg(state.activationDigest),
|
|
2592
|
+
i64Arg(state.shardCount),
|
|
2593
|
+
];
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
function bootstrapBindArgs(artifacts, nextBootstrapState, tokenState) {
|
|
2597
|
+
const template = templateParts(artifacts.kcc20);
|
|
2598
|
+
return [
|
|
2599
|
+
...bootstrapStateArgs(nextBootstrapState),
|
|
2600
|
+
i64Arg(1),
|
|
2601
|
+
i64Arg(0),
|
|
2602
|
+
i64Arg(1),
|
|
2603
|
+
{ type: "approval" },
|
|
2604
|
+
dataArg(template.prefix),
|
|
2605
|
+
dataArg(template.suffix),
|
|
2606
|
+
...kcc20StateArgs(tokenState),
|
|
2607
|
+
dataArg(dispatchTagFor(artifacts.bootstrap, "bindToken")),
|
|
2608
|
+
];
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
function bootstrapSealArgs(artifacts, nextBootstrapState) {
|
|
2612
|
+
return [
|
|
2613
|
+
...bootstrapStateArgs(nextBootstrapState),
|
|
2614
|
+
i64Arg(0),
|
|
2615
|
+
{ type: "approval" },
|
|
2616
|
+
dataArg(dispatchTagFor(artifacts.bootstrap, "seal")),
|
|
2617
|
+
];
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
function bootstrapCancellationArgs(
|
|
2621
|
+
artifacts,
|
|
2622
|
+
bound,
|
|
2623
|
+
sinkOutputIndex,
|
|
2624
|
+
sinkTokenState,
|
|
2625
|
+
) {
|
|
2626
|
+
return [
|
|
2627
|
+
{ type: "approval" },
|
|
2628
|
+
i64Arg(sinkOutputIndex),
|
|
2629
|
+
...(bound
|
|
2630
|
+
? [
|
|
2631
|
+
i64Arg(0),
|
|
2632
|
+
dataArg(templateParts(artifacts.kcc20).prefix),
|
|
2633
|
+
dataArg(templateParts(artifacts.kcc20).suffix),
|
|
2634
|
+
...kcc20StateArgs(sinkTokenState),
|
|
2635
|
+
]
|
|
2636
|
+
: []),
|
|
2637
|
+
dataArg(
|
|
2638
|
+
dispatchTagFor(
|
|
2639
|
+
artifacts.bootstrap,
|
|
2640
|
+
bound ? "cancelBound" : "cancelUnbound",
|
|
2641
|
+
),
|
|
2642
|
+
),
|
|
2643
|
+
];
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
function bootstrapApprovalRecipes(scriptHex, args) {
|
|
2647
|
+
const zeroSignature = "00".repeat(65);
|
|
2648
|
+
const replace = (dynamicSlot) =>
|
|
2649
|
+
args.map((arg) => {
|
|
2650
|
+
if (arg.type === dynamicSlot) return { type: "signature" };
|
|
2651
|
+
if (arg.type === "approval") {
|
|
2652
|
+
return { type: "data", hex: zeroSignature };
|
|
2653
|
+
}
|
|
2654
|
+
return arg;
|
|
2655
|
+
});
|
|
2656
|
+
const script = (normalizedArgs) => ({
|
|
2657
|
+
inputIndex: 0,
|
|
2658
|
+
scriptHex,
|
|
2659
|
+
signatureScript: { mode: "ordered-args", args: normalizedArgs },
|
|
2660
|
+
});
|
|
2661
|
+
return {
|
|
2662
|
+
placeholder: { args: replace("none") },
|
|
2663
|
+
approval: script(replace("approval")),
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function signatureScriptArgsBytes(args, signature = new Uint8Array(65)) {
|
|
2668
|
+
const builder = new kaspaWasm.ScriptBuilder({
|
|
2669
|
+
flags: { covenantsEnabled: true },
|
|
2670
|
+
});
|
|
2671
|
+
for (const arg of args || []) {
|
|
2672
|
+
if (arg.type === "i64") {
|
|
2673
|
+
builder.addI64(BigInt(arg.value));
|
|
2674
|
+
} else if (arg.type === "byte") {
|
|
2675
|
+
builder.addData(Uint8Array.of(arg.value));
|
|
2676
|
+
} else if (arg.type === "data") {
|
|
2677
|
+
builder.addData(hexToBytes(arg.hex));
|
|
2678
|
+
} else if (arg.type === "signature") {
|
|
2679
|
+
const prefix = hexToBytes(arg.prefixHex || "");
|
|
2680
|
+
builder.addData(concatBytes(prefix, signature));
|
|
2681
|
+
} else {
|
|
2682
|
+
throw new Error(`unsupported snapshot signature argument ${arg.type}`);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
return builder.drain();
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
function encodeControllerState(state) {
|
|
2689
|
+
return concatBytes(
|
|
2690
|
+
pushData(state.tokenId),
|
|
2691
|
+
pushData(state.holderExtensionCommitment),
|
|
2692
|
+
pushData(state.manifestCommitment),
|
|
2693
|
+
pushData(state.snapshotContext),
|
|
2694
|
+
pushData(state.kcc20TemplateHash),
|
|
2695
|
+
pushData(i64(state.claimNotBeforeDaaScore)),
|
|
2696
|
+
pushData(i64(state.shardIndex)),
|
|
2697
|
+
pushData(i64(state.globalIndexStart)),
|
|
2698
|
+
pushData(i64(state.holderCount)),
|
|
2699
|
+
pushData(i64(state.treeDepth)),
|
|
2700
|
+
pushData(i64(state.kcc20TemplatePrefixLen)),
|
|
2701
|
+
pushData(i64(state.kcc20TemplateSuffixLen)),
|
|
2702
|
+
pushData(i64(state.controllerCellKas)),
|
|
2703
|
+
pushData(i64(state.reserveCellKas)),
|
|
2704
|
+
pushData(i64(state.recipientMinCellKas)),
|
|
2705
|
+
pushData(state.remainingRoot),
|
|
2706
|
+
pushData(i64(state.remainingAmount)),
|
|
2707
|
+
pushData(i64(state.claimsProcessed)),
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
function snapshotControllerStateCommitment(state): string {
|
|
2712
|
+
return bytesToHex(
|
|
2713
|
+
blake2b(
|
|
2714
|
+
concatBytes(
|
|
2715
|
+
CONTROLLER_STATE_COMMITMENT_DOMAIN,
|
|
2716
|
+
encodeControllerState(state),
|
|
2717
|
+
),
|
|
2718
|
+
{ dkLen: 32 },
|
|
2719
|
+
),
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
function controllerClaimOrderedArgs({
|
|
2724
|
+
controllerArtifact,
|
|
2725
|
+
proof,
|
|
2726
|
+
nextReserveState,
|
|
2727
|
+
recipientState,
|
|
2728
|
+
template,
|
|
2729
|
+
ownerWitness,
|
|
2730
|
+
}) {
|
|
2731
|
+
return [
|
|
2732
|
+
i64Arg(proof.globalIndex),
|
|
2733
|
+
i64Arg(proof.shardLeafIndex),
|
|
2734
|
+
byteArg(proof.ownerScheme),
|
|
2735
|
+
dataArg(hexToBytes(proof.owner)),
|
|
2736
|
+
ownerWitness,
|
|
2737
|
+
i64Arg(proof.amount),
|
|
2738
|
+
dataArg(concatBytes(...proof.siblings.map(hexToBytes))),
|
|
2739
|
+
...kcc20StateArgs(nextReserveState),
|
|
2740
|
+
...kcc20StateArgs(recipientState),
|
|
2741
|
+
dataArg(template.prefix),
|
|
2742
|
+
dataArg(template.suffix),
|
|
2743
|
+
dataArg(dispatchTagFor(controllerArtifact, "claim")),
|
|
2744
|
+
];
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function kcc20StateArgs(state) {
|
|
2748
|
+
const value =
|
|
2749
|
+
state ||
|
|
2750
|
+
({
|
|
2751
|
+
amount: 0n,
|
|
2752
|
+
owner: hexToBytes(ZERO_HASH),
|
|
2753
|
+
ownerScheme: 0,
|
|
2754
|
+
borrowScheme: 0,
|
|
2755
|
+
borrowGuard: hexToBytes(ZERO_HASH),
|
|
2756
|
+
extensionCommitment: hexToBytes(ZERO_HASH),
|
|
2757
|
+
} as const);
|
|
2758
|
+
return [
|
|
2759
|
+
i64Arg(value.amount),
|
|
2760
|
+
dataArg(value.owner),
|
|
2761
|
+
byteArg(value.ownerScheme),
|
|
2762
|
+
byteArg(value.borrowScheme),
|
|
2763
|
+
dataArg(value.borrowGuard),
|
|
2764
|
+
dataArg(value.extensionCommitment),
|
|
2765
|
+
];
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
function outputEnvelope(transaction, signInputs, scripts, metadata) {
|
|
2769
|
+
const psktTransactionJson = transaction.serializeToSafeJSON();
|
|
2770
|
+
if (typeof psktTransactionJson !== "string") {
|
|
2771
|
+
throw new Error("snapshot transaction serialization did not return JSON");
|
|
2772
|
+
}
|
|
2773
|
+
return {
|
|
2774
|
+
schema: OUTPUT_SCHEMA,
|
|
2775
|
+
psktTransactionJson,
|
|
2776
|
+
signInputs,
|
|
2777
|
+
...(scripts.length ? { scripts } : {}),
|
|
2778
|
+
submitTransactionSupported: true,
|
|
2779
|
+
metadata,
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
async function readPinnedArtifacts(
|
|
2784
|
+
pins,
|
|
2785
|
+
requireController,
|
|
2786
|
+
requireBootstrap = false,
|
|
2787
|
+
) {
|
|
2788
|
+
const kcc20 = await readPinnedArtifact(
|
|
2789
|
+
KCC20_ARTIFACT_PATH,
|
|
2790
|
+
pins?.kcc20ArtifactSha256,
|
|
2791
|
+
"KCC20",
|
|
2792
|
+
);
|
|
2793
|
+
const controller = requireController
|
|
2794
|
+
? await readPinnedArtifact(
|
|
2795
|
+
CONTROLLER_ARTIFACT_PATH,
|
|
2796
|
+
pins?.controllerArtifactSha256,
|
|
2797
|
+
"KCC20SnapshotController",
|
|
2798
|
+
)
|
|
2799
|
+
: undefined;
|
|
2800
|
+
const bootstrap = requireBootstrap
|
|
2801
|
+
? await readPinnedArtifact(
|
|
2802
|
+
BOOTSTRAP_ARTIFACT_PATH,
|
|
2803
|
+
pins?.bootstrapArtifactSha256,
|
|
2804
|
+
"KCC20SnapshotBootstrap",
|
|
2805
|
+
)
|
|
2806
|
+
: undefined;
|
|
2807
|
+
dispatchTagFor(kcc20, "transfer");
|
|
2808
|
+
if (controller) dispatchTagFor(controller, "claim");
|
|
2809
|
+
if (bootstrap) dispatchTagFor(bootstrap, "start");
|
|
2810
|
+
return { kcc20, controller, bootstrap };
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
async function readPinnedArtifact(path, expectedHash, contractName) {
|
|
2814
|
+
const expected = requireHex32(
|
|
2815
|
+
expectedHash,
|
|
2816
|
+
`${contractName} artifact hash`,
|
|
2817
|
+
);
|
|
2818
|
+
let cached = artifactCache.get(path);
|
|
2819
|
+
if (!cached) {
|
|
2820
|
+
cached = Promise.resolve(
|
|
2821
|
+
options.artifacts?.[path] ?? options.artifactProvider?.(path),
|
|
2822
|
+
).then(async (supplied) => {
|
|
2823
|
+
if (!supplied)
|
|
2824
|
+
throw new Error(`Missing supplied snapshot artifact: ${path}`);
|
|
2825
|
+
const envelope =
|
|
2826
|
+
supplied && typeof supplied === "object" && !Array.isArray(supplied)
|
|
2827
|
+
? supplied
|
|
2828
|
+
: undefined;
|
|
2829
|
+
const artifactValue = envelope?.artifact ?? supplied;
|
|
2830
|
+
const rawValue = envelope?.rawBytes ?? envelope?.rawText;
|
|
2831
|
+
const raw =
|
|
2832
|
+
rawValue instanceof Uint8Array
|
|
2833
|
+
? rawValue
|
|
2834
|
+
: typeof rawValue === "string"
|
|
2835
|
+
? new TextEncoder().encode(rawValue)
|
|
2836
|
+
: typeof supplied === "string"
|
|
2837
|
+
? new TextEncoder().encode(supplied)
|
|
2838
|
+
: undefined;
|
|
2839
|
+
if (!raw) {
|
|
2840
|
+
throw new Error(
|
|
2841
|
+
`${contractName} artifact provider must include the exact raw bytes`,
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
const parsed =
|
|
2845
|
+
typeof artifactValue === "string"
|
|
2846
|
+
? JSON.parse(artifactValue)
|
|
2847
|
+
: artifactValue;
|
|
2848
|
+
return {
|
|
2849
|
+
raw,
|
|
2850
|
+
artifact: normalizeSilverScriptArtifact(parsed),
|
|
2851
|
+
};
|
|
2852
|
+
});
|
|
2853
|
+
artifactCache.set(path, cached);
|
|
2854
|
+
}
|
|
2855
|
+
const value = await cached;
|
|
2856
|
+
const actual = bytesToHex(sha256(value.raw));
|
|
2857
|
+
if (actual !== expected) {
|
|
2858
|
+
throw new Error(
|
|
2859
|
+
`${contractName} artifact does not match the migration pin`,
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2862
|
+
const artifact = value.artifact;
|
|
2863
|
+
if (artifact.contract_name !== contractName) {
|
|
2864
|
+
throw new Error(`pinned artifact is not ${contractName}`);
|
|
2865
|
+
}
|
|
2866
|
+
return artifact;
|
|
2867
|
+
}
|
|
2868
|
+
function snapshotShard(value) {
|
|
2869
|
+
const shard = value || {};
|
|
2870
|
+
const normalized = {
|
|
2871
|
+
shardIndex: unsignedInteger(shard.shardIndex, "shard index"),
|
|
2872
|
+
globalIndexStart: unsignedInteger(
|
|
2873
|
+
shard.globalIndexStart,
|
|
2874
|
+
"shard global index start",
|
|
2875
|
+
),
|
|
2876
|
+
holderCount: unsignedInteger(shard.holderCount, "shard holder count"),
|
|
2877
|
+
depth: unsignedInteger(shard.depth, "shard tree depth"),
|
|
2878
|
+
allocationTotal: positiveI64(
|
|
2879
|
+
shard.allocationTotal,
|
|
2880
|
+
"shard allocation total",
|
|
2881
|
+
),
|
|
2882
|
+
initialRoot: requireHex32(shard.initialRoot, "shard initial root"),
|
|
2883
|
+
};
|
|
2884
|
+
if (normalized.holderCount < 1)
|
|
2885
|
+
throw new Error("shard must contain holders");
|
|
2886
|
+
if (normalized.depth < 1 || normalized.depth > 32) {
|
|
2887
|
+
throw new Error("shard tree depth must be 1..32");
|
|
2888
|
+
}
|
|
2889
|
+
if (normalized.holderCount > 2 ** normalized.depth) {
|
|
2890
|
+
throw new Error("shard holder count exceeds its Merkle width");
|
|
2891
|
+
}
|
|
2892
|
+
return normalized;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
function reserveShards(values, totalSupply) {
|
|
2896
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > 8) {
|
|
2897
|
+
throw new Error("atomic reserve handoff requires 1..8 shards");
|
|
2898
|
+
}
|
|
2899
|
+
const indexes = new Set<number>();
|
|
2900
|
+
const controllerIds = new Set<string>();
|
|
2901
|
+
const shards = values.map((value) => {
|
|
2902
|
+
const shardIndex = unsignedInteger(value?.shardIndex, "shard index");
|
|
2903
|
+
const controllerId = requireHex32(value?.controllerId, "controller id");
|
|
2904
|
+
const allocationTotal = positiveI64(
|
|
2905
|
+
value?.allocationTotal,
|
|
2906
|
+
"shard allocation total",
|
|
2907
|
+
);
|
|
2908
|
+
if (indexes.has(shardIndex) || controllerIds.has(controllerId)) {
|
|
2909
|
+
throw new Error("reserve handoff shard identities must be unique");
|
|
2910
|
+
}
|
|
2911
|
+
indexes.add(shardIndex);
|
|
2912
|
+
controllerIds.add(controllerId);
|
|
2913
|
+
return { shardIndex, controllerId, allocationTotal };
|
|
2914
|
+
});
|
|
2915
|
+
if (
|
|
2916
|
+
shards.reduce((sum, shard) => sum + shard.allocationTotal, 0n) !==
|
|
2917
|
+
totalSupply
|
|
2918
|
+
) {
|
|
2919
|
+
throw new Error("reserve handoff allocations do not equal total supply");
|
|
2920
|
+
}
|
|
2921
|
+
return shards.sort((left, right) => left.shardIndex - right.shardIndex);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
function snapshotShardPlanCommitment({
|
|
2925
|
+
migrationCommitment,
|
|
2926
|
+
manifestCommitment,
|
|
2927
|
+
tokenId,
|
|
2928
|
+
totalSupply,
|
|
2929
|
+
shards,
|
|
2930
|
+
}) {
|
|
2931
|
+
return bytesToHex(
|
|
2932
|
+
blake2b(
|
|
2933
|
+
concatBytes(
|
|
2934
|
+
Uint8Array.of(0x20),
|
|
2935
|
+
hexToBytes(migrationCommitment),
|
|
2936
|
+
hexToBytes(manifestCommitment),
|
|
2937
|
+
hexToBytes(tokenId),
|
|
2938
|
+
i64(totalSupply),
|
|
2939
|
+
i64(BigInt(shards.length)),
|
|
2940
|
+
...shards.flatMap((shard, index) => [
|
|
2941
|
+
i64(BigInt(index)),
|
|
2942
|
+
hexToBytes(shard.controllerId),
|
|
2943
|
+
i64(shard.allocationTotal),
|
|
2944
|
+
]),
|
|
2945
|
+
),
|
|
2946
|
+
{ dkLen: 32 },
|
|
2947
|
+
),
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function claimProof(value, shard) {
|
|
2952
|
+
const proof = value || {};
|
|
2953
|
+
const normalized = {
|
|
2954
|
+
globalIndex: unsignedInteger(proof.globalIndex, "claim global index"),
|
|
2955
|
+
shardIndex: unsignedInteger(proof.shardIndex, "claim shard index"),
|
|
2956
|
+
shardLeafIndex: unsignedInteger(
|
|
2957
|
+
proof.shardLeafIndex,
|
|
2958
|
+
"claim shard leaf index",
|
|
2959
|
+
),
|
|
2960
|
+
ownerScheme: unsignedInteger(proof.ownerScheme, "claim owner scheme"),
|
|
2961
|
+
owner: requireHex32(proof.owner, "claim owner"),
|
|
2962
|
+
amount: positiveI64(proof.amount, "claim amount"),
|
|
2963
|
+
siblings: Array.isArray(proof.siblings)
|
|
2964
|
+
? proof.siblings.map((value) => requireHex32(value, "proof sibling"))
|
|
2965
|
+
: [],
|
|
2966
|
+
previousRoot: requireHex32(proof.previousRoot, "proof previous root"),
|
|
2967
|
+
nextRoot: requireHex32(proof.nextRoot, "proof next root"),
|
|
2968
|
+
};
|
|
2969
|
+
if (
|
|
2970
|
+
normalized.shardIndex !== shard.shardIndex ||
|
|
2971
|
+
normalized.globalIndex !==
|
|
2972
|
+
shard.globalIndexStart + normalized.shardLeafIndex ||
|
|
2973
|
+
normalized.shardLeafIndex >= shard.holderCount ||
|
|
2974
|
+
normalized.siblings.length !== shard.depth
|
|
2975
|
+
) {
|
|
2976
|
+
throw new Error("claim proof identity does not match the shard");
|
|
2977
|
+
}
|
|
2978
|
+
return normalized;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
function assertClaimProofRoots(proof, snapshotContext, currentRoot) {
|
|
2982
|
+
const context = hexToBytes(snapshotContext);
|
|
2983
|
+
const entitlement = hashMerkle(
|
|
2984
|
+
Uint8Array.of(0x10),
|
|
2985
|
+
context,
|
|
2986
|
+
leI64(proof.globalIndex),
|
|
2987
|
+
Uint8Array.of(proof.ownerScheme),
|
|
2988
|
+
hexToBytes(proof.owner),
|
|
2989
|
+
leI64(proof.amount),
|
|
2990
|
+
);
|
|
2991
|
+
const claimed = hashMerkle(
|
|
2992
|
+
Uint8Array.of(0x12),
|
|
2993
|
+
context,
|
|
2994
|
+
leI64(proof.globalIndex),
|
|
2995
|
+
);
|
|
2996
|
+
const current = foldMerkle(
|
|
2997
|
+
entitlement,
|
|
2998
|
+
proof.siblings,
|
|
2999
|
+
proof.shardLeafIndex,
|
|
3000
|
+
);
|
|
3001
|
+
const next = foldMerkle(claimed, proof.siblings, proof.shardLeafIndex);
|
|
3002
|
+
if (bytesToHex(current) !== currentRoot) {
|
|
3003
|
+
throw new Error(
|
|
3004
|
+
"claim proof does not reconstruct the current shard root",
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
if (bytesToHex(next) !== proof.nextRoot) {
|
|
3008
|
+
throw new Error("claim proof next root does not match leaf retirement");
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
function foldMerkle(start, siblings, leafIndex) {
|
|
3013
|
+
let node = Uint8Array.from(start);
|
|
3014
|
+
let cursor = leafIndex;
|
|
3015
|
+
for (const siblingHex of siblings) {
|
|
3016
|
+
const sibling = hexToBytes(siblingHex);
|
|
3017
|
+
node =
|
|
3018
|
+
cursor % 2 === 0
|
|
3019
|
+
? hashMerkle(Uint8Array.of(0x11), node, sibling)
|
|
3020
|
+
: hashMerkle(Uint8Array.of(0x11), sibling, node);
|
|
3021
|
+
cursor >>= 1;
|
|
3022
|
+
}
|
|
3023
|
+
return node;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
function hashMerkle(...parts) {
|
|
3027
|
+
return Uint8Array.from(blake2b(concatBytes(...parts), { dkLen: 32 }));
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
function leI64(value) {
|
|
3031
|
+
let cursor = BigInt(value);
|
|
3032
|
+
const bytes = new Uint8Array(8);
|
|
3033
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
3034
|
+
bytes[index] = Number(cursor & 0xffn);
|
|
3035
|
+
cursor >>= 8n;
|
|
3036
|
+
}
|
|
3037
|
+
return bytes;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function kcc20State(value) {
|
|
3041
|
+
return {
|
|
3042
|
+
amount: positiveI64(value?.amount, "KCC20 amount"),
|
|
3043
|
+
owner: hexToBytes(requireHex32(value?.owner, "KCC20 owner")),
|
|
3044
|
+
ownerScheme: unsignedInteger(value?.ownerScheme, "KCC20 owner scheme"),
|
|
3045
|
+
borrowScheme: unsignedInteger(
|
|
3046
|
+
value?.borrowScheme ?? 0,
|
|
3047
|
+
"KCC20 borrow scheme",
|
|
3048
|
+
),
|
|
3049
|
+
borrowGuard: hexToBytes(
|
|
3050
|
+
requireHex32(value?.borrowGuard ?? ZERO_HASH, "KCC20 borrow guard"),
|
|
3051
|
+
),
|
|
3052
|
+
extensionCommitment: hexToBytes(
|
|
3053
|
+
requireHex32(value?.extensionCommitment, "KCC20 extension commitment"),
|
|
3054
|
+
),
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
function sourceDescriptor(value, label, derivedAddress) {
|
|
3059
|
+
const txid = requireHex32(value?.txidHex, `${label} txid`);
|
|
3060
|
+
const vout = unsignedInteger(value?.vout, `${label} vout`);
|
|
3061
|
+
if (vout > MAX_U32) {
|
|
3062
|
+
throw new Error(`${label} vout must fit in uint32`);
|
|
3063
|
+
}
|
|
3064
|
+
const suppliedAddress = String(value?.address || "")
|
|
3065
|
+
.trim()
|
|
3066
|
+
.toLowerCase();
|
|
3067
|
+
const address =
|
|
3068
|
+
suppliedAddress || String(derivedAddress || "").toLowerCase();
|
|
3069
|
+
if (!/^(?:kaspa|kaspatest):[a-z0-9]{40,100}$/.test(address)) {
|
|
3070
|
+
throw new Error(`${label} address is invalid`);
|
|
3071
|
+
}
|
|
3072
|
+
if (
|
|
3073
|
+
suppliedAddress &&
|
|
3074
|
+
derivedAddress &&
|
|
3075
|
+
suppliedAddress !== derivedAddress
|
|
3076
|
+
) {
|
|
3077
|
+
throw new Error(`${label} address does not match reconstructed state`);
|
|
3078
|
+
}
|
|
3079
|
+
return { txid, vout, address };
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
function findExactEntry(entries, source) {
|
|
3083
|
+
const entry = (entries || []).find(
|
|
3084
|
+
(candidate) =>
|
|
3085
|
+
String(candidate.outpoint?.transactionId).toLowerCase() ===
|
|
3086
|
+
source.txid && Number(candidate.outpoint?.index) === source.vout,
|
|
3087
|
+
);
|
|
3088
|
+
if (!entry)
|
|
3089
|
+
throw new Error(`active UTXO ${source.txid}:${source.vout} not found`);
|
|
3090
|
+
return entry;
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
function assertCovenantId(entry, expected, label) {
|
|
3094
|
+
const candidates = [
|
|
3095
|
+
entry?.covenantId,
|
|
3096
|
+
entry?.entry?.covenantId,
|
|
3097
|
+
entry?.utxoEntry?.covenantId,
|
|
3098
|
+
entry?.covenant?.covenantId,
|
|
3099
|
+
entry?.entry?.covenant?.covenantId,
|
|
3100
|
+
entry?.utxoEntry?.covenant?.covenantId,
|
|
3101
|
+
].filter((value) => value !== undefined && value !== null);
|
|
3102
|
+
const covenantIds = candidates.map((value) =>
|
|
3103
|
+
String(value?.toString?.() ?? value)
|
|
3104
|
+
.trim()
|
|
3105
|
+
.toLowerCase(),
|
|
3106
|
+
);
|
|
3107
|
+
if (covenantIds.some((value) => !/^[a-f0-9]{64}$/.test(value))) {
|
|
3108
|
+
throw new Error(`${label} covenant ID is malformed`);
|
|
3109
|
+
}
|
|
3110
|
+
const unique = [...new Set(covenantIds)];
|
|
3111
|
+
if (unique.length > 1) {
|
|
3112
|
+
throw new Error(`${label} covenant ID fields conflict`);
|
|
3113
|
+
}
|
|
3114
|
+
const actual = unique[0] ?? "";
|
|
3115
|
+
if (actual !== expected)
|
|
3116
|
+
throw new Error(`${label} covenant ID does not match`);
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function assertCovenantSource(entry, { covenantId, script, amount, label }) {
|
|
3120
|
+
assertCovenantId(entry, covenantId, label);
|
|
3121
|
+
const expectedScriptPublicKey = String(
|
|
3122
|
+
kaspaWasm.payToScriptHashScript(script).script,
|
|
3123
|
+
).toLowerCase();
|
|
3124
|
+
const actualScriptPublicKey = scriptPublicKeyHex(entry?.scriptPublicKey);
|
|
3125
|
+
if (actualScriptPublicKey !== expectedScriptPublicKey) {
|
|
3126
|
+
throw new Error(`${label} script public key does not match`);
|
|
3127
|
+
}
|
|
3128
|
+
if (utxoAmountSompi(entry) !== BigInt(amount)) {
|
|
3129
|
+
throw new Error(`${label} cell KAS does not match`);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
function scriptPublicKeyHex(scriptPublicKey) {
|
|
3134
|
+
const hex = String(scriptPublicKey?.script ?? scriptPublicKey ?? "")
|
|
3135
|
+
.trim()
|
|
3136
|
+
.toLowerCase();
|
|
3137
|
+
if (!/^(?:[0-9a-f]{2})+$/.test(hex)) {
|
|
3138
|
+
throw new Error("snapshot source script public key is malformed");
|
|
3139
|
+
}
|
|
3140
|
+
return hex;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function assertScriptPublicKey(entry, expectedHex, label) {
|
|
3144
|
+
if (scriptPublicKeyHex(entry?.scriptPublicKey) !== expectedHex) {
|
|
3145
|
+
throw new Error(`${label} script public key does not match`);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
function p2shAuthorityDescriptor(value, expectedOwner, network, label) {
|
|
3150
|
+
const owner = requireHex32(expectedOwner, `${label} owner`);
|
|
3151
|
+
const revealScript = arbitraryBytes(
|
|
3152
|
+
value?.revealScriptHex,
|
|
3153
|
+
`${label} reveal script`,
|
|
3154
|
+
);
|
|
3155
|
+
const unlockPrefix = arbitraryBytes(
|
|
3156
|
+
value?.unlockPrefixHex ?? "",
|
|
3157
|
+
`${label} unlock prefix`,
|
|
3158
|
+
true,
|
|
3159
|
+
);
|
|
3160
|
+
const scriptPublicKey = kaspaWasm.payToScriptHashScript(revealScript);
|
|
3161
|
+
const scriptPublicKeyHexValue = scriptPublicKeyHex(scriptPublicKey);
|
|
3162
|
+
if (
|
|
3163
|
+
!scriptPublicKeyHexValue.match(/^aa20[0-9a-f]{64}87$/) ||
|
|
3164
|
+
scriptPublicKeyHexValue.slice(4, 68) !== owner
|
|
3165
|
+
) {
|
|
3166
|
+
throw new Error(
|
|
3167
|
+
`${label} reveal script does not match the entitlement owner`,
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const address = kaspaWasm
|
|
3171
|
+
.addressFromScriptPublicKey(scriptPublicKey, network)
|
|
3172
|
+
.toString();
|
|
3173
|
+
const source = sourceDescriptor(value?.source, `${label} source`, address);
|
|
3174
|
+
return {
|
|
3175
|
+
owner,
|
|
3176
|
+
revealScript,
|
|
3177
|
+
unlockPrefix,
|
|
3178
|
+
scriptPublicKeyHex: scriptPublicKeyHexValue,
|
|
3179
|
+
source,
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
function withoutOutpoints(entries, outpoints) {
|
|
3184
|
+
const excluded = new Set(
|
|
3185
|
+
outpoints.map(
|
|
3186
|
+
(outpoint) =>
|
|
3187
|
+
`${String(outpoint?.transactionId).toLowerCase()}:${Number(outpoint?.index)}`,
|
|
3188
|
+
),
|
|
3189
|
+
);
|
|
3190
|
+
return (entries || []).filter(
|
|
3191
|
+
(entry) =>
|
|
3192
|
+
!excluded.has(
|
|
3193
|
+
`${String(entry.outpoint?.transactionId).toLowerCase()}:${Number(entry.outpoint?.index)}`,
|
|
3194
|
+
),
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function appendFundingChange(
|
|
3199
|
+
outputs,
|
|
3200
|
+
inputs,
|
|
3201
|
+
walletAddress,
|
|
3202
|
+
covenantOutputSompi,
|
|
3203
|
+
minimumFee,
|
|
3204
|
+
) {
|
|
3205
|
+
const inputSompi = inputs.reduce(
|
|
3206
|
+
(sum, entry) => sum + utxoAmountSompi(entry),
|
|
3207
|
+
0n,
|
|
3208
|
+
);
|
|
3209
|
+
const change =
|
|
3210
|
+
BigInt(inputSompi) - BigInt(covenantOutputSompi) - BigInt(minimumFee);
|
|
3211
|
+
if (change <= 10_000n) {
|
|
3212
|
+
throw new Error("snapshot builder selected insufficient funding change");
|
|
3213
|
+
}
|
|
3214
|
+
outputs.push(
|
|
3215
|
+
new kaspaWasm.TransactionOutput(
|
|
3216
|
+
change,
|
|
3217
|
+
kaspaWasm.payToAddressScript(walletAddress),
|
|
3218
|
+
),
|
|
3219
|
+
);
|
|
3220
|
+
return outputs.length - 1;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
function publicInput(entry, computeBudget) {
|
|
3224
|
+
return {
|
|
3225
|
+
previousOutpoint: entry.outpoint,
|
|
3226
|
+
utxo: entry,
|
|
3227
|
+
sequence: 0n,
|
|
3228
|
+
sigOpCount: 0,
|
|
3229
|
+
computeBudget,
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
function covenantInput(entry, computeBudget) {
|
|
3234
|
+
return publicInput(entry, computeBudget);
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
function encodeP2sh(prefix, script) {
|
|
3238
|
+
return kaspaWasm.ScriptBuilder.fromScript(script, {
|
|
3239
|
+
flags: { covenantsEnabled: true },
|
|
3240
|
+
}).encodePayToScriptHashSignatureScript(prefix);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
function feeOf(transaction) {
|
|
3244
|
+
const inputs = [...transaction.inputs].reduce(
|
|
3245
|
+
(sum, input) => sum + BigInt(input.utxo?.amount ?? input.utxo?.value),
|
|
3246
|
+
0n,
|
|
3247
|
+
);
|
|
3248
|
+
const outputs = [...transaction.outputs].reduce(
|
|
3249
|
+
(sum, output) => sum + BigInt(output.value ?? output.amount),
|
|
3250
|
+
0n,
|
|
3251
|
+
);
|
|
3252
|
+
return inputs - outputs;
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
function requiredRecord(value: unknown, label: string) {
|
|
3256
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3257
|
+
throw new Error(`${label} is required`);
|
|
3258
|
+
}
|
|
3259
|
+
return value as Record<string, unknown>;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
function signatureScriptHex(transaction, inputIndex) {
|
|
3263
|
+
const json = JSON.parse(transaction.serializeToSafeJSON());
|
|
3264
|
+
const value = json?.inputs?.[inputIndex]?.signatureScript;
|
|
3265
|
+
if (typeof value !== "string" || !/^(?:[0-9a-f]{2})+$/i.test(value)) {
|
|
3266
|
+
throw new Error(
|
|
3267
|
+
"snapshot transaction static signature script is invalid",
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
return value.toLowerCase();
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
function controllerDeploymentPayload(definition, state) {
|
|
3274
|
+
return bytesToHex(
|
|
3275
|
+
new TextEncoder().encode(
|
|
3276
|
+
JSON.stringify({
|
|
3277
|
+
tn10: {
|
|
3278
|
+
v: 1,
|
|
3279
|
+
tmpl: "KCC20SnapshotController",
|
|
3280
|
+
migrationId: definition.migrationId,
|
|
3281
|
+
manifestCommitment: definition.manifestCommitment,
|
|
3282
|
+
shardIndex: definition.shard.shardIndex,
|
|
3283
|
+
args: [
|
|
3284
|
+
{
|
|
3285
|
+
name: "controllerState",
|
|
3286
|
+
type: "object",
|
|
3287
|
+
value: jsonControllerState(state),
|
|
3288
|
+
},
|
|
3289
|
+
],
|
|
3290
|
+
},
|
|
3291
|
+
}),
|
|
3292
|
+
),
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
function jsonControllerState(state) {
|
|
3297
|
+
return Object.fromEntries(
|
|
3298
|
+
Object.entries(state).map(([key, value]) => [
|
|
3299
|
+
key,
|
|
3300
|
+
value instanceof Uint8Array
|
|
3301
|
+
? bytesToHex(value)
|
|
3302
|
+
: controllerInteger(value, key),
|
|
3303
|
+
]),
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
function controllerInteger(value: unknown, key: string): string {
|
|
3308
|
+
if (
|
|
3309
|
+
typeof value !== "string" &&
|
|
3310
|
+
typeof value !== "number" &&
|
|
3311
|
+
typeof value !== "bigint" &&
|
|
3312
|
+
typeof value !== "boolean"
|
|
3313
|
+
) {
|
|
3314
|
+
throw new Error(`controller state ${key} is not an integer`);
|
|
3315
|
+
}
|
|
3316
|
+
return BigInt(value).toString();
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function scriptAddress(script, network) {
|
|
3320
|
+
return kaspaWasm
|
|
3321
|
+
.addressFromScriptPublicKey(
|
|
3322
|
+
kaspaWasm.payToScriptHashScript(script),
|
|
3323
|
+
network,
|
|
3324
|
+
)
|
|
3325
|
+
.toString();
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
function rpcFor(network) {
|
|
3329
|
+
return new kaspaWasm.RpcClient({
|
|
3330
|
+
url: config.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
3331
|
+
encoding: kaspaWasm.Encoding.Borsh,
|
|
3332
|
+
networkId: network,
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
function networkOf(request) {
|
|
3337
|
+
const network = String(
|
|
3338
|
+
request.network || config.KASPA_NETWORK || "testnet-10",
|
|
3339
|
+
)
|
|
3340
|
+
.trim()
|
|
3341
|
+
.toLowerCase();
|
|
3342
|
+
if (!/^[a-z0-9-]{2,32}$/.test(network))
|
|
3343
|
+
throw new Error("network is invalid");
|
|
3344
|
+
return network;
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
function walletAddressOf(request) {
|
|
3348
|
+
const address = String(request.owner?.walletAddress || "")
|
|
3349
|
+
.trim()
|
|
3350
|
+
.toLowerCase();
|
|
3351
|
+
if (!/^(?:kaspa|kaspatest):[a-z0-9]{40,100}$/.test(address)) {
|
|
3352
|
+
throw new Error("authenticated wallet address is invalid");
|
|
3353
|
+
}
|
|
3354
|
+
return address;
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
function priorityFee() {
|
|
3358
|
+
return nonnegativeI64(
|
|
3359
|
+
config.KCC20_SNAPSHOT_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE,
|
|
3360
|
+
"snapshot priority fee",
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
function minimumCell(value, label) {
|
|
3365
|
+
const amount = positiveI64(value ?? DEFAULT_CELL_SOMPI, label);
|
|
3366
|
+
if (amount < DEFAULT_CELL_SOMPI) {
|
|
3367
|
+
throw new Error(`${label} must be at least ${DEFAULT_CELL_SOMPI}`);
|
|
3368
|
+
}
|
|
3369
|
+
return amount;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
function positiveI64(value, label) {
|
|
3373
|
+
let amount;
|
|
3374
|
+
try {
|
|
3375
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
3376
|
+
throw new Error("unsafe integer");
|
|
3377
|
+
}
|
|
3378
|
+
amount = BigInt(value);
|
|
3379
|
+
} catch {
|
|
3380
|
+
throw new Error(`${label} must be a positive signed 64-bit integer`);
|
|
3381
|
+
}
|
|
3382
|
+
if (amount <= 0n || amount > MAX_I64) {
|
|
3383
|
+
throw new Error(`${label} must be a positive signed 64-bit integer`);
|
|
3384
|
+
}
|
|
3385
|
+
return amount;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function nonnegativeI64(value, label) {
|
|
3389
|
+
let amount;
|
|
3390
|
+
try {
|
|
3391
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
3392
|
+
throw new Error("unsafe integer");
|
|
3393
|
+
}
|
|
3394
|
+
amount = BigInt(value);
|
|
3395
|
+
} catch {
|
|
3396
|
+
throw new Error(`${label} must be a nonnegative signed 64-bit integer`);
|
|
3397
|
+
}
|
|
3398
|
+
if (amount < 0n || amount > MAX_I64) {
|
|
3399
|
+
throw new Error(`${label} must be a nonnegative signed 64-bit integer`);
|
|
3400
|
+
}
|
|
3401
|
+
return amount;
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
function unsignedInteger(value, label) {
|
|
3405
|
+
const number = Number(value);
|
|
3406
|
+
if (!Number.isSafeInteger(number) || number < 0) {
|
|
3407
|
+
throw new Error(`${label} must be a nonnegative safe integer`);
|
|
3408
|
+
}
|
|
3409
|
+
return number;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
function dataArg(value) {
|
|
3413
|
+
return { type: "data", hex: bytesToHex(Uint8Array.from(value || [])) };
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
function arbitraryBytes(value, label, allowEmpty = false) {
|
|
3417
|
+
const normalized = String(value ?? "")
|
|
3418
|
+
.trim()
|
|
3419
|
+
.toLowerCase();
|
|
3420
|
+
if (
|
|
3421
|
+
(!allowEmpty && normalized.length === 0) ||
|
|
3422
|
+
normalized.length % 2 !== 0
|
|
3423
|
+
) {
|
|
3424
|
+
throw new Error(`${label} must be complete hexadecimal bytes`);
|
|
3425
|
+
}
|
|
3426
|
+
if (!/^[0-9a-f]*$/.test(normalized)) {
|
|
3427
|
+
throw new Error(`${label} must be complete hexadecimal bytes`);
|
|
3428
|
+
}
|
|
3429
|
+
return hexToBytes(normalized);
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
function byteArg(value) {
|
|
3433
|
+
const number = Number(value);
|
|
3434
|
+
if (!Number.isInteger(number) || number < 0 || number > 255) {
|
|
3435
|
+
throw new Error("signature-script byte argument is invalid");
|
|
3436
|
+
}
|
|
3437
|
+
return { type: "byte", value: number };
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
function i64Arg(value) {
|
|
3441
|
+
return { type: "i64", value: BigInt(value).toString() };
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function pushData(value) {
|
|
3445
|
+
const bytes = Uint8Array.from(value);
|
|
3446
|
+
if (bytes.length > 75)
|
|
3447
|
+
throw new Error("controller state push is too large");
|
|
3448
|
+
return concatBytes(Uint8Array.of(bytes.length), bytes);
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
function i64(value) {
|
|
3452
|
+
let cursor = nonnegativeI64(value, "controller state integer");
|
|
3453
|
+
const bytes = new Uint8Array(8);
|
|
3454
|
+
for (let index = 0; index < 8; index += 1) {
|
|
3455
|
+
bytes[index] = Number(cursor & 0xffn);
|
|
3456
|
+
cursor >>= 8n;
|
|
3457
|
+
}
|
|
3458
|
+
return bytes;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
function concatBytes(...values) {
|
|
3462
|
+
const arrays = values.map((value) => Uint8Array.from(value));
|
|
3463
|
+
const output = new Uint8Array(
|
|
3464
|
+
arrays.reduce((sum, value) => sum + value.length, 0),
|
|
3465
|
+
);
|
|
3466
|
+
let offset = 0;
|
|
3467
|
+
for (const value of arrays) {
|
|
3468
|
+
output.set(value, offset);
|
|
3469
|
+
offset += value.length;
|
|
3470
|
+
}
|
|
3471
|
+
return output;
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
return buildSnapshotMigrationPskt;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
/** Pure script constructors shared by browser verification and server projections. */
|
|
3478
|
+
function buildSnapshotControllerScriptForState(artifact, state) {
|
|
3479
|
+
const normalized = normalizeSilverScriptArtifact(artifact);
|
|
3480
|
+
const encoded = encodePortableControllerState(state);
|
|
3481
|
+
const start = normalized.state_layout?.start;
|
|
3482
|
+
const length = normalized.state_layout?.len;
|
|
3483
|
+
if (
|
|
3484
|
+
!Number.isInteger(start) ||
|
|
3485
|
+
!Number.isInteger(length) ||
|
|
3486
|
+
encoded.length !== length
|
|
3487
|
+
) {
|
|
3488
|
+
throw new Error("snapshot controller artifact state layout is invalid");
|
|
3489
|
+
}
|
|
3490
|
+
return portableConcatBytes(
|
|
3491
|
+
Uint8Array.from(normalized.script).slice(0, start),
|
|
3492
|
+
encoded,
|
|
3493
|
+
Uint8Array.from(normalized.script).slice(start + length),
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
function buildSnapshotBootstrapScriptForState(artifact, state) {
|
|
3498
|
+
const normalized = normalizeSilverScriptArtifact(artifact);
|
|
3499
|
+
const encoded = encodePortableBootstrapState(state);
|
|
3500
|
+
const start = normalized.state_layout?.start;
|
|
3501
|
+
const length = normalized.state_layout?.len;
|
|
3502
|
+
if (
|
|
3503
|
+
normalized.contract_name !== "KCC20SnapshotBootstrap" ||
|
|
3504
|
+
!Number.isInteger(start) ||
|
|
3505
|
+
!Number.isInteger(length) ||
|
|
3506
|
+
encoded.length !== length
|
|
3507
|
+
) {
|
|
3508
|
+
throw new Error("snapshot bootstrap artifact state layout is invalid");
|
|
3509
|
+
}
|
|
3510
|
+
return portableConcatBytes(
|
|
3511
|
+
Uint8Array.from(normalized.script).slice(0, start),
|
|
3512
|
+
encoded,
|
|
3513
|
+
Uint8Array.from(normalized.script).slice(start + length),
|
|
3514
|
+
);
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
function encodePortableControllerState(state) {
|
|
3518
|
+
return portableConcatBytes(
|
|
3519
|
+
portablePushData(state.tokenId),
|
|
3520
|
+
portablePushData(state.holderExtensionCommitment),
|
|
3521
|
+
portablePushData(state.manifestCommitment),
|
|
3522
|
+
portablePushData(state.snapshotContext),
|
|
3523
|
+
portablePushData(state.kcc20TemplateHash),
|
|
3524
|
+
portablePushData(portableI64(state.claimNotBeforeDaaScore)),
|
|
3525
|
+
portablePushData(portableI64(state.shardIndex)),
|
|
3526
|
+
portablePushData(portableI64(state.globalIndexStart)),
|
|
3527
|
+
portablePushData(portableI64(state.holderCount)),
|
|
3528
|
+
portablePushData(portableI64(state.treeDepth)),
|
|
3529
|
+
portablePushData(portableI64(state.kcc20TemplatePrefixLen)),
|
|
3530
|
+
portablePushData(portableI64(state.kcc20TemplateSuffixLen)),
|
|
3531
|
+
portablePushData(portableI64(state.controllerCellKas)),
|
|
3532
|
+
portablePushData(portableI64(state.reserveCellKas)),
|
|
3533
|
+
portablePushData(portableI64(state.recipientMinCellKas)),
|
|
3534
|
+
portablePushData(state.remainingRoot),
|
|
3535
|
+
portablePushData(portableI64(state.remainingAmount)),
|
|
3536
|
+
portablePushData(portableI64(state.claimsProcessed)),
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
function encodePortableBootstrapState(state) {
|
|
3541
|
+
return portableConcatBytes(
|
|
3542
|
+
portablePushData(state.migrationCommitment),
|
|
3543
|
+
portablePushData(state.manifestCommitment),
|
|
3544
|
+
portablePushData(state.holderExtensionCommitment),
|
|
3545
|
+
portablePushData(portableI64(state.totalSupply)),
|
|
3546
|
+
portablePushData(portableI64(state.claimNotBeforeDaaScore)),
|
|
3547
|
+
portablePushData(state.kcc20TemplateHash),
|
|
3548
|
+
portablePushData(portableI64(state.kcc20TemplatePrefixLen)),
|
|
3549
|
+
portablePushData(portableI64(state.kcc20TemplateSuffixLen)),
|
|
3550
|
+
portablePushData(portableI64(state.tokenCellKas)),
|
|
3551
|
+
portablePushData(portableI64(state.reserveCellKas)),
|
|
3552
|
+
portablePushData(portableI64(state.bootstrapCellKas)),
|
|
3553
|
+
portablePushData(state.approvalKey ?? state.approvalKeyA),
|
|
3554
|
+
portablePushData(portableI64(state.phase)),
|
|
3555
|
+
portablePushData(state.tokenId),
|
|
3556
|
+
portablePushData(state.shardPlanCommitment),
|
|
3557
|
+
portablePushData(state.activationDigest),
|
|
3558
|
+
portablePushData(portableI64(state.shardCount)),
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
function portablePushData(value) {
|
|
3563
|
+
const bytes = Uint8Array.from(value);
|
|
3564
|
+
if (bytes.length > 75) throw new Error("snapshot state push is too large");
|
|
3565
|
+
return portableConcatBytes(Uint8Array.of(bytes.length), bytes);
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
function portableI64(value) {
|
|
3569
|
+
let cursor = BigInt(value);
|
|
3570
|
+
if (cursor < 0n || cursor > 9_223_372_036_854_775_807n) {
|
|
3571
|
+
throw new Error("snapshot state integer is outside signed i64 range");
|
|
3572
|
+
}
|
|
3573
|
+
const bytes = new Uint8Array(8);
|
|
3574
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
3575
|
+
bytes[index] = Number(cursor & 0xffn);
|
|
3576
|
+
cursor >>= 8n;
|
|
3577
|
+
}
|
|
3578
|
+
return bytes;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
function portableConcatBytes(...values) {
|
|
3582
|
+
const arrays = values.map((value) => Uint8Array.from(value));
|
|
3583
|
+
const output = new Uint8Array(
|
|
3584
|
+
arrays.reduce((sum, value) => sum + value.length, 0),
|
|
3585
|
+
);
|
|
3586
|
+
let offset = 0;
|
|
3587
|
+
for (const value of arrays) {
|
|
3588
|
+
output.set(value, offset);
|
|
3589
|
+
offset += value.length;
|
|
3590
|
+
}
|
|
3591
|
+
return output;
|
|
3592
|
+
}
|