@kaspacom/kcc20-tx-builder 0.2.5 → 0.5.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 +26 -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 +26 -0
- package/dist/cjs/snapshot-operation.js +1308 -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 +100 -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 +27 -0
- package/dist/snapshot-operation.d.ts.map +1 -0
- package/dist/snapshot-operation.js +1303 -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 +96 -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 +1920 -0
- package/src/snapshot-signerless-transactions.ts +1272 -0
- package/src/snapshot-signerless.ts +1525 -0
- package/src/snapshot-verification.ts +90 -0
- package/src/token-operation.ts +3 -3
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { buildKcc20ScriptForState, buildKcc20TransferSigScript, holderExtensionCommitment, ownerAuthorizationWitness, p2pkOwnerWitness, templateParts, } from "./abi.js";
|
|
3
|
+
import { buildClaimSignatureScripts, buildSnapshotStartSignatureScripts, encodeSnapshotBootstrapState, encodeSnapshotControllerState, parseSnapshotBootstrapState, parseSnapshotControllerState, transitionClaim, ZERO_32, } from "./snapshot-signerless.js";
|
|
4
|
+
const NATIVE_SUBNETWORK_ID = "00".repeat(20);
|
|
5
|
+
const MAX_I64 = 9223372036854775807n;
|
|
6
|
+
const MAX_COMPUTE_BUDGET = 65_535;
|
|
7
|
+
const DEFAULT_COVENANT_COMPUTE_BUDGET = 1_000;
|
|
8
|
+
const START_CONTROLLER_COMPUTE_BUDGET = 100;
|
|
9
|
+
export function buildSnapshotKcc20DeploymentPayload({ creator, ticker, name, displayScale, totalSupply, holderExtensionCommitment: expectedHolderExtensionCommitment, migrationId, manifestCommitment, reserves, treasury = creator, protocolFeeRecipient = creator, protocolFeeBps = 0, }) {
|
|
10
|
+
const normalizedCreator = hex32(creator, "snapshot token creator");
|
|
11
|
+
const normalizedTreasury = hex32(treasury, "snapshot token treasury");
|
|
12
|
+
const normalizedFeeRecipient = hex32(protocolFeeRecipient, "snapshot token protocol fee recipient");
|
|
13
|
+
const normalizedTicker = printableText(ticker, "snapshot token ticker");
|
|
14
|
+
const normalizedName = printableText(name, "snapshot token name");
|
|
15
|
+
const normalizedDisplayScale = positiveI64(displayScale, "snapshot token display scale");
|
|
16
|
+
const normalizedTotalSupply = positiveI64(totalSupply, "snapshot token total supply");
|
|
17
|
+
const normalizedProtocolFeeBps = nonnegativeI64(protocolFeeBps, "snapshot token protocol fee bps");
|
|
18
|
+
if (normalizedProtocolFeeBps > 10000n) {
|
|
19
|
+
throw new Error("snapshot token protocol fee bps exceeds 10000");
|
|
20
|
+
}
|
|
21
|
+
const normalizedMigrationId = hex32(migrationId, "snapshot migration id");
|
|
22
|
+
const normalizedManifestCommitment = hex32(manifestCommitment, "snapshot manifest commitment");
|
|
23
|
+
if (!Array.isArray(reserves) || reserves.length < 1 || reserves.length > 8) {
|
|
24
|
+
throw new Error("snapshot token reserves must contain 1..8 shards");
|
|
25
|
+
}
|
|
26
|
+
const normalizedReserves = reserves.map((reserve, index) => ({
|
|
27
|
+
shardIndex: index.toString(),
|
|
28
|
+
amount: positiveI64(reserve?.amount, `snapshot reserve ${index} amount`).toString(),
|
|
29
|
+
owner: hex32(reserve?.owner, `snapshot reserve ${index} owner`),
|
|
30
|
+
ownerScheme: Number(reserve?.ownerScheme),
|
|
31
|
+
}));
|
|
32
|
+
if (normalizedReserves.some((reserve) => reserve.ownerScheme !== 4) ||
|
|
33
|
+
normalizedReserves.reduce((sum, reserve) => sum + BigInt(reserve.amount), 0n) !== normalizedTotalSupply) {
|
|
34
|
+
throw new Error("snapshot token reserves must be covenant-owned and conserve total supply");
|
|
35
|
+
}
|
|
36
|
+
const extension = {
|
|
37
|
+
kind: 1,
|
|
38
|
+
creator: hexBytes(normalizedCreator, "snapshot token creator"),
|
|
39
|
+
ticker: fixedText32(normalizedTicker, "snapshot token ticker"),
|
|
40
|
+
name: fixedText32(normalizedName, "snapshot token name"),
|
|
41
|
+
displayScale: normalizedDisplayScale,
|
|
42
|
+
maxSupply: normalizedTotalSupply,
|
|
43
|
+
mintLaneCount: 1n,
|
|
44
|
+
mintPolicy: 0,
|
|
45
|
+
mintPriceSompi: 0n,
|
|
46
|
+
treasury: hexBytes(normalizedTreasury, "snapshot token treasury"),
|
|
47
|
+
protocolFeeRecipient: hexBytes(normalizedFeeRecipient, "snapshot token protocol fee recipient"),
|
|
48
|
+
protocolFeeBps: normalizedProtocolFeeBps,
|
|
49
|
+
};
|
|
50
|
+
const computedCommitment = bytesHex(holderExtensionCommitment(extension));
|
|
51
|
+
const expectedCommitment = hex32(expectedHolderExtensionCommitment, "snapshot holder extension commitment");
|
|
52
|
+
if (computedCommitment !== expectedCommitment) {
|
|
53
|
+
throw new Error("snapshot token metadata does not open the committed holder extension");
|
|
54
|
+
}
|
|
55
|
+
const firstReserve = normalizedReserves[0];
|
|
56
|
+
const args = [
|
|
57
|
+
claimArg("creator", "byte[32]", normalizedCreator),
|
|
58
|
+
claimArg("maxSupply", "u64", normalizedTotalSupply.toString()),
|
|
59
|
+
claimArg("mintLaneCount", "u64", "1"),
|
|
60
|
+
claimArg("premintSupply", "u64", normalizedTotalSupply.toString()),
|
|
61
|
+
claimArg("premintRecipient", "byte[32]", firstReserve.owner),
|
|
62
|
+
claimArg("premintOwnerScheme", "u64", "4"),
|
|
63
|
+
claimArg("mintPolicy", "u64", "0"),
|
|
64
|
+
claimArg("displayScale", "u64", normalizedDisplayScale.toString()),
|
|
65
|
+
claimArg("mintPriceSompi", "u64", "0"),
|
|
66
|
+
claimArg("treasury", "byte[32]", normalizedTreasury),
|
|
67
|
+
claimArg("protocolFeeRecipient", "byte[32]", normalizedFeeRecipient),
|
|
68
|
+
claimArg("protocolFeeBps", "u64", normalizedProtocolFeeBps.toString()),
|
|
69
|
+
claimArg("ticker", "string", normalizedTicker),
|
|
70
|
+
claimArg("name", "string", normalizedName),
|
|
71
|
+
claimArg("holderExtensionCommitment", "byte[32]", expectedCommitment),
|
|
72
|
+
claimArg("snapshotMigrationId", "byte[32]", normalizedMigrationId),
|
|
73
|
+
claimArg("snapshotManifestCommitment", "byte[32]", normalizedManifestCommitment),
|
|
74
|
+
claimArg("snapshotReserves", "object", normalizedReserves),
|
|
75
|
+
];
|
|
76
|
+
return new TextEncoder().encode(JSON.stringify({ tn10: { v: 1, tmpl: "KCC20", args } }));
|
|
77
|
+
}
|
|
78
|
+
export function buildSnapshotControllerScriptForState(artifact, state) {
|
|
79
|
+
return scriptForState(artifact, encodeSnapshotControllerState(state), "snapshot controller");
|
|
80
|
+
}
|
|
81
|
+
export function buildSnapshotBootstrapScriptForState(artifact, state) {
|
|
82
|
+
return scriptForState(artifact, encodeSnapshotBootstrapState(state), "snapshot bootstrap");
|
|
83
|
+
}
|
|
84
|
+
export function deriveSnapshotTokenId(kw, { controllerGenesisOutpoint, reserves, kcc20Artifact, reserveCellKas }) {
|
|
85
|
+
const outputs = reserves.map((state) => new kw.TransactionOutput(positiveI64(reserveCellKas, "reserve cell KAS"), kw.payToScriptHashScript(buildKcc20ScriptForState(kcc20Artifact, parseKcc20State(state)))));
|
|
86
|
+
return kw
|
|
87
|
+
.covenantId(canonicalOutpoint(controllerGenesisOutpoint, "shard-zero controller outpoint"), outputs.map((output, index) => ({ index, output })))
|
|
88
|
+
.toString();
|
|
89
|
+
}
|
|
90
|
+
export function buildControllerGenesisUnsignedTransaction(kw, { plan, controllerArtifact, fundingInput, changeAddress, feeSompi, payload = null, }) {
|
|
91
|
+
requirePlan(plan, "snapshot-controller-genesis-plan");
|
|
92
|
+
const script = buildSnapshotControllerScriptForState(controllerArtifact, parseSnapshotControllerState(plan.controllerState));
|
|
93
|
+
return genesisEnvelope(kw, {
|
|
94
|
+
plan,
|
|
95
|
+
fundingInput,
|
|
96
|
+
changeAddress,
|
|
97
|
+
feeSompi,
|
|
98
|
+
payload: payload ??
|
|
99
|
+
deploymentPayload("KCC20SnapshotController", "controllerState", plan.controllerState),
|
|
100
|
+
script,
|
|
101
|
+
cellKas: BigInt(plan.controllerCellKas),
|
|
102
|
+
role: "snapshot-controller",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function buildBootstrapGenesisUnsignedTransaction(kw, { plan, bootstrapArtifact, fundingInput, changeAddress, feeSompi, payload = null, }) {
|
|
106
|
+
requirePlan(plan, "snapshot-bootstrap-genesis-plan");
|
|
107
|
+
const script = buildSnapshotBootstrapScriptForState(bootstrapArtifact, parseSnapshotBootstrapState(plan.bootstrapState));
|
|
108
|
+
return genesisEnvelope(kw, {
|
|
109
|
+
plan,
|
|
110
|
+
fundingInput,
|
|
111
|
+
changeAddress,
|
|
112
|
+
feeSompi,
|
|
113
|
+
payload: payload ??
|
|
114
|
+
deploymentPayload("KCC20SnapshotBootstrap", "bootstrapState", plan.bootstrapState),
|
|
115
|
+
script,
|
|
116
|
+
cellKas: BigInt(plan.bootstrapCellKas),
|
|
117
|
+
role: "snapshot-bootstrap",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function buildSnapshotStartUnsignedTransaction(kw, { plan, bootstrapArtifact, controllerArtifact, kcc20Artifact, bootstrapInput, controllerInputs, fundingInput, changeAddress, feeSompi, payload = new Uint8Array(), }) {
|
|
121
|
+
requirePlan(plan, "snapshot-start-plan");
|
|
122
|
+
if (!Array.isArray(controllerInputs) ||
|
|
123
|
+
controllerInputs.length !== plan.controllers.length) {
|
|
124
|
+
throw new Error("snapshot start requires every committed controller input");
|
|
125
|
+
}
|
|
126
|
+
const bootstrapState = parseSnapshotBootstrapState(plan.bootstrapState);
|
|
127
|
+
assertCovenantInputMatches(kw, {
|
|
128
|
+
input: bootstrapInput,
|
|
129
|
+
expectedScript: buildSnapshotBootstrapScriptForState(bootstrapArtifact, bootstrapState),
|
|
130
|
+
expectedAmount: plan.bootstrapCellKas,
|
|
131
|
+
label: "bootstrap input",
|
|
132
|
+
});
|
|
133
|
+
const controllerStatesBefore = plan.controllers.map((controller, index) => {
|
|
134
|
+
const state = parseSnapshotControllerState(controller.stateBefore);
|
|
135
|
+
assertCovenantInputMatches(kw, {
|
|
136
|
+
input: controllerInputs[index],
|
|
137
|
+
expectedScript: buildSnapshotControllerScriptForState(controllerArtifact, state),
|
|
138
|
+
expectedAmount: plan.controllerCellKas,
|
|
139
|
+
label: `controller ${index} input`,
|
|
140
|
+
});
|
|
141
|
+
const actualId = String(controllerInputs[index]?.utxo?.covenantId ?? "").toLowerCase();
|
|
142
|
+
if (actualId && actualId !== controller.controllerId) {
|
|
143
|
+
throw new Error(`controller ${index} covenant id does not match the start plan`);
|
|
144
|
+
}
|
|
145
|
+
return state;
|
|
146
|
+
});
|
|
147
|
+
const inputs = [
|
|
148
|
+
covenantInput(bootstrapInput),
|
|
149
|
+
...controllerInputs.map((input) => covenantInput(input, START_CONTROLLER_COMPUTE_BUDGET)),
|
|
150
|
+
publicInput(fundingInput),
|
|
151
|
+
];
|
|
152
|
+
const outputs = plan.reserves.map((reserve) => new kw.TransactionOutput(BigInt(plan.reserveCellKas), kw.payToScriptHashScript(buildKcc20ScriptForState(kcc20Artifact, parseKcc20State(reserve)))));
|
|
153
|
+
for (const [index, controller] of plan.controllers.entries()) {
|
|
154
|
+
outputs.push(covenantOutput(kw, buildSnapshotControllerScriptForState(controllerArtifact, parseSnapshotControllerState(controller.stateAfter)), BigInt(plan.controllerCellKas), controller.controllerId, index + 1));
|
|
155
|
+
}
|
|
156
|
+
appendRequiredFundingChange(kw, inputs, outputs, feeSompi, fundingInput, changeAddress);
|
|
157
|
+
const transaction = transactionOf(kw, inputs, outputs, 0n, payload);
|
|
158
|
+
transaction.populateGenesisCovenants([
|
|
159
|
+
new kw.GenesisCovenantGroup(1, plan.reserves.map((_, index) => index)),
|
|
160
|
+
]);
|
|
161
|
+
const tokenIds = JSON.parse(transaction.serializeToSafeJSON())
|
|
162
|
+
.outputs.slice(0, plan.reserves.length)
|
|
163
|
+
.map((output) => output.covenant?.covenantId);
|
|
164
|
+
if (tokenIds.some((tokenId) => String(tokenId).toLowerCase() !== plan.tokenId)) {
|
|
165
|
+
throw new Error("derived snapshot token id does not match immutable bootstrap commitment");
|
|
166
|
+
}
|
|
167
|
+
const scripts = buildSnapshotStartSignatureScripts(kw, {
|
|
168
|
+
plan,
|
|
169
|
+
bootstrapArtifact,
|
|
170
|
+
controllerArtifact,
|
|
171
|
+
kcc20Artifact,
|
|
172
|
+
});
|
|
173
|
+
setP2shSignatureScript(kw, transaction, 0, bootstrapInput.script, scripts.bootstrapSignatureScript);
|
|
174
|
+
for (const entry of scripts.controllerSignatureScripts) {
|
|
175
|
+
setP2shSignatureScript(kw, transaction, entry.inputIndex, controllerInputs[entry.inputIndex - 1].script, entry.signatureScript);
|
|
176
|
+
}
|
|
177
|
+
transaction.finalize();
|
|
178
|
+
return unsignedEnvelope(kw, transaction, plan, [
|
|
179
|
+
{ inputIndex: inputs.length - 1, role: "funding-owner", ownerScheme: 0 },
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
export function buildClaimUnsignedTransaction(kw, { plan, controllerArtifact, kcc20Artifact, controllerInput, reserveInput, authorityInput = null, fundingInput, changeAddress, feeSompi, ownerWitness, payload = "", }) {
|
|
183
|
+
requirePlan(plan, "snapshot-claim-plan");
|
|
184
|
+
const before = parseSnapshotControllerState(plan.controllerStateBefore);
|
|
185
|
+
assertCovenantInputMatches(kw, {
|
|
186
|
+
input: controllerInput,
|
|
187
|
+
expectedScript: buildSnapshotControllerScriptForState(controllerArtifact, before),
|
|
188
|
+
expectedAmount: before.controllerCellKas,
|
|
189
|
+
label: "controller input",
|
|
190
|
+
});
|
|
191
|
+
assertCovenantInputMatches(kw, {
|
|
192
|
+
input: reserveInput,
|
|
193
|
+
expectedScript: buildKcc20ScriptForState(kcc20Artifact, {
|
|
194
|
+
amount: before.remainingAmount,
|
|
195
|
+
owner: hexBytes(plan.controllerId, "controller id"),
|
|
196
|
+
ownerScheme: 4,
|
|
197
|
+
borrowScheme: 0,
|
|
198
|
+
borrowGuard: ZERO_32,
|
|
199
|
+
extensionCommitment: before.holderExtensionCommitment,
|
|
200
|
+
}),
|
|
201
|
+
expectedAmount: before.reserveCellKas,
|
|
202
|
+
label: "reserve input",
|
|
203
|
+
});
|
|
204
|
+
const p2shOwner = plan.authorizationRequired === "kcc2-p2sh-participating-input";
|
|
205
|
+
if (p2shOwner !== Boolean(authorityInput)) {
|
|
206
|
+
throw new Error("P2SH claims require exactly one explicit authority input");
|
|
207
|
+
}
|
|
208
|
+
if (authorityInput) {
|
|
209
|
+
authorityInput = normalizeP2shAuthorityInput(kw, {
|
|
210
|
+
authorityInput,
|
|
211
|
+
expectedOwner: plan.tokenOutputs.at(-1).state.owner,
|
|
212
|
+
label: "P2SH authority",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
assertClaimOutputPlan(plan, before);
|
|
216
|
+
const inputs = [
|
|
217
|
+
covenantInput(controllerInput),
|
|
218
|
+
covenantInput(reserveInput),
|
|
219
|
+
...(authorityInput ? [publicInput(authorityInput)] : []),
|
|
220
|
+
publicInput(fundingInput),
|
|
221
|
+
];
|
|
222
|
+
const outputs = plan.tokenOutputs.map((output) => covenantOutput(kw, buildKcc20ScriptForState(kcc20Artifact, parseKcc20State(output.state)), BigInt(output.cellKas), plan.tokenId, 1));
|
|
223
|
+
if (plan.controllerStateAfter) {
|
|
224
|
+
outputs.push(covenantOutput(kw, buildSnapshotControllerScriptForState(controllerArtifact, parseSnapshotControllerState(plan.controllerStateAfter)), BigInt(plan.controllerSuccessor.cellKas), plan.controllerId, 0));
|
|
225
|
+
}
|
|
226
|
+
appendOptionalFundingChange(kw, inputs, outputs, feeSompi, fundingInput, changeAddress);
|
|
227
|
+
const transaction = transactionOf(kw, inputs, outputs, BigInt(plan.transaction.lockTime), payload);
|
|
228
|
+
transaction.inputs[0].sequence = BigInt(plan.transaction.controllerInputSequence);
|
|
229
|
+
const recipient = parseKcc20State(plan.tokenOutputs.at(-1).state);
|
|
230
|
+
const p2shWitnessIndex = 2;
|
|
231
|
+
if (authorityInput && ownerWitness != null) {
|
|
232
|
+
const supplied = Uint8Array.from(ownerWitness);
|
|
233
|
+
if (supplied.length !== 1 || supplied[0] !== p2shWitnessIndex) {
|
|
234
|
+
throw new Error("P2SH authority witness must select the dedicated authority input at index 2");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const claim = {
|
|
238
|
+
globalIndex: plan.globalIndex,
|
|
239
|
+
shardLeafIndex: plan.shardLeafIndex,
|
|
240
|
+
ownerScheme: recipient.ownerScheme,
|
|
241
|
+
owner: recipient.owner,
|
|
242
|
+
ownerWitness: ownerWitness ??
|
|
243
|
+
(p2shOwner ? Uint8Array.of(p2shWitnessIndex) : new Uint8Array(65)),
|
|
244
|
+
amount: BigInt(plan.claimAmount),
|
|
245
|
+
siblings: plan.proofSiblings.map((sibling) => hexBytes(sibling, "proof sibling")),
|
|
246
|
+
};
|
|
247
|
+
const reserveNext = plan.tokenOutputs[0]?.role === "reserve"
|
|
248
|
+
? parseKcc20State(plan.tokenOutputs[0].state)
|
|
249
|
+
: emptyKcc20State();
|
|
250
|
+
const template = templateParts(kcc20Artifact);
|
|
251
|
+
const scripts = buildClaimSignatureScripts(kw, {
|
|
252
|
+
controllerArtifact,
|
|
253
|
+
kcc20Artifact,
|
|
254
|
+
claim,
|
|
255
|
+
reserveNext,
|
|
256
|
+
recipient,
|
|
257
|
+
kcc20TemplatePrefix: template.prefix,
|
|
258
|
+
kcc20TemplateSuffix: template.suffix,
|
|
259
|
+
nextTokenStates: plan.tokenOutputs.map((output) => parseKcc20State(output.state)),
|
|
260
|
+
});
|
|
261
|
+
setP2shSignatureScript(kw, transaction, 0, controllerInput.script, scripts.controllerSignatureScript);
|
|
262
|
+
setP2shSignatureScript(kw, transaction, 1, reserveInput.script, scripts.kcc20SignatureScript);
|
|
263
|
+
if (authorityInput)
|
|
264
|
+
setP2shSignatureScript(kw, transaction, 2, authorityInput.script, authorityInput.unlockPrefix);
|
|
265
|
+
transaction.finalize();
|
|
266
|
+
const fundingIndex = inputs.length - 1;
|
|
267
|
+
const signInputs = p2shOwner
|
|
268
|
+
? [
|
|
269
|
+
{
|
|
270
|
+
inputIndex: 2,
|
|
271
|
+
role: "p2sh-owner-authority",
|
|
272
|
+
ownerScheme: 3,
|
|
273
|
+
explicitRevealRecipe: true,
|
|
274
|
+
},
|
|
275
|
+
{ inputIndex: fundingIndex, role: "funding-owner", ownerScheme: 0 },
|
|
276
|
+
]
|
|
277
|
+
: [
|
|
278
|
+
{
|
|
279
|
+
inputIndex: 0,
|
|
280
|
+
role: "entitlement-owner",
|
|
281
|
+
ownerScheme: 0,
|
|
282
|
+
rewritesControllerWitness: true,
|
|
283
|
+
},
|
|
284
|
+
{ inputIndex: fundingIndex, role: "funding-owner", ownerScheme: 0 },
|
|
285
|
+
];
|
|
286
|
+
return unsignedEnvelope(kw, transaction, plan, signInputs);
|
|
287
|
+
}
|
|
288
|
+
export function buildRecipientTransferUnsignedTransaction(kw, { kcc20Artifact, migrationId, tokenId, tokenInput, fundingInput, authorityInput = null, recipient, tokenChange = null, tokenCellKas, changeAddress, feeSompi, payload = "", }) {
|
|
289
|
+
const inputState = parseKcc20State(tokenInput.state);
|
|
290
|
+
const states = [
|
|
291
|
+
parseKcc20State(recipient),
|
|
292
|
+
...(tokenChange ? [parseKcc20State(tokenChange)] : []),
|
|
293
|
+
];
|
|
294
|
+
const p2shOwner = inputState.ownerScheme === 3;
|
|
295
|
+
if (![0, 3].includes(inputState.ownerScheme) ||
|
|
296
|
+
p2shOwner !== Boolean(authorityInput)) {
|
|
297
|
+
throw new Error("claimed token owner authorization is invalid");
|
|
298
|
+
}
|
|
299
|
+
if (states.reduce((sum, state) => sum + state.amount, 0n) !== inputState.amount) {
|
|
300
|
+
throw new Error("recipient transfer token outputs do not conserve the claimed input");
|
|
301
|
+
}
|
|
302
|
+
if (authorityInput) {
|
|
303
|
+
authorityInput = normalizeP2shAuthorityInput(kw, {
|
|
304
|
+
authorityInput,
|
|
305
|
+
expectedOwner: bytesToHex(inputState.owner),
|
|
306
|
+
label: "P2SH claimed-token authority",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
const inputs = [
|
|
310
|
+
covenantInput(tokenInput),
|
|
311
|
+
...(authorityInput ? [publicInput(authorityInput)] : []),
|
|
312
|
+
publicInput(fundingInput),
|
|
313
|
+
];
|
|
314
|
+
const outputs = states.map((state) => covenantOutput(kw, buildKcc20ScriptForState(kcc20Artifact, state), tokenCellKas, tokenId, 0));
|
|
315
|
+
appendOptionalFundingChange(kw, inputs, outputs, feeSompi, fundingInput, changeAddress);
|
|
316
|
+
const transaction = transactionOf(kw, inputs, outputs, 0n, payload);
|
|
317
|
+
setP2shSignatureScript(kw, transaction, 0, tokenInput.script, buildKcc20TransferSigScript(kw, kcc20Artifact, {
|
|
318
|
+
nextStates: states,
|
|
319
|
+
witness: p2shOwner
|
|
320
|
+
? ownerAuthorizationWitness(Uint8Array.of(1))
|
|
321
|
+
: p2pkOwnerWitness(new Uint8Array(65)),
|
|
322
|
+
}));
|
|
323
|
+
if (authorityInput)
|
|
324
|
+
setP2shSignatureScript(kw, transaction, 1, authorityInput.script, authorityInput.unlockPrefix);
|
|
325
|
+
transaction.finalize();
|
|
326
|
+
const fundingIndex = inputs.length - 1;
|
|
327
|
+
return unsignedEnvelope(kw, transaction, {
|
|
328
|
+
schema: "kaspacom-snapshot-wallet-operation/v2",
|
|
329
|
+
kind: "snapshot-recipient-transfer",
|
|
330
|
+
migrationId,
|
|
331
|
+
tokenId,
|
|
332
|
+
}, p2shOwner
|
|
333
|
+
? [
|
|
334
|
+
{
|
|
335
|
+
inputIndex: 1,
|
|
336
|
+
role: "p2sh-claimed-token-authority",
|
|
337
|
+
ownerScheme: 3,
|
|
338
|
+
explicitRevealRecipe: true,
|
|
339
|
+
},
|
|
340
|
+
{ inputIndex: fundingIndex, role: "funding-owner", ownerScheme: 0 },
|
|
341
|
+
]
|
|
342
|
+
: [
|
|
343
|
+
{ inputIndex: 0, role: "claimed-token-owner", ownerScheme: 0 },
|
|
344
|
+
{ inputIndex: fundingIndex, role: "funding-owner", ownerScheme: 0 },
|
|
345
|
+
]);
|
|
346
|
+
}
|
|
347
|
+
function scriptForState(artifact, encoded, label) {
|
|
348
|
+
const script = Uint8Array.from(artifact?.script ?? artifact?.bytecode ?? []);
|
|
349
|
+
const start = artifact?.state_layout?.start;
|
|
350
|
+
const length = artifact?.state_layout?.len;
|
|
351
|
+
if (!Number.isInteger(start) ||
|
|
352
|
+
!Number.isInteger(length) ||
|
|
353
|
+
encoded.length !== length) {
|
|
354
|
+
throw new Error(`${label} artifact state layout does not match encoded state`);
|
|
355
|
+
}
|
|
356
|
+
return concat(script.slice(0, start), encoded, script.slice(start + length));
|
|
357
|
+
}
|
|
358
|
+
function genesisEnvelope(kw, { plan, fundingInput, changeAddress, feeSompi, payload, script, cellKas, role, }) {
|
|
359
|
+
const inputs = [publicInput(fundingInput)];
|
|
360
|
+
const outputs = [
|
|
361
|
+
new kw.TransactionOutput(cellKas, kw.payToScriptHashScript(script)),
|
|
362
|
+
];
|
|
363
|
+
appendOptionalFundingChange(kw, inputs, outputs, feeSompi, fundingInput, changeAddress);
|
|
364
|
+
const transaction = transactionOf(kw, inputs, outputs, 0n, payload);
|
|
365
|
+
transaction.populateGenesisCovenants([new kw.GenesisCovenantGroup(0, [0])]);
|
|
366
|
+
transaction.finalize();
|
|
367
|
+
return unsignedEnvelope(kw, transaction, { ...plan, outputRole: role }, [
|
|
368
|
+
{ inputIndex: 0, role: "funding-owner", ownerScheme: 0 },
|
|
369
|
+
]);
|
|
370
|
+
}
|
|
371
|
+
function deploymentPayload(template, name, value) {
|
|
372
|
+
return new TextEncoder().encode(JSON.stringify({
|
|
373
|
+
tn10: { v: 1, tmpl: template, args: [{ name, type: "object", value }] },
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
function claimArg(name, type, value) {
|
|
377
|
+
return { name, type, value };
|
|
378
|
+
}
|
|
379
|
+
function printableText(value, label) {
|
|
380
|
+
const normalized = String(value ?? "").trim();
|
|
381
|
+
const bytes = new TextEncoder().encode(normalized);
|
|
382
|
+
if (bytes.length < 1 ||
|
|
383
|
+
bytes.length > 32 ||
|
|
384
|
+
![...bytes].every((byte) => byte >= 0x20 && byte <= 0x7e)) {
|
|
385
|
+
throw new Error(`${label} must be 1..32 printable ASCII bytes`);
|
|
386
|
+
}
|
|
387
|
+
return normalized;
|
|
388
|
+
}
|
|
389
|
+
function fixedText32(value, label) {
|
|
390
|
+
const bytes = new TextEncoder().encode(printableText(value, label));
|
|
391
|
+
const result = new Uint8Array(32);
|
|
392
|
+
result.set(bytes);
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
function bytesHex(value) {
|
|
396
|
+
return [...value]
|
|
397
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
398
|
+
.join("");
|
|
399
|
+
}
|
|
400
|
+
function transactionOf(kw, inputs, outputs, lockTime, payload) {
|
|
401
|
+
assertDistinctInputOutpoints(inputs);
|
|
402
|
+
return new kw.Transaction({
|
|
403
|
+
version: 1,
|
|
404
|
+
lockTime,
|
|
405
|
+
inputs,
|
|
406
|
+
outputs,
|
|
407
|
+
subnetworkId: NATIVE_SUBNETWORK_ID,
|
|
408
|
+
gas: 0n,
|
|
409
|
+
payload,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function publicInput(input) {
|
|
413
|
+
const previous = canonicalOutpoint(input?.previousOutpoint, "previous outpoint");
|
|
414
|
+
const utxo = canonicalOutpoint(input?.utxo?.outpoint, "UTXO outpoint");
|
|
415
|
+
if (previous.transactionId !== utxo.transactionId ||
|
|
416
|
+
previous.index !== utxo.index) {
|
|
417
|
+
throw new Error("public input previous outpoint does not match the UTXO outpoint");
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
previousOutpoint: input.previousOutpoint,
|
|
421
|
+
utxo: input.utxo,
|
|
422
|
+
sequence: 0n,
|
|
423
|
+
sigOpCount: 0,
|
|
424
|
+
computeBudget: normalizedComputeBudget(input.computeBudget, 30),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function covenantInput(input, defaultComputeBudget = DEFAULT_COVENANT_COMPUTE_BUDGET) {
|
|
428
|
+
if (!input?.script)
|
|
429
|
+
throw new Error("covenant input is missing its public reveal script");
|
|
430
|
+
return {
|
|
431
|
+
...publicInput(input),
|
|
432
|
+
computeBudget: normalizedComputeBudget(input.computeBudget, defaultComputeBudget),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function normalizedComputeBudget(value, fallback) {
|
|
436
|
+
const budget = Number(value ?? fallback);
|
|
437
|
+
if (!Number.isSafeInteger(budget) ||
|
|
438
|
+
budget < 0 ||
|
|
439
|
+
budget > MAX_COMPUTE_BUDGET) {
|
|
440
|
+
throw new Error("input compute budget must be an unsigned 16-bit integer");
|
|
441
|
+
}
|
|
442
|
+
return budget;
|
|
443
|
+
}
|
|
444
|
+
function assertCovenantInputMatches(kw, { input, expectedScript, expectedAmount, label }) {
|
|
445
|
+
const actualScript = arbitraryBytes(input?.script, `${label} reveal script`);
|
|
446
|
+
if (!equalBytes(actualScript, expectedScript))
|
|
447
|
+
throw new Error(`${label} reveal script does not match the operation plan`);
|
|
448
|
+
const expectedSpk = String(kw.payToScriptHashScript(expectedScript).script).toLowerCase();
|
|
449
|
+
const actualSpk = String(input?.utxo?.scriptPublicKey?.script ?? input?.utxo?.scriptPublicKey ?? "").toLowerCase();
|
|
450
|
+
if (actualSpk !== expectedSpk)
|
|
451
|
+
throw new Error(`${label} script public key does not match the reveal script`);
|
|
452
|
+
if (inputAmount(input.utxo) !== BigInt(expectedAmount))
|
|
453
|
+
throw new Error(`${label} cell KAS does not match the operation plan`);
|
|
454
|
+
}
|
|
455
|
+
function covenantOutput(kw, script, amount, covenantId, authorizingInput) {
|
|
456
|
+
return new kw.TransactionOutput(positiveI64(amount, "covenant output KAS"), kw.payToScriptHashScript(script), new kw.CovenantBinding(authorizingInput, new kw.Hash(covenantId)));
|
|
457
|
+
}
|
|
458
|
+
function appendRequiredFundingChange(kw, inputs, outputs, feeSompi, fundingInput, changeAddress) {
|
|
459
|
+
const before = outputs.length;
|
|
460
|
+
appendOptionalFundingChange(kw, inputs, outputs, feeSompi, fundingInput, changeAddress);
|
|
461
|
+
if (outputs.length !== before + 1) {
|
|
462
|
+
throw new Error("snapshot start requires one positive plain funding-change output");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function appendOptionalFundingChange(kw, inputs, outputs, feeSompi, fundingInput, changeAddress) {
|
|
466
|
+
const totalInput = inputs.reduce((sum, input) => sum + inputAmount(input.utxo), 0n);
|
|
467
|
+
const totalOutput = outputs.reduce((sum, output) => sum + BigInt(output.value ?? output.amount), 0n);
|
|
468
|
+
const change = totalInput - totalOutput - positiveI64(feeSompi, "fee");
|
|
469
|
+
if (change < 0n)
|
|
470
|
+
throw new Error("public inputs do not fund outputs and fee");
|
|
471
|
+
if (change === 0n)
|
|
472
|
+
return;
|
|
473
|
+
const scriptPublicKey = fundingInput.utxo?.scriptPublicKey;
|
|
474
|
+
if (!scriptPublicKey)
|
|
475
|
+
throw new Error("funding UTXO is missing its script public key");
|
|
476
|
+
const networkId = fundingInput.networkId ?? "testnet-10";
|
|
477
|
+
const fundingAddress = kw
|
|
478
|
+
.addressFromScriptPublicKey(scriptPublicKey, networkId)
|
|
479
|
+
.toString();
|
|
480
|
+
if (changeAddress && changeAddress !== fundingAddress) {
|
|
481
|
+
throw new Error("change address does not match the funding input script");
|
|
482
|
+
}
|
|
483
|
+
outputs.push(new kw.TransactionOutput(change, scriptPublicKey));
|
|
484
|
+
}
|
|
485
|
+
function assertDistinctInputOutpoints(inputs) {
|
|
486
|
+
const seen = new Set();
|
|
487
|
+
for (const input of inputs) {
|
|
488
|
+
const outpoint = canonicalOutpoint(input.previousOutpoint, "transaction input previous outpoint");
|
|
489
|
+
const key = `${outpoint.transactionId}:${outpoint.index}`;
|
|
490
|
+
if (seen.has(key))
|
|
491
|
+
throw new Error(`duplicate transaction input outpoint ${key}`);
|
|
492
|
+
seen.add(key);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function canonicalOutpoint(outpoint, label) {
|
|
496
|
+
const transactionId = String(outpoint?.transactionId ?? "").toLowerCase();
|
|
497
|
+
const index = Number(outpoint?.index);
|
|
498
|
+
if (!/^[0-9a-f]{64}$/.test(transactionId) ||
|
|
499
|
+
!Number.isSafeInteger(index) ||
|
|
500
|
+
index < 0 ||
|
|
501
|
+
index > 0xffffffff) {
|
|
502
|
+
throw new Error(`${label} is malformed`);
|
|
503
|
+
}
|
|
504
|
+
return { transactionId, index };
|
|
505
|
+
}
|
|
506
|
+
function setP2shSignatureScript(kw, transaction, inputIndex, revealScript, prefix) {
|
|
507
|
+
transaction.inputs[inputIndex].signatureScript =
|
|
508
|
+
kw.ScriptBuilder.fromScript(revealScript).encodePayToScriptHashSignatureScript(prefix);
|
|
509
|
+
}
|
|
510
|
+
function unsignedEnvelope(kw, transaction, intent, signInputs) {
|
|
511
|
+
return {
|
|
512
|
+
schema: "kaspacom-funded-unsigned-transaction/v2",
|
|
513
|
+
submitted: false,
|
|
514
|
+
transaction,
|
|
515
|
+
unsignedTransaction: JSON.parse(transaction.serializeToSafeJSON()),
|
|
516
|
+
legacySdkCombinedMass: kw
|
|
517
|
+
.calculateTransactionMass("testnet-10", transaction, 1)
|
|
518
|
+
.toString(),
|
|
519
|
+
legacySdkStandardMaximum: kw.maximumStandardTransactionMass().toString(),
|
|
520
|
+
networkValidationRequired: true,
|
|
521
|
+
readyForSigning: false,
|
|
522
|
+
signInputs,
|
|
523
|
+
intent: JSON.parse(JSON.stringify(intent)),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function requirePlan(plan, kind) {
|
|
527
|
+
if (plan?.schema !== "kaspacom-snapshot-wallet-operation/v2" ||
|
|
528
|
+
plan?.kind !== kind ||
|
|
529
|
+
plan?.planOnly !== true) {
|
|
530
|
+
throw new Error(`expected ${kind}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function normalizeP2shAuthorityInput(kw, { authorityInput, expectedOwner, label }) {
|
|
534
|
+
if (!authorityInput?.script ||
|
|
535
|
+
!Object.hasOwn(authorityInput, "unlockPrefix")) {
|
|
536
|
+
throw new Error(`${label} input requires an explicit reveal script and unlock prefix`);
|
|
537
|
+
}
|
|
538
|
+
const reveal = arbitraryBytes(authorityInput.script, `${label} reveal script`);
|
|
539
|
+
const unlockPrefix = arbitraryBytes(authorityInput.unlockPrefix, `${label} unlock prefix`, true);
|
|
540
|
+
const actualSpk = String(authorityInput.utxo?.scriptPublicKey?.script ??
|
|
541
|
+
authorityInput.utxo?.scriptPublicKey ??
|
|
542
|
+
"").toLowerCase();
|
|
543
|
+
if (actualSpk !== `aa20${String(expectedOwner).toLowerCase()}87` ||
|
|
544
|
+
String(kw.payToScriptHashScript(reveal).script).toLowerCase() !== actualSpk) {
|
|
545
|
+
throw new Error(`${label} input does not match the token owner`);
|
|
546
|
+
}
|
|
547
|
+
return { ...authorityInput, script: reveal, unlockPrefix };
|
|
548
|
+
}
|
|
549
|
+
function assertClaimOutputPlan(plan, before) {
|
|
550
|
+
const outputs = Array.isArray(plan.tokenOutputs) ? plan.tokenOutputs : [];
|
|
551
|
+
const transition = transitionClaim({
|
|
552
|
+
context: before.snapshotContext,
|
|
553
|
+
shard: {
|
|
554
|
+
shardIndex: before.shardIndex,
|
|
555
|
+
globalIndexStart: before.globalIndexStart,
|
|
556
|
+
holderCount: before.holderCount,
|
|
557
|
+
depth: before.treeDepth,
|
|
558
|
+
allocationTotal: before.remainingAmount,
|
|
559
|
+
initialRoot: before.remainingRoot,
|
|
560
|
+
},
|
|
561
|
+
currentRoot: before.remainingRoot,
|
|
562
|
+
currentReserveAmount: before.remainingAmount,
|
|
563
|
+
claim: {
|
|
564
|
+
globalIndex: plan.globalIndex,
|
|
565
|
+
shardLeafIndex: plan.shardLeafIndex,
|
|
566
|
+
ownerScheme: plan.claimOwnerScheme,
|
|
567
|
+
owner: plan.claimOwner,
|
|
568
|
+
amount: plan.claimAmount,
|
|
569
|
+
siblings: plan.proofSiblings,
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
const nextRoot = bytesToHex(transition.nextRoot);
|
|
573
|
+
const previousRoot = bytesToHex(before.remainingRoot);
|
|
574
|
+
const expectedAuthorization = transition.ownerScheme === 0
|
|
575
|
+
? "kcc2-p2pk-schnorr-signature"
|
|
576
|
+
: "kcc2-p2sh-participating-input";
|
|
577
|
+
if (plan.previousRoot !== previousRoot ||
|
|
578
|
+
plan.nextRoot !== nextRoot ||
|
|
579
|
+
BigInt(plan.claimAmount) !== transition.claimAmount ||
|
|
580
|
+
Boolean(plan.terminal) !== transition.terminal ||
|
|
581
|
+
plan.authorizationRequired !== expectedAuthorization) {
|
|
582
|
+
throw new Error("claim transition summary does not match the bound entitlement proof");
|
|
583
|
+
}
|
|
584
|
+
if (plan.terminal) {
|
|
585
|
+
if (outputs.length !== 1 ||
|
|
586
|
+
outputs[0]?.role !== "recipient" ||
|
|
587
|
+
plan.controllerStateAfter ||
|
|
588
|
+
plan.controllerSuccessor) {
|
|
589
|
+
throw new Error("terminal claim plan must contain only the recipient output");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else if (outputs.length !== 2 ||
|
|
593
|
+
outputs[0]?.role !== "reserve" ||
|
|
594
|
+
outputs[1]?.role !== "recipient" ||
|
|
595
|
+
!plan.controllerStateAfter ||
|
|
596
|
+
!plan.controllerSuccessor) {
|
|
597
|
+
throw new Error("nonterminal claim plan must contain reserve, recipient, and controller successor outputs");
|
|
598
|
+
}
|
|
599
|
+
const recipient = parseKcc20State(outputs.at(-1).state);
|
|
600
|
+
const expectedRecipientKas = transition.terminal
|
|
601
|
+
? before.reserveCellKas + before.controllerCellKas
|
|
602
|
+
: before.recipientMinCellKas;
|
|
603
|
+
if (BigInt(outputs.at(-1).cellKas) !== expectedRecipientKas ||
|
|
604
|
+
recipient.amount !== transition.claimAmount ||
|
|
605
|
+
recipient.ownerScheme !== transition.ownerScheme ||
|
|
606
|
+
!equalBytes(recipient.owner, transition.owner) ||
|
|
607
|
+
recipient.borrowScheme !== 0 ||
|
|
608
|
+
!equalBytes(recipient.borrowGuard, ZERO_32) ||
|
|
609
|
+
!equalBytes(recipient.extensionCommitment, before.holderExtensionCommitment)) {
|
|
610
|
+
throw new Error("claim recipient output does not match the bound entitlement state");
|
|
611
|
+
}
|
|
612
|
+
if (transition.terminal)
|
|
613
|
+
return;
|
|
614
|
+
const reserve = parseKcc20State(outputs[0].state);
|
|
615
|
+
if (BigInt(outputs[0].cellKas) !== before.reserveCellKas ||
|
|
616
|
+
reserve.amount !== transition.remainingAmount ||
|
|
617
|
+
reserve.ownerScheme !== 4 ||
|
|
618
|
+
!equalBytes(reserve.owner, hexBytes(plan.controllerId, "controller id")) ||
|
|
619
|
+
reserve.borrowScheme !== 0 ||
|
|
620
|
+
!equalBytes(reserve.borrowGuard, ZERO_32) ||
|
|
621
|
+
!equalBytes(reserve.extensionCommitment, before.holderExtensionCommitment)) {
|
|
622
|
+
throw new Error("claim reserve output does not match the controller-bound remainder");
|
|
623
|
+
}
|
|
624
|
+
const after = parseSnapshotControllerState(plan.controllerStateAfter);
|
|
625
|
+
const expectedAfter = {
|
|
626
|
+
...before,
|
|
627
|
+
remainingRoot: transition.nextRoot,
|
|
628
|
+
remainingAmount: transition.remainingAmount,
|
|
629
|
+
claimsProcessed: before.claimsProcessed + 1n,
|
|
630
|
+
};
|
|
631
|
+
for (const [key, expected] of Object.entries(expectedAfter)) {
|
|
632
|
+
const actual = after[key];
|
|
633
|
+
const matches = expected instanceof Uint8Array
|
|
634
|
+
? equalBytes(actual, expected)
|
|
635
|
+
: actual === expected;
|
|
636
|
+
if (!matches)
|
|
637
|
+
throw new Error(`claim controller successor changed ${key}`);
|
|
638
|
+
}
|
|
639
|
+
if (BigInt(plan.controllerSuccessor.cellKas) !== before.controllerCellKas ||
|
|
640
|
+
plan.controllerSuccessor.remainingRoot !== nextRoot ||
|
|
641
|
+
BigInt(plan.controllerSuccessor.remainingAmount) !==
|
|
642
|
+
transition.remainingAmount ||
|
|
643
|
+
BigInt(plan.controllerSuccessor.claimsProcessed) !==
|
|
644
|
+
before.claimsProcessed + 1n) {
|
|
645
|
+
throw new Error("claim controller successor summary does not match its state");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function parseKcc20State(state) {
|
|
649
|
+
return {
|
|
650
|
+
amount: BigInt(state.amount),
|
|
651
|
+
owner: hexBytes(state.owner, "KCC20 owner"),
|
|
652
|
+
ownerScheme: Number(state.ownerScheme),
|
|
653
|
+
borrowScheme: Number(state.borrowScheme ?? 0),
|
|
654
|
+
borrowGuard: hexBytes(state.borrowGuard ?? "00".repeat(32), "KCC20 borrow guard"),
|
|
655
|
+
extensionCommitment: hexBytes(state.extensionCommitment, "KCC20 extension commitment"),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function emptyKcc20State() {
|
|
659
|
+
return {
|
|
660
|
+
amount: 0n,
|
|
661
|
+
owner: ZERO_32,
|
|
662
|
+
ownerScheme: 0,
|
|
663
|
+
borrowScheme: 0,
|
|
664
|
+
borrowGuard: ZERO_32,
|
|
665
|
+
extensionCommitment: ZERO_32,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function inputAmount(utxo) {
|
|
669
|
+
const value = utxo?.amount ?? utxo?.utxoEntry?.amount;
|
|
670
|
+
if (value == null)
|
|
671
|
+
throw new Error("UTXO is missing public amount");
|
|
672
|
+
return BigInt(value);
|
|
673
|
+
}
|
|
674
|
+
function positiveI64(value, label) {
|
|
675
|
+
const number = BigInt(value);
|
|
676
|
+
if (number <= 0n || number > MAX_I64)
|
|
677
|
+
throw new Error(`${label} must be a positive signed 64-bit integer`);
|
|
678
|
+
return number;
|
|
679
|
+
}
|
|
680
|
+
function nonnegativeI64(value, label) {
|
|
681
|
+
const number = BigInt(value);
|
|
682
|
+
if (number < 0n || number > MAX_I64)
|
|
683
|
+
throw new Error(`${label} must be a nonnegative signed 64-bit integer`);
|
|
684
|
+
return number;
|
|
685
|
+
}
|
|
686
|
+
function hex32(value, label) {
|
|
687
|
+
return bytesHex(hexBytes(value, label));
|
|
688
|
+
}
|
|
689
|
+
function hexBytes(value, label) {
|
|
690
|
+
const normalized = String(value).replace(/^0x/, "");
|
|
691
|
+
if (!/^(?:[0-9a-fA-F]{2}){32}$/.test(normalized)) {
|
|
692
|
+
throw new Error(`${label} must be exactly 32 bytes of hexadecimal data`);
|
|
693
|
+
}
|
|
694
|
+
return decodeHex(normalized);
|
|
695
|
+
}
|
|
696
|
+
function arbitraryBytes(value, label, allowEmpty = false) {
|
|
697
|
+
if (typeof value !== "string") {
|
|
698
|
+
const bytes = Uint8Array.from(value ?? []);
|
|
699
|
+
if (!allowEmpty && bytes.length === 0)
|
|
700
|
+
throw new Error(`${label} must not be empty`);
|
|
701
|
+
return bytes;
|
|
702
|
+
}
|
|
703
|
+
const normalized = value.replace(/^0x/, "");
|
|
704
|
+
if ((!allowEmpty && normalized.length === 0) ||
|
|
705
|
+
!/^(?:[0-9a-fA-F]{2})*$/.test(normalized)) {
|
|
706
|
+
throw new Error(`${label} must be complete hexadecimal bytes`);
|
|
707
|
+
}
|
|
708
|
+
return decodeHex(normalized);
|
|
709
|
+
}
|
|
710
|
+
function decodeHex(value) {
|
|
711
|
+
const bytes = new Uint8Array(value.length / 2);
|
|
712
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
713
|
+
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
|
714
|
+
}
|
|
715
|
+
return bytes;
|
|
716
|
+
}
|
|
717
|
+
function bytesToHex(value) {
|
|
718
|
+
return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
719
|
+
}
|
|
720
|
+
function equalBytes(a, b) {
|
|
721
|
+
const left = Uint8Array.from(a ?? []);
|
|
722
|
+
const right = Uint8Array.from(b ?? []);
|
|
723
|
+
return (left.length === right.length &&
|
|
724
|
+
left.every((byte, index) => byte === right[index]));
|
|
725
|
+
}
|
|
726
|
+
function concat(...parts) {
|
|
727
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
728
|
+
let offset = 0;
|
|
729
|
+
for (const part of parts) {
|
|
730
|
+
out.set(part, offset);
|
|
731
|
+
offset += part.length;
|
|
732
|
+
}
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
//# sourceMappingURL=snapshot-signerless-transactions.js.map
|