@kaspacom/kcc20-tx-builder 0.8.0-fee.4 → 0.8.0-fee.6
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 +11 -0
- package/RELEASING.md +16 -0
- package/artifacts/KCC20FeePolicyV1.manifest.json +4 -4
- package/artifacts/KCC20WrapperFeeArgentV1.placeholder.json +1816 -360
- package/artifacts/KCC20WrapperFeeV1.placeholder.json +1818 -362
- package/dist/artifacts.d.ts +5 -5
- package/dist/artifacts.js +5 -5
- package/dist/cjs/artifacts.d.ts +5 -5
- package/dist/cjs/artifacts.js +5 -5
- package/dist/cjs/engine.js +2770 -0
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/escrow-market-artifacts.d.ts +90 -0
- package/dist/cjs/escrow-market-artifacts.js +128 -0
- package/dist/cjs/escrow-market-artifacts.js.map +1 -0
- package/dist/cjs/escrow-market-setup.d.ts +429 -0
- package/dist/cjs/escrow-market-setup.js +140 -0
- package/dist/cjs/escrow-market-setup.js.map +1 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/operations.d.ts +1 -1
- package/dist/cjs/operations.js +14 -0
- package/dist/cjs/operations.js.map +1 -1
- package/dist/cjs/sale-common.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +2770 -0
- package/dist/engine.js.map +1 -1
- package/dist/escrow-market-artifacts.d.ts +91 -0
- package/dist/escrow-market-artifacts.d.ts.map +1 -0
- package/dist/escrow-market-artifacts.js +124 -0
- package/dist/escrow-market-artifacts.js.map +1 -0
- package/dist/escrow-market-setup.d.ts +430 -0
- package/dist/escrow-market-setup.d.ts.map +1 -0
- package/dist/escrow-market-setup.js +133 -0
- package/dist/escrow-market-setup.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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 +14 -0
- package/dist/operations.js.map +1 -1
- package/dist/sale-common.d.ts +1 -1
- package/docs/COMPATIBILITY.json +1 -1
- package/package.json +3 -3
- package/src/artifacts.ts +5 -5
- package/src/engine.ts +2986 -0
- package/src/escrow-market-artifacts.ts +129 -0
- package/src/escrow-market-setup.ts +182 -0
- package/src/index.ts +2 -0
- package/src/operations.ts +14 -0
package/dist/engine.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import { planTokenOwnerWitness } from "./token-authorization.js";
|
|
3
3
|
import { sha256 } from "@noble/hashes/sha256";
|
|
4
|
+
import { blake3 } from "@noble/hashes/blake3";
|
|
4
5
|
import { authenticateTokenArtifact, authenticateTokenWrapper, validateTokenProfileRegistry, requireTokenCapability, resolveTokenProfilePriceScale, } from "./token-profiles.js";
|
|
5
6
|
import { buildAuthenticatedKcc20TransferPayload } from "./transfer-payload.js";
|
|
6
7
|
import { BUNDLED_TOKEN_PROFILES, BUNDLED_TOKEN_PROFILE_DIGEST, } from "./token-profiles.generated.js";
|
|
@@ -8,6 +9,14 @@ import { DEFAULT_COMPUTE_BUDGET, DEFAULT_KCC20_FEE_BPS, DEFAULT_KCC20_MIN_PROTOC
|
|
|
8
9
|
import { buildKcc20BurnSigScript, buildKcc20MintByOwnerSigScript, buildKcc20MintPublicSigScript, buildKcc20ScriptForState, buildKcc20SetPublicMintActiveSigScript, buildKcc20TransferSigScript, dispatchTagFor, templateHash, encodeKcc20StateRecordArrays, holderExtensionCommitment, mintExtensionCommitment, normalizeSilverScriptArtifact, ownerAuthorizationWitness, } from "./abi.js";
|
|
9
10
|
import { KCC20_BUILDER_KEYS } from "./operations.js";
|
|
10
11
|
import { buildKcc20InitialFeeAccount, planKcc20FeeAccountBinding } from "./escrow-account-setup.js";
|
|
12
|
+
import { prepareKcc20FeeMarketGenesis, prepareKcc20FeeMarketBinding, prepareKcc20FeeMarketWrappedHolder, prepareKcc20FeeMarketAsk, prepareKcc20FeeMarketBid } from "./escrow-market-setup.js";
|
|
13
|
+
import { buildKcc20FeeMarketRoleScript } from "./escrow-market-artifacts.js";
|
|
14
|
+
import { calculateKcc20EscrowFees } from "./escrow-fee.js";
|
|
15
|
+
import { kcc20CreatorFeeConsent } from "./escrow-policy.js";
|
|
16
|
+
import { encodeKcc20FeeVector, kcc20FeeGroupCommitment } from "./escrow-group.js";
|
|
17
|
+
import { buildKcc20FeeEscrowScript } from "./escrow-receipt.js";
|
|
18
|
+
import { verifyKcc20FeeReceiptForClaim, verifyKcc20FeeAccountForClaim } from "./escrow-receipt.js";
|
|
19
|
+
import { planKcc20FeeClaimTransition } from "./escrow-claim-plan.js";
|
|
11
20
|
import { createKcc20SnapshotPsktBuilder } from "./snapshot-operation.js";
|
|
12
21
|
import { splitKcc20MintSupply } from "./protocol.js";
|
|
13
22
|
import { buildKcc20DeployIntentOutputs } from "./deploy-receipt.js";
|
|
@@ -335,6 +344,20 @@ export function createKcc20PsktBuilderEngine(options) {
|
|
|
335
344
|
"fee-ticket.transfer": buildFeeTicketTransferPskt,
|
|
336
345
|
"kcc20fee.create-account": buildFeeAccountCreatePskt,
|
|
337
346
|
"kcc20fee.bind-account": buildFeeAccountBindPskt,
|
|
347
|
+
"kcc20fee.deploy-market-genesis": buildFeeMarketGenesisPskt,
|
|
348
|
+
"kcc20fee.bind-market": buildFeeMarketBindPskt,
|
|
349
|
+
"kcc20fee.wrap": buildFeeMarketWrapPskt,
|
|
350
|
+
"kcc20fee.unwrap": buildFeeMarketUnwrapPskt,
|
|
351
|
+
"kcc20fee.merge-holders": buildFeeMarketMergeHoldersPskt,
|
|
352
|
+
"kcc20fee.create-ask": buildFeeMarketCreateAskPskt,
|
|
353
|
+
"kcc20fee.cancel-ask": buildFeeMarketCancelAskPskt,
|
|
354
|
+
"kcc20fee.create-bid": buildFeeMarketCreateBidPskt,
|
|
355
|
+
"kcc20fee.cancel-bid": buildFeeMarketCancelBidPskt,
|
|
356
|
+
"kcc20fee.buy-ask": buildFeeMarketBuyAskPskt,
|
|
357
|
+
"kcc20fee.buy-two-asks": buildFeeMarketBuyTwoAsksPskt,
|
|
358
|
+
"kcc20fee.sell-into-bids": buildFeeMarketSellIntoBidsPskt,
|
|
359
|
+
"kcc20fee.claim": buildFeeMarketClaimPskt,
|
|
360
|
+
"kcc20fee.rotate-account": buildFeeMarketRotateAccountPskt,
|
|
338
361
|
"kcc20wrapper.deploy-market": buildWrapperMarketDeployPskt,
|
|
339
362
|
"kcc20wrapper.wrap": buildWrapperWrapPskt,
|
|
340
363
|
"kcc20wrapper.unwrap": buildWrapperUnwrapPskt,
|
|
@@ -1927,6 +1950,2753 @@ export function createKcc20PsktBuilderEngine(options) {
|
|
|
1927
1950
|
applyDummyFeeTicketAndFundingSignatures(kw, predicted, details);
|
|
1928
1951
|
return predicted;
|
|
1929
1952
|
}
|
|
1953
|
+
async function readFeeMarketArtifacts() {
|
|
1954
|
+
const names = {
|
|
1955
|
+
receipt: "KCC20FeeEscrowV1", root: "KCC20OrderbookFeeRootV1",
|
|
1956
|
+
holder: "KCC20OrderbookFeeHolderV1", ask: "KCC20OrderbookFeeAskV1",
|
|
1957
|
+
bid: "KCC20OrderbookFeeBidV1", ticket: "KCC20FeeTicket",
|
|
1958
|
+
wrapper: "KCC20WrapperFeeV1", argentWrapper: "KCC20WrapperFeeArgentV1",
|
|
1959
|
+
};
|
|
1960
|
+
const entries = await Promise.all(Object.entries(names).map(async ([role, name]) => {
|
|
1961
|
+
const key = `${name}.placeholder.json`;
|
|
1962
|
+
const supplied = options.artifacts?.[key] ?? await options.artifactProvider?.(key);
|
|
1963
|
+
const artifact = supplied?.artifact ?? supplied;
|
|
1964
|
+
if (!artifact)
|
|
1965
|
+
throw new Error(`missing pinned ${name} artifact`);
|
|
1966
|
+
return [role, artifact];
|
|
1967
|
+
}));
|
|
1968
|
+
return Object.fromEntries(entries);
|
|
1969
|
+
}
|
|
1970
|
+
function addFeeMarketWrapperState(builder, state) {
|
|
1971
|
+
builder.addData(hexToBytes(state.canonicalTokenId));
|
|
1972
|
+
addExplicitByte(builder, state.enabled ? 1 : 0);
|
|
1973
|
+
builder.addI64(BigInt(state.priceScale));
|
|
1974
|
+
builder.addData(hexToBytes(state.feeTicketId));
|
|
1975
|
+
builder.addData(hexToBytes(state.feePolicy));
|
|
1976
|
+
builder.addData(hexToBytes(state.authenticatedMarketId));
|
|
1977
|
+
}
|
|
1978
|
+
function addFeeMarketRoleState(builder, state) {
|
|
1979
|
+
builder.addData(hexToBytes(state.canonicalTokenId));
|
|
1980
|
+
builder.addData(hexToBytes(state.owner));
|
|
1981
|
+
addExplicitByte(builder, state.owner_scheme);
|
|
1982
|
+
for (const value of [state.amount, state.mode, state.unitPrice]) {
|
|
1983
|
+
builder.addI64(BigInt(value));
|
|
1984
|
+
}
|
|
1985
|
+
builder.addData(hexToBytes(state.feeTicketId));
|
|
1986
|
+
builder.addI64(BigInt(state.priceScale));
|
|
1987
|
+
builder.addData(hexToBytes(state.feePolicy));
|
|
1988
|
+
builder.addData(hexToBytes(state.payout_script_hash));
|
|
1989
|
+
builder.addData(hexToBytes(state.wrapper_spk_hash));
|
|
1990
|
+
}
|
|
1991
|
+
function feeAccountStateTemplateArgs(state) {
|
|
1992
|
+
return [i64TemplateArg(state.role),
|
|
1993
|
+
...[state.token, state.market, state.policy,
|
|
1994
|
+
state.protocol_account, state.creator_account].map(value => dataTemplateArg(hexToBytes(value))),
|
|
1995
|
+
i64TemplateArg(state.protocol_amount),
|
|
1996
|
+
i64TemplateArg(state.creator_amount),
|
|
1997
|
+
dataTemplateArg(hexToBytes(state.owner)),
|
|
1998
|
+
byteTemplateArg(state.owner_scheme),
|
|
1999
|
+
dataTemplateArg(hexToBytes(state.guard_template)),
|
|
2000
|
+
i64TemplateArg(state.guard_prefix),
|
|
2001
|
+
i64TemplateArg(state.guard_suffix),
|
|
2002
|
+
dataTemplateArg(hexToBytes(state.action_hash)),
|
|
2003
|
+
dataTemplateArg(hexToBytes(state.self_template)),
|
|
2004
|
+
i64TemplateArg(state.self_prefix),
|
|
2005
|
+
i64TemplateArg(state.self_suffix),
|
|
2006
|
+
];
|
|
2007
|
+
}
|
|
2008
|
+
async function buildFeeMarketGenesisPskt(input) {
|
|
2009
|
+
const request = input.request || {};
|
|
2010
|
+
const params = request.params || {};
|
|
2011
|
+
const { appCanonicalTokenId, contractCanonicalTokenId } = resolveWrapperMarketDeployTokenIds(params);
|
|
2012
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "fee ticket id");
|
|
2013
|
+
const principalSompi = parseU64(params.wrapperPrincipalSompi, "wrapper principal");
|
|
2014
|
+
if (principalSompi < 50000000n)
|
|
2015
|
+
throw new Error("wrapper principal is below 0.5 KAS");
|
|
2016
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
2017
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
2018
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_PRIORITY_FEE_SOMPI || DEFAULT_WRAPPER_DEPLOY_PRIORITY_FEE.toString(), "priorityFee");
|
|
2019
|
+
const evidence = params.tokenUtxo;
|
|
2020
|
+
if (!evidence)
|
|
2021
|
+
throw new Error("authenticated token output is required before fee-market genesis");
|
|
2022
|
+
const sourceState = kcc20StateFromUtxo(evidence, request.owner);
|
|
2023
|
+
const sourceProfile = await resolveKcc20ArtifactForState(sourceState, requireKaspaAddress(evidence.address), network);
|
|
2024
|
+
requireTokenCapability(sourceProfile.profile, "wrap");
|
|
2025
|
+
const wrapperProfile = sourceProfile.profile.id === "argent-reference-kcc20-v1" ? "argent" : "native";
|
|
2026
|
+
if (wrapperProfile === "native" && sourceProfile.profile.id !== "kaspacom-kcc20-last-call-v1") {
|
|
2027
|
+
throw new Error("unsupported fee-market token profile");
|
|
2028
|
+
}
|
|
2029
|
+
const priceScale = BigInt(resolveTokenProfilePriceScale(sourceProfile.profile, params.priceScale === undefined ? undefined : parsePositiveU64(params.priceScale, "priceScale"), DEFAULT_KCC20_PRICE_SCALE));
|
|
2030
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
2031
|
+
if (wrapperProfile === "argent" && params.feePolicy?.creatorFeeBps !== 0) {
|
|
2032
|
+
throw new Error("Argent token profile cannot authorize creator fees");
|
|
2033
|
+
}
|
|
2034
|
+
const genesis = prepareKcc20FeeMarketGenesis({
|
|
2035
|
+
tokenId: contractCanonicalTokenId, feeTicketId, priceScale,
|
|
2036
|
+
policy: params.feePolicy, wrapperProfile, artifacts,
|
|
2037
|
+
});
|
|
2038
|
+
if (genesis.policyId !== requireHex32(params.policyId, "policy ID")) {
|
|
2039
|
+
throw new Error("fee-market policy ID differs from authenticated policy");
|
|
2040
|
+
}
|
|
2041
|
+
const account = params.protocolAccount;
|
|
2042
|
+
if (!account || requireHex32(account.accountId, "protocol account ID") !==
|
|
2043
|
+
params.feePolicy.protocolAccountId.toLowerCase()) {
|
|
2044
|
+
throw new Error("protocol fee account does not match immutable policy");
|
|
2045
|
+
}
|
|
2046
|
+
const accountPlan = buildKcc20InitialFeeAccount({
|
|
2047
|
+
artifact: artifacts.receipt, tokenId: contractCanonicalTokenId,
|
|
2048
|
+
ownerSchnorrPublicKey: account.state?.owner,
|
|
2049
|
+
principalSompi: parseU64(account.amountSompiExact, "protocol account principal"),
|
|
2050
|
+
});
|
|
2051
|
+
if (account.scriptHashHex?.toLowerCase() !== accountPlan.scriptHash ||
|
|
2052
|
+
account.state?.market !== ZERO_HASH || account.state?.policy !== ZERO_HASH) {
|
|
2053
|
+
throw new Error("protocol fee account must be an authenticated unbound account");
|
|
2054
|
+
}
|
|
2055
|
+
assertScriptAddress(kaspaWasm, accountPlan.redeemScript, requireKaspaAddress(account.address), network, "protocol fee account address");
|
|
2056
|
+
const creatorAccount = params.feePolicy.creatorFeeBps > 0 ? params.creatorAccount : null;
|
|
2057
|
+
if (params.feePolicy.creatorFeeBps > 0 &&
|
|
2058
|
+
(!creatorAccount || requireHex32(creatorAccount.accountId, "creator account ID") !==
|
|
2059
|
+
params.feePolicy.creatorAccountId.toLowerCase())) {
|
|
2060
|
+
throw new Error("creator fee account does not match immutable policy");
|
|
2061
|
+
}
|
|
2062
|
+
const creatorPlan = creatorAccount ? buildKcc20InitialFeeAccount({
|
|
2063
|
+
artifact: artifacts.receipt, tokenId: contractCanonicalTokenId,
|
|
2064
|
+
ownerSchnorrPublicKey: creatorAccount.state?.owner,
|
|
2065
|
+
principalSompi: parseU64(creatorAccount.amountSompiExact, "creator account principal"),
|
|
2066
|
+
}) : null;
|
|
2067
|
+
if (creatorAccount) {
|
|
2068
|
+
if (creatorAccount.scriptHashHex?.toLowerCase() !== creatorPlan.scriptHash ||
|
|
2069
|
+
creatorAccount.state?.market !== ZERO_HASH ||
|
|
2070
|
+
creatorAccount.state?.policy !== ZERO_HASH) {
|
|
2071
|
+
throw new Error("creator fee account must be an authenticated unbound account");
|
|
2072
|
+
}
|
|
2073
|
+
assertScriptAddress(kaspaWasm, creatorPlan.redeemScript, requireKaspaAddress(creatorAccount.address), network, "creator fee account address");
|
|
2074
|
+
}
|
|
2075
|
+
const kw = kaspaWasm;
|
|
2076
|
+
const { Encoding, GenesisCovenantGroup, RpcClient, Transaction, TransactionOutput, covenantId, payToAddressScript, payToScriptHashScript } = kw;
|
|
2077
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
2078
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
2079
|
+
await rpc.connect();
|
|
2080
|
+
try {
|
|
2081
|
+
const [tokenUtxos, accountUtxos, creatorUtxos, walletUtxos] = await Promise.all([
|
|
2082
|
+
getUtxosByAddresses(rpc, [requireKaspaAddress(evidence.address)]),
|
|
2083
|
+
getUtxosByAddresses(rpc, [requireKaspaAddress(account.address)]),
|
|
2084
|
+
creatorAccount ? getUtxosByAddresses(rpc, [requireKaspaAddress(creatorAccount.address)])
|
|
2085
|
+
: Promise.resolve({ entries: [] }),
|
|
2086
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
2087
|
+
]);
|
|
2088
|
+
const tokenEntry = findUtxoEntry(tokenUtxos.entries, requireHex32(evidence.txidHex, "token evidence txid"), parseVout(evidence.vout, "token evidence vout"));
|
|
2089
|
+
if (!tokenEntry)
|
|
2090
|
+
throw new Error("token evidence is spent or unavailable");
|
|
2091
|
+
requireMatchingCovenantId(tokenEntry, contractCanonicalTokenId, "fee-market token evidence");
|
|
2092
|
+
const tokenSpk = tokenEntry.scriptPublicKey ??
|
|
2093
|
+
tokenEntry.entry?.scriptPublicKey ?? tokenEntry.utxoEntry?.scriptPublicKey;
|
|
2094
|
+
if (!tokenSpk || kw.addressFromScriptPublicKey(tokenSpk, network)?.toString() !== evidence.address) {
|
|
2095
|
+
throw new Error("fee-market token evidence script differs from live RPC");
|
|
2096
|
+
}
|
|
2097
|
+
const accountEntry = findUtxoEntry(accountUtxos.entries, requireHex32(account.txidHex, "protocol account txid"), parseVout(account.vout, "protocol account vout"));
|
|
2098
|
+
if (!accountEntry)
|
|
2099
|
+
throw new Error("protocol fee account is spent or unavailable");
|
|
2100
|
+
requireMatchingCovenantId(accountEntry, account.accountId, "protocol fee account");
|
|
2101
|
+
const accountSpk = accountEntry.scriptPublicKey ??
|
|
2102
|
+
accountEntry.entry?.scriptPublicKey ?? accountEntry.utxoEntry?.scriptPublicKey;
|
|
2103
|
+
if (Number(accountSpk?.version) !== 0 ||
|
|
2104
|
+
String(accountSpk?.script).toLowerCase() !== accountPlan.scriptPublicKey.scriptHex ||
|
|
2105
|
+
utxoAmountSompi(accountEntry) !== accountPlan.amountSompi) {
|
|
2106
|
+
throw new Error("protocol fee account live UTXO differs from unbound account proof");
|
|
2107
|
+
}
|
|
2108
|
+
if (creatorAccount) {
|
|
2109
|
+
const creatorEntry = findUtxoEntry(creatorUtxos.entries, requireHex32(creatorAccount.txidHex, "creator account txid"), parseVout(creatorAccount.vout, "creator account vout"));
|
|
2110
|
+
if (!creatorEntry)
|
|
2111
|
+
throw new Error("creator fee account is spent or unavailable");
|
|
2112
|
+
requireMatchingCovenantId(creatorEntry, creatorAccount.accountId, "creator fee account");
|
|
2113
|
+
const creatorSpk = creatorEntry.scriptPublicKey ??
|
|
2114
|
+
creatorEntry.entry?.scriptPublicKey ?? creatorEntry.utxoEntry?.scriptPublicKey;
|
|
2115
|
+
if (Number(creatorSpk?.version) !== 0 ||
|
|
2116
|
+
String(creatorSpk?.script).toLowerCase() !== creatorPlan.scriptPublicKey.scriptHex ||
|
|
2117
|
+
utxoAmountSompi(creatorEntry) !== creatorPlan.amountSompi) {
|
|
2118
|
+
throw new Error("creator fee account live UTXO differs from unbound account proof");
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, principalSompi + priorityFee + 10000n, [], params.fundingOutpoint);
|
|
2122
|
+
const fundingAmount = utxoAmountSompi(fundingEntry);
|
|
2123
|
+
const change = fundingAmount - principalSompi - priorityFee;
|
|
2124
|
+
if (change <= 10000n)
|
|
2125
|
+
throw new Error("fee-market genesis funding change is too small");
|
|
2126
|
+
const transaction = new Transaction({
|
|
2127
|
+
version: 1, lockTime: 0n,
|
|
2128
|
+
inputs: [{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
2129
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 }],
|
|
2130
|
+
outputs: [
|
|
2131
|
+
new TransactionOutput(principalSompi, payToScriptHashScript(genesis.wrapper.redeemScript)),
|
|
2132
|
+
new TransactionOutput(change, payToAddressScript(walletAddress)),
|
|
2133
|
+
],
|
|
2134
|
+
subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
2135
|
+
});
|
|
2136
|
+
setVersionOneInputMassFields(transaction, 30);
|
|
2137
|
+
transaction.populateGenesisCovenants([new GenesisCovenantGroup(0, [0])]);
|
|
2138
|
+
transaction.finalize();
|
|
2139
|
+
const marketId = transaction.outputs[0]?.covenant?.covenantId?.toString();
|
|
2140
|
+
if (!marketId || marketId !== covenantId(fundingEntry.outpoint, [{ index: 0, output: transaction.outputs[0] }]).toString()) {
|
|
2141
|
+
throw new Error("fee-market genesis covenant ID mismatch");
|
|
2142
|
+
}
|
|
2143
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, 1, network, "fee-market genesis", candidate => {
|
|
2144
|
+
const predicted = cloneTransactionForMassPreflight(kw, candidate, "fee-market genesis");
|
|
2145
|
+
predicted.inputs[0].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
2146
|
+
return predicted;
|
|
2147
|
+
});
|
|
2148
|
+
const nonContextualMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market genesis").transactionMass;
|
|
2149
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market genesis");
|
|
2150
|
+
if (nonContextualMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
2151
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
2152
|
+
throw kcc20PsktBuilderError("fee-market genesis exceeds the signed mass target", "TRANSACTION_MASS_EXCEEDED", { nonContextualMass, storageMass });
|
|
2153
|
+
}
|
|
2154
|
+
if (marketId !== covenantId(fundingEntry.outpoint, [{ index: 0, output: feeResult.transaction.outputs[0] }]).toString()) {
|
|
2155
|
+
throw new Error("fee-market genesis ID changed after fee calculation");
|
|
2156
|
+
}
|
|
2157
|
+
return {
|
|
2158
|
+
schema: OUTPUT_SCHEMA,
|
|
2159
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
2160
|
+
signInputs: [{ index: 0, sighashType: 1 }], scripts: [],
|
|
2161
|
+
submitTransactionSupported: true,
|
|
2162
|
+
metadata: {
|
|
2163
|
+
builderKey: "kcc20fee.deploy-market-genesis", operation: "fee-market-genesis",
|
|
2164
|
+
network, walletAddress, tokenId: appCanonicalTokenId,
|
|
2165
|
+
contractCanonicalTokenId, wrapperProfile, policyId: genesis.policyId,
|
|
2166
|
+
marketId, wrapperPrincipalSompi: principalSompi.toString(),
|
|
2167
|
+
wrapperGenesisHint: {
|
|
2168
|
+
vout: 0,
|
|
2169
|
+
address: kw.addressFromScriptPublicKey(payToScriptHashScript(genesis.wrapper.redeemScript), network).toString(),
|
|
2170
|
+
covenantIdHex: marketId, scriptHashHex: genesis.wrapper.scriptHash,
|
|
2171
|
+
amountSompiExact: principalSompi.toString(), state: genesis.state,
|
|
2172
|
+
},
|
|
2173
|
+
},
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
finally {
|
|
2177
|
+
await rpc.disconnect();
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
function verifyReusableCreatorConsent(params, tokenId, marketId, feeTicketId, priceScale, requireAccount) {
|
|
2181
|
+
if (params.feePolicy?.creatorFeeBps === 0)
|
|
2182
|
+
return null;
|
|
2183
|
+
const evidence = params.creatorEvidence;
|
|
2184
|
+
const approval = params.creatorConsent;
|
|
2185
|
+
const creatorOwner = requireHex32(evidence?.creatorOwner, "verified creator owner");
|
|
2186
|
+
if (evidence?.claimVerified !== true || evidence?.verificationStatus !== "verified" ||
|
|
2187
|
+
evidence?.classificationStatus !== "matched" || evidence?.ownerScheme !== 0 ||
|
|
2188
|
+
evidence?.creatorConsentAvailable !== true ||
|
|
2189
|
+
evidence?.contractCanonicalTokenId !== tokenId ||
|
|
2190
|
+
requireHex32(approval?.creatorOwner, "consenting creator owner") !== creatorOwner ||
|
|
2191
|
+
(requireAccount && requireHex32(params.creatorAccount?.accountId, "creator account ID") !== requireHex32(params.feePolicy.creatorAccountId, "policy creator account ID"))) {
|
|
2192
|
+
throw new Error("creator fees require verified token creator evidence and the fixed fee account");
|
|
2193
|
+
}
|
|
2194
|
+
const signature = String(approval?.signature || "").toLowerCase();
|
|
2195
|
+
if (!/^[0-9a-f]{128}$/.test(signature)) {
|
|
2196
|
+
throw new Error("creator consent signature must be a 64-byte Schnorr signature");
|
|
2197
|
+
}
|
|
2198
|
+
const consent = kcc20CreatorFeeConsent({ tokenId, marketId, feeTicketId,
|
|
2199
|
+
priceScale, policy: params.feePolicy });
|
|
2200
|
+
if (typeof kaspaWasm.verifyMessage !== "function" ||
|
|
2201
|
+
!kaspaWasm.verifyMessage({ message: consent.message,
|
|
2202
|
+
signature, publicKey: creatorOwner })) {
|
|
2203
|
+
throw new Error("creator consent does not authorize this exact market and fee policy");
|
|
2204
|
+
}
|
|
2205
|
+
return { commitment: consent.commitment, owner: creatorOwner, signature };
|
|
2206
|
+
}
|
|
2207
|
+
async function buildFeeMarketBindPskt(input) {
|
|
2208
|
+
const request = input.request || {};
|
|
2209
|
+
const params = request.params || {};
|
|
2210
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
2211
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
2212
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "fee ticket ID");
|
|
2213
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
2214
|
+
const rootOwner = requireHex32(request.owner?.kcc20Owner, "root owner");
|
|
2215
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
2216
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
2217
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, rootOwner);
|
|
2218
|
+
const wrapperProfile = params.wrapperProfile;
|
|
2219
|
+
if (wrapperProfile !== "native" && wrapperProfile !== "argent") {
|
|
2220
|
+
throw new Error("unsupported fee-market wrapper profile");
|
|
2221
|
+
}
|
|
2222
|
+
const creatorConsent = verifyReusableCreatorConsent(params, tokenId, marketId, feeTicketId, priceScale, true);
|
|
2223
|
+
const creatorConsentCommitment = creatorConsent?.commitment ?? null;
|
|
2224
|
+
const creatorConsentOwner = creatorConsent?.owner ?? null;
|
|
2225
|
+
const principalSompi = parseU64(params.rootPrincipalSompi, "root principal");
|
|
2226
|
+
if (principalSompi < 50000000n)
|
|
2227
|
+
throw new Error("root principal is below 0.5 KAS");
|
|
2228
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_PRIORITY_FEE_SOMPI || DEFAULT_WRAPPER_DEPLOY_PRIORITY_FEE.toString(), "priorityFee");
|
|
2229
|
+
const computeBudget = parsePositiveNumber(env.KCC20_FEE_MARKET_BIND_COMPUTE_BUDGET || 2000, "computeBudget");
|
|
2230
|
+
const kw = kaspaWasm;
|
|
2231
|
+
const payoutSpk = p2pkScriptPubKey(kw, rootOwner);
|
|
2232
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
2233
|
+
const serializedPayout = new Uint8Array(11 + payoutScript.length);
|
|
2234
|
+
serializedPayout[0] = 0xe3;
|
|
2235
|
+
new DataView(serializedPayout.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
2236
|
+
new DataView(serializedPayout.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
2237
|
+
serializedPayout.set(payoutScript, 11);
|
|
2238
|
+
const rootPayoutScriptHash = bytesToHex(blake3(serializedPayout));
|
|
2239
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
2240
|
+
const plan = prepareKcc20FeeMarketBinding({
|
|
2241
|
+
tokenId, feeTicketId, priceScale: BigInt(priceScale), policy: params.feePolicy,
|
|
2242
|
+
wrapperProfile, artifacts, marketId, rootOwner, rootOwnerScheme: 0,
|
|
2243
|
+
rootPayoutScriptHash,
|
|
2244
|
+
});
|
|
2245
|
+
if (plan.genesis.policyId !== requireHex32(params.policyId, "policy ID")) {
|
|
2246
|
+
throw new Error("fee-market binding policy ID mismatch");
|
|
2247
|
+
}
|
|
2248
|
+
const hint = params.wrapperGenesisHint;
|
|
2249
|
+
if (!hint || requireHex32(hint.covenantIdHex, "genesis market ID") !== marketId ||
|
|
2250
|
+
hint.scriptHashHex?.toLowerCase() !== plan.genesis.wrapper.scriptHash ||
|
|
2251
|
+
!hint.state || Object.keys(hint.state).length !== Object.keys(plan.genesis.state).length ||
|
|
2252
|
+
Object.entries(plan.genesis.state).some(([field, value]) => hint.state[field] !== value)) {
|
|
2253
|
+
throw new Error("unbound wrapper genesis hint differs from pinned policy and artifact");
|
|
2254
|
+
}
|
|
2255
|
+
const wrapperAddress = requireKaspaAddress(hint.address);
|
|
2256
|
+
assertScriptAddress(kw, plan.genesis.wrapper.redeemScript, wrapperAddress, network, "unbound wrapper address");
|
|
2257
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
2258
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
2259
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
2260
|
+
await rpc.connect();
|
|
2261
|
+
try {
|
|
2262
|
+
const [wrapperUtxos, walletUtxos] = await Promise.all([
|
|
2263
|
+
getUtxosByAddresses(rpc, [wrapperAddress]),
|
|
2264
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
2265
|
+
]);
|
|
2266
|
+
const txid = requireHex32(hint.txidHex, "wrapper genesis txid");
|
|
2267
|
+
const vout = parseVout(hint.vout, "wrapper genesis vout");
|
|
2268
|
+
const wrapperEntry = findUtxoEntry(wrapperUtxos.entries, txid, vout);
|
|
2269
|
+
if (!wrapperEntry)
|
|
2270
|
+
throw new Error("unbound wrapper is spent or unavailable");
|
|
2271
|
+
requireMatchingCovenantId(wrapperEntry, marketId, "unbound wrapper");
|
|
2272
|
+
const wrapperSpk = wrapperEntry.scriptPublicKey ??
|
|
2273
|
+
wrapperEntry.entry?.scriptPublicKey ?? wrapperEntry.utxoEntry?.scriptPublicKey;
|
|
2274
|
+
if (Number(wrapperSpk?.version) !== 0 ||
|
|
2275
|
+
String(wrapperSpk?.script).toLowerCase() !== plan.genesis.wrapper.scriptPublicKey.scriptHex ||
|
|
2276
|
+
utxoAmountSompi(wrapperEntry).toString() !== hint.amountSompiExact) {
|
|
2277
|
+
throw new Error("unbound wrapper live UTXO differs from genesis hint");
|
|
2278
|
+
}
|
|
2279
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, principalSompi + priorityFee + 10000n, [wrapperEntry.outpoint], params.fundingOutpoint);
|
|
2280
|
+
const fundingAmount = utxoAmountSompi(fundingEntry);
|
|
2281
|
+
const change = fundingAmount - principalSompi - priorityFee;
|
|
2282
|
+
if (change <= 10000n)
|
|
2283
|
+
throw new Error("fee-market binding funding change is too small");
|
|
2284
|
+
const unsignedTx = new Transaction({
|
|
2285
|
+
version: 1, lockTime: 0n,
|
|
2286
|
+
inputs: [
|
|
2287
|
+
{ previousOutpoint: wrapperEntry.outpoint, utxo: wrapperEntry,
|
|
2288
|
+
sequence: 0n, sigOpCount: 0, computeBudget },
|
|
2289
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
2290
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
2291
|
+
],
|
|
2292
|
+
outputs: [
|
|
2293
|
+
new TransactionOutput(utxoAmountSompi(wrapperEntry), payToScriptHashScript(plan.wrapper.redeemScript), new CovenantBinding(0, new Hash(marketId))),
|
|
2294
|
+
new TransactionOutput(principalSompi, payToScriptHashScript(plan.root.redeemScript), new CovenantBinding(0, new Hash(marketId))),
|
|
2295
|
+
new TransactionOutput(change, payToAddressScript(walletAddress)),
|
|
2296
|
+
],
|
|
2297
|
+
subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
2298
|
+
});
|
|
2299
|
+
setVersionOneInputMassFields(unsignedTx, computeBudget);
|
|
2300
|
+
unsignedTx.inputs[1].computeBudget = 30;
|
|
2301
|
+
assertUniqueTransactionInputOutpoints(unsignedTx, "fee-market binding");
|
|
2302
|
+
const wrapperArtifact = artifacts[wrapperProfile === "native" ? "wrapper" : "argentWrapper"];
|
|
2303
|
+
const rootTemplate = plan.root.template;
|
|
2304
|
+
const rootScript = plan.root.redeemScript;
|
|
2305
|
+
const rootPrefix = rootScript.slice(0, rootTemplate.prefixLength);
|
|
2306
|
+
const rootSuffix = rootScript.slice(rootScript.length - rootTemplate.suffixLength);
|
|
2307
|
+
const scripts = [{
|
|
2308
|
+
inputIndex: 0,
|
|
2309
|
+
scriptHex: bytesToHex(plan.genesis.wrapper.redeemScript),
|
|
2310
|
+
signType: 1,
|
|
2311
|
+
signatureScript: { mode: "ordered-args", args: [
|
|
2312
|
+
i64TemplateArg(0), i64TemplateArg(1),
|
|
2313
|
+
dataTemplateArg(rootPrefix), dataTemplateArg(rootSuffix),
|
|
2314
|
+
dataTemplateArg(hexToBytes(rootOwner)), byteTemplateArg(0),
|
|
2315
|
+
dataTemplateArg(hexToBytes(rootPayoutScriptHash)),
|
|
2316
|
+
dataTemplateArg(hexToBytes(plan.wrapper.scriptHash)),
|
|
2317
|
+
dataTemplateArg(dispatchTagFor(wrapperArtifact, "bindMarket")),
|
|
2318
|
+
] },
|
|
2319
|
+
}];
|
|
2320
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, unsignedTx, 2, network, "fee-market binding", candidate => {
|
|
2321
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
2322
|
+
if (typeof serialized !== "string")
|
|
2323
|
+
throw new Error("fee-market binding preflight serialization failed");
|
|
2324
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
2325
|
+
predictedJson.inputs[1].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
2326
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
2327
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
2328
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
2329
|
+
});
|
|
2330
|
+
const nonContextualMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market binding").transactionMass;
|
|
2331
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market binding");
|
|
2332
|
+
if (nonContextualMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
2333
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
2334
|
+
throw kcc20PsktBuilderError("fee-market binding exceeds the signed mass target", "TRANSACTION_MASS_EXCEEDED", { nonContextualMass, storageMass });
|
|
2335
|
+
}
|
|
2336
|
+
const psktTransactionJson = feeResult.transaction.serializeToSafeJSON();
|
|
2337
|
+
if (typeof psktTransactionJson !== "string")
|
|
2338
|
+
throw new Error("fee-market binding PSKT serialization failed");
|
|
2339
|
+
return {
|
|
2340
|
+
schema: OUTPUT_SCHEMA, psktTransactionJson,
|
|
2341
|
+
signInputs: [{ index: 1, sighashType: 1 }], scripts,
|
|
2342
|
+
submitTransactionSupported: true,
|
|
2343
|
+
metadata: {
|
|
2344
|
+
builderKey: "kcc20fee.bind-market", operation: "bind-fee-market",
|
|
2345
|
+
network, walletAddress, tokenId, marketId, policyId: plan.genesis.policyId,
|
|
2346
|
+
creatorConsentCommitment, creatorConsentOwner,
|
|
2347
|
+
wrapperPrincipalSompi: utxoAmountSompi(wrapperEntry).toString(),
|
|
2348
|
+
rootPrincipalSompi: principalSompi.toString(),
|
|
2349
|
+
wrapperAddress: kw.addressFromScriptPublicKey(payToScriptHashScript(plan.wrapper.redeemScript), network).toString(),
|
|
2350
|
+
rootAddress: kw.addressFromScriptPublicKey(payToScriptHashScript(plan.root.redeemScript), network).toString(),
|
|
2351
|
+
},
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
finally {
|
|
2355
|
+
await rpc.disconnect();
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
async function buildFeeMarketWrapPskt(input) {
|
|
2359
|
+
const request = input.request || {};
|
|
2360
|
+
const params = request.params || {};
|
|
2361
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
2362
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
2363
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
2364
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
2365
|
+
const tokenAmount = parsePositiveU64(params.tokenAmount, "wrapped token amount");
|
|
2366
|
+
const recipientOwner = requireHex32(params.recipientOwner || request.owner?.kcc20Owner, "wrapped recipient owner");
|
|
2367
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
2368
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, recipientOwner);
|
|
2369
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
2370
|
+
const source = params.tokenUtxo;
|
|
2371
|
+
const wrapperUtxo = params.wrapperUtxo;
|
|
2372
|
+
if (!source || !wrapperUtxo)
|
|
2373
|
+
throw new Error("current token and bound wrapper UTXOs are required");
|
|
2374
|
+
const tokenState = kcc20StateFromUtxo(source, request.owner);
|
|
2375
|
+
assertKcc20HolderStateForWrap(tokenState);
|
|
2376
|
+
if (tokenState.ownerScheme !== 0 || bytesToHex(tokenState.owner) !== recipientOwner) {
|
|
2377
|
+
throw new Error("token holder is not controlled by the signing wallet");
|
|
2378
|
+
}
|
|
2379
|
+
if (tokenState.amount < tokenAmount)
|
|
2380
|
+
throw new Error("wrapped amount exceeds token input");
|
|
2381
|
+
const tokenProfile = await resolveKcc20ArtifactForState(tokenState, requireKaspaAddress(source.address), network);
|
|
2382
|
+
requireTokenCapability(tokenProfile.profile, "wrap");
|
|
2383
|
+
if (tokenProfile.profile.id !== "kaspacom-kcc20-last-call-v1") {
|
|
2384
|
+
throw new Error("fee-market wrap for this token profile needs its signed witness proof");
|
|
2385
|
+
}
|
|
2386
|
+
const creatorConsent = verifyReusableCreatorConsent(params, tokenId, marketId, feeTicketId, priceScale, false);
|
|
2387
|
+
let creatorHolderExtension = new Uint8Array(0);
|
|
2388
|
+
if (creatorConsent) {
|
|
2389
|
+
creatorHolderExtension = hexToBytes(String(params.creatorHolderExtensionHex || ""));
|
|
2390
|
+
if (creatorHolderExtension.length !== 202 || creatorHolderExtension[0] !== 1 ||
|
|
2391
|
+
bytesToHex(creatorHolderExtension.subarray(1, 33)) !== creatorConsent.owner ||
|
|
2392
|
+
new DataView(creatorHolderExtension.buffer, creatorHolderExtension.byteOffset + 97, 8).getBigInt64(0, true) !== priceScale ||
|
|
2393
|
+
bytesToHex(blake3(new Uint8Array([
|
|
2394
|
+
...new TextEncoder().encode("KASPACOM/KCC20/HOLDER/V1"),
|
|
2395
|
+
...creatorHolderExtension,
|
|
2396
|
+
]))) !== bytesToHex(tokenState.extensionCommitment)) {
|
|
2397
|
+
throw new Error("creator holder extension differs from the authenticated token input");
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const kw = kaspaWasm;
|
|
2401
|
+
const payoutSpk = p2pkScriptPubKey(kw, recipientOwner);
|
|
2402
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
2403
|
+
const serializedPayout = new Uint8Array(11 + payoutScript.length);
|
|
2404
|
+
serializedPayout[0] = 0xe3;
|
|
2405
|
+
new DataView(serializedPayout.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
2406
|
+
new DataView(serializedPayout.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
2407
|
+
serializedPayout.set(payoutScript, 11);
|
|
2408
|
+
const payoutHash = bytesToHex(blake3(serializedPayout));
|
|
2409
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
2410
|
+
const plan = prepareKcc20FeeMarketWrappedHolder({
|
|
2411
|
+
tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
2412
|
+
wrapperProfile: "native", artifacts, marketId, recipientOwner,
|
|
2413
|
+
recipientOwnerScheme: 0, recipientPayoutScriptHash: payoutHash,
|
|
2414
|
+
tokenAmount,
|
|
2415
|
+
});
|
|
2416
|
+
if (prepareKcc20FeeMarketGenesis({ tokenId, feeTicketId, priceScale,
|
|
2417
|
+
policy: params.feePolicy, wrapperProfile: "native", artifacts }).policyId !==
|
|
2418
|
+
requireHex32(params.policyId, "policy ID")) {
|
|
2419
|
+
throw new Error("fee-market wrap policy ID mismatch");
|
|
2420
|
+
}
|
|
2421
|
+
const tokenScript = buildKcc20ScriptForState(tokenProfile.artifact, tokenState);
|
|
2422
|
+
assertScriptAddress(kw, tokenScript, requireKaspaAddress(source.address), network, "fee-market token source address");
|
|
2423
|
+
assertScriptAddress(kw, plan.wrapper.redeemScript, requireKaspaAddress(wrapperUtxo.address), network, "fee-market wrapper address");
|
|
2424
|
+
const tokenOutputSompi = parseU64(env.KCC20_WRAP_TOKEN_OUTPUT_SOMPI || DEFAULT_TOKEN_OUTPUT_SOMPI.toString(), "tokenOutputSompi");
|
|
2425
|
+
if (tokenOutputSompi < 50000000n)
|
|
2426
|
+
throw new Error("fee-market token deposit below 0.5 KAS");
|
|
2427
|
+
const priorityFee = parseU64(env.KCC20_WRAP_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
2428
|
+
const computeBudget = parsePositiveNumber(env.KCC20_FEE_MARKET_WRAP_COMPUTE_BUDGET || 300, "computeBudget");
|
|
2429
|
+
const tokenComputeBudget = parsePositiveNumber(env.KCC20_FEE_MARKET_WRAP_TOKEN_COMPUTE_BUDGET || 100, "tokenComputeBudget");
|
|
2430
|
+
if (computeBudget < 300 || tokenComputeBudget < 100) {
|
|
2431
|
+
throw new Error("fee-market wrap compute budgets are below the signed proof bounds");
|
|
2432
|
+
}
|
|
2433
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
2434
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
2435
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
2436
|
+
await rpc.connect();
|
|
2437
|
+
try {
|
|
2438
|
+
const [covenantUtxos, walletUtxos] = await Promise.all([
|
|
2439
|
+
getUtxosByAddresses(rpc, [source.address, wrapperUtxo.address]),
|
|
2440
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
2441
|
+
]);
|
|
2442
|
+
const tokenEntry = findUtxoEntry(covenantUtxos.entries, requireHex32(source.txidHex, "token txid"), parseVout(source.vout, "token vout"));
|
|
2443
|
+
const wrapperEntry = findUtxoEntry(covenantUtxos.entries, requireHex32(wrapperUtxo.txidHex, "wrapper txid"), parseVout(wrapperUtxo.vout, "wrapper vout"));
|
|
2444
|
+
if (!tokenEntry || !wrapperEntry)
|
|
2445
|
+
throw new Error("fee-market wrap input is spent or unavailable");
|
|
2446
|
+
requireMatchingCovenantId(tokenEntry, tokenId, "fee-market token input");
|
|
2447
|
+
requireMatchingCovenantId(wrapperEntry, marketId, "fee-market wrapper input");
|
|
2448
|
+
const tokenSpk = tokenEntry.scriptPublicKey ?? tokenEntry.entry?.scriptPublicKey ??
|
|
2449
|
+
tokenEntry.utxoEntry?.scriptPublicKey;
|
|
2450
|
+
const wrapperSpk = wrapperEntry.scriptPublicKey ?? wrapperEntry.entry?.scriptPublicKey ??
|
|
2451
|
+
wrapperEntry.utxoEntry?.scriptPublicKey;
|
|
2452
|
+
if (Number(tokenSpk?.version) !== 0 ||
|
|
2453
|
+
String(tokenSpk?.script).toLowerCase() !==
|
|
2454
|
+
String(payToScriptHashScript(tokenScript).script).toLowerCase() ||
|
|
2455
|
+
Number(wrapperSpk?.version) !== 0 ||
|
|
2456
|
+
String(wrapperSpk?.script).toLowerCase() !== plan.wrapper.scriptPublicKey.scriptHex ||
|
|
2457
|
+
utxoAmountSompi(tokenEntry).toString() !== String(source.amountSompiExact) ||
|
|
2458
|
+
utxoAmountSompi(wrapperEntry).toString() !== String(wrapperUtxo.amountSompiExact)) {
|
|
2459
|
+
throw new Error("fee-market wrap input differs from live node UTXO");
|
|
2460
|
+
}
|
|
2461
|
+
const lockedState = { ...tokenState, owner: hexToBytes(plan.wrapper.scriptHash),
|
|
2462
|
+
ownerIdentifier: hexToBytes(plan.wrapper.scriptHash), ownerScheme: 3,
|
|
2463
|
+
identifierType: 3, borrowScheme: 0, borrowGuard: hexToBytes(ZERO_HASH),
|
|
2464
|
+
amount: tokenAmount };
|
|
2465
|
+
const remaining = tokenState.amount - tokenAmount;
|
|
2466
|
+
const changeState = remaining > 0n ? { ...tokenState, amount: remaining } : null;
|
|
2467
|
+
const outputs = [
|
|
2468
|
+
tokenOutput(kw, tokenProfile.artifact, lockedState, tokenOutputSompi, tokenId, 0),
|
|
2469
|
+
new TransactionOutput(tokenOutputSompi, payToScriptHashScript(plan.holder.redeemScript), new CovenantBinding(1, new Hash(marketId))),
|
|
2470
|
+
new TransactionOutput(utxoAmountSompi(wrapperEntry), payToScriptHashScript(plan.wrapper.redeemScript), new CovenantBinding(1, new Hash(marketId))),
|
|
2471
|
+
];
|
|
2472
|
+
if (changeState)
|
|
2473
|
+
outputs.push(tokenOutput(kw, tokenProfile.artifact, changeState, utxoAmountSompi(tokenEntry), tokenId, 0));
|
|
2474
|
+
const outputSompi = outputs.reduce((sum, output) => sum + BigInt(output.value), 0n);
|
|
2475
|
+
const covenantInputSompi = utxoAmountSompi(tokenEntry) + utxoAmountSompi(wrapperEntry);
|
|
2476
|
+
const requiredFunding = outputSompi + priorityFee > covenantInputSompi
|
|
2477
|
+
? outputSompi + priorityFee - covenantInputSompi + 10000n
|
|
2478
|
+
: priorityFee + 10000n;
|
|
2479
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, requiredFunding, [tokenEntry.outpoint, wrapperEntry.outpoint], params.fundingOutpoint);
|
|
2480
|
+
const fundingChange = covenantInputSompi + utxoAmountSompi(fundingEntry) - outputSompi - priorityFee;
|
|
2481
|
+
if (fundingChange <= 10000n)
|
|
2482
|
+
throw new Error("fee-market wrap funding change is too small");
|
|
2483
|
+
const fundingChangeIndex = outputs.length;
|
|
2484
|
+
outputs.push(new TransactionOutput(fundingChange, payToAddressScript(walletAddress)));
|
|
2485
|
+
const transaction = new Transaction({
|
|
2486
|
+
version: 1, lockTime: 0n,
|
|
2487
|
+
inputs: [
|
|
2488
|
+
{ previousOutpoint: tokenEntry.outpoint, utxo: tokenEntry,
|
|
2489
|
+
sequence: 0n, sigOpCount: 0, computeBudget: tokenComputeBudget },
|
|
2490
|
+
{ previousOutpoint: wrapperEntry.outpoint, utxo: wrapperEntry,
|
|
2491
|
+
sequence: 0n, sigOpCount: 0, computeBudget },
|
|
2492
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
2493
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
2494
|
+
],
|
|
2495
|
+
outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
2496
|
+
});
|
|
2497
|
+
setVersionOneInputMassFields(transaction, computeBudget);
|
|
2498
|
+
transaction.inputs[0].computeBudget = tokenComputeBudget;
|
|
2499
|
+
transaction.inputs[2].computeBudget = 30;
|
|
2500
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market wrap");
|
|
2501
|
+
const wrapperPrefix = covenantScriptBuilder(kw);
|
|
2502
|
+
addFeeMarketWrapperState(wrapperPrefix, plan.wrapperState);
|
|
2503
|
+
addFeeMarketWrapperState(wrapperPrefix, plan.wrapperState);
|
|
2504
|
+
for (const index of [0, 0, 1, 2])
|
|
2505
|
+
wrapperPrefix.addI64(BigInt(index));
|
|
2506
|
+
wrapperPrefix.addData(hexToBytes(recipientOwner));
|
|
2507
|
+
addExplicitByte(wrapperPrefix, 0);
|
|
2508
|
+
wrapperPrefix.addData(hexToBytes(payoutHash));
|
|
2509
|
+
wrapperPrefix.addI64(tokenAmount);
|
|
2510
|
+
const holderParts = templateParts(normalizeSilverScriptArtifact(artifacts.holder));
|
|
2511
|
+
wrapperPrefix.addData(holderParts.prefix);
|
|
2512
|
+
wrapperPrefix.addData(holderParts.suffix);
|
|
2513
|
+
addKcc20State(wrapperPrefix, lockedState);
|
|
2514
|
+
addFeeMarketRoleState(wrapperPrefix, plan.holderState);
|
|
2515
|
+
wrapperPrefix.addData(creatorHolderExtension);
|
|
2516
|
+
wrapperPrefix.addData(creatorConsent
|
|
2517
|
+
? hexToBytes(creatorConsent.signature) : new Uint8Array(64));
|
|
2518
|
+
wrapperPrefix.addData(dispatchTagFor(artifacts.wrapper, "wrap"));
|
|
2519
|
+
transaction.inputs[1].signatureScript = encodeCovenantP2shSignatureScript(wrapperPrefix.drain(), plan.wrapper.redeemScript);
|
|
2520
|
+
const scripts = [{ inputIndex: 0, scriptHex: bytesToHex(tokenScript),
|
|
2521
|
+
signType: 1, signatureScript: { mode: "ordered-args",
|
|
2522
|
+
args: kcc20TransferOrderedArgs(tokenProfile.artifact, changeState ? [lockedState, changeState] : [lockedState]) } }];
|
|
2523
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market wrap", candidate => {
|
|
2524
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
2525
|
+
if (typeof serialized !== "string")
|
|
2526
|
+
throw new Error("fee-market wrap preflight serialization failed");
|
|
2527
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
2528
|
+
predictedJson.inputs[2].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
2529
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
2530
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
2531
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
2532
|
+
});
|
|
2533
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market wrap");
|
|
2534
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market wrap");
|
|
2535
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
2536
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
2537
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
2538
|
+
throw kcc20PsktBuilderError("fee-market wrap exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", {
|
|
2539
|
+
computeMass: signedMass.computeMass,
|
|
2540
|
+
transientMass: signedMass.transientMass,
|
|
2541
|
+
storageMass,
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
return { schema: OUTPUT_SCHEMA,
|
|
2545
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
2546
|
+
signInputs: [{ index: 2, sighashType: 1 }], scripts,
|
|
2547
|
+
submitTransactionSupported: true,
|
|
2548
|
+
metadata: { builderKey: "kcc20fee.wrap", operation: "fee-market-wrap",
|
|
2549
|
+
network, walletAddress, tokenId, marketId, policyId: params.policyId,
|
|
2550
|
+
tokenAmount: tokenAmount.toString(), remainingTokenAmount: remaining.toString(),
|
|
2551
|
+
reserveOutputIndex: 0, holderOutputIndex: 1, wrapperOutputIndex: 2,
|
|
2552
|
+
tokenChangeOutputIndex: changeState ? 3 : -1,
|
|
2553
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
2554
|
+
transient: signedMass.transientMass.toString(),
|
|
2555
|
+
storage: storageMass.toString() } },
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
finally {
|
|
2559
|
+
await rpc.disconnect();
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
async function buildFeeMarketUnwrapPskt(input) {
|
|
2563
|
+
const request = input.request || {};
|
|
2564
|
+
const params = request.params || {};
|
|
2565
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
2566
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
2567
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
2568
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
2569
|
+
const tokenAmount = parsePositiveU64(params.tokenAmount, "unwrapped token amount");
|
|
2570
|
+
const recipientOwner = requireHex32(params.recipientOwner || request.owner?.kcc20Owner, "unwrapped recipient owner");
|
|
2571
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
2572
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, recipientOwner);
|
|
2573
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
2574
|
+
const wrappedUtxo = params.wrappedUtxo;
|
|
2575
|
+
const reserveUtxo = params.reserveUtxo;
|
|
2576
|
+
const wrapperUtxo = params.wrapperUtxo;
|
|
2577
|
+
if (!wrappedUtxo || !reserveUtxo || !wrapperUtxo) {
|
|
2578
|
+
throw new Error("current wrapped holder, reserve and wrapper UTXOs are required");
|
|
2579
|
+
}
|
|
2580
|
+
const wrappedAmount = parsePositiveU64(wrappedUtxo.state?.amount, "wrapped holder amount");
|
|
2581
|
+
if (wrappedAmount < tokenAmount)
|
|
2582
|
+
throw new Error("unwrapped amount exceeds wrapped holder");
|
|
2583
|
+
if (String(wrappedUtxo.state?.mode) !== "0" ||
|
|
2584
|
+
Number(wrappedUtxo.state?.owner_scheme) !== 0 ||
|
|
2585
|
+
String(wrappedUtxo.state?.owner).toLowerCase() !== recipientOwner) {
|
|
2586
|
+
throw new Error("wrapped holder is not owned by the signing wallet");
|
|
2587
|
+
}
|
|
2588
|
+
const reserveState = kcc20StateFromUtxo(reserveUtxo, request.owner);
|
|
2589
|
+
assertKcc20HolderStateForWrap(reserveState);
|
|
2590
|
+
if (reserveState.ownerScheme !== 3 || reserveState.amount < tokenAmount) {
|
|
2591
|
+
throw new Error("fee-market reserve has wrong owner scheme or insufficient tokens");
|
|
2592
|
+
}
|
|
2593
|
+
const tokenProfile = await resolveKcc20ArtifactForState(reserveState, requireKaspaAddress(reserveUtxo.address), network);
|
|
2594
|
+
requireTokenCapability(tokenProfile.profile, "unwrap");
|
|
2595
|
+
if (tokenProfile.profile.id !== "kaspacom-kcc20-last-call-v1") {
|
|
2596
|
+
throw new Error("fee-market unwrap for this token profile needs its signed witness proof");
|
|
2597
|
+
}
|
|
2598
|
+
const kw = kaspaWasm;
|
|
2599
|
+
const payoutSpk = p2pkScriptPubKey(kw, recipientOwner);
|
|
2600
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
2601
|
+
const serializedPayout = new Uint8Array(11 + payoutScript.length);
|
|
2602
|
+
serializedPayout[0] = 0xe3;
|
|
2603
|
+
new DataView(serializedPayout.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
2604
|
+
new DataView(serializedPayout.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
2605
|
+
serializedPayout.set(payoutScript, 11);
|
|
2606
|
+
const payoutHash = bytesToHex(blake3(serializedPayout));
|
|
2607
|
+
if (String(wrappedUtxo.state?.payout_script_hash).toLowerCase() !== payoutHash) {
|
|
2608
|
+
throw new Error("wrapped holder refund destination differs from wallet");
|
|
2609
|
+
}
|
|
2610
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
2611
|
+
const planInput = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
2612
|
+
wrapperProfile: "native", artifacts, marketId, recipientOwner,
|
|
2613
|
+
recipientOwnerScheme: 0, recipientPayoutScriptHash: payoutHash };
|
|
2614
|
+
const plan = prepareKcc20FeeMarketWrappedHolder({ ...planInput,
|
|
2615
|
+
tokenAmount: wrappedAmount });
|
|
2616
|
+
if (prepareKcc20FeeMarketGenesis(planInput).policyId !==
|
|
2617
|
+
requireHex32(params.policyId, "policy ID")) {
|
|
2618
|
+
throw new Error("fee-market unwrap policy ID mismatch");
|
|
2619
|
+
}
|
|
2620
|
+
if (bytesToHex(reserveState.owner) !== plan.wrapper.scriptHash) {
|
|
2621
|
+
throw new Error("native reserve belongs to a different wrapper script");
|
|
2622
|
+
}
|
|
2623
|
+
const reserveScript = buildKcc20ScriptForState(tokenProfile.artifact, reserveState);
|
|
2624
|
+
assertScriptAddress(kw, reserveScript, requireKaspaAddress(reserveUtxo.address), network, "fee-market reserve address");
|
|
2625
|
+
assertScriptAddress(kw, plan.holder.redeemScript, requireKaspaAddress(wrappedUtxo.address), network, "fee-market holder address");
|
|
2626
|
+
assertScriptAddress(kw, plan.wrapper.redeemScript, requireKaspaAddress(wrapperUtxo.address), network, "fee-market wrapper address");
|
|
2627
|
+
const recipientOutputSompi = parseU64(env.KCC20_UNWRAP_TOKEN_OUTPUT_SOMPI || DEFAULT_TOKEN_OUTPUT_SOMPI.toString(), "recipientOutputSompi");
|
|
2628
|
+
if (recipientOutputSompi < 50000000n)
|
|
2629
|
+
throw new Error("token recipient deposit below 0.5 KAS");
|
|
2630
|
+
const priorityFee = parseU64(env.KCC20_UNWRAP_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
2631
|
+
const wrapperBudget = parsePositiveNumber(env.KCC20_FEE_MARKET_UNWRAP_WRAPPER_COMPUTE_BUDGET || 300, "wrapperBudget");
|
|
2632
|
+
const holderBudget = parsePositiveNumber(env.KCC20_FEE_MARKET_UNWRAP_HOLDER_COMPUTE_BUDGET || 300, "holderBudget");
|
|
2633
|
+
const tokenBudget = parsePositiveNumber(env.KCC20_FEE_MARKET_UNWRAP_TOKEN_COMPUTE_BUDGET || 100, "tokenBudget");
|
|
2634
|
+
if (wrapperBudget < 300 || holderBudget < 300 || tokenBudget < 100) {
|
|
2635
|
+
throw new Error("fee-market unwrap compute budgets are below signed proof bounds");
|
|
2636
|
+
}
|
|
2637
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
2638
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
2639
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
2640
|
+
await rpc.connect();
|
|
2641
|
+
try {
|
|
2642
|
+
const [covenantUtxos, walletUtxos] = await Promise.all([
|
|
2643
|
+
getUtxosByAddresses(rpc, [wrappedUtxo.address, reserveUtxo.address,
|
|
2644
|
+
wrapperUtxo.address]),
|
|
2645
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
2646
|
+
]);
|
|
2647
|
+
const getExact = (utxo, label) => {
|
|
2648
|
+
const entry = findUtxoEntry(covenantUtxos.entries, requireHex32(utxo.txidHex, `${label} txid`), parseVout(utxo.vout, `${label} vout`));
|
|
2649
|
+
if (!entry)
|
|
2650
|
+
throw new Error(`${label} is spent or unavailable`);
|
|
2651
|
+
return entry;
|
|
2652
|
+
};
|
|
2653
|
+
const wrappedEntry = getExact(wrappedUtxo, "wrapped holder");
|
|
2654
|
+
const reserveEntry = getExact(reserveUtxo, "native reserve");
|
|
2655
|
+
const wrapperEntry = getExact(wrapperUtxo, "fee-market wrapper");
|
|
2656
|
+
for (const [entry, expected, label] of [
|
|
2657
|
+
[wrappedEntry, marketId, "wrapped holder"],
|
|
2658
|
+
[reserveEntry, tokenId, "native reserve"],
|
|
2659
|
+
[wrapperEntry, marketId, "fee-market wrapper"],
|
|
2660
|
+
])
|
|
2661
|
+
requireMatchingCovenantId(entry, expected, label);
|
|
2662
|
+
for (const [entry, utxo, script, label] of [
|
|
2663
|
+
[wrappedEntry, wrappedUtxo, plan.holder.redeemScript, "wrapped holder"],
|
|
2664
|
+
[reserveEntry, reserveUtxo, reserveScript, "native reserve"],
|
|
2665
|
+
[wrapperEntry, wrapperUtxo, plan.wrapper.redeemScript, "fee-market wrapper"],
|
|
2666
|
+
]) {
|
|
2667
|
+
const spk = entry.scriptPublicKey ?? entry.entry?.scriptPublicKey ??
|
|
2668
|
+
entry.utxoEntry?.scriptPublicKey;
|
|
2669
|
+
if (Number(spk?.version) !== 0 ||
|
|
2670
|
+
String(spk?.script).toLowerCase() !==
|
|
2671
|
+
String(payToScriptHashScript(script).script).toLowerCase() ||
|
|
2672
|
+
utxoAmountSompi(entry).toString() !== String(utxo.amountSompiExact)) {
|
|
2673
|
+
throw new Error(`${label} differs from live node UTXO`);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
const recipientState = { ...reserveState, owner: hexToBytes(recipientOwner),
|
|
2677
|
+
ownerIdentifier: hexToBytes(recipientOwner), ownerScheme: 0, identifierType: 0,
|
|
2678
|
+
amount: tokenAmount };
|
|
2679
|
+
const reserveRemaining = reserveState.amount - tokenAmount;
|
|
2680
|
+
const wrappedRemaining = wrappedAmount - tokenAmount;
|
|
2681
|
+
const reserveChangeState = { ...reserveState, amount: reserveRemaining };
|
|
2682
|
+
const wrappedChangeState = { ...plan.holderState,
|
|
2683
|
+
amount: wrappedRemaining.toString() };
|
|
2684
|
+
const outputs = [tokenOutput(kw, tokenProfile.artifact, recipientState, reserveRemaining > 0n ? recipientOutputSompi : utxoAmountSompi(reserveEntry), tokenId, 1)];
|
|
2685
|
+
const reserveChangeIndex = reserveRemaining > 0n ? outputs.length : -1;
|
|
2686
|
+
if (reserveRemaining > 0n)
|
|
2687
|
+
outputs.push(tokenOutput(kw, tokenProfile.artifact, reserveChangeState, utxoAmountSompi(reserveEntry), tokenId, 1));
|
|
2688
|
+
const wrappedChangeIndex = wrappedRemaining > 0n ? outputs.length : -1;
|
|
2689
|
+
if (wrappedRemaining > 0n) {
|
|
2690
|
+
const changedHolder = prepareKcc20FeeMarketWrappedHolder({ ...planInput,
|
|
2691
|
+
tokenAmount: wrappedRemaining });
|
|
2692
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(wrappedEntry), payToScriptHashScript(changedHolder.holder.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
2693
|
+
}
|
|
2694
|
+
const refundIndex = wrappedRemaining === 0n ? outputs.length : -1;
|
|
2695
|
+
if (wrappedRemaining === 0n) {
|
|
2696
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(wrappedEntry), payoutSpk));
|
|
2697
|
+
}
|
|
2698
|
+
const wrapperOutputIndex = outputs.length;
|
|
2699
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(wrapperEntry), payToScriptHashScript(plan.wrapper.redeemScript), new CovenantBinding(2, new Hash(marketId))));
|
|
2700
|
+
const outputSompi = outputs.reduce((sum, output) => sum + BigInt(output.value), 0n);
|
|
2701
|
+
const covenantInputSompi = utxoAmountSompi(wrappedEntry) +
|
|
2702
|
+
utxoAmountSompi(reserveEntry) + utxoAmountSompi(wrapperEntry);
|
|
2703
|
+
const requiredFunding = outputSompi + priorityFee > covenantInputSompi
|
|
2704
|
+
? outputSompi + priorityFee - covenantInputSompi + 10000n
|
|
2705
|
+
: priorityFee + 10000n;
|
|
2706
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, requiredFunding, [wrappedEntry.outpoint, reserveEntry.outpoint, wrapperEntry.outpoint], params.fundingOutpoint);
|
|
2707
|
+
const fundingChange = covenantInputSompi + utxoAmountSompi(fundingEntry) -
|
|
2708
|
+
outputSompi - priorityFee;
|
|
2709
|
+
if (fundingChange <= 10000n)
|
|
2710
|
+
throw new Error("fee-market unwrap funding change is too small");
|
|
2711
|
+
const fundingChangeIndex = outputs.length;
|
|
2712
|
+
outputs.push(new TransactionOutput(fundingChange, payToAddressScript(walletAddress)));
|
|
2713
|
+
const transaction = new Transaction({
|
|
2714
|
+
version: 1, lockTime: 0n,
|
|
2715
|
+
inputs: [
|
|
2716
|
+
{ previousOutpoint: wrappedEntry.outpoint, utxo: wrappedEntry,
|
|
2717
|
+
sequence: 0n, sigOpCount: 0, computeBudget: holderBudget },
|
|
2718
|
+
{ previousOutpoint: reserveEntry.outpoint, utxo: reserveEntry,
|
|
2719
|
+
sequence: 0n, sigOpCount: 0, computeBudget: tokenBudget },
|
|
2720
|
+
{ previousOutpoint: wrapperEntry.outpoint, utxo: wrapperEntry,
|
|
2721
|
+
sequence: 0n, sigOpCount: 0, computeBudget: wrapperBudget },
|
|
2722
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
2723
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
2724
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
2725
|
+
});
|
|
2726
|
+
setVersionOneInputMassFields(transaction, wrapperBudget);
|
|
2727
|
+
transaction.inputs[0].computeBudget = holderBudget;
|
|
2728
|
+
transaction.inputs[1].computeBudget = tokenBudget;
|
|
2729
|
+
transaction.inputs[3].computeBudget = 30;
|
|
2730
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market unwrap");
|
|
2731
|
+
const wrapperPrefix = covenantScriptBuilder(kw);
|
|
2732
|
+
addFeeMarketWrapperState(wrapperPrefix, plan.wrapperState);
|
|
2733
|
+
addFeeMarketWrapperState(wrapperPrefix, plan.wrapperState);
|
|
2734
|
+
for (const index of [0, 1, 0, reserveChangeIndex,
|
|
2735
|
+
wrappedChangeIndex, wrapperOutputIndex])
|
|
2736
|
+
wrapperPrefix.addI64(BigInt(index));
|
|
2737
|
+
wrapperPrefix.addData(hexToBytes(recipientOwner));
|
|
2738
|
+
addExplicitByte(wrapperPrefix, 0);
|
|
2739
|
+
wrapperPrefix.addI64(tokenAmount);
|
|
2740
|
+
addKcc20State(wrapperPrefix, recipientState);
|
|
2741
|
+
addKcc20State(wrapperPrefix, reserveChangeState);
|
|
2742
|
+
addFeeMarketRoleState(wrapperPrefix, wrappedChangeState);
|
|
2743
|
+
wrapperPrefix.addData(dispatchTagFor(artifacts.wrapper, "unwrap"));
|
|
2744
|
+
transaction.inputs[2].signatureScript = encodeCovenantP2shSignatureScript(wrapperPrefix.drain(), plan.wrapper.redeemScript);
|
|
2745
|
+
const reservePrefix = buildKcc20TransferSigScript(kw, tokenProfile.artifact, {
|
|
2746
|
+
nextStates: reserveRemaining > 0n
|
|
2747
|
+
? [recipientState, reserveChangeState] : [recipientState],
|
|
2748
|
+
witness: Uint8Array.of(0, 2),
|
|
2749
|
+
});
|
|
2750
|
+
transaction.inputs[1].signatureScript = encodeCovenantP2shSignatureScript(reservePrefix, reserveScript);
|
|
2751
|
+
const scripts = [{ inputIndex: 0, scriptHex: bytesToHex(plan.holder.redeemScript),
|
|
2752
|
+
signType: 1, signatureScript: { mode: "ordered-args", args: [
|
|
2753
|
+
{ type: "signature" }, i64TemplateArg(2), i64TemplateArg(tokenAmount),
|
|
2754
|
+
i64TemplateArg(wrappedChangeIndex), i64TemplateArg(refundIndex),
|
|
2755
|
+
dataTemplateArg(dispatchTagFor(artifacts.holder, "unwrap")),
|
|
2756
|
+
] } }];
|
|
2757
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market unwrap", candidate => {
|
|
2758
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
2759
|
+
if (typeof serialized !== "string")
|
|
2760
|
+
throw new Error("fee-market unwrap preflight serialization failed");
|
|
2761
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
2762
|
+
predictedJson.inputs[3].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
2763
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
2764
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
2765
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
2766
|
+
});
|
|
2767
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market unwrap");
|
|
2768
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market unwrap");
|
|
2769
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
2770
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
2771
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
2772
|
+
throw kcc20PsktBuilderError("fee-market unwrap exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
2773
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
2774
|
+
}
|
|
2775
|
+
return { schema: OUTPUT_SCHEMA,
|
|
2776
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
2777
|
+
signInputs: [{ index: 3, sighashType: 1 }], scripts,
|
|
2778
|
+
submitTransactionSupported: true,
|
|
2779
|
+
metadata: { builderKey: "kcc20fee.unwrap", operation: "fee-market-unwrap",
|
|
2780
|
+
network, walletAddress, tokenId, marketId, policyId: params.policyId,
|
|
2781
|
+
tokenAmount: tokenAmount.toString(), reserveRemaining: reserveRemaining.toString(),
|
|
2782
|
+
wrappedRemaining: wrappedRemaining.toString(), tokenRecipientOutputIndex: 0,
|
|
2783
|
+
reserveChangeIndex, wrappedChangeIndex, refundIndex, wrapperOutputIndex,
|
|
2784
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
2785
|
+
transient: signedMass.transientMass.toString(),
|
|
2786
|
+
storage: storageMass.toString() } },
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
finally {
|
|
2790
|
+
await rpc.disconnect();
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
async function buildFeeMarketMergeHoldersPskt(input) {
|
|
2794
|
+
const request = input.request || {};
|
|
2795
|
+
const params = request.params || {};
|
|
2796
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
2797
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
2798
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
2799
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
2800
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
2801
|
+
const owner = requireHex32(request.owner?.kcc20Owner, "holder owner");
|
|
2802
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, owner);
|
|
2803
|
+
const sources = params.holderUtxos;
|
|
2804
|
+
if (!Array.isArray(sources) || sources.length < 2 || sources.length > 3) {
|
|
2805
|
+
throw new Error("fee-market holder merge requires two or three current holders");
|
|
2806
|
+
}
|
|
2807
|
+
const kw = kaspaWasm;
|
|
2808
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
2809
|
+
const payoutSpk = p2pkScriptPubKey(kw, owner);
|
|
2810
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
2811
|
+
const payoutBytes = new Uint8Array(11 + payoutScript.length);
|
|
2812
|
+
payoutBytes[0] = 0xe3;
|
|
2813
|
+
new DataView(payoutBytes.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
2814
|
+
new DataView(payoutBytes.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
2815
|
+
payoutBytes.set(payoutScript, 11);
|
|
2816
|
+
const payoutHash = bytesToHex(blake3(payoutBytes));
|
|
2817
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
2818
|
+
const planInput = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
2819
|
+
wrapperProfile: params.wrapperProfile || "native", artifacts, marketId,
|
|
2820
|
+
recipientOwner: owner, recipientOwnerScheme: 0,
|
|
2821
|
+
recipientPayoutScriptHash: payoutHash };
|
|
2822
|
+
if (prepareKcc20FeeMarketGenesis(planInput).policyId !==
|
|
2823
|
+
requireHex32(params.policyId, "policy ID")) {
|
|
2824
|
+
throw new Error("fee-market holder merge policy ID mismatch");
|
|
2825
|
+
}
|
|
2826
|
+
let mergedAmount = 0n;
|
|
2827
|
+
const sourcePlans = sources.map((source, index) => {
|
|
2828
|
+
if (String(source?.state?.mode) !== "0" ||
|
|
2829
|
+
Number(source.state?.owner_scheme) !== 0 ||
|
|
2830
|
+
String(source.state?.owner).toLowerCase() !== owner) {
|
|
2831
|
+
throw new Error(`fee-market merge holder ${index} is not owned by the signing wallet`);
|
|
2832
|
+
}
|
|
2833
|
+
const amount = parsePositiveU64(source.state.amount, `holder ${index} amount`);
|
|
2834
|
+
const plan = prepareKcc20FeeMarketWrappedHolder({ ...planInput,
|
|
2835
|
+
tokenAmount: amount });
|
|
2836
|
+
if (String(source.state?.canonicalTokenId).toLowerCase() !== tokenId ||
|
|
2837
|
+
String(source.state?.feeTicketId).toLowerCase() !== feeTicketId ||
|
|
2838
|
+
BigInt(source.state?.priceScale ?? -1) !== priceScale ||
|
|
2839
|
+
String(source.state?.feePolicy).toLowerCase() !== plan.holderState.feePolicy ||
|
|
2840
|
+
String(source.state?.payout_script_hash).toLowerCase() !== payoutHash ||
|
|
2841
|
+
String(source.state?.wrapper_spk_hash).toLowerCase() !==
|
|
2842
|
+
plan.wrapper.scriptHash) {
|
|
2843
|
+
throw new Error(`fee-market merge holder ${index} differs from fixed policy`);
|
|
2844
|
+
}
|
|
2845
|
+
assertScriptAddress(kw, plan.holder.redeemScript, requireKaspaAddress(source.address), network, `fee-market merge holder ${index}`);
|
|
2846
|
+
mergedAmount += amount;
|
|
2847
|
+
if (mergedAmount > (1n << 63n) - 1n) {
|
|
2848
|
+
throw new Error("fee-market merged amount exceeds contract range");
|
|
2849
|
+
}
|
|
2850
|
+
return plan;
|
|
2851
|
+
});
|
|
2852
|
+
const mergedPlan = prepareKcc20FeeMarketWrappedHolder({ ...planInput,
|
|
2853
|
+
tokenAmount: mergedAmount });
|
|
2854
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_ORDER_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
2855
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
2856
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
2857
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
2858
|
+
await rpc.connect();
|
|
2859
|
+
try {
|
|
2860
|
+
const [sourceUtxos, walletUtxos] = await Promise.all([
|
|
2861
|
+
getUtxosByAddresses(rpc, [...new Set(sources.map(source => requireKaspaAddress(source.address)))]),
|
|
2862
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
2863
|
+
]);
|
|
2864
|
+
const entries = sources.map((source, index) => {
|
|
2865
|
+
const entry = findUtxoEntry(sourceUtxos.entries, requireHex32(source.txidHex, `holder ${index} txid`), parseVout(source.vout, `holder ${index} vout`));
|
|
2866
|
+
if (!entry)
|
|
2867
|
+
throw new Error(`fee-market merge holder ${index} is spent or unavailable`);
|
|
2868
|
+
requireMatchingCovenantId(entry, marketId, `fee-market merge holder ${index}`);
|
|
2869
|
+
const spk = entry.scriptPublicKey ?? entry.entry?.scriptPublicKey ??
|
|
2870
|
+
entry.utxoEntry?.scriptPublicKey;
|
|
2871
|
+
if (Number(spk?.version) !== 0 ||
|
|
2872
|
+
String(spk?.script).toLowerCase() !==
|
|
2873
|
+
sourcePlans[index].holder.scriptPublicKey.scriptHex ||
|
|
2874
|
+
utxoAmountSompi(entry).toString() !== String(source.amountSompiExact) ||
|
|
2875
|
+
utxoAmountSompi(entry) < 50000000n) {
|
|
2876
|
+
throw new Error(`fee-market merge holder ${index} differs from live node UTXO`);
|
|
2877
|
+
}
|
|
2878
|
+
return entry;
|
|
2879
|
+
});
|
|
2880
|
+
const inputSompi = entries.reduce((sum, entry) => sum + utxoAmountSompi(entry), 0n);
|
|
2881
|
+
const refundSompi = inputSompi - 50000000n;
|
|
2882
|
+
if (refundSompi < 50000000n) {
|
|
2883
|
+
throw new Error("fee-market merge refund is below the output floor");
|
|
2884
|
+
}
|
|
2885
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, priorityFee + 10000n, entries.map(entry => entry.outpoint), params.fundingOutpoint);
|
|
2886
|
+
const fundingChange = utxoAmountSompi(fundingEntry) - priorityFee;
|
|
2887
|
+
if (fundingChange <= 10000n) {
|
|
2888
|
+
throw new Error("fee-market merge funding change is too small");
|
|
2889
|
+
}
|
|
2890
|
+
const outputs = [
|
|
2891
|
+
new TransactionOutput(50000000n, payToScriptHashScript(mergedPlan.holder.redeemScript), new CovenantBinding(0, new Hash(marketId))),
|
|
2892
|
+
new TransactionOutput(refundSompi, payToAddressScript(walletAddress)),
|
|
2893
|
+
new TransactionOutput(fundingChange, payToAddressScript(walletAddress)),
|
|
2894
|
+
];
|
|
2895
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
2896
|
+
inputs: [
|
|
2897
|
+
...entries.map(entry => ({ previousOutpoint: entry.outpoint, utxo: entry,
|
|
2898
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 750 })),
|
|
2899
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
2900
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
2901
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
2902
|
+
});
|
|
2903
|
+
setVersionOneInputMassFields(transaction, 750);
|
|
2904
|
+
transaction.inputs[entries.length].computeBudget = 30;
|
|
2905
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market holder merge");
|
|
2906
|
+
const scripts = sourcePlans.map((plan, index) => ({ inputIndex: index,
|
|
2907
|
+
scriptHex: bytesToHex(plan.holder.redeemScript), signType: 1,
|
|
2908
|
+
signatureScript: { mode: "ordered-args", args: index === 0 ? [
|
|
2909
|
+
{ type: "signature" },
|
|
2910
|
+
dataTemplateArg(dispatchTagFor(artifacts.holder, "mergeHolders")),
|
|
2911
|
+
] : [
|
|
2912
|
+
{ type: "signature" }, i64TemplateArg(0),
|
|
2913
|
+
dataTemplateArg(dispatchTagFor(artifacts.holder, "delegateHolderMerge")),
|
|
2914
|
+
] } }));
|
|
2915
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, 2, network, "fee-market holder merge", candidate => {
|
|
2916
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
2917
|
+
if (typeof serialized !== "string") {
|
|
2918
|
+
throw new Error("holder merge preflight serialization failed");
|
|
2919
|
+
}
|
|
2920
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
2921
|
+
predictedJson.inputs[entries.length].signatureScript =
|
|
2922
|
+
encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
2923
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
2924
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
2925
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
2926
|
+
});
|
|
2927
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market holder merge");
|
|
2928
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market holder merge");
|
|
2929
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
2930
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
2931
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
2932
|
+
throw kcc20PsktBuilderError("fee-market holder merge exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
2933
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
2934
|
+
}
|
|
2935
|
+
return { schema: OUTPUT_SCHEMA,
|
|
2936
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
2937
|
+
signInputs: [{ index: entries.length, sighashType: 1 }], scripts,
|
|
2938
|
+
submitTransactionSupported: true,
|
|
2939
|
+
metadata: { builderKey: "kcc20fee.merge-holders",
|
|
2940
|
+
operation: "fee-market-merge-holders", network, walletAddress,
|
|
2941
|
+
tokenId, marketId, policyId: params.policyId,
|
|
2942
|
+
sourceCount: entries.length, mergedAmount: mergedAmount.toString(),
|
|
2943
|
+
refundSompi: refundSompi.toString(), mergedOutputIndex: 0,
|
|
2944
|
+
refundOutputIndex: 1,
|
|
2945
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
2946
|
+
transient: signedMass.transientMass.toString(),
|
|
2947
|
+
storage: storageMass.toString() } },
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
finally {
|
|
2951
|
+
await rpc.disconnect();
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
async function buildFeeMarketCreateAskPskt(input) {
|
|
2955
|
+
const request = input.request || {};
|
|
2956
|
+
const params = request.params || {};
|
|
2957
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
2958
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
2959
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
2960
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
2961
|
+
const askAmount = parsePositiveU64(params.askAmount, "ask amount");
|
|
2962
|
+
const askUnitPrice = parsePositiveU64(params.askUnitPriceSompi, "ask unit price");
|
|
2963
|
+
const gross = exactGrossSompi(askAmount, askUnitPrice, priceScale, "fee-market ask");
|
|
2964
|
+
if (gross < MIN_FILL_GROSS_SOMPI)
|
|
2965
|
+
throw minimumFillGrossError("fee-market ask is below 0.5 KAS");
|
|
2966
|
+
if (gross > (1n << 63n) - 1n)
|
|
2967
|
+
throw new Error("fee-market ask gross exceeds contract range");
|
|
2968
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
2969
|
+
const owner = requireHex32(request.owner?.kcc20Owner, "ask owner");
|
|
2970
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, owner);
|
|
2971
|
+
const source = params.holderUtxo;
|
|
2972
|
+
if (!source)
|
|
2973
|
+
throw new Error("current wrapped holder UTXO is required");
|
|
2974
|
+
if (String(source.state?.mode) !== "0" || Number(source.state?.owner_scheme) !== 0 ||
|
|
2975
|
+
String(source.state?.owner).toLowerCase() !== owner) {
|
|
2976
|
+
throw new Error("fee-market ask source is not the signing wallet's holder");
|
|
2977
|
+
}
|
|
2978
|
+
const holderAmount = parsePositiveU64(source.state.amount, "holder amount");
|
|
2979
|
+
const kw = kaspaWasm;
|
|
2980
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
2981
|
+
const payoutSpk = p2pkScriptPubKey(kw, owner);
|
|
2982
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
2983
|
+
const payoutBytes = new Uint8Array(11 + payoutScript.length);
|
|
2984
|
+
payoutBytes[0] = 0xe3;
|
|
2985
|
+
new DataView(payoutBytes.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
2986
|
+
new DataView(payoutBytes.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
2987
|
+
payoutBytes.set(payoutScript, 11);
|
|
2988
|
+
const payoutHash = bytesToHex(blake3(payoutBytes));
|
|
2989
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
2990
|
+
const planInput = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
2991
|
+
wrapperProfile: "native", artifacts, marketId, owner, ownerScheme: 0,
|
|
2992
|
+
payoutScriptHash: payoutHash, holderAmount, askAmount, askUnitPrice };
|
|
2993
|
+
const plan = prepareKcc20FeeMarketAsk(planInput);
|
|
2994
|
+
if (prepareKcc20FeeMarketGenesis(planInput).policyId !==
|
|
2995
|
+
requireHex32(params.policyId, "policy ID")) {
|
|
2996
|
+
throw new Error("fee-market ask policy ID mismatch");
|
|
2997
|
+
}
|
|
2998
|
+
if (String(source.state?.payout_script_hash).toLowerCase() !== payoutHash ||
|
|
2999
|
+
String(source.state?.wrapper_spk_hash).toLowerCase() !== plan.holder.wrapper.scriptHash ||
|
|
3000
|
+
String(source.state?.feePolicy).toLowerCase() !== plan.holder.holderState.feePolicy ||
|
|
3001
|
+
String(source.state?.canonicalTokenId).toLowerCase() !== tokenId ||
|
|
3002
|
+
String(source.state?.feeTicketId).toLowerCase() !== feeTicketId ||
|
|
3003
|
+
BigInt(source.state?.priceScale ?? -1) !== priceScale) {
|
|
3004
|
+
throw new Error("fee-market holder state differs from fixed policy");
|
|
3005
|
+
}
|
|
3006
|
+
const sourceAddress = requireKaspaAddress(source.address);
|
|
3007
|
+
assertScriptAddress(kw, plan.holder.holder.redeemScript, sourceAddress, network, "fee-market holder address");
|
|
3008
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_ORDER_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
3009
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
3010
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
3011
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
3012
|
+
await rpc.connect();
|
|
3013
|
+
try {
|
|
3014
|
+
const [sourceUtxos, walletUtxos] = await Promise.all([
|
|
3015
|
+
getUtxosByAddresses(rpc, [sourceAddress]),
|
|
3016
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
3017
|
+
]);
|
|
3018
|
+
const holderEntry = findUtxoEntry(sourceUtxos.entries, requireHex32(source.txidHex, "holder txid"), parseVout(source.vout, "holder vout"));
|
|
3019
|
+
if (!holderEntry)
|
|
3020
|
+
throw new Error("fee-market holder is spent or unavailable");
|
|
3021
|
+
requireMatchingCovenantId(holderEntry, marketId, "fee-market holder");
|
|
3022
|
+
const spk = holderEntry.scriptPublicKey ?? holderEntry.entry?.scriptPublicKey ??
|
|
3023
|
+
holderEntry.utxoEntry?.scriptPublicKey;
|
|
3024
|
+
if (Number(spk?.version) !== 0 ||
|
|
3025
|
+
String(spk?.script).toLowerCase() !== plan.holder.holder.scriptPublicKey.scriptHex ||
|
|
3026
|
+
utxoAmountSompi(holderEntry).toString() !== String(source.amountSompiExact)) {
|
|
3027
|
+
throw new Error("fee-market holder differs from live node UTXO");
|
|
3028
|
+
}
|
|
3029
|
+
const outputs = [new TransactionOutput(utxoAmountSompi(holderEntry), payToScriptHashScript(plan.ask.redeemScript), new CovenantBinding(0, new Hash(marketId)))];
|
|
3030
|
+
const changeIndex = plan.change ? 1 : -1;
|
|
3031
|
+
if (plan.change)
|
|
3032
|
+
outputs.push(new TransactionOutput(50000000n, payToScriptHashScript(plan.change.holder.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
3033
|
+
const outputSompi = outputs.reduce((sum, output) => sum + BigInt(output.value), 0n);
|
|
3034
|
+
const requiredFunding = outputSompi + priorityFee > utxoAmountSompi(holderEntry)
|
|
3035
|
+
? outputSompi + priorityFee - utxoAmountSompi(holderEntry) + 10000n
|
|
3036
|
+
: priorityFee + 10000n;
|
|
3037
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, requiredFunding, [holderEntry.outpoint], params.fundingOutpoint);
|
|
3038
|
+
const fundingChange = utxoAmountSompi(holderEntry) + utxoAmountSompi(fundingEntry) -
|
|
3039
|
+
outputSompi - priorityFee;
|
|
3040
|
+
if (fundingChange <= 10000n)
|
|
3041
|
+
throw new Error("fee-market ask funding change is too small");
|
|
3042
|
+
const fundingChangeIndex = outputs.length;
|
|
3043
|
+
outputs.push(new TransactionOutput(fundingChange, payToAddressScript(walletAddress)));
|
|
3044
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
3045
|
+
inputs: [
|
|
3046
|
+
{ previousOutpoint: holderEntry.outpoint, utxo: holderEntry,
|
|
3047
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 300 },
|
|
3048
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
3049
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
3050
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
3051
|
+
});
|
|
3052
|
+
setVersionOneInputMassFields(transaction, 300);
|
|
3053
|
+
transaction.inputs[1].computeBudget = 30;
|
|
3054
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market create ask");
|
|
3055
|
+
const askParts = templateParts(normalizeSilverScriptArtifact(artifacts.ask));
|
|
3056
|
+
const scripts = [{ inputIndex: 0,
|
|
3057
|
+
scriptHex: bytesToHex(plan.holder.holder.redeemScript), signType: 1,
|
|
3058
|
+
signatureScript: { mode: "ordered-args", args: [
|
|
3059
|
+
{ type: "signature" }, i64TemplateArg(askAmount), i64TemplateArg(askUnitPrice),
|
|
3060
|
+
dataTemplateArg(hexToBytes(payoutHash)), i64TemplateArg(0),
|
|
3061
|
+
i64TemplateArg(changeIndex), dataTemplateArg(askParts.prefix),
|
|
3062
|
+
dataTemplateArg(askParts.suffix),
|
|
3063
|
+
dataTemplateArg(dispatchTagFor(artifacts.holder, "createAsk")),
|
|
3064
|
+
] } }];
|
|
3065
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market create ask", candidate => {
|
|
3066
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
3067
|
+
if (typeof serialized !== "string")
|
|
3068
|
+
throw new Error("ask preflight serialization failed");
|
|
3069
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
3070
|
+
predictedJson.inputs[1].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
3071
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
3072
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
3073
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
3074
|
+
});
|
|
3075
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market create ask");
|
|
3076
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market create ask");
|
|
3077
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
3078
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
3079
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
3080
|
+
throw kcc20PsktBuilderError("fee-market ask exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
3081
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
3082
|
+
}
|
|
3083
|
+
return { schema: OUTPUT_SCHEMA,
|
|
3084
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
3085
|
+
signInputs: [{ index: 1, sighashType: 1 }], scripts,
|
|
3086
|
+
submitTransactionSupported: true,
|
|
3087
|
+
metadata: { builderKey: "kcc20fee.create-ask", operation: "fee-market-create-ask",
|
|
3088
|
+
network, walletAddress, tokenId, marketId, policyId: params.policyId,
|
|
3089
|
+
askAmount: askAmount.toString(), askUnitPriceSompi: askUnitPrice.toString(),
|
|
3090
|
+
grossSompi: gross.toString(), askOutputIndex: 0, holderChangeIndex: changeIndex,
|
|
3091
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
3092
|
+
transient: signedMass.transientMass.toString(),
|
|
3093
|
+
storage: storageMass.toString() } },
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
finally {
|
|
3097
|
+
await rpc.disconnect();
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
async function buildFeeMarketCancelAskPskt(input) {
|
|
3101
|
+
const request = input.request || {};
|
|
3102
|
+
const params = request.params || {};
|
|
3103
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
3104
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
3105
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
3106
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
3107
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
3108
|
+
const owner = requireHex32(request.owner?.kcc20Owner, "ask owner");
|
|
3109
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, owner);
|
|
3110
|
+
const source = params.askUtxo;
|
|
3111
|
+
if (!source || String(source.state?.mode) !== "2" ||
|
|
3112
|
+
Number(source.state?.owner_scheme) !== 0 ||
|
|
3113
|
+
String(source.state?.owner).toLowerCase() !== owner) {
|
|
3114
|
+
throw new Error("current fee-market ask owned by signing wallet is required");
|
|
3115
|
+
}
|
|
3116
|
+
const askAmount = parsePositiveU64(source.state.amount, "ask amount");
|
|
3117
|
+
const askUnitPrice = parsePositiveU64(source.state.unitPrice, "ask price");
|
|
3118
|
+
const payoutHash = requireHex32(source.state.payout_script_hash, "ask payout hash");
|
|
3119
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
3120
|
+
const kw = kaspaWasm;
|
|
3121
|
+
const payoutSpk = p2pkScriptPubKey(kw, owner);
|
|
3122
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
3123
|
+
const payoutBytes = new Uint8Array(11 + payoutScript.length);
|
|
3124
|
+
payoutBytes[0] = 0xe3;
|
|
3125
|
+
new DataView(payoutBytes.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
3126
|
+
new DataView(payoutBytes.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
3127
|
+
payoutBytes.set(payoutScript, 11);
|
|
3128
|
+
if (bytesToHex(blake3(payoutBytes)) !== payoutHash) {
|
|
3129
|
+
throw new Error("ask payout differs from signing wallet");
|
|
3130
|
+
}
|
|
3131
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
3132
|
+
const planInput = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
3133
|
+
wrapperProfile: "native", artifacts, marketId, owner, ownerScheme: 0,
|
|
3134
|
+
payoutScriptHash: payoutHash, holderAmount: askAmount, askAmount, askUnitPrice };
|
|
3135
|
+
const plan = prepareKcc20FeeMarketAsk(planInput);
|
|
3136
|
+
if (prepareKcc20FeeMarketGenesis(planInput).policyId !==
|
|
3137
|
+
requireHex32(params.policyId, "policy ID")) {
|
|
3138
|
+
throw new Error("fee-market cancel ask policy ID mismatch");
|
|
3139
|
+
}
|
|
3140
|
+
for (const field of ["canonicalTokenId", "feeTicketId", "feePolicy", "wrapper_spk_hash"]) {
|
|
3141
|
+
const expected = field === "canonicalTokenId" ? tokenId :
|
|
3142
|
+
field === "feeTicketId" ? feeTicketId :
|
|
3143
|
+
field === "feePolicy" ? plan.askState.feePolicy : plan.askState.wrapper_spk_hash;
|
|
3144
|
+
if (String(source.state?.[field]).toLowerCase() !== expected) {
|
|
3145
|
+
throw new Error(`fee-market ask ${field} differs from fixed policy`);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
if (BigInt(source.state?.priceScale ?? -1) !== priceScale) {
|
|
3149
|
+
throw new Error("fee-market ask price scale differs from fixed policy");
|
|
3150
|
+
}
|
|
3151
|
+
const sourceAddress = requireKaspaAddress(source.address);
|
|
3152
|
+
assertScriptAddress(kw, plan.ask.redeemScript, sourceAddress, network, "fee-market ask address");
|
|
3153
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_ORDER_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
3154
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
3155
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
3156
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
3157
|
+
await rpc.connect();
|
|
3158
|
+
try {
|
|
3159
|
+
const [sourceUtxos, walletUtxos] = await Promise.all([
|
|
3160
|
+
getUtxosByAddresses(rpc, [sourceAddress]), getUtxosByAddresses(rpc, [walletAddress]),
|
|
3161
|
+
]);
|
|
3162
|
+
const askEntry = findUtxoEntry(sourceUtxos.entries, requireHex32(source.txidHex, "ask txid"), parseVout(source.vout, "ask vout"));
|
|
3163
|
+
if (!askEntry)
|
|
3164
|
+
throw new Error("fee-market ask is spent or unavailable");
|
|
3165
|
+
requireMatchingCovenantId(askEntry, marketId, "fee-market ask");
|
|
3166
|
+
const spk = askEntry.scriptPublicKey ?? askEntry.entry?.scriptPublicKey ??
|
|
3167
|
+
askEntry.utxoEntry?.scriptPublicKey;
|
|
3168
|
+
if (Number(spk?.version) !== 0 ||
|
|
3169
|
+
String(spk?.script).toLowerCase() !== plan.ask.scriptPublicKey.scriptHex ||
|
|
3170
|
+
utxoAmountSompi(askEntry).toString() !== String(source.amountSompiExact)) {
|
|
3171
|
+
throw new Error("fee-market ask differs from live node UTXO");
|
|
3172
|
+
}
|
|
3173
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, priorityFee + 10000n, [askEntry.outpoint], params.fundingOutpoint);
|
|
3174
|
+
const outputs = [new TransactionOutput(utxoAmountSompi(askEntry), payToScriptHashScript(plan.holder.holder.redeemScript), new CovenantBinding(0, new Hash(marketId)))];
|
|
3175
|
+
const fundingChangeIndex = outputs.length;
|
|
3176
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(fundingEntry) - priorityFee, payToAddressScript(walletAddress)));
|
|
3177
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
3178
|
+
inputs: [
|
|
3179
|
+
{ previousOutpoint: askEntry.outpoint, utxo: askEntry,
|
|
3180
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 300 },
|
|
3181
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
3182
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
3183
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
3184
|
+
});
|
|
3185
|
+
setVersionOneInputMassFields(transaction, 300);
|
|
3186
|
+
transaction.inputs[1].computeBudget = 30;
|
|
3187
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market cancel ask");
|
|
3188
|
+
const holderParts = templateParts(normalizeSilverScriptArtifact(artifacts.holder));
|
|
3189
|
+
const scripts = [{ inputIndex: 0, scriptHex: bytesToHex(plan.ask.redeemScript),
|
|
3190
|
+
signType: 1, signatureScript: { mode: "ordered-args", args: [
|
|
3191
|
+
{ type: "signature" }, i64TemplateArg(0),
|
|
3192
|
+
dataTemplateArg(holderParts.prefix), dataTemplateArg(holderParts.suffix),
|
|
3193
|
+
dataTemplateArg(dispatchTagFor(artifacts.ask, "cancelAsk")),
|
|
3194
|
+
] } }];
|
|
3195
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market cancel ask", candidate => {
|
|
3196
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
3197
|
+
if (typeof serialized !== "string")
|
|
3198
|
+
throw new Error("cancel ask preflight serialization failed");
|
|
3199
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
3200
|
+
predictedJson.inputs[1].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
3201
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
3202
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
3203
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
3204
|
+
});
|
|
3205
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market cancel ask");
|
|
3206
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market cancel ask");
|
|
3207
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
3208
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
3209
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
3210
|
+
throw kcc20PsktBuilderError("fee-market cancel ask exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
3211
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
3212
|
+
}
|
|
3213
|
+
return { schema: OUTPUT_SCHEMA,
|
|
3214
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
3215
|
+
signInputs: [{ index: 1, sighashType: 1 }], scripts,
|
|
3216
|
+
submitTransactionSupported: true,
|
|
3217
|
+
metadata: { builderKey: "kcc20fee.cancel-ask", operation: "fee-market-cancel-ask",
|
|
3218
|
+
network, walletAddress, tokenId, marketId, policyId: params.policyId,
|
|
3219
|
+
askAmount: askAmount.toString(), holderOutputIndex: 0,
|
|
3220
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
3221
|
+
transient: signedMass.transientMass.toString(),
|
|
3222
|
+
storage: storageMass.toString() } },
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
finally {
|
|
3226
|
+
await rpc.disconnect();
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
async function buildFeeMarketCreateBidPskt(input) {
|
|
3230
|
+
const request = input.request || {};
|
|
3231
|
+
const params = request.params || {};
|
|
3232
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
3233
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
3234
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
3235
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
3236
|
+
const bidAmount = parsePositiveU64(params.bidAmount, "bid amount");
|
|
3237
|
+
const bidUnitPrice = parsePositiveU64(params.bidUnitPriceSompi, "bid unit price");
|
|
3238
|
+
const gross = exactGrossSompi(bidAmount, bidUnitPrice, priceScale, "fee-market bid");
|
|
3239
|
+
if (gross < MIN_FILL_GROSS_SOMPI)
|
|
3240
|
+
throw minimumFillGrossError("fee-market bid is below 0.5 KAS");
|
|
3241
|
+
const bidDeposit = 200000000n;
|
|
3242
|
+
if (gross > (1n << 63n) - 1n - bidDeposit) {
|
|
3243
|
+
throw new Error("fee-market bid gross exceeds contract range");
|
|
3244
|
+
}
|
|
3245
|
+
const rootUtxo = params.rootUtxo;
|
|
3246
|
+
if (!rootUtxo || String(rootUtxo.state?.mode) !== "1" ||
|
|
3247
|
+
String(rootUtxo.state?.amount) !== "0") {
|
|
3248
|
+
throw new Error("current fee-market root UTXO is required");
|
|
3249
|
+
}
|
|
3250
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
3251
|
+
const buyer = requireHex32(request.owner?.kcc20Owner, "bid buyer");
|
|
3252
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, buyer);
|
|
3253
|
+
const kw = kaspaWasm;
|
|
3254
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
3255
|
+
const payoutSpk = p2pkScriptPubKey(kw, buyer);
|
|
3256
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
3257
|
+
const payoutBytes = new Uint8Array(11 + payoutScript.length);
|
|
3258
|
+
payoutBytes[0] = 0xe3;
|
|
3259
|
+
new DataView(payoutBytes.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
3260
|
+
new DataView(payoutBytes.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
3261
|
+
payoutBytes.set(payoutScript, 11);
|
|
3262
|
+
const payoutHash = bytesToHex(blake3(payoutBytes));
|
|
3263
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
3264
|
+
const planInput = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
3265
|
+
wrapperProfile: "native", artifacts, marketId,
|
|
3266
|
+
rootOwner: requireHex32(rootUtxo.state.owner, "root owner"),
|
|
3267
|
+
rootOwnerScheme: Number(rootUtxo.state.owner_scheme),
|
|
3268
|
+
rootPayoutScriptHash: requireHex32(rootUtxo.state.payout_script_hash, "root payout hash"),
|
|
3269
|
+
buyer, buyerScheme: 0, buyerPayoutScriptHash: payoutHash,
|
|
3270
|
+
bidAmount, bidUnitPrice };
|
|
3271
|
+
const plan = prepareKcc20FeeMarketBid(planInput);
|
|
3272
|
+
if (plan.binding.genesis.policyId !== requireHex32(params.policyId, "policy ID")) {
|
|
3273
|
+
throw new Error("fee-market bid policy ID mismatch");
|
|
3274
|
+
}
|
|
3275
|
+
for (const field of ["canonicalTokenId", "feeTicketId", "feePolicy", "wrapper_spk_hash"]) {
|
|
3276
|
+
const expected = plan.binding.rootState[field];
|
|
3277
|
+
if (String(rootUtxo.state?.[field]).toLowerCase() !== expected) {
|
|
3278
|
+
throw new Error(`fee-market root ${field} differs from fixed policy`);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
if (BigInt(rootUtxo.state?.priceScale ?? -1) !== priceScale) {
|
|
3282
|
+
throw new Error("fee-market root price scale differs from fixed policy");
|
|
3283
|
+
}
|
|
3284
|
+
const rootAddress = requireKaspaAddress(rootUtxo.address);
|
|
3285
|
+
assertScriptAddress(kw, plan.binding.root.redeemScript, rootAddress, network, "fee-market root address");
|
|
3286
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_ORDER_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
3287
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
3288
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
3289
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
3290
|
+
await rpc.connect();
|
|
3291
|
+
try {
|
|
3292
|
+
const [rootUtxos, walletUtxos] = await Promise.all([
|
|
3293
|
+
getUtxosByAddresses(rpc, [rootAddress]), getUtxosByAddresses(rpc, [walletAddress]),
|
|
3294
|
+
]);
|
|
3295
|
+
const rootEntry = findUtxoEntry(rootUtxos.entries, requireHex32(rootUtxo.txidHex, "root txid"), parseVout(rootUtxo.vout, "root vout"));
|
|
3296
|
+
if (!rootEntry)
|
|
3297
|
+
throw new Error("fee-market root is spent or unavailable");
|
|
3298
|
+
requireMatchingCovenantId(rootEntry, marketId, "fee-market root");
|
|
3299
|
+
const spk = rootEntry.scriptPublicKey ?? rootEntry.entry?.scriptPublicKey ??
|
|
3300
|
+
rootEntry.utxoEntry?.scriptPublicKey;
|
|
3301
|
+
if (Number(spk?.version) !== 0 ||
|
|
3302
|
+
String(spk?.script).toLowerCase() !== plan.binding.root.scriptPublicKey.scriptHex ||
|
|
3303
|
+
utxoAmountSompi(rootEntry).toString() !== String(rootUtxo.amountSompiExact)) {
|
|
3304
|
+
throw new Error("fee-market root differs from live node UTXO");
|
|
3305
|
+
}
|
|
3306
|
+
const outputs = [new TransactionOutput(gross + bidDeposit, payToScriptHashScript(plan.bid.redeemScript), new CovenantBinding(0, new Hash(marketId))),
|
|
3307
|
+
new TransactionOutput(utxoAmountSompi(rootEntry), payToScriptHashScript(plan.binding.root.redeemScript), new CovenantBinding(0, new Hash(marketId)))];
|
|
3308
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, gross + bidDeposit + priorityFee + 10000n, [rootEntry.outpoint], params.fundingOutpoint);
|
|
3309
|
+
const fundingChange = utxoAmountSompi(fundingEntry) - gross - bidDeposit - priorityFee;
|
|
3310
|
+
if (fundingChange <= 10000n)
|
|
3311
|
+
throw new Error("fee-market bid funding change is too small");
|
|
3312
|
+
const fundingChangeIndex = outputs.length;
|
|
3313
|
+
outputs.push(new TransactionOutput(fundingChange, payToAddressScript(walletAddress)));
|
|
3314
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
3315
|
+
inputs: [
|
|
3316
|
+
{ previousOutpoint: rootEntry.outpoint, utxo: rootEntry,
|
|
3317
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 300 },
|
|
3318
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
3319
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
3320
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
3321
|
+
});
|
|
3322
|
+
setVersionOneInputMassFields(transaction, 300);
|
|
3323
|
+
transaction.inputs[1].computeBudget = 30;
|
|
3324
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market create bid");
|
|
3325
|
+
const bidParts = templateParts(normalizeSilverScriptArtifact(artifacts.bid));
|
|
3326
|
+
const prefix = covenantScriptBuilder(kw);
|
|
3327
|
+
prefix.addData(hexToBytes(buyer));
|
|
3328
|
+
addExplicitByte(prefix, 0);
|
|
3329
|
+
prefix.addData(hexToBytes(payoutHash));
|
|
3330
|
+
for (const value of [bidAmount, bidUnitPrice, 0, 1])
|
|
3331
|
+
prefix.addI64(BigInt(value));
|
|
3332
|
+
prefix.addData(bidParts.prefix);
|
|
3333
|
+
prefix.addData(bidParts.suffix);
|
|
3334
|
+
prefix.addData(dispatchTagFor(artifacts.root, "createBid"));
|
|
3335
|
+
transaction.inputs[0].signatureScript = encodeCovenantP2shSignatureScript(prefix.drain(), plan.binding.root.redeemScript);
|
|
3336
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market create bid", candidate => {
|
|
3337
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
3338
|
+
if (typeof serialized !== "string")
|
|
3339
|
+
throw new Error("bid preflight serialization failed");
|
|
3340
|
+
const predictedJson = JSON.parse(serialized);
|
|
3341
|
+
predictedJson.inputs[1].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
3342
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
3343
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
3344
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
3345
|
+
});
|
|
3346
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market create bid");
|
|
3347
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market create bid");
|
|
3348
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
3349
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
3350
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
3351
|
+
throw kcc20PsktBuilderError("fee-market bid exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
3352
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
3353
|
+
}
|
|
3354
|
+
return { schema: OUTPUT_SCHEMA,
|
|
3355
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
3356
|
+
signInputs: [{ index: 1, sighashType: 1 }], scripts: [],
|
|
3357
|
+
submitTransactionSupported: true,
|
|
3358
|
+
metadata: { builderKey: "kcc20fee.create-bid", operation: "fee-market-create-bid",
|
|
3359
|
+
network, walletAddress, tokenId, marketId, policyId: params.policyId,
|
|
3360
|
+
bidAmount: bidAmount.toString(), bidUnitPriceSompi: bidUnitPrice.toString(),
|
|
3361
|
+
grossSompi: gross.toString(), bidDepositSompi: bidDeposit.toString(),
|
|
3362
|
+
bidOutputIndex: 0, rootOutputIndex: 1,
|
|
3363
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
3364
|
+
transient: signedMass.transientMass.toString(),
|
|
3365
|
+
storage: storageMass.toString() } },
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
finally {
|
|
3369
|
+
await rpc.disconnect();
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
async function buildFeeMarketCancelBidPskt(input) {
|
|
3373
|
+
const request = input.request || {};
|
|
3374
|
+
const params = request.params || {};
|
|
3375
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
3376
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
3377
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
3378
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
3379
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
3380
|
+
const owner = requireHex32(request.owner?.kcc20Owner, "bid owner");
|
|
3381
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, owner);
|
|
3382
|
+
const source = params.bidUtxo;
|
|
3383
|
+
if (!source || String(source.state?.mode) !== "3" ||
|
|
3384
|
+
Number(source.state?.owner_scheme) !== 0 ||
|
|
3385
|
+
String(source.state?.owner).toLowerCase() !== owner) {
|
|
3386
|
+
throw new Error("current fee-market bid owned by signing wallet is required");
|
|
3387
|
+
}
|
|
3388
|
+
const bidAmount = parsePositiveU64(source.state.amount, "bid amount");
|
|
3389
|
+
const bidUnitPrice = parsePositiveU64(source.state.unitPrice, "bid price");
|
|
3390
|
+
const kw = kaspaWasm;
|
|
3391
|
+
const payoutSpk = p2pkScriptPubKey(kw, owner);
|
|
3392
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
3393
|
+
const payoutBytes = new Uint8Array(11 + payoutScript.length);
|
|
3394
|
+
payoutBytes[0] = 0xe3;
|
|
3395
|
+
new DataView(payoutBytes.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
3396
|
+
new DataView(payoutBytes.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
3397
|
+
payoutBytes.set(payoutScript, 11);
|
|
3398
|
+
const payoutHash = bytesToHex(blake3(payoutBytes));
|
|
3399
|
+
if (String(source.state?.payout_script_hash).toLowerCase() !== payoutHash) {
|
|
3400
|
+
throw new Error("bid payout differs from signing wallet");
|
|
3401
|
+
}
|
|
3402
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
3403
|
+
const planInput = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
3404
|
+
wrapperProfile: "native", artifacts, marketId,
|
|
3405
|
+
rootOwner: owner, rootOwnerScheme: 0, rootPayoutScriptHash: payoutHash,
|
|
3406
|
+
buyer: owner, buyerScheme: 0, buyerPayoutScriptHash: payoutHash,
|
|
3407
|
+
bidAmount, bidUnitPrice };
|
|
3408
|
+
const plan = prepareKcc20FeeMarketBid(planInput);
|
|
3409
|
+
if (plan.binding.genesis.policyId !== requireHex32(params.policyId, "policy ID")) {
|
|
3410
|
+
throw new Error("fee-market cancel bid policy ID mismatch");
|
|
3411
|
+
}
|
|
3412
|
+
for (const field of ["canonicalTokenId", "feeTicketId", "feePolicy", "wrapper_spk_hash"]) {
|
|
3413
|
+
if (String(source.state?.[field]).toLowerCase() !== plan.bidState[field]) {
|
|
3414
|
+
throw new Error(`fee-market bid ${field} differs from fixed policy`);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
if (BigInt(source.state?.priceScale ?? -1) !== priceScale) {
|
|
3418
|
+
throw new Error("fee-market bid price scale differs from fixed policy");
|
|
3419
|
+
}
|
|
3420
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
3421
|
+
const sourceAddress = requireKaspaAddress(source.address);
|
|
3422
|
+
assertScriptAddress(kw, plan.bid.redeemScript, sourceAddress, network, "fee-market bid address");
|
|
3423
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_ORDER_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
3424
|
+
const { Encoding, RpcClient, Transaction, TransactionOutput, payToAddressScript } = kw;
|
|
3425
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
3426
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
3427
|
+
await rpc.connect();
|
|
3428
|
+
try {
|
|
3429
|
+
const [bidUtxos, walletUtxos] = await Promise.all([
|
|
3430
|
+
getUtxosByAddresses(rpc, [sourceAddress]), getUtxosByAddresses(rpc, [walletAddress]),
|
|
3431
|
+
]);
|
|
3432
|
+
const bidEntry = findUtxoEntry(bidUtxos.entries, requireHex32(source.txidHex, "bid txid"), parseVout(source.vout, "bid vout"));
|
|
3433
|
+
if (!bidEntry)
|
|
3434
|
+
throw new Error("fee-market bid is spent or unavailable");
|
|
3435
|
+
requireMatchingCovenantId(bidEntry, marketId, "fee-market bid");
|
|
3436
|
+
const spk = bidEntry.scriptPublicKey ?? bidEntry.entry?.scriptPublicKey ??
|
|
3437
|
+
bidEntry.utxoEntry?.scriptPublicKey;
|
|
3438
|
+
if (Number(spk?.version) !== 0 ||
|
|
3439
|
+
String(spk?.script).toLowerCase() !== plan.bid.scriptPublicKey.scriptHex ||
|
|
3440
|
+
utxoAmountSompi(bidEntry).toString() !== String(source.amountSompiExact)) {
|
|
3441
|
+
throw new Error("fee-market bid differs from live node UTXO");
|
|
3442
|
+
}
|
|
3443
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, priorityFee + 10000n, [bidEntry.outpoint], params.fundingOutpoint);
|
|
3444
|
+
const outputs = [new TransactionOutput(utxoAmountSompi(bidEntry), payToAddressScript(walletAddress))];
|
|
3445
|
+
const fundingChangeIndex = outputs.length;
|
|
3446
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(fundingEntry) - priorityFee, payToAddressScript(walletAddress)));
|
|
3447
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
3448
|
+
inputs: [
|
|
3449
|
+
{ previousOutpoint: bidEntry.outpoint, utxo: bidEntry,
|
|
3450
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 300 },
|
|
3451
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
3452
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
3453
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
3454
|
+
});
|
|
3455
|
+
setVersionOneInputMassFields(transaction, 300);
|
|
3456
|
+
transaction.inputs[1].computeBudget = 30;
|
|
3457
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market cancel bid");
|
|
3458
|
+
const scripts = [{ inputIndex: 0, scriptHex: bytesToHex(plan.bid.redeemScript),
|
|
3459
|
+
signType: 1, signatureScript: { mode: "ordered-args", args: [
|
|
3460
|
+
{ type: "signature" }, i64TemplateArg(0),
|
|
3461
|
+
dataTemplateArg(dispatchTagFor(artifacts.bid, "cancelBid")),
|
|
3462
|
+
] } }];
|
|
3463
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market cancel bid", candidate => {
|
|
3464
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
3465
|
+
if (typeof serialized !== "string")
|
|
3466
|
+
throw new Error("cancel bid preflight serialization failed");
|
|
3467
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
3468
|
+
predictedJson.inputs[1].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
3469
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
3470
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
3471
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
3472
|
+
});
|
|
3473
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market cancel bid");
|
|
3474
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market cancel bid");
|
|
3475
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
3476
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
3477
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
3478
|
+
throw kcc20PsktBuilderError("fee-market cancel bid exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
3479
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
3480
|
+
}
|
|
3481
|
+
return { schema: OUTPUT_SCHEMA,
|
|
3482
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
3483
|
+
signInputs: [{ index: 1, sighashType: 1 }], scripts,
|
|
3484
|
+
submitTransactionSupported: true,
|
|
3485
|
+
metadata: { builderKey: "kcc20fee.cancel-bid", operation: "fee-market-cancel-bid",
|
|
3486
|
+
network, walletAddress, tokenId, marketId, policyId: params.policyId,
|
|
3487
|
+
bidAmount: bidAmount.toString(), refundOutputIndex: 0,
|
|
3488
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
3489
|
+
transient: signedMass.transientMass.toString(),
|
|
3490
|
+
storage: storageMass.toString() } },
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
finally {
|
|
3494
|
+
await rpc.disconnect();
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
async function feeMarketTicketForTaker(params, owner, taker, network, feeTicketId) {
|
|
3498
|
+
const ticketRequest = params.feeTicketUtxo || null;
|
|
3499
|
+
if (!ticketRequest)
|
|
3500
|
+
return null;
|
|
3501
|
+
if (feeTicketId === ZERO_HASH) {
|
|
3502
|
+
throw new Error("a fee-market ticket waiver requires the bound FeeTicket root");
|
|
3503
|
+
}
|
|
3504
|
+
const state = kcc20FeeTicketStateFromUtxo(ticketRequest, owner);
|
|
3505
|
+
if (state.mode !== 2 || bytesToHex(state.ownerIdentifier) !== taker) {
|
|
3506
|
+
throw new Error("fee-market ticket must be owned by the taker wallet");
|
|
3507
|
+
}
|
|
3508
|
+
const address = requireKaspaAddress(ticketRequest.address);
|
|
3509
|
+
const info = selectFeeTicketArtifactForState(await readFeeTicketArtifacts(), state, address, network, "fee-market FeeTicket");
|
|
3510
|
+
const pinned = templateParts(normalizeSilverScriptArtifact(info.artifact));
|
|
3511
|
+
const expected = params.feePolicy.ticketTemplate;
|
|
3512
|
+
if (bytesToHex(templateHash(pinned.prefix, pinned.suffix)) !==
|
|
3513
|
+
requireHex32(expected?.hash, "ticket template") ||
|
|
3514
|
+
pinned.prefix.length !== Number(expected?.prefixLength) ||
|
|
3515
|
+
pinned.suffix.length !== Number(expected?.suffixLength)) {
|
|
3516
|
+
throw new Error("fee-market ticket artifact differs from fixed policy");
|
|
3517
|
+
}
|
|
3518
|
+
return { ...info, state, address,
|
|
3519
|
+
txid: requireHex32(ticketRequest.txidHex, "FeeTicket txid"),
|
|
3520
|
+
vout: parseVout(ticketRequest.vout, "FeeTicket vout"),
|
|
3521
|
+
amountSompiExact: String(ticketRequest.amountSompiExact) };
|
|
3522
|
+
}
|
|
3523
|
+
async function buildFeeMarketBuyAskPskt(input) {
|
|
3524
|
+
const request = input.request || {};
|
|
3525
|
+
const params = request.params || {};
|
|
3526
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
3527
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
3528
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
3529
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
3530
|
+
const fillAmount = parsePositiveU64(params.fillAmount, "fill amount");
|
|
3531
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
3532
|
+
const buyer = requireHex32(request.owner?.kcc20Owner, "buyer");
|
|
3533
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, buyer);
|
|
3534
|
+
const askUtxo = params.askUtxo;
|
|
3535
|
+
if (!askUtxo || String(askUtxo.state?.mode) !== "2") {
|
|
3536
|
+
throw new Error("current fee-market ask UTXO is required");
|
|
3537
|
+
}
|
|
3538
|
+
const askAmount = parsePositiveU64(askUtxo.state.amount, "ask amount");
|
|
3539
|
+
const askUnitPrice = parsePositiveU64(askUtxo.state.unitPrice, "ask unit price");
|
|
3540
|
+
if (fillAmount > askAmount)
|
|
3541
|
+
throw new Error("fill amount exceeds ask");
|
|
3542
|
+
const gross = exactGrossSompi(fillAmount, askUnitPrice, priceScale, "fee-market buy");
|
|
3543
|
+
if (gross < MIN_FILL_GROSS_SOMPI || gross > (1n << 63n) - 1n) {
|
|
3544
|
+
throw new Error("fee-market fill gross is outside contract range");
|
|
3545
|
+
}
|
|
3546
|
+
const remainder = askAmount - fillAmount;
|
|
3547
|
+
if (remainder > 0n &&
|
|
3548
|
+
exactGrossSompi(remainder, askUnitPrice, priceScale, "fee-market ask remainder") < MIN_FILL_GROSS_SOMPI) {
|
|
3549
|
+
throw new Error("partial ask remainder is below 0.5 KAS");
|
|
3550
|
+
}
|
|
3551
|
+
const kw = kaspaWasm;
|
|
3552
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
3553
|
+
const ticket = await feeMarketTicketForTaker(params, request.owner, buyer, network, feeTicketId);
|
|
3554
|
+
const payoutHashForOwner = owner => {
|
|
3555
|
+
const spk = p2pkScriptPubKey(kw, owner);
|
|
3556
|
+
const script = hexToBytes(String(spk.script));
|
|
3557
|
+
const serialized = new Uint8Array(11 + script.length);
|
|
3558
|
+
serialized[0] = 0xe3;
|
|
3559
|
+
new DataView(serialized.buffer).setUint16(1, Number(spk.version), true);
|
|
3560
|
+
new DataView(serialized.buffer).setBigUint64(3, BigInt(script.length), true);
|
|
3561
|
+
serialized.set(script, 11);
|
|
3562
|
+
return { spk, hash: bytesToHex(blake3(serialized)) };
|
|
3563
|
+
};
|
|
3564
|
+
const buyerPayout = payoutHashForOwner(buyer);
|
|
3565
|
+
const seller = requireHex32(askUtxo.state.owner, "ask seller");
|
|
3566
|
+
const sellerScheme = Number(askUtxo.state.owner_scheme);
|
|
3567
|
+
if (!Number.isInteger(sellerScheme) || sellerScheme < 0 || sellerScheme > 4) {
|
|
3568
|
+
throw new Error("unsupported ask seller authority");
|
|
3569
|
+
}
|
|
3570
|
+
const sellerPayout = sellerScheme === 0
|
|
3571
|
+
? payoutHashForOwner(seller)
|
|
3572
|
+
: (() => {
|
|
3573
|
+
const address = requireKaspaAddress(params.sellerPayoutAddress);
|
|
3574
|
+
const spk = kw.payToAddressScript(address);
|
|
3575
|
+
const script = hexToBytes(String(spk.script));
|
|
3576
|
+
const serialized = new Uint8Array(11 + script.length);
|
|
3577
|
+
serialized[0] = 0xe3;
|
|
3578
|
+
new DataView(serialized.buffer).setUint16(1, Number(spk.version), true);
|
|
3579
|
+
new DataView(serialized.buffer).setBigUint64(3, BigInt(script.length), true);
|
|
3580
|
+
serialized.set(script, 11);
|
|
3581
|
+
return { spk, hash: bytesToHex(blake3(serialized)) };
|
|
3582
|
+
})();
|
|
3583
|
+
if (String(askUtxo.state.payout_script_hash).toLowerCase() !== sellerPayout.hash) {
|
|
3584
|
+
throw new Error("ask seller payout does not match authenticated owner");
|
|
3585
|
+
}
|
|
3586
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
3587
|
+
const planInput = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
3588
|
+
wrapperProfile: "native", artifacts, marketId,
|
|
3589
|
+
recipientOwner: buyer, recipientOwnerScheme: 0,
|
|
3590
|
+
recipientPayoutScriptHash: buyerPayout.hash, tokenAmount: fillAmount };
|
|
3591
|
+
const buyerPlan = prepareKcc20FeeMarketWrappedHolder(planInput);
|
|
3592
|
+
const policyId = prepareKcc20FeeMarketGenesis(planInput).policyId;
|
|
3593
|
+
if (policyId !== requireHex32(params.policyId, "policy ID")) {
|
|
3594
|
+
throw new Error("fee-market buy policy ID mismatch");
|
|
3595
|
+
}
|
|
3596
|
+
if (String(askUtxo.state.canonicalTokenId).toLowerCase() !== tokenId ||
|
|
3597
|
+
String(askUtxo.state.feeTicketId).toLowerCase() !== feeTicketId ||
|
|
3598
|
+
String(askUtxo.state.feePolicy).toLowerCase() !== buyerPlan.holderState.feePolicy ||
|
|
3599
|
+
String(askUtxo.state.wrapper_spk_hash).toLowerCase() !== buyerPlan.wrapper.scriptHash ||
|
|
3600
|
+
BigInt(askUtxo.state.priceScale ?? -1) !== priceScale) {
|
|
3601
|
+
throw new Error("fee-market ask differs from fixed market policy");
|
|
3602
|
+
}
|
|
3603
|
+
const sellerPlan = prepareKcc20FeeMarketAsk({ tokenId, feeTicketId,
|
|
3604
|
+
priceScale, policy: params.feePolicy, wrapperProfile: "native", artifacts,
|
|
3605
|
+
marketId, owner: seller, ownerScheme: sellerScheme,
|
|
3606
|
+
payoutScriptHash: sellerPayout.hash, holderAmount: askAmount,
|
|
3607
|
+
askAmount, askUnitPrice });
|
|
3608
|
+
const askScript = sellerPlan.ask;
|
|
3609
|
+
const remainingState = remainder > 0n
|
|
3610
|
+
? { ...sellerPlan.askState, amount: remainder.toString() } : null;
|
|
3611
|
+
const remainingScript = remainingState
|
|
3612
|
+
? buildKcc20FeeMarketRoleScript({ artifact: artifacts.ask,
|
|
3613
|
+
role: "ask", state: remainingState }) : null;
|
|
3614
|
+
const fees = calculateKcc20EscrowFees({ grossSompi: gross,
|
|
3615
|
+
creatorFeeBps: params.feePolicy.creatorFeeBps, protocolWaived: Boolean(ticket) });
|
|
3616
|
+
const vector = encodeKcc20FeeVector([{ protocolFeeSompi: fees.protocolFeeSompi,
|
|
3617
|
+
creatorFeeSompi: fees.creatorFeeSompi }]);
|
|
3618
|
+
const askAddress = requireKaspaAddress(askUtxo.address);
|
|
3619
|
+
assertScriptAddress(kw, askScript.redeemScript, askAddress, network, "fee-market ask address");
|
|
3620
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_TRADE_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
3621
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
3622
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
3623
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
3624
|
+
await rpc.connect();
|
|
3625
|
+
try {
|
|
3626
|
+
const [askUtxos, ticketUtxos, walletUtxos] = await Promise.all([
|
|
3627
|
+
getUtxosByAddresses(rpc, [askAddress]),
|
|
3628
|
+
ticket ? getUtxosByAddresses(rpc, [ticket.address]) : Promise.resolve({ entries: [] }),
|
|
3629
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
3630
|
+
]);
|
|
3631
|
+
const askEntry = findUtxoEntry(askUtxos.entries, requireHex32(askUtxo.txidHex, "ask txid"), parseVout(askUtxo.vout, "ask vout"));
|
|
3632
|
+
if (!askEntry)
|
|
3633
|
+
throw new Error("fee-market ask is spent or unavailable");
|
|
3634
|
+
requireMatchingCovenantId(askEntry, marketId, "fee-market ask");
|
|
3635
|
+
const spk = askEntry.scriptPublicKey ?? askEntry.entry?.scriptPublicKey ??
|
|
3636
|
+
askEntry.utxoEntry?.scriptPublicKey;
|
|
3637
|
+
if (Number(spk?.version) !== 0 ||
|
|
3638
|
+
String(spk?.script).toLowerCase() !== askScript.scriptPublicKey.scriptHex ||
|
|
3639
|
+
utxoAmountSompi(askEntry).toString() !== String(askUtxo.amountSompiExact)) {
|
|
3640
|
+
throw new Error("fee-market ask differs from live node UTXO");
|
|
3641
|
+
}
|
|
3642
|
+
const ticketEntry = ticket ? findUtxoEntry(ticketUtxos.entries, ticket.txid, ticket.vout) : null;
|
|
3643
|
+
if (ticket) {
|
|
3644
|
+
if (!ticketEntry)
|
|
3645
|
+
throw new Error("fee-market FeeTicket is spent or unavailable");
|
|
3646
|
+
requireMatchingCovenantId(ticketEntry, feeTicketId, "fee-market FeeTicket");
|
|
3647
|
+
const ticketSpk = ticketEntry.scriptPublicKey ?? ticketEntry.entry?.scriptPublicKey ??
|
|
3648
|
+
ticketEntry.utxoEntry?.scriptPublicKey;
|
|
3649
|
+
const expectedSpk = kw.payToScriptHashScript(ticket.script);
|
|
3650
|
+
if (Number(ticketSpk?.version) !== Number(expectedSpk.version) ||
|
|
3651
|
+
String(ticketSpk?.script).toLowerCase() !== String(expectedSpk.script).toLowerCase() ||
|
|
3652
|
+
utxoAmountSompi(ticketEntry).toString() !== ticket.amountSompiExact) {
|
|
3653
|
+
throw new Error("fee-market FeeTicket differs from live node UTXO");
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
const groupHash = kcc20FeeGroupCommitment({ tokenId, marketId,
|
|
3657
|
+
policyId, taker: buyer, marketInputs: [askEntry.outpoint],
|
|
3658
|
+
feeSlots: [{ protocolFeeSompi: fees.protocolFeeSompi,
|
|
3659
|
+
creatorFeeSompi: fees.creatorFeeSompi }],
|
|
3660
|
+
ticketOutpoint: ticketEntry?.outpoint });
|
|
3661
|
+
const receiptState = { role: "0", token: tokenId, market: marketId,
|
|
3662
|
+
policy: policyId, protocol_account: params.feePolicy.protocolAccountId,
|
|
3663
|
+
creator_account: params.feePolicy.creatorAccountId,
|
|
3664
|
+
protocol_amount: fees.protocolFeeSompi.toString(),
|
|
3665
|
+
creator_amount: fees.creatorFeeSompi.toString(),
|
|
3666
|
+
owner: ZERO_HASH, owner_scheme: 0, guard_template: ZERO_HASH,
|
|
3667
|
+
guard_prefix: "0", guard_suffix: "0", action_hash: groupHash,
|
|
3668
|
+
self_template: params.feePolicy.receiptTemplate.hash,
|
|
3669
|
+
self_prefix: String(params.feePolicy.receiptTemplate.prefixLength),
|
|
3670
|
+
self_suffix: String(params.feePolicy.receiptTemplate.suffixLength) };
|
|
3671
|
+
const receipt = buildKcc20FeeEscrowScript(artifacts.receipt, receiptState);
|
|
3672
|
+
const outputs = [new TransactionOutput(50000000n, payToScriptHashScript(buyerPlan.holder.redeemScript), new CovenantBinding(0, new Hash(marketId)))];
|
|
3673
|
+
const continuationIndex = remainingScript ? 1 : 0;
|
|
3674
|
+
if (remainingScript)
|
|
3675
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(askEntry), payToScriptHashScript(remainingScript.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
3676
|
+
const sellerOutputIndex = outputs.length;
|
|
3677
|
+
outputs.push(new TransactionOutput(gross + (remainingScript ? 0n : utxoAmountSompi(askEntry)), sellerPayout.spk));
|
|
3678
|
+
const receiptOutputIndex = fees.totalFeeSompi > 0n ? outputs.length : -1;
|
|
3679
|
+
if (receiptOutputIndex >= 0)
|
|
3680
|
+
outputs.push(new TransactionOutput(fees.totalFeeSompi, payToScriptHashScript(receipt.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
3681
|
+
const ticketRefundIndex = ticketEntry ? outputs.length : -1;
|
|
3682
|
+
if (ticketEntry)
|
|
3683
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(ticketEntry), payoutHashForOwner(buyer).spk));
|
|
3684
|
+
const outputSompi = outputs.reduce((sum, output) => sum + BigInt(output.value), 0n);
|
|
3685
|
+
const covenantSompi = utxoAmountSompi(askEntry) +
|
|
3686
|
+
(ticketEntry ? utxoAmountSompi(ticketEntry) : 0n);
|
|
3687
|
+
const requiredFunding = outputSompi + priorityFee - covenantSompi + 10000n;
|
|
3688
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, requiredFunding, ticketEntry ? [askEntry.outpoint, ticketEntry.outpoint] : [askEntry.outpoint], params.fundingOutpoint);
|
|
3689
|
+
const fundingChange = covenantSompi + utxoAmountSompi(fundingEntry) -
|
|
3690
|
+
outputSompi - priorityFee;
|
|
3691
|
+
if (fundingChange <= 10000n)
|
|
3692
|
+
throw new Error("fee-market buy funding change is too small");
|
|
3693
|
+
const fundingChangeIndex = outputs.length;
|
|
3694
|
+
outputs.push(new TransactionOutput(fundingChange, payToAddressScript(walletAddress)));
|
|
3695
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
3696
|
+
inputs: [
|
|
3697
|
+
{ previousOutpoint: askEntry.outpoint, utxo: askEntry,
|
|
3698
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 1500 },
|
|
3699
|
+
...(ticketEntry ? [{ previousOutpoint: ticketEntry.outpoint, utxo: ticketEntry,
|
|
3700
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 300 }] : []),
|
|
3701
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
3702
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
3703
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
3704
|
+
});
|
|
3705
|
+
setVersionOneInputMassFields(transaction, 1500);
|
|
3706
|
+
const ticketInputIndex = ticketEntry ? 1 : -1;
|
|
3707
|
+
const fundingInputIndex = ticketEntry ? 2 : 1;
|
|
3708
|
+
if (ticketEntry)
|
|
3709
|
+
transaction.inputs[ticketInputIndex].computeBudget = 300;
|
|
3710
|
+
transaction.inputs[fundingInputIndex].computeBudget = 30;
|
|
3711
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market buy ask");
|
|
3712
|
+
const legs = new Uint8Array(32);
|
|
3713
|
+
const legView = new DataView(legs.buffer);
|
|
3714
|
+
for (const [index, value] of [fillAmount, 0, continuationIndex,
|
|
3715
|
+
sellerOutputIndex].entries())
|
|
3716
|
+
legView.setBigInt64(index * 8, BigInt(value), true);
|
|
3717
|
+
const receiptParts = templateParts(normalizeSilverScriptArtifact(artifacts.receipt));
|
|
3718
|
+
const holderParts = templateParts(normalizeSilverScriptArtifact(artifacts.holder));
|
|
3719
|
+
const prefix = covenantScriptBuilder(kw);
|
|
3720
|
+
prefix.addData(hexToBytes(buyer));
|
|
3721
|
+
addExplicitByte(prefix, 0);
|
|
3722
|
+
prefix.addData(hexToBytes(buyerPayout.hash));
|
|
3723
|
+
prefix.addI64(1n);
|
|
3724
|
+
prefix.addData(legs);
|
|
3725
|
+
prefix.addData(vector);
|
|
3726
|
+
for (const index of [ticketInputIndex, receiptOutputIndex, ticketRefundIndex])
|
|
3727
|
+
prefix.addI64(BigInt(index));
|
|
3728
|
+
prefix.addData(fees.totalFeeSompi > 0n ? receiptParts.prefix : new Uint8Array());
|
|
3729
|
+
prefix.addData(fees.totalFeeSompi > 0n ? receiptParts.suffix : new Uint8Array());
|
|
3730
|
+
prefix.addData(holderParts.prefix);
|
|
3731
|
+
prefix.addData(holderParts.suffix);
|
|
3732
|
+
prefix.addData(dispatchTagFor(artifacts.ask, "buyAsks"));
|
|
3733
|
+
transaction.inputs[0].signatureScript = encodeCovenantP2shSignatureScript(prefix.drain(), askScript.redeemScript);
|
|
3734
|
+
const scripts = ticketEntry ? [feeTicketBurnScriptHint(ticketInputIndex, ticket.script, ticket.burnDispatchTag, ticketRefundIndex, ticket.burnTakesRefundOutput)] : [];
|
|
3735
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market buy ask", candidate => {
|
|
3736
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
3737
|
+
if (typeof serialized !== "string")
|
|
3738
|
+
throw new Error("buy ask preflight serialization failed");
|
|
3739
|
+
const predictedJson = JSON.parse(serialized);
|
|
3740
|
+
const predictedWithTicket = transactionJsonWithScriptHints(kw, predictedJson, scripts);
|
|
3741
|
+
predictedWithTicket.inputs[fundingInputIndex].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
3742
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
3743
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedWithTicket))
|
|
3744
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedWithTicket));
|
|
3745
|
+
});
|
|
3746
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market buy ask");
|
|
3747
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market buy ask");
|
|
3748
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
3749
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
3750
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
3751
|
+
throw kcc20PsktBuilderError("fee-market buy ask exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
3752
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
3753
|
+
}
|
|
3754
|
+
return { schema: OUTPUT_SCHEMA,
|
|
3755
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
3756
|
+
signInputs: [{ index: fundingInputIndex, sighashType: 1 }], scripts,
|
|
3757
|
+
submitTransactionSupported: true,
|
|
3758
|
+
metadata: { builderKey: "kcc20fee.buy-ask", operation: "fee-market-buy-ask",
|
|
3759
|
+
network, walletAddress, tokenId, marketId, policyId,
|
|
3760
|
+
fillAmount: fillAmount.toString(), grossSompi: gross.toString(),
|
|
3761
|
+
protocolFeeSompi: fees.protocolFeeSompi.toString(),
|
|
3762
|
+
creatorFeeSompi: fees.creatorFeeSompi.toString(),
|
|
3763
|
+
buyerHolderOutputIndex: 0, askContinuationOutputIndex: remainingScript ? 1 : -1,
|
|
3764
|
+
sellerOutputIndex, receiptOutputIndex, ticketInputIndex, ticketRefundIndex,
|
|
3765
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
3766
|
+
transient: signedMass.transientMass.toString(),
|
|
3767
|
+
storage: storageMass.toString() } },
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
finally {
|
|
3771
|
+
await rpc.disconnect();
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
async function buildFeeMarketBuyTwoAsksPskt(input) {
|
|
3775
|
+
const request = input.request || {};
|
|
3776
|
+
const params = request.params || {};
|
|
3777
|
+
const asks = params.asks;
|
|
3778
|
+
if (!Array.isArray(asks) || asks.length !== 2) {
|
|
3779
|
+
throw new Error("fee-market buyer group requires exactly two complete asks");
|
|
3780
|
+
}
|
|
3781
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
3782
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
3783
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
3784
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
3785
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
3786
|
+
const buyer = requireHex32(request.owner?.kcc20Owner, "buyer");
|
|
3787
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, buyer);
|
|
3788
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
3789
|
+
const ticket = await feeMarketTicketForTaker(params, request.owner, buyer, network, feeTicketId);
|
|
3790
|
+
const kw = kaspaWasm;
|
|
3791
|
+
const payoutSpk = p2pkScriptPubKey(kw, buyer);
|
|
3792
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
3793
|
+
const payoutBytes = new Uint8Array(11 + payoutScript.length);
|
|
3794
|
+
payoutBytes[0] = 0xe3;
|
|
3795
|
+
new DataView(payoutBytes.buffer).setUint16(1, Number(payoutSpk.version), true);
|
|
3796
|
+
new DataView(payoutBytes.buffer).setBigUint64(3, BigInt(payoutScript.length), true);
|
|
3797
|
+
payoutBytes.set(payoutScript, 11);
|
|
3798
|
+
const buyerPayoutHash = bytesToHex(blake3(payoutBytes));
|
|
3799
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
3800
|
+
const base = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
3801
|
+
wrapperProfile: "native", artifacts, marketId };
|
|
3802
|
+
const policyId = prepareKcc20FeeMarketGenesis(base).policyId;
|
|
3803
|
+
if (policyId !== requireHex32(params.policyId, "policy ID")) {
|
|
3804
|
+
throw new Error("fee-market buyer group policy ID mismatch");
|
|
3805
|
+
}
|
|
3806
|
+
const legs = asks.map((ask, index) => {
|
|
3807
|
+
if (!ask || String(ask.state?.mode) !== "2") {
|
|
3808
|
+
throw new Error(`fee-market ask ${index} is not an active ask`);
|
|
3809
|
+
}
|
|
3810
|
+
const amount = parsePositiveU64(ask.state.amount, `ask ${index} amount`);
|
|
3811
|
+
const price = parsePositiveU64(ask.state.unitPrice, `ask ${index} unit price`);
|
|
3812
|
+
const gross = exactGrossSompi(amount, price, priceScale, "fee-market buyer group");
|
|
3813
|
+
if (gross < MIN_FILL_GROSS_SOMPI || gross > (1n << 63n) - 1n) {
|
|
3814
|
+
throw new Error(`fee-market ask ${index} gross is outside contract range`);
|
|
3815
|
+
}
|
|
3816
|
+
const seller = requireHex32(ask.state.owner, `ask ${index} seller`);
|
|
3817
|
+
if (Number(ask.state.owner_scheme) !== 0) {
|
|
3818
|
+
throw new Error("buyer group requires wallet-owned asks in this builder version");
|
|
3819
|
+
}
|
|
3820
|
+
const sellerSpk = p2pkScriptPubKey(kw, seller);
|
|
3821
|
+
const sellerScript = hexToBytes(String(sellerSpk.script));
|
|
3822
|
+
const sellerBytes = new Uint8Array(11 + sellerScript.length);
|
|
3823
|
+
sellerBytes[0] = 0xe3;
|
|
3824
|
+
new DataView(sellerBytes.buffer).setUint16(1, Number(sellerSpk.version), true);
|
|
3825
|
+
new DataView(sellerBytes.buffer).setBigUint64(3, BigInt(sellerScript.length), true);
|
|
3826
|
+
sellerBytes.set(sellerScript, 11);
|
|
3827
|
+
const sellerPayoutHash = bytesToHex(blake3(sellerBytes));
|
|
3828
|
+
const holder = prepareKcc20FeeMarketWrappedHolder({ ...base,
|
|
3829
|
+
recipientOwner: buyer, recipientOwnerScheme: 0,
|
|
3830
|
+
recipientPayoutScriptHash: buyerPayoutHash, tokenAmount: amount });
|
|
3831
|
+
if (String(ask.state.canonicalTokenId).toLowerCase() !== tokenId ||
|
|
3832
|
+
String(ask.state.feeTicketId).toLowerCase() !== feeTicketId ||
|
|
3833
|
+
String(ask.state.feePolicy).toLowerCase() !== holder.holderState.feePolicy ||
|
|
3834
|
+
String(ask.state.wrapper_spk_hash).toLowerCase() !== holder.wrapper.scriptHash ||
|
|
3835
|
+
String(ask.state.payout_script_hash).toLowerCase() !== sellerPayoutHash ||
|
|
3836
|
+
BigInt(ask.state.priceScale ?? -1) !== priceScale) {
|
|
3837
|
+
throw new Error(`fee-market ask ${index} differs from fixed policy or payout`);
|
|
3838
|
+
}
|
|
3839
|
+
const sellerPlan = prepareKcc20FeeMarketAsk({ ...base, owner: seller,
|
|
3840
|
+
ownerScheme: 0, payoutScriptHash: sellerPayoutHash,
|
|
3841
|
+
holderAmount: amount, askAmount: amount, askUnitPrice: price });
|
|
3842
|
+
const address = requireKaspaAddress(ask.address);
|
|
3843
|
+
assertScriptAddress(kw, sellerPlan.ask.redeemScript, address, network, `fee-market ask ${index} address`);
|
|
3844
|
+
const fee = calculateKcc20EscrowFees({ grossSompi: gross,
|
|
3845
|
+
creatorFeeBps: params.feePolicy.creatorFeeBps, protocolWaived: Boolean(ticket) });
|
|
3846
|
+
return { ask, amount, gross, sellerSpk, address, holder,
|
|
3847
|
+
askScript: sellerPlan.ask, fee };
|
|
3848
|
+
});
|
|
3849
|
+
if (asks[0].txidHex === asks[1].txidHex && Number(asks[0].vout) === Number(asks[1].vout)) {
|
|
3850
|
+
throw new Error("buyer group cannot spend the same ask twice");
|
|
3851
|
+
}
|
|
3852
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_TRADE_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
3853
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
3854
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
3855
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
3856
|
+
await rpc.connect();
|
|
3857
|
+
try {
|
|
3858
|
+
const [sourceUtxos, ticketUtxos, walletUtxos] = await Promise.all([
|
|
3859
|
+
getUtxosByAddresses(rpc, legs.map(leg => leg.address)),
|
|
3860
|
+
ticket ? getUtxosByAddresses(rpc, [ticket.address]) : Promise.resolve({ entries: [] }),
|
|
3861
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
3862
|
+
]);
|
|
3863
|
+
const entries = legs.map((leg, index) => {
|
|
3864
|
+
const entry = findUtxoEntry(sourceUtxos.entries, requireHex32(leg.ask.txidHex, `ask ${index} txid`), parseVout(leg.ask.vout, `ask ${index} vout`));
|
|
3865
|
+
if (!entry)
|
|
3866
|
+
throw new Error(`fee-market ask ${index} is spent or unavailable`);
|
|
3867
|
+
requireMatchingCovenantId(entry, marketId, `fee-market ask ${index}`);
|
|
3868
|
+
const spk = entry.scriptPublicKey ?? entry.entry?.scriptPublicKey ??
|
|
3869
|
+
entry.utxoEntry?.scriptPublicKey;
|
|
3870
|
+
if (Number(spk?.version) !== 0 ||
|
|
3871
|
+
String(spk?.script).toLowerCase() !== leg.askScript.scriptPublicKey.scriptHex ||
|
|
3872
|
+
utxoAmountSompi(entry).toString() !== String(leg.ask.amountSompiExact)) {
|
|
3873
|
+
throw new Error(`fee-market ask ${index} differs from live node UTXO`);
|
|
3874
|
+
}
|
|
3875
|
+
return entry;
|
|
3876
|
+
});
|
|
3877
|
+
const ticketEntry = ticket ? findUtxoEntry(ticketUtxos.entries, ticket.txid, ticket.vout) : null;
|
|
3878
|
+
if (ticket) {
|
|
3879
|
+
if (!ticketEntry)
|
|
3880
|
+
throw new Error("fee-market FeeTicket is spent or unavailable");
|
|
3881
|
+
requireMatchingCovenantId(ticketEntry, feeTicketId, "fee-market FeeTicket");
|
|
3882
|
+
const ticketSpk = ticketEntry.scriptPublicKey ?? ticketEntry.entry?.scriptPublicKey ??
|
|
3883
|
+
ticketEntry.utxoEntry?.scriptPublicKey;
|
|
3884
|
+
const expectedSpk = kw.payToScriptHashScript(ticket.script);
|
|
3885
|
+
if (Number(ticketSpk?.version) !== Number(expectedSpk.version) ||
|
|
3886
|
+
String(ticketSpk?.script).toLowerCase() !== String(expectedSpk.script).toLowerCase() ||
|
|
3887
|
+
utxoAmountSompi(ticketEntry).toString() !== ticket.amountSompiExact) {
|
|
3888
|
+
throw new Error("fee-market FeeTicket differs from live node UTXO");
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
const feeSlots = legs.map(leg => ({ protocolFeeSompi: leg.fee.protocolFeeSompi,
|
|
3892
|
+
creatorFeeSompi: leg.fee.creatorFeeSompi }));
|
|
3893
|
+
const protocolTotal = feeSlots.reduce((sum, fee) => sum + fee.protocolFeeSompi, 0n);
|
|
3894
|
+
const creatorTotal = feeSlots.reduce((sum, fee) => sum + fee.creatorFeeSompi, 0n);
|
|
3895
|
+
const feeTotal = protocolTotal + creatorTotal;
|
|
3896
|
+
const vector = encodeKcc20FeeVector(feeSlots);
|
|
3897
|
+
const groupHash = kcc20FeeGroupCommitment({ tokenId, marketId, policyId,
|
|
3898
|
+
taker: buyer, marketInputs: entries.map(entry => entry.outpoint), feeSlots,
|
|
3899
|
+
ticketOutpoint: ticketEntry?.outpoint });
|
|
3900
|
+
const receiptState = { role: "0", token: tokenId, market: marketId,
|
|
3901
|
+
policy: policyId, protocol_account: params.feePolicy.protocolAccountId,
|
|
3902
|
+
creator_account: params.feePolicy.creatorAccountId,
|
|
3903
|
+
protocol_amount: protocolTotal.toString(), creator_amount: creatorTotal.toString(),
|
|
3904
|
+
owner: ZERO_HASH, owner_scheme: 0, guard_template: ZERO_HASH,
|
|
3905
|
+
guard_prefix: "0", guard_suffix: "0", action_hash: groupHash,
|
|
3906
|
+
self_template: params.feePolicy.receiptTemplate.hash,
|
|
3907
|
+
self_prefix: String(params.feePolicy.receiptTemplate.prefixLength),
|
|
3908
|
+
self_suffix: String(params.feePolicy.receiptTemplate.suffixLength) };
|
|
3909
|
+
const receipt = buildKcc20FeeEscrowScript(artifacts.receipt, receiptState);
|
|
3910
|
+
const outputs = [];
|
|
3911
|
+
for (let index = 0; index < legs.length; index++) {
|
|
3912
|
+
const leg = legs[index];
|
|
3913
|
+
outputs.push(new TransactionOutput(50000000n, payToScriptHashScript(leg.holder.holder.redeemScript), new CovenantBinding(index, new Hash(marketId))));
|
|
3914
|
+
outputs.push(new TransactionOutput(leg.gross + utxoAmountSompi(entries[index]), leg.sellerSpk));
|
|
3915
|
+
}
|
|
3916
|
+
const receiptOutputIndex = feeTotal > 0n ? outputs.length : -1;
|
|
3917
|
+
if (receiptOutputIndex >= 0)
|
|
3918
|
+
outputs.push(new TransactionOutput(feeTotal, payToScriptHashScript(receipt.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
3919
|
+
const ticketRefundIndex = ticketEntry ? outputs.length : -1;
|
|
3920
|
+
if (ticketEntry)
|
|
3921
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(ticketEntry), payoutSpk));
|
|
3922
|
+
const outputSompi = outputs.reduce((sum, output) => sum + BigInt(output.value), 0n);
|
|
3923
|
+
const covenantInputSompi = entries.reduce((sum, entry) => sum + utxoAmountSompi(entry), 0n) +
|
|
3924
|
+
(ticketEntry ? utxoAmountSompi(ticketEntry) : 0n);
|
|
3925
|
+
const requiredFunding = outputSompi + priorityFee - covenantInputSompi + 10000n;
|
|
3926
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, requiredFunding, [...entries.map(entry => entry.outpoint),
|
|
3927
|
+
...(ticketEntry ? [ticketEntry.outpoint] : [])], params.fundingOutpoint);
|
|
3928
|
+
const fundingChange = covenantInputSompi + utxoAmountSompi(fundingEntry) -
|
|
3929
|
+
outputSompi - priorityFee;
|
|
3930
|
+
if (fundingChange <= 10000n)
|
|
3931
|
+
throw new Error("buyer group funding change is too small");
|
|
3932
|
+
const fundingChangeIndex = outputs.length;
|
|
3933
|
+
outputs.push(new TransactionOutput(fundingChange, payToAddressScript(walletAddress)));
|
|
3934
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
3935
|
+
inputs: [
|
|
3936
|
+
...entries.map((entry, index) => ({ previousOutpoint: entry.outpoint, utxo: entry,
|
|
3937
|
+
sequence: 0n, sigOpCount: 0, computeBudget: index === 0 ? 1500 : 600 })),
|
|
3938
|
+
...(ticketEntry ? [{ previousOutpoint: ticketEntry.outpoint, utxo: ticketEntry,
|
|
3939
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 300 }] : []),
|
|
3940
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
3941
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
3942
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
3943
|
+
});
|
|
3944
|
+
setVersionOneInputMassFields(transaction, 1500);
|
|
3945
|
+
transaction.inputs[1].computeBudget = 600;
|
|
3946
|
+
const ticketInputIndex = ticketEntry ? 2 : -1;
|
|
3947
|
+
const fundingInputIndex = ticketEntry ? 3 : 2;
|
|
3948
|
+
if (ticketEntry)
|
|
3949
|
+
transaction.inputs[ticketInputIndex].computeBudget = 300;
|
|
3950
|
+
transaction.inputs[fundingInputIndex].computeBudget = 30;
|
|
3951
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market buyer group");
|
|
3952
|
+
const legBytes = new Uint8Array(64);
|
|
3953
|
+
const view = new DataView(legBytes.buffer);
|
|
3954
|
+
for (let index = 0; index < 2; index++) {
|
|
3955
|
+
for (const [slot, value] of [legs[index].amount, index * 2, 0, index * 2 + 1].entries()) {
|
|
3956
|
+
view.setBigInt64(index * 32 + slot * 8, BigInt(value), true);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
const receiptParts = templateParts(normalizeSilverScriptArtifact(artifacts.receipt));
|
|
3960
|
+
const holderParts = templateParts(normalizeSilverScriptArtifact(artifacts.holder));
|
|
3961
|
+
const leader = covenantScriptBuilder(kw);
|
|
3962
|
+
leader.addData(hexToBytes(buyer));
|
|
3963
|
+
addExplicitByte(leader, 0);
|
|
3964
|
+
leader.addData(hexToBytes(buyerPayoutHash));
|
|
3965
|
+
leader.addI64(2n);
|
|
3966
|
+
leader.addData(legBytes);
|
|
3967
|
+
leader.addData(vector);
|
|
3968
|
+
for (const value of [ticketInputIndex, receiptOutputIndex, ticketRefundIndex])
|
|
3969
|
+
leader.addI64(BigInt(value));
|
|
3970
|
+
leader.addData(feeTotal > 0n ? receiptParts.prefix : new Uint8Array());
|
|
3971
|
+
leader.addData(feeTotal > 0n ? receiptParts.suffix : new Uint8Array());
|
|
3972
|
+
leader.addData(holderParts.prefix);
|
|
3973
|
+
leader.addData(holderParts.suffix);
|
|
3974
|
+
leader.addData(dispatchTagFor(artifacts.ask, "buyAsks"));
|
|
3975
|
+
transaction.inputs[0].signatureScript = encodeCovenantP2shSignatureScript(leader.drain(), legs[0].askScript.redeemScript);
|
|
3976
|
+
const follower = covenantScriptBuilder(kw);
|
|
3977
|
+
for (const value of [0, 1, 2, -1])
|
|
3978
|
+
follower.addI64(BigInt(value));
|
|
3979
|
+
addExplicitByte(follower, 0);
|
|
3980
|
+
follower.addData(dispatchTagFor(artifacts.ask, "delegateBuyerGroup"));
|
|
3981
|
+
transaction.inputs[1].signatureScript = encodeCovenantP2shSignatureScript(follower.drain(), legs[1].askScript.redeemScript);
|
|
3982
|
+
const scripts = ticketEntry ? [feeTicketBurnScriptHint(ticketInputIndex, ticket.script, ticket.burnDispatchTag, ticketRefundIndex, ticket.burnTakesRefundOutput)] : [];
|
|
3983
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market buyer group", candidate => {
|
|
3984
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
3985
|
+
if (typeof serialized !== "string")
|
|
3986
|
+
throw new Error("buyer group preflight serialization failed");
|
|
3987
|
+
const predicted = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
3988
|
+
predicted.inputs[fundingInputIndex].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
3989
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
3990
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predicted))
|
|
3991
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predicted));
|
|
3992
|
+
});
|
|
3993
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market buyer group");
|
|
3994
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market buyer group");
|
|
3995
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
3996
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
3997
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
3998
|
+
throw kcc20PsktBuilderError("fee-market buyer group exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
3999
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
4000
|
+
}
|
|
4001
|
+
return { schema: OUTPUT_SCHEMA,
|
|
4002
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
4003
|
+
signInputs: [{ index: fundingInputIndex, sighashType: 1 }], scripts,
|
|
4004
|
+
// Follower input 1 has a 600-unit budget without constrained consensus proof.
|
|
4005
|
+
submitTransactionSupported: false,
|
|
4006
|
+
metadata: { builderKey: "kcc20fee.buy-two-asks",
|
|
4007
|
+
operation: "fee-market-buy-two-asks", network, walletAddress,
|
|
4008
|
+
tokenId, marketId, policyId, fillCount: 2,
|
|
4009
|
+
grossSompi: legs.reduce((sum, leg) => sum + leg.gross, 0n).toString(),
|
|
4010
|
+
protocolFeeSompi: protocolTotal.toString(),
|
|
4011
|
+
creatorFeeSompi: creatorTotal.toString(), receiptOutputIndex,
|
|
4012
|
+
ticketInputIndex, ticketRefundIndex,
|
|
4013
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
4014
|
+
transient: signedMass.transientMass.toString(), storage: storageMass.toString() } },
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
finally {
|
|
4018
|
+
await rpc.disconnect();
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
async function buildFeeMarketSellIntoBidsPskt(input) {
|
|
4022
|
+
const request = input.request || {};
|
|
4023
|
+
const params = request.params || {};
|
|
4024
|
+
const bids = params.bids;
|
|
4025
|
+
if (!Array.isArray(bids) || bids.length < 1 || bids.length > 10) {
|
|
4026
|
+
throw new Error("fee-market seller group requires one to ten bids");
|
|
4027
|
+
}
|
|
4028
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
4029
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
4030
|
+
const feeTicketId = requireHex32(params.feeTicketId || ZERO_HASH, "FeeTicket ID");
|
|
4031
|
+
const priceScale = parsePositiveU64(params.priceScale, "priceScale");
|
|
4032
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
4033
|
+
const seller = requireHex32(request.owner?.kcc20Owner, "seller");
|
|
4034
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, seller);
|
|
4035
|
+
const ticketRequest = params.feeTicketUtxo || null;
|
|
4036
|
+
if (ticketRequest && feeTicketId === ZERO_HASH) {
|
|
4037
|
+
throw new Error("a fee-market ticket waiver requires the bound FeeTicket root");
|
|
4038
|
+
}
|
|
4039
|
+
const source = params.holderUtxo;
|
|
4040
|
+
if (!source || String(source.state?.mode) !== "0" ||
|
|
4041
|
+
Number(source.state.owner_scheme) !== 0 ||
|
|
4042
|
+
String(source.state.owner).toLowerCase() !== seller) {
|
|
4043
|
+
throw new Error("current wallet-owned fee-market holder UTXO is required");
|
|
4044
|
+
}
|
|
4045
|
+
const holderAmount = parsePositiveU64(source.state.amount, "holder amount");
|
|
4046
|
+
const kw = kaspaWasm;
|
|
4047
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
4048
|
+
const sellerSpk = p2pkScriptPubKey(kw, seller);
|
|
4049
|
+
const sellerScript = hexToBytes(String(sellerSpk.script));
|
|
4050
|
+
const sellerPayoutBytes = new Uint8Array(11 + sellerScript.length);
|
|
4051
|
+
sellerPayoutBytes[0] = 0xe3;
|
|
4052
|
+
new DataView(sellerPayoutBytes.buffer).setUint16(1, Number(sellerSpk.version), true);
|
|
4053
|
+
new DataView(sellerPayoutBytes.buffer).setBigUint64(3, BigInt(sellerScript.length), true);
|
|
4054
|
+
sellerPayoutBytes.set(sellerScript, 11);
|
|
4055
|
+
const sellerPayoutHash = bytesToHex(blake3(sellerPayoutBytes));
|
|
4056
|
+
const artifacts = await readFeeMarketArtifacts();
|
|
4057
|
+
let ticket = null;
|
|
4058
|
+
if (ticketRequest) {
|
|
4059
|
+
const ticketState = kcc20FeeTicketStateFromUtxo(ticketRequest, request.owner);
|
|
4060
|
+
if (ticketState.mode !== 2 || bytesToHex(ticketState.ownerIdentifier) !== seller) {
|
|
4061
|
+
throw new Error("fee-market ticket must be owned by the selling wallet");
|
|
4062
|
+
}
|
|
4063
|
+
const ticketArtifacts = await readFeeTicketArtifacts();
|
|
4064
|
+
const address = requireKaspaAddress(ticketRequest.address);
|
|
4065
|
+
const info = selectFeeTicketArtifactForState(ticketArtifacts, ticketState, address, network, "fee-market FeeTicket");
|
|
4066
|
+
const pinned = templateParts(normalizeSilverScriptArtifact(info.artifact));
|
|
4067
|
+
const expected = params.feePolicy.ticketTemplate;
|
|
4068
|
+
if (bytesToHex(templateHash(pinned.prefix, pinned.suffix)) !==
|
|
4069
|
+
requireHex32(expected?.hash, "ticket template") ||
|
|
4070
|
+
pinned.prefix.length !== Number(expected?.prefixLength) ||
|
|
4071
|
+
pinned.suffix.length !== Number(expected?.suffixLength)) {
|
|
4072
|
+
throw new Error("fee-market ticket artifact differs from fixed policy");
|
|
4073
|
+
}
|
|
4074
|
+
ticket = { ...info, state: ticketState, address,
|
|
4075
|
+
txid: requireHex32(ticketRequest.txidHex, "FeeTicket txid"),
|
|
4076
|
+
vout: parseVout(ticketRequest.vout, "FeeTicket vout"),
|
|
4077
|
+
amountSompiExact: String(ticketRequest.amountSompiExact) };
|
|
4078
|
+
}
|
|
4079
|
+
const base = { tokenId, feeTicketId, priceScale, policy: params.feePolicy,
|
|
4080
|
+
wrapperProfile: "native", artifacts, marketId };
|
|
4081
|
+
const policyId = prepareKcc20FeeMarketGenesis(base).policyId;
|
|
4082
|
+
if (policyId !== requireHex32(params.policyId, "policy ID")) {
|
|
4083
|
+
throw new Error("fee-market seller group policy ID mismatch");
|
|
4084
|
+
}
|
|
4085
|
+
const holderPlan = prepareKcc20FeeMarketWrappedHolder({ ...base,
|
|
4086
|
+
recipientOwner: seller, recipientOwnerScheme: 0,
|
|
4087
|
+
recipientPayoutScriptHash: sellerPayoutHash, tokenAmount: holderAmount });
|
|
4088
|
+
if (String(source.state.canonicalTokenId).toLowerCase() !== tokenId ||
|
|
4089
|
+
String(source.state.feeTicketId).toLowerCase() !== feeTicketId ||
|
|
4090
|
+
BigInt(source.state.priceScale ?? -1) !== priceScale ||
|
|
4091
|
+
String(source.state.feePolicy).toLowerCase() !== holderPlan.holderState.feePolicy ||
|
|
4092
|
+
String(source.state.wrapper_spk_hash).toLowerCase() !== holderPlan.wrapper.scriptHash ||
|
|
4093
|
+
String(source.state.payout_script_hash).toLowerCase() !== sellerPayoutHash) {
|
|
4094
|
+
throw new Error("fee-market seller holder differs from fixed policy or payout");
|
|
4095
|
+
}
|
|
4096
|
+
const holderAddress = requireKaspaAddress(source.address);
|
|
4097
|
+
assertScriptAddress(kw, holderPlan.holder.redeemScript, holderAddress, network, "fee-market seller holder address");
|
|
4098
|
+
let sold = 0n;
|
|
4099
|
+
const legs = bids.map((bid, slot) => {
|
|
4100
|
+
const utxo = bid?.bidUtxo;
|
|
4101
|
+
if (!utxo || String(utxo.state?.mode) !== "3") {
|
|
4102
|
+
throw new Error(`fee-market bid ${slot} is not active`);
|
|
4103
|
+
}
|
|
4104
|
+
const amount = parsePositiveU64(utxo.state.amount, `bid ${slot} amount`);
|
|
4105
|
+
const price = parsePositiveU64(utxo.state.unitPrice, `bid ${slot} unit price`);
|
|
4106
|
+
const fill = parsePositiveU64(bid.fillAmount, `bid ${slot} fill amount`);
|
|
4107
|
+
if (fill > amount)
|
|
4108
|
+
throw new Error(`bid ${slot} fill exceeds available tokens`);
|
|
4109
|
+
const gross = exactGrossSompi(fill, price, priceScale, "fee-market seller fill");
|
|
4110
|
+
if (gross < MIN_FILL_GROSS_SOMPI || gross > (1n << 63n) - 1n) {
|
|
4111
|
+
throw new Error(`bid ${slot} fill gross is outside contract range`);
|
|
4112
|
+
}
|
|
4113
|
+
const remaining = amount - fill;
|
|
4114
|
+
if (remaining > 0n && exactGrossSompi(remaining, price, priceScale, "fee-market remaining bid") < MIN_FILL_GROSS_SOMPI) {
|
|
4115
|
+
throw new Error(`bid ${slot} remainder is below 0.5 KAS`);
|
|
4116
|
+
}
|
|
4117
|
+
sold += fill;
|
|
4118
|
+
const buyer = requireHex32(utxo.state.owner, `bid ${slot} buyer`);
|
|
4119
|
+
const scheme = Number(utxo.state.owner_scheme);
|
|
4120
|
+
if (!Number.isInteger(scheme) || scheme < 0 || scheme > 4) {
|
|
4121
|
+
throw new Error(`bid ${slot} has an unsupported owner scheme`);
|
|
4122
|
+
}
|
|
4123
|
+
const payoutHash = requireHex32(utxo.state.payout_script_hash, `bid ${slot} buyer payout hash`);
|
|
4124
|
+
if (String(utxo.state.canonicalTokenId).toLowerCase() !== tokenId ||
|
|
4125
|
+
String(utxo.state.feeTicketId).toLowerCase() !== feeTicketId ||
|
|
4126
|
+
BigInt(utxo.state.priceScale ?? -1) !== priceScale ||
|
|
4127
|
+
String(utxo.state.feePolicy).toLowerCase() !== holderPlan.holderState.feePolicy ||
|
|
4128
|
+
String(utxo.state.wrapper_spk_hash).toLowerCase() !== holderPlan.wrapper.scriptHash) {
|
|
4129
|
+
throw new Error(`bid ${slot} differs from fixed market policy`);
|
|
4130
|
+
}
|
|
4131
|
+
const bidScript = buildKcc20FeeMarketRoleScript({ artifact: artifacts.bid,
|
|
4132
|
+
role: "bid", state: utxo.state });
|
|
4133
|
+
const buyerPlan = prepareKcc20FeeMarketWrappedHolder({ ...base,
|
|
4134
|
+
recipientOwner: buyer, recipientOwnerScheme: scheme,
|
|
4135
|
+
recipientPayoutScriptHash: payoutHash, tokenAmount: fill });
|
|
4136
|
+
const continuation = remaining > 0n
|
|
4137
|
+
? buildKcc20FeeMarketRoleScript({ artifact: artifacts.bid,
|
|
4138
|
+
role: "bid", state: { ...utxo.state, amount: remaining.toString() } })
|
|
4139
|
+
: null;
|
|
4140
|
+
const fee = calculateKcc20EscrowFees({ grossSompi: gross,
|
|
4141
|
+
creatorFeeBps: params.feePolicy.creatorFeeBps, protocolWaived: Boolean(ticket) });
|
|
4142
|
+
if (gross <= fee.totalFeeSompi) {
|
|
4143
|
+
throw new Error(`bid ${slot} leaves no seller proceeds after fees`);
|
|
4144
|
+
}
|
|
4145
|
+
const address = requireKaspaAddress(utxo.address);
|
|
4146
|
+
assertScriptAddress(kw, bidScript.redeemScript, address, network, `fee-market bid ${slot} address`);
|
|
4147
|
+
return { utxo, amount, fill, gross, remaining, buyerPlan, bidScript,
|
|
4148
|
+
continuation, fee, address };
|
|
4149
|
+
});
|
|
4150
|
+
if (sold > holderAmount)
|
|
4151
|
+
throw new Error("seller group exceeds holder token amount");
|
|
4152
|
+
const seenBids = new Set(legs.map(leg => `${leg.utxo.txidHex}:${leg.utxo.vout}`));
|
|
4153
|
+
if (seenBids.size !== legs.length)
|
|
4154
|
+
throw new Error("seller group repeats a bid UTXO");
|
|
4155
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_TRADE_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
4156
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
4157
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
4158
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
4159
|
+
await rpc.connect();
|
|
4160
|
+
try {
|
|
4161
|
+
const [sourceUtxos, bidUtxos, ticketUtxos, walletUtxos] = await Promise.all([
|
|
4162
|
+
getUtxosByAddresses(rpc, [holderAddress]),
|
|
4163
|
+
getUtxosByAddresses(rpc, legs.map(leg => leg.address)),
|
|
4164
|
+
ticket ? getUtxosByAddresses(rpc, [ticket.address]) : Promise.resolve({ entries: [] }),
|
|
4165
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
4166
|
+
]);
|
|
4167
|
+
const holderEntry = findUtxoEntry(sourceUtxos.entries, requireHex32(source.txidHex, "holder txid"), parseVout(source.vout, "holder vout"));
|
|
4168
|
+
if (!holderEntry)
|
|
4169
|
+
throw new Error("fee-market seller holder is spent or unavailable");
|
|
4170
|
+
requireMatchingCovenantId(holderEntry, marketId, "fee-market seller holder");
|
|
4171
|
+
const holderSpk = holderEntry.scriptPublicKey ?? holderEntry.entry?.scriptPublicKey ??
|
|
4172
|
+
holderEntry.utxoEntry?.scriptPublicKey;
|
|
4173
|
+
if (Number(holderSpk?.version) !== 0 ||
|
|
4174
|
+
String(holderSpk?.script).toLowerCase() !== holderPlan.holder.scriptPublicKey.scriptHex ||
|
|
4175
|
+
utxoAmountSompi(holderEntry).toString() !== String(source.amountSompiExact)) {
|
|
4176
|
+
throw new Error("fee-market seller holder differs from live node UTXO");
|
|
4177
|
+
}
|
|
4178
|
+
const bidEntries = legs.map((leg, slot) => {
|
|
4179
|
+
const entry = findUtxoEntry(bidUtxos.entries, requireHex32(leg.utxo.txidHex, `bid ${slot} txid`), parseVout(leg.utxo.vout, `bid ${slot} vout`));
|
|
4180
|
+
if (!entry)
|
|
4181
|
+
throw new Error(`fee-market bid ${slot} is spent or unavailable`);
|
|
4182
|
+
requireMatchingCovenantId(entry, marketId, `fee-market bid ${slot}`);
|
|
4183
|
+
const spk = entry.scriptPublicKey ?? entry.entry?.scriptPublicKey ??
|
|
4184
|
+
entry.utxoEntry?.scriptPublicKey;
|
|
4185
|
+
if (Number(spk?.version) !== 0 ||
|
|
4186
|
+
String(spk?.script).toLowerCase() !== leg.bidScript.scriptPublicKey.scriptHex ||
|
|
4187
|
+
utxoAmountSompi(entry).toString() !== String(leg.utxo.amountSompiExact)) {
|
|
4188
|
+
throw new Error(`fee-market bid ${slot} differs from live node UTXO`);
|
|
4189
|
+
}
|
|
4190
|
+
const originalGross = exactGrossSompi(leg.amount, BigInt(leg.utxo.state.unitPrice), priceScale, "fee-market bid gross");
|
|
4191
|
+
const deposit = utxoAmountSompi(entry) - originalGross;
|
|
4192
|
+
if (deposit < MIN_FILL_GROSS_SOMPI || deposit > 200000000n) {
|
|
4193
|
+
throw new Error(`fee-market bid ${slot} has an invalid locked deposit`);
|
|
4194
|
+
}
|
|
4195
|
+
return { entry, deposit };
|
|
4196
|
+
});
|
|
4197
|
+
const ticketEntry = ticket ? findUtxoEntry(ticketUtxos.entries, ticket.txid, ticket.vout) : null;
|
|
4198
|
+
if (ticket) {
|
|
4199
|
+
if (!ticketEntry)
|
|
4200
|
+
throw new Error("fee-market FeeTicket is spent or unavailable");
|
|
4201
|
+
requireMatchingCovenantId(ticketEntry, feeTicketId, "fee-market FeeTicket");
|
|
4202
|
+
const spk = ticketEntry.scriptPublicKey ?? ticketEntry.entry?.scriptPublicKey ??
|
|
4203
|
+
ticketEntry.utxoEntry?.scriptPublicKey;
|
|
4204
|
+
const expectedSpk = kw.payToScriptHashScript(ticket.script);
|
|
4205
|
+
if (Number(spk?.version) !== Number(expectedSpk.version) ||
|
|
4206
|
+
String(spk?.script).toLowerCase() !== String(expectedSpk.script).toLowerCase() ||
|
|
4207
|
+
utxoAmountSompi(ticketEntry).toString() !== ticket.amountSompiExact) {
|
|
4208
|
+
throw new Error("fee-market FeeTicket differs from live node UTXO");
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
const feeSlots = legs.map(leg => ({ protocolFeeSompi: leg.fee.protocolFeeSompi,
|
|
4212
|
+
creatorFeeSompi: leg.fee.creatorFeeSompi }));
|
|
4213
|
+
const protocolTotal = feeSlots.reduce((sum, fee) => sum + fee.protocolFeeSompi, 0n);
|
|
4214
|
+
const creatorTotal = feeSlots.reduce((sum, fee) => sum + fee.creatorFeeSompi, 0n);
|
|
4215
|
+
const feeTotal = protocolTotal + creatorTotal;
|
|
4216
|
+
const vector = encodeKcc20FeeVector(feeSlots);
|
|
4217
|
+
const groupHash = kcc20FeeGroupCommitment({ tokenId, marketId, policyId,
|
|
4218
|
+
taker: seller, marketInputs: [holderEntry.outpoint,
|
|
4219
|
+
...bidEntries.map(({ entry }) => entry.outpoint)], feeSlots,
|
|
4220
|
+
ticketOutpoint: ticketEntry?.outpoint });
|
|
4221
|
+
const receiptState = { role: "0", token: tokenId, market: marketId,
|
|
4222
|
+
policy: policyId, protocol_account: params.feePolicy.protocolAccountId,
|
|
4223
|
+
creator_account: params.feePolicy.creatorAccountId,
|
|
4224
|
+
protocol_amount: protocolTotal.toString(), creator_amount: creatorTotal.toString(),
|
|
4225
|
+
owner: ZERO_HASH, owner_scheme: 0, guard_template: ZERO_HASH,
|
|
4226
|
+
guard_prefix: "0", guard_suffix: "0", action_hash: groupHash,
|
|
4227
|
+
self_template: params.feePolicy.receiptTemplate.hash,
|
|
4228
|
+
self_prefix: String(params.feePolicy.receiptTemplate.prefixLength),
|
|
4229
|
+
self_suffix: String(params.feePolicy.receiptTemplate.suffixLength) };
|
|
4230
|
+
const receipt = buildKcc20FeeEscrowScript(artifacts.receipt, receiptState);
|
|
4231
|
+
const outputs = [];
|
|
4232
|
+
const legIndexes = [];
|
|
4233
|
+
for (let slot = 0; slot < legs.length; slot++) {
|
|
4234
|
+
const leg = legs[slot];
|
|
4235
|
+
const { entry, deposit } = bidEntries[slot];
|
|
4236
|
+
const buyerIndex = outputs.length;
|
|
4237
|
+
outputs.push(new TransactionOutput(leg.remaining > 0n ? 50000000n : deposit, payToScriptHashScript(leg.buyerPlan.holder.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
4238
|
+
const continuationIndex = leg.continuation ? outputs.length : -1;
|
|
4239
|
+
if (leg.continuation) {
|
|
4240
|
+
const remainingGross = exactGrossSompi(leg.remaining, BigInt(leg.utxo.state.unitPrice), priceScale, "fee-market bid continuation");
|
|
4241
|
+
outputs.push(new TransactionOutput(remainingGross + deposit, payToScriptHashScript(leg.continuation.redeemScript), new CovenantBinding(slot + 1, new Hash(marketId))));
|
|
4242
|
+
}
|
|
4243
|
+
const sellerIndex = outputs.length;
|
|
4244
|
+
outputs.push(new TransactionOutput(leg.gross - leg.fee.totalFeeSompi, sellerSpk));
|
|
4245
|
+
legIndexes.push({ buyerIndex, continuationIndex, sellerIndex });
|
|
4246
|
+
}
|
|
4247
|
+
const holderRemainder = holderAmount - sold;
|
|
4248
|
+
const changeIndex = holderRemainder > 0n ? outputs.length : -1;
|
|
4249
|
+
if (holderRemainder > 0n) {
|
|
4250
|
+
const change = prepareKcc20FeeMarketWrappedHolder({ ...base,
|
|
4251
|
+
recipientOwner: seller, recipientOwnerScheme: 0,
|
|
4252
|
+
recipientPayoutScriptHash: sellerPayoutHash, tokenAmount: holderRemainder });
|
|
4253
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(holderEntry), payToScriptHashScript(change.holder.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
4254
|
+
}
|
|
4255
|
+
const receiptOutputIndex = feeTotal > 0n ? outputs.length : -1;
|
|
4256
|
+
if (feeTotal > 0n)
|
|
4257
|
+
outputs.push(new TransactionOutput(feeTotal, payToScriptHashScript(receipt.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
4258
|
+
const refundIndex = holderRemainder === 0n ? outputs.length : -1;
|
|
4259
|
+
if (refundIndex >= 0) {
|
|
4260
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(holderEntry), sellerSpk));
|
|
4261
|
+
}
|
|
4262
|
+
const ticketRefundIndex = ticketEntry ? outputs.length : -1;
|
|
4263
|
+
if (ticketEntry)
|
|
4264
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(ticketEntry), sellerSpk));
|
|
4265
|
+
const covenantInputs = [holderEntry, ...bidEntries.map(({ entry }) => entry)];
|
|
4266
|
+
const spentEntries = ticketEntry ? [...covenantInputs, ticketEntry] : covenantInputs;
|
|
4267
|
+
const covenantSompi = spentEntries.reduce((sum, entry) => sum + utxoAmountSompi(entry), 0n);
|
|
4268
|
+
const outputSompi = outputs.reduce((sum, output) => sum + BigInt(output.value), 0n);
|
|
4269
|
+
const fundingNeeded = outputSompi + priorityFee > covenantSompi
|
|
4270
|
+
? outputSompi + priorityFee - covenantSompi + 10000n
|
|
4271
|
+
: priorityFee + 10000n;
|
|
4272
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, fundingNeeded, spentEntries.map(entry => entry.outpoint), params.fundingOutpoint);
|
|
4273
|
+
const fundingChange = covenantSompi + utxoAmountSompi(fundingEntry) - outputSompi - priorityFee;
|
|
4274
|
+
if (fundingChange <= 10000n)
|
|
4275
|
+
throw new Error("seller group funding change is too small");
|
|
4276
|
+
const fundingChangeIndex = outputs.length;
|
|
4277
|
+
outputs.push(new TransactionOutput(fundingChange, payToAddressScript(walletAddress)));
|
|
4278
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
4279
|
+
inputs: [
|
|
4280
|
+
{ previousOutpoint: holderEntry.outpoint, utxo: holderEntry,
|
|
4281
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 1000 },
|
|
4282
|
+
...bidEntries.map(({ entry }) => ({ previousOutpoint: entry.outpoint, utxo: entry,
|
|
4283
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 100 })),
|
|
4284
|
+
...(ticketEntry ? [{ previousOutpoint: ticketEntry.outpoint, utxo: ticketEntry,
|
|
4285
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 600 }] : []),
|
|
4286
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
4287
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
4288
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
4289
|
+
});
|
|
4290
|
+
// Pinned signed consensus proof uses 394 units for ten-bid leader and
|
|
4291
|
+
// 43 for each bid delegator. Leave more than 2x headroom per role.
|
|
4292
|
+
setVersionOneInputMassFields(transaction, 1000);
|
|
4293
|
+
for (let slot = 0; slot < legs.length; slot++)
|
|
4294
|
+
transaction.inputs[slot + 1].computeBudget = 100;
|
|
4295
|
+
const ticketInputIndex = ticketEntry ? legs.length + 1 : -1;
|
|
4296
|
+
if (ticketEntry)
|
|
4297
|
+
transaction.inputs[ticketInputIndex].computeBudget = 600;
|
|
4298
|
+
const fundingInputIndex = legs.length + 1 + (ticketEntry ? 1 : 0);
|
|
4299
|
+
transaction.inputs[fundingInputIndex].computeBudget = 30;
|
|
4300
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market seller group");
|
|
4301
|
+
const legBytes = new Uint8Array(legs.length * 32);
|
|
4302
|
+
const view = new DataView(legBytes.buffer);
|
|
4303
|
+
for (let slot = 0; slot < legs.length; slot++) {
|
|
4304
|
+
const indexes = legIndexes[slot];
|
|
4305
|
+
for (const [field, value] of [legs[slot].fill, indexes.buyerIndex,
|
|
4306
|
+
indexes.continuationIndex < 0 ? 0 : indexes.continuationIndex,
|
|
4307
|
+
indexes.sellerIndex].entries()) {
|
|
4308
|
+
view.setBigInt64(slot * 32 + field * 8, BigInt(value), true);
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
const receiptParts = templateParts(normalizeSilverScriptArtifact(artifacts.receipt));
|
|
4312
|
+
const scripts = [{ inputIndex: 0,
|
|
4313
|
+
scriptHex: bytesToHex(holderPlan.holder.redeemScript), signType: 1,
|
|
4314
|
+
signatureScript: { mode: "ordered-args", args: [
|
|
4315
|
+
{ type: "signature" }, i64TemplateArg(legs.length), dataTemplateArg(legBytes),
|
|
4316
|
+
dataTemplateArg(vector), i64TemplateArg(ticketInputIndex), i64TemplateArg(receiptOutputIndex),
|
|
4317
|
+
i64TemplateArg(changeIndex), i64TemplateArg(refundIndex), i64TemplateArg(ticketRefundIndex),
|
|
4318
|
+
dataTemplateArg(feeTotal > 0n ? receiptParts.prefix : new Uint8Array()),
|
|
4319
|
+
dataTemplateArg(feeTotal > 0n ? receiptParts.suffix : new Uint8Array()),
|
|
4320
|
+
dataTemplateArg(dispatchTagFor(artifacts.holder, "sellIntoBids")),
|
|
4321
|
+
] } }];
|
|
4322
|
+
if (ticketEntry)
|
|
4323
|
+
scripts.push(feeTicketBurnScriptHint(ticketInputIndex, ticket.script, ticket.burnDispatchTag, ticketRefundIndex, ticket.burnTakesRefundOutput));
|
|
4324
|
+
for (let slot = 0; slot < legs.length; slot++) {
|
|
4325
|
+
const delegate = covenantScriptBuilder(kw);
|
|
4326
|
+
for (const value of [0, slot, legs[slot].remaining,
|
|
4327
|
+
legIndexes[slot].continuationIndex])
|
|
4328
|
+
delegate.addI64(BigInt(value));
|
|
4329
|
+
delegate.addData(dispatchTagFor(artifacts.bid, "delegateSellerGroup"));
|
|
4330
|
+
transaction.inputs[slot + 1].signatureScript = encodeCovenantP2shSignatureScript(delegate.drain(), legs[slot].bidScript.redeemScript);
|
|
4331
|
+
}
|
|
4332
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market seller group", candidate => {
|
|
4333
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
4334
|
+
if (typeof serialized !== "string")
|
|
4335
|
+
throw new Error("seller group preflight serialization failed");
|
|
4336
|
+
const predicted = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
4337
|
+
predicted.inputs[fundingInputIndex].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
4338
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
4339
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predicted))
|
|
4340
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predicted));
|
|
4341
|
+
});
|
|
4342
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market seller group");
|
|
4343
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market seller group");
|
|
4344
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
4345
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
4346
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
4347
|
+
throw kcc20PsktBuilderError("fee-market seller group exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
4348
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
4349
|
+
}
|
|
4350
|
+
return { schema: OUTPUT_SCHEMA,
|
|
4351
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
4352
|
+
signInputs: [{ index: fundingInputIndex, sighashType: 1 }], scripts,
|
|
4353
|
+
submitTransactionSupported: true,
|
|
4354
|
+
metadata: { builderKey: "kcc20fee.sell-into-bids",
|
|
4355
|
+
operation: "fee-market-sell-into-bids", network, walletAddress,
|
|
4356
|
+
tokenId, marketId, policyId, fillCount: legs.length,
|
|
4357
|
+
soldAmount: sold.toString(),
|
|
4358
|
+
grossSompi: legs.reduce((sum, leg) => sum + leg.gross, 0n).toString(),
|
|
4359
|
+
protocolFeeSompi: protocolTotal.toString(),
|
|
4360
|
+
creatorFeeSompi: creatorTotal.toString(), receiptOutputIndex,
|
|
4361
|
+
ticketInputIndex, ticketRefundIndex,
|
|
4362
|
+
sellerChangeIndex: changeIndex, sellerRefundIndex: refundIndex,
|
|
4363
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
4364
|
+
transient: signedMass.transientMass.toString(), storage: storageMass.toString() } },
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4367
|
+
finally {
|
|
4368
|
+
await rpc.disconnect();
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
async function buildFeeMarketClaimPskt(input) {
|
|
4372
|
+
const request = input.request || {};
|
|
4373
|
+
const params = request.params || {};
|
|
4374
|
+
const party = params.party;
|
|
4375
|
+
if (party !== "protocol" && party !== "creator") {
|
|
4376
|
+
throw new Error("fee-market claim party must be protocol or creator");
|
|
4377
|
+
}
|
|
4378
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
4379
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
4380
|
+
const policyId = requireHex32(params.policyId, "fee-market policy ID");
|
|
4381
|
+
const protocolAccountId = requireHex32(params.protocolAccountId, "protocol account ID");
|
|
4382
|
+
const creatorAccountId = requireHex32(params.creatorAccountId || ZERO_HASH, "creator account ID");
|
|
4383
|
+
const accountId = party === "protocol" ? protocolAccountId : creatorAccountId;
|
|
4384
|
+
if (accountId === ZERO_HASH)
|
|
4385
|
+
throw new Error("creator claim is disabled by this market policy");
|
|
4386
|
+
const indexedReceipts = params.receipts;
|
|
4387
|
+
const indexedAccount = params.account;
|
|
4388
|
+
if (!Array.isArray(indexedReceipts) || indexedReceipts.length < 1 ||
|
|
4389
|
+
indexedReceipts.length > 5 || !indexedAccount) {
|
|
4390
|
+
throw new Error("fee-market claim requires one to five receipts and an account");
|
|
4391
|
+
}
|
|
4392
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
4393
|
+
const owner = requireHex32(request.owner?.kcc20Owner, "claim owner");
|
|
4394
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, owner);
|
|
4395
|
+
const accountOwnerScheme = Number(indexedAccount.state?.owner_scheme);
|
|
4396
|
+
const accountOwner = accountOwnerScheme === 1
|
|
4397
|
+
? bytesToHex(blake3(hexToBytes(owner))) : owner;
|
|
4398
|
+
if ((accountOwnerScheme !== 0 && accountOwnerScheme !== 1) ||
|
|
4399
|
+
String(indexedAccount.state?.owner).toLowerCase() !== accountOwner) {
|
|
4400
|
+
throw new Error("fee account is not controlled by the signing wallet");
|
|
4401
|
+
}
|
|
4402
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
4403
|
+
const artifact = (await readFeeMarketArtifacts()).receipt;
|
|
4404
|
+
const kw = kaspaWasm;
|
|
4405
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
4406
|
+
const payoutSpk = payToAddressScript(walletAddress);
|
|
4407
|
+
const payoutScript = hexToBytes(String(payoutSpk.script));
|
|
4408
|
+
const payoutBytes = new Uint8Array(2 + payoutScript.length);
|
|
4409
|
+
new DataView(payoutBytes.buffer).setUint16(0, Number(payoutSpk.version), true);
|
|
4410
|
+
payoutBytes.set(payoutScript, 2);
|
|
4411
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_CLAIM_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
4412
|
+
const sourceAddresses = [...indexedReceipts, indexedAccount]
|
|
4413
|
+
.map(item => requireKaspaAddress(item.address));
|
|
4414
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
4415
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
4416
|
+
await rpc.connect();
|
|
4417
|
+
try {
|
|
4418
|
+
const [covenantUtxos, walletUtxos] = await Promise.all([
|
|
4419
|
+
getUtxosByAddresses(rpc, sourceAddresses),
|
|
4420
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
4421
|
+
]);
|
|
4422
|
+
const getLive = (indexed, label) => {
|
|
4423
|
+
const entry = findUtxoEntry(covenantUtxos.entries, requireHex32(indexed.txidHex, `${label} txid`), parseVout(indexed.vout, `${label} vout`));
|
|
4424
|
+
if (!entry)
|
|
4425
|
+
throw new Error(`${label} is spent or unavailable`);
|
|
4426
|
+
const spk = entry.scriptPublicKey ?? entry.entry?.scriptPublicKey ??
|
|
4427
|
+
entry.utxoEntry?.scriptPublicKey;
|
|
4428
|
+
return { entry, live: { outpoint: entry.outpoint,
|
|
4429
|
+
amountSompi: utxoAmountSompi(entry),
|
|
4430
|
+
covenantIdHex: requireMatchingCovenantId(entry, indexed.covenantIdHex, `${label} live`),
|
|
4431
|
+
scriptPublicKey: { version: Number(spk?.version),
|
|
4432
|
+
scriptHex: String(spk?.script).toLowerCase() } } };
|
|
4433
|
+
};
|
|
4434
|
+
const checkedReceipts = indexedReceipts.map(indexed => {
|
|
4435
|
+
const { entry, live } = getLive(indexed, "fee receipt");
|
|
4436
|
+
const checked = verifyKcc20FeeReceiptForClaim({ artifact,
|
|
4437
|
+
indexed, live, expected: { tokenId, marketId, policyId,
|
|
4438
|
+
protocolAccountId, creatorAccountId } });
|
|
4439
|
+
assertScriptAddress(kw, checked.redeemScript, indexed.address, network, "fee receipt address");
|
|
4440
|
+
return { entry, ...checked };
|
|
4441
|
+
});
|
|
4442
|
+
const { entry: accountEntry, live: accountLive } = getLive(indexedAccount, "fee account");
|
|
4443
|
+
const checkedAccount = verifyKcc20FeeAccountForClaim({ artifact,
|
|
4444
|
+
indexed: indexedAccount, live: accountLive,
|
|
4445
|
+
expected: { tokenId, marketId, policyId, accountId } });
|
|
4446
|
+
assertScriptAddress(kw, checkedAccount.redeemScript, indexedAccount.address, network, "fee account address");
|
|
4447
|
+
const transition = planKcc20FeeClaimTransition({
|
|
4448
|
+
receipts: checkedReceipts.map(item => item.receipt), party,
|
|
4449
|
+
account: checkedAccount.account,
|
|
4450
|
+
escrowTemplate: checkedAccount.escrowTemplate,
|
|
4451
|
+
payoutScriptPublicKey: payoutBytes,
|
|
4452
|
+
});
|
|
4453
|
+
const successor = buildKcc20FeeEscrowScript(artifact, transition.successorAccountState);
|
|
4454
|
+
const remainder = transition.remainderReceiptState
|
|
4455
|
+
? buildKcc20FeeEscrowScript(artifact, transition.remainderReceiptState)
|
|
4456
|
+
: null;
|
|
4457
|
+
const accountInputIndex = checkedReceipts.length;
|
|
4458
|
+
const fundingInputIndex = accountInputIndex + 1;
|
|
4459
|
+
const outputs = [new TransactionOutput(transition.claimSompi, payoutSpk),
|
|
4460
|
+
new TransactionOutput(transition.accountPrincipalSompi, payToScriptHashScript(successor.redeemScript), new CovenantBinding(accountInputIndex, new Hash(accountId)))];
|
|
4461
|
+
if (remainder)
|
|
4462
|
+
outputs.push(new TransactionOutput(transition.remainderSompi, payToScriptHashScript(remainder.redeemScript), new CovenantBinding(0, new Hash(marketId))));
|
|
4463
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, priorityFee + 10000n, [...checkedReceipts.map(item => item.entry.outpoint), accountEntry.outpoint], params.fundingOutpoint);
|
|
4464
|
+
const fundingChangeIndex = outputs.length;
|
|
4465
|
+
outputs.push(new TransactionOutput(utxoAmountSompi(fundingEntry) - priorityFee, payoutSpk));
|
|
4466
|
+
const inputs = [
|
|
4467
|
+
...checkedReceipts.map((item, index) => ({
|
|
4468
|
+
previousOutpoint: item.entry.outpoint, utxo: item.entry,
|
|
4469
|
+
sequence: 0n, sigOpCount: 0, computeBudget: index === 0 ? 200 : 50,
|
|
4470
|
+
})),
|
|
4471
|
+
{ previousOutpoint: accountEntry.outpoint, utxo: accountEntry,
|
|
4472
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 100 },
|
|
4473
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
4474
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
4475
|
+
];
|
|
4476
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
4477
|
+
inputs, outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
4478
|
+
});
|
|
4479
|
+
setVersionOneInputMassFields(transaction, 200);
|
|
4480
|
+
checkedReceipts.forEach((_, index) => {
|
|
4481
|
+
transaction.inputs[index].computeBudget = index === 0 ? 200 : 50;
|
|
4482
|
+
});
|
|
4483
|
+
transaction.inputs[accountInputIndex].computeBudget = 100;
|
|
4484
|
+
transaction.inputs[fundingInputIndex].computeBudget = 30;
|
|
4485
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee-market claim");
|
|
4486
|
+
for (const [index, checked] of checkedReceipts.entries()) {
|
|
4487
|
+
const prefix = covenantScriptBuilder(kw);
|
|
4488
|
+
addExplicitByte(prefix, party === "creator" ? 1 : 0);
|
|
4489
|
+
for (const value of [accountInputIndex, 1, 0])
|
|
4490
|
+
prefix.addI64(BigInt(value));
|
|
4491
|
+
prefix.addData(payoutBytes);
|
|
4492
|
+
prefix.addI64(remainder ? 2n : -1n);
|
|
4493
|
+
prefix.addData(dispatchTagFor(artifact, "claim"));
|
|
4494
|
+
transaction.inputs[index].signatureScript = encodeCovenantP2shSignatureScript(prefix.drain(), checked.redeemScript);
|
|
4495
|
+
}
|
|
4496
|
+
const scripts = [{ inputIndex: accountInputIndex,
|
|
4497
|
+
scriptHex: bytesToHex(checkedAccount.redeemScript), signType: 1,
|
|
4498
|
+
signatureScript: { mode: "ordered-args", args: [
|
|
4499
|
+
i64TemplateArg(1),
|
|
4500
|
+
...feeAccountStateTemplateArgs(transition.successorAccountState),
|
|
4501
|
+
i64TemplateArg(1), { type: "signature",
|
|
4502
|
+
...(accountOwnerScheme === 1 ? { prefixHex: owner } : {}) },
|
|
4503
|
+
i64TemplateArg(-1), i64TemplateArg(-1),
|
|
4504
|
+
...feeAccountStateTemplateArgs(transition.successorAccountState),
|
|
4505
|
+
dataTemplateArg(new Uint8Array(0)),
|
|
4506
|
+
dataTemplateArg(new Uint8Array(0)),
|
|
4507
|
+
dataTemplateArg(dispatchTagFor(artifact, "authorize")),
|
|
4508
|
+
] } }];
|
|
4509
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, fundingChangeIndex, network, "fee-market claim", candidate => {
|
|
4510
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
4511
|
+
if (typeof serialized !== "string")
|
|
4512
|
+
throw new Error("claim preflight serialization failed");
|
|
4513
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
4514
|
+
predictedJson.inputs[fundingInputIndex].signatureScript =
|
|
4515
|
+
encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
4516
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
4517
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
4518
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
4519
|
+
});
|
|
4520
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee-market claim");
|
|
4521
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee-market claim");
|
|
4522
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
4523
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
4524
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
4525
|
+
throw kcc20PsktBuilderError("fee-market claim exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
4526
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
4527
|
+
}
|
|
4528
|
+
return { schema: OUTPUT_SCHEMA,
|
|
4529
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
4530
|
+
signInputs: [{ index: fundingInputIndex, sighashType: 1 }], scripts,
|
|
4531
|
+
submitTransactionSupported: true,
|
|
4532
|
+
metadata: { builderKey: "kcc20fee.claim", operation: "fee-market-claim",
|
|
4533
|
+
network, walletAddress, tokenId, marketId, policyId, party, accountId,
|
|
4534
|
+
receiptCount: checkedReceipts.length,
|
|
4535
|
+
claimSompi: transition.claimSompi.toString(),
|
|
4536
|
+
remainderSompi: transition.remainderSompi.toString(),
|
|
4537
|
+
payoutOutputIndex: 0, accountOutputIndex: 1,
|
|
4538
|
+
remainderOutputIndex: remainder ? 2 : -1,
|
|
4539
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
4540
|
+
transient: signedMass.transientMass.toString(),
|
|
4541
|
+
storage: storageMass.toString() } },
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
4544
|
+
finally {
|
|
4545
|
+
await rpc.disconnect();
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
async function buildFeeMarketRotateAccountPskt(input) {
|
|
4549
|
+
const request = input.request || {};
|
|
4550
|
+
const params = request.params || {};
|
|
4551
|
+
const tokenId = requireHex32(params.contractCanonicalTokenId, "fee-market token ID");
|
|
4552
|
+
const marketId = requireHex32(params.marketId, "fee-market ID");
|
|
4553
|
+
const policyId = requireHex32(params.policyId, "fee-market policy ID");
|
|
4554
|
+
const accountId = requireHex32(params.accountId, "fee account ID");
|
|
4555
|
+
const indexedAccount = params.account;
|
|
4556
|
+
if (!indexedAccount)
|
|
4557
|
+
throw new Error("current fee account UTXO is required");
|
|
4558
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
4559
|
+
const currentOwner = requireHex32(request.owner?.kcc20Owner, "current fee account owner");
|
|
4560
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, currentOwner);
|
|
4561
|
+
const accountOwnerScheme = Number(indexedAccount.state?.owner_scheme);
|
|
4562
|
+
const accountOwner = accountOwnerScheme === 1
|
|
4563
|
+
? bytesToHex(blake3(hexToBytes(currentOwner))) : currentOwner;
|
|
4564
|
+
if ((accountOwnerScheme !== 0 && accountOwnerScheme !== 1) ||
|
|
4565
|
+
String(indexedAccount.state?.owner).toLowerCase() !== accountOwner) {
|
|
4566
|
+
throw new Error("fee account is not controlled by the signing wallet");
|
|
4567
|
+
}
|
|
4568
|
+
const newOwner = requireHex32(params.newOwner, "new fee account owner");
|
|
4569
|
+
const newScheme = Number(params.newOwnerScheme);
|
|
4570
|
+
if (!Number.isInteger(newScheme) || newScheme < 0 || newScheme > 4) {
|
|
4571
|
+
throw new Error("unsupported new fee account owner scheme");
|
|
4572
|
+
}
|
|
4573
|
+
if (newScheme === 1 || newScheme === 2) {
|
|
4574
|
+
const keyHex = String(params.newOwnerPublicKey || "");
|
|
4575
|
+
const width = newScheme === 1 ? 64 : 66;
|
|
4576
|
+
if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(keyHex) ||
|
|
4577
|
+
bytesToHex(blake3(hexToBytes(keyHex))) !== newOwner) {
|
|
4578
|
+
throw new Error("new fee account key does not match its owner hash");
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
const zero = ZERO_HASH;
|
|
4582
|
+
const guardTemplate = requireHex32(params.newGuardTemplate || zero, "new fee account guard template");
|
|
4583
|
+
const guardPrefix = parseU64(params.newGuardPrefix ?? 0, "new guard prefix");
|
|
4584
|
+
const guardSuffix = parseU64(params.newGuardSuffix ?? 0, "new guard suffix");
|
|
4585
|
+
if ((newScheme <= 2 && (guardTemplate !== zero || guardPrefix !== 0n || guardSuffix !== 0n)) ||
|
|
4586
|
+
(newScheme === 3 && (guardTemplate === zero || guardPrefix !== 0n || guardSuffix !== 0n)) ||
|
|
4587
|
+
(newScheme === 4 && (guardTemplate === zero || guardPrefix === 0n || guardSuffix === 0n))) {
|
|
4588
|
+
throw new Error("new fee account authority guard is invalid");
|
|
4589
|
+
}
|
|
4590
|
+
if (newOwner === currentOwner && newScheme === 0) {
|
|
4591
|
+
throw new Error("fee account rotation must change authority");
|
|
4592
|
+
}
|
|
4593
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
4594
|
+
const artifact = (await readFeeMarketArtifacts()).receipt;
|
|
4595
|
+
const kw = kaspaWasm;
|
|
4596
|
+
if (newScheme === 3) {
|
|
4597
|
+
const redeemHex = String(params.newOwnerP2shRedeemScriptHex || "");
|
|
4598
|
+
if (!/^(?:[0-9a-fA-F]{2}){1,10000}$/.test(redeemHex)) {
|
|
4599
|
+
throw new Error("new P2SH fee account owner script is required");
|
|
4600
|
+
}
|
|
4601
|
+
const program = kw.payToScriptHashScript(hexToBytes(redeemHex));
|
|
4602
|
+
if (String(program.script).slice(4, 68).toLowerCase() !== newOwner) {
|
|
4603
|
+
throw new Error("new P2SH fee account owner does not match its script");
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction, TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
4607
|
+
const accountAddress = requireKaspaAddress(indexedAccount.address);
|
|
4608
|
+
const priorityFee = parseU64(env.KCC20_FEE_MARKET_ROTATE_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(), "priorityFee");
|
|
4609
|
+
const rpc = new RpcClient({ url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
4610
|
+
encoding: Encoding.Borsh, networkId: network });
|
|
4611
|
+
await rpc.connect();
|
|
4612
|
+
try {
|
|
4613
|
+
const [accountUtxos, walletUtxos] = await Promise.all([
|
|
4614
|
+
getUtxosByAddresses(rpc, [accountAddress]),
|
|
4615
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
4616
|
+
]);
|
|
4617
|
+
const accountEntry = findUtxoEntry(accountUtxos.entries, requireHex32(indexedAccount.txidHex, "account txid"), parseVout(indexedAccount.vout, "account vout"));
|
|
4618
|
+
if (!accountEntry)
|
|
4619
|
+
throw new Error("fee account is spent or unavailable");
|
|
4620
|
+
const spk = accountEntry.scriptPublicKey ?? accountEntry.entry?.scriptPublicKey ??
|
|
4621
|
+
accountEntry.utxoEntry?.scriptPublicKey;
|
|
4622
|
+
const checked = verifyKcc20FeeAccountForClaim({ artifact,
|
|
4623
|
+
indexed: indexedAccount,
|
|
4624
|
+
live: { outpoint: accountEntry.outpoint,
|
|
4625
|
+
amountSompi: utxoAmountSompi(accountEntry),
|
|
4626
|
+
covenantIdHex: requireMatchingCovenantId(accountEntry, accountId, "fee account live"),
|
|
4627
|
+
scriptPublicKey: { version: Number(spk?.version),
|
|
4628
|
+
scriptHex: String(spk?.script).toLowerCase() } },
|
|
4629
|
+
expected: { tokenId, marketId, policyId, accountId } });
|
|
4630
|
+
assertScriptAddress(kw, checked.redeemScript, accountAddress, network, "fee account address");
|
|
4631
|
+
const nextState = { ...checked.account.state, owner: newOwner,
|
|
4632
|
+
owner_scheme: newScheme, guard_template: guardTemplate,
|
|
4633
|
+
guard_prefix: guardPrefix.toString(), guard_suffix: guardSuffix.toString(),
|
|
4634
|
+
action_hash: zero };
|
|
4635
|
+
const successor = buildKcc20FeeEscrowScript(artifact, nextState);
|
|
4636
|
+
const fundingEntry = selectFundingEntry(walletUtxos.entries, priorityFee + 10000n, [accountEntry.outpoint], params.fundingOutpoint);
|
|
4637
|
+
const fundingChange = utxoAmountSompi(fundingEntry) - priorityFee;
|
|
4638
|
+
if (fundingChange <= 10000n)
|
|
4639
|
+
throw new Error("fee account rotation funding is too small");
|
|
4640
|
+
const outputs = [new TransactionOutput(checked.account.principalSompi, payToScriptHashScript(successor.redeemScript), new CovenantBinding(0, new Hash(accountId))),
|
|
4641
|
+
new TransactionOutput(fundingChange, payToAddressScript(walletAddress))];
|
|
4642
|
+
const transaction = new Transaction({ version: 1, lockTime: 0n,
|
|
4643
|
+
inputs: [
|
|
4644
|
+
{ previousOutpoint: accountEntry.outpoint, utxo: accountEntry,
|
|
4645
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 100 },
|
|
4646
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
4647
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
4648
|
+
], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
|
|
4649
|
+
});
|
|
4650
|
+
setVersionOneInputMassFields(transaction, 100);
|
|
4651
|
+
transaction.inputs[1].computeBudget = 30;
|
|
4652
|
+
assertUniqueTransactionInputOutpoints(transaction, "fee account rotation");
|
|
4653
|
+
const scripts = [{ inputIndex: 0, scriptHex: bytesToHex(checked.redeemScript),
|
|
4654
|
+
signType: 1, signatureScript: { mode: "ordered-args", args: [
|
|
4655
|
+
i64TemplateArg(2), ...feeAccountStateTemplateArgs(nextState),
|
|
4656
|
+
i64TemplateArg(0), { type: "signature",
|
|
4657
|
+
...(accountOwnerScheme === 1 ? { prefixHex: currentOwner } : {}) },
|
|
4658
|
+
i64TemplateArg(-1), i64TemplateArg(-1),
|
|
4659
|
+
...feeAccountStateTemplateArgs(nextState),
|
|
4660
|
+
dataTemplateArg(new Uint8Array(0)),
|
|
4661
|
+
dataTemplateArg(new Uint8Array(0)),
|
|
4662
|
+
dataTemplateArg(dispatchTagFor(artifact, "authorize")),
|
|
4663
|
+
] } }];
|
|
4664
|
+
const feeResult = transactionWithPredictedSignedFeeChange(kw, transaction, 1, network, "fee account rotation", candidate => {
|
|
4665
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
4666
|
+
if (typeof serialized !== "string")
|
|
4667
|
+
throw new Error("rotation preflight serialization failed");
|
|
4668
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
4669
|
+
predictedJson.inputs[1].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
4670
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
4671
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
4672
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
4673
|
+
});
|
|
4674
|
+
const signedMass = calculateToccataV1NonContextualMass(feeResult.predictedTransaction, "fee account rotation");
|
|
4675
|
+
const storageMass = calculateToccataStorageMass(feeResult.predictedTransaction, "fee account rotation");
|
|
4676
|
+
if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
4677
|
+
signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
|
|
4678
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
4679
|
+
throw kcc20PsktBuilderError("fee account rotation exceeds signed mass target", "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
|
|
4680
|
+
transientMass: signedMass.transientMass, storageMass });
|
|
4681
|
+
}
|
|
4682
|
+
return { schema: OUTPUT_SCHEMA,
|
|
4683
|
+
psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
|
|
4684
|
+
signInputs: [{ index: 1, sighashType: 1 }], scripts,
|
|
4685
|
+
submitTransactionSupported: true,
|
|
4686
|
+
metadata: { builderKey: "kcc20fee.rotate-account",
|
|
4687
|
+
operation: "fee-market-rotate-account", network, walletAddress,
|
|
4688
|
+
tokenId, marketId, policyId, accountId, newOwner,
|
|
4689
|
+
newOwnerScheme: newScheme, accountOutputIndex: 0,
|
|
4690
|
+
transfersAllUnclaimedEntitlements: true,
|
|
4691
|
+
signedMass: { compute: signedMass.computeMass.toString(),
|
|
4692
|
+
transient: signedMass.transientMass.toString(),
|
|
4693
|
+
storage: storageMass.toString() } },
|
|
4694
|
+
};
|
|
4695
|
+
}
|
|
4696
|
+
finally {
|
|
4697
|
+
await rpc.disconnect();
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
1930
4700
|
async function buildWrapperMarketDeployPskt(input) {
|
|
1931
4701
|
const request = input.request || {};
|
|
1932
4702
|
const params = request.params || {};
|