@kaspacom/kcc20-tx-builder 0.8.0-fee.2 → 0.8.0-fee.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/RELEASING.md +7 -6
- package/dist/cjs/engine.js +263 -0
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/escrow-account-setup.d.ts +53 -0
- package/dist/cjs/escrow-account-setup.js +103 -0
- package/dist/cjs/escrow-account-setup.js.map +1 -0
- package/dist/cjs/escrow-claim-plan.d.ts +1 -1
- package/dist/cjs/escrow-receipt.d.ts +32 -0
- package/dist/cjs/escrow-receipt.js +50 -1
- package/dist/cjs/escrow-receipt.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/operations.d.ts +1 -1
- package/dist/cjs/operations.js +2 -0
- package/dist/cjs/operations.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +263 -0
- package/dist/engine.js.map +1 -1
- package/dist/escrow-account-setup.d.ts +54 -0
- package/dist/escrow-account-setup.d.ts.map +1 -0
- package/dist/escrow-account-setup.js +99 -0
- package/dist/escrow-account-setup.js.map +1 -0
- package/dist/escrow-claim-plan.d.ts +1 -1
- package/dist/escrow-receipt.d.ts +32 -0
- package/dist/escrow-receipt.d.ts.map +1 -1
- package/dist/escrow-receipt.js +48 -1
- package/dist/escrow-receipt.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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 +2 -0
- package/dist/operations.js.map +1 -1
- package/docs/COMPATIBILITY.json +1 -1
- package/docs/escrow-fees.md +30 -3
- package/package.json +3 -3
- package/src/engine.ts +306 -0
- package/src/escrow-account-setup.ts +124 -0
- package/src/escrow-receipt.ts +64 -1
- package/src/index.ts +1 -0
- package/src/operations.ts +2 -0
package/src/engine.ts
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
ownerAuthorizationWitness,
|
|
78
78
|
} from "./abi.js";
|
|
79
79
|
import { KCC20_BUILDER_KEYS } from "./operations.js";
|
|
80
|
+
import { buildKcc20InitialFeeAccount, planKcc20FeeAccountBinding } from "./escrow-account-setup.js";
|
|
80
81
|
import { createKcc20SnapshotPsktBuilder } from "./snapshot-operation.js";
|
|
81
82
|
import { splitKcc20MintSupply } from "./protocol.js";
|
|
82
83
|
import { buildKcc20DeployIntentOutputs } from "./deploy-receipt.js";
|
|
@@ -553,6 +554,8 @@ export function createKcc20PsktBuilderEngine(
|
|
|
553
554
|
"fee-ticket.create-from-utility-burn": buildFeeTicketCreatePskt,
|
|
554
555
|
"fee-ticket.burn": buildFeeTicketBurnPskt,
|
|
555
556
|
"fee-ticket.transfer": buildFeeTicketTransferPskt,
|
|
557
|
+
"kcc20fee.create-account": buildFeeAccountCreatePskt,
|
|
558
|
+
"kcc20fee.bind-account": buildFeeAccountBindPskt,
|
|
556
559
|
"kcc20wrapper.deploy-market": buildWrapperMarketDeployPskt,
|
|
557
560
|
"kcc20wrapper.wrap": buildWrapperWrapPskt,
|
|
558
561
|
"kcc20wrapper.unwrap": buildWrapperUnwrapPskt,
|
|
@@ -606,6 +609,298 @@ export function createKcc20PsktBuilderEngine(
|
|
|
606
609
|
return builder(input);
|
|
607
610
|
}
|
|
608
611
|
|
|
612
|
+
async function buildFeeAccountCreatePskt(input) {
|
|
613
|
+
const request = input.request || {};
|
|
614
|
+
const params = request.params || {};
|
|
615
|
+
const tokenId = requireHex32(params.tokenId, "fee account token id");
|
|
616
|
+
const owner = requireHex32(request.owner?.kcc20Owner, "fee account owner");
|
|
617
|
+
const principalSompi = parseU64(params.principalSompi, "fee account principal");
|
|
618
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
619
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
620
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, owner);
|
|
621
|
+
const priorityFee = parseU64(
|
|
622
|
+
env.KCC20_FEE_ACCOUNT_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(),
|
|
623
|
+
"priorityFee",
|
|
624
|
+
);
|
|
625
|
+
const artifactKey = "KCC20FeeEscrowV1.placeholder.json";
|
|
626
|
+
const supplied = options.artifacts?.[artifactKey] ?? await options.artifactProvider?.(artifactKey);
|
|
627
|
+
const artifact = supplied?.artifact ?? supplied;
|
|
628
|
+
if (!artifact) throw new Error("missing pinned fee escrow artifact");
|
|
629
|
+
const initial = buildKcc20InitialFeeAccount({
|
|
630
|
+
artifact, tokenId, ownerSchnorrPublicKey: owner, principalSompi,
|
|
631
|
+
});
|
|
632
|
+
const kw = kaspaWasm;
|
|
633
|
+
const { Encoding, GenesisCovenantGroup, RpcClient, Transaction,
|
|
634
|
+
TransactionOutput, covenantId, payToAddressScript, payToScriptHashScript } = kw;
|
|
635
|
+
const rpc = new RpcClient({
|
|
636
|
+
url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
637
|
+
encoding: Encoding.Borsh,
|
|
638
|
+
networkId: network,
|
|
639
|
+
});
|
|
640
|
+
await rpc.connect();
|
|
641
|
+
try {
|
|
642
|
+
const walletUtxos = await getUtxosByAddresses(rpc, [walletAddress]);
|
|
643
|
+
const fundingEntry = selectFundingEntry(
|
|
644
|
+
walletUtxos.entries,
|
|
645
|
+
principalSompi + priorityFee + 10_000n,
|
|
646
|
+
[],
|
|
647
|
+
params.fundingOutpoint,
|
|
648
|
+
);
|
|
649
|
+
const fundingAmount = utxoAmountSompi(fundingEntry);
|
|
650
|
+
const change = fundingAmount - principalSompi - priorityFee;
|
|
651
|
+
if (change <= 10_000n) throw new Error("fee account creation funding change is too small");
|
|
652
|
+
const transaction = new Transaction({
|
|
653
|
+
version: 1,
|
|
654
|
+
lockTime: 0n,
|
|
655
|
+
inputs: [{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
656
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 }],
|
|
657
|
+
outputs: [
|
|
658
|
+
new TransactionOutput(principalSompi, payToScriptHashScript(initial.redeemScript)),
|
|
659
|
+
new TransactionOutput(change, payToAddressScript(walletAddress)),
|
|
660
|
+
],
|
|
661
|
+
subnetworkId: SUBNETWORK_ID_NATIVE,
|
|
662
|
+
gas: 0n,
|
|
663
|
+
payload: "",
|
|
664
|
+
});
|
|
665
|
+
setVersionOneInputMassFields(transaction, 30);
|
|
666
|
+
transaction.populateGenesisCovenants([new GenesisCovenantGroup(0, [0])]);
|
|
667
|
+
transaction.finalize();
|
|
668
|
+
const accountId = transaction.outputs[0]?.covenant?.covenantId?.toString();
|
|
669
|
+
if (!accountId) throw new Error("fee account genesis covenant id is missing");
|
|
670
|
+
const expectedId = covenantId(fundingEntry.outpoint, [
|
|
671
|
+
{ index: 0, output: transaction.outputs[0] },
|
|
672
|
+
]).toString();
|
|
673
|
+
if (accountId !== expectedId) throw new Error("fee account genesis covenant id mismatch");
|
|
674
|
+
const feeResult = transactionWithPredictedSignedFeeChange(
|
|
675
|
+
kw, transaction, 1, network, "fee account creation", candidate => {
|
|
676
|
+
const predicted = cloneTransactionForMassPreflight(kw, candidate, "fee account creation");
|
|
677
|
+
predicted.inputs[0].signatureScript = encodeP2pkSignatureScript(bytesToHex(dummySignatureBytes()));
|
|
678
|
+
return predicted;
|
|
679
|
+
},
|
|
680
|
+
);
|
|
681
|
+
const nonContextualMass = calculateToccataV1NonContextualMass(
|
|
682
|
+
feeResult.predictedTransaction, "fee account creation",
|
|
683
|
+
).transactionMass;
|
|
684
|
+
const storageMass = calculateToccataStorageMass(
|
|
685
|
+
feeResult.predictedTransaction, "fee account creation",
|
|
686
|
+
);
|
|
687
|
+
if (nonContextualMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
688
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
689
|
+
throw kcc20PsktBuilderError(
|
|
690
|
+
"fee account creation exceeds the signed transaction mass target",
|
|
691
|
+
"TRANSACTION_MASS_EXCEEDED",
|
|
692
|
+
{ nonContextualMass, storageMass, targetMass: CONSOLIDATION_TARGET_TRANSACTION_MASS },
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const finalAccountId = covenantId(fundingEntry.outpoint, [
|
|
696
|
+
{ index: 0, output: feeResult.transaction.outputs[0] },
|
|
697
|
+
]).toString();
|
|
698
|
+
if (finalAccountId !== accountId) throw new Error("fee account genesis id changed after fee calculation");
|
|
699
|
+
const psktTransactionJson = feeResult.transaction.serializeToSafeJSON();
|
|
700
|
+
if (typeof psktTransactionJson !== "string") throw new Error("fee account creation PSKT serialization failed");
|
|
701
|
+
return {
|
|
702
|
+
schema: OUTPUT_SCHEMA,
|
|
703
|
+
psktTransactionJson,
|
|
704
|
+
signInputs: [{ index: 0, sighashType: 1 }],
|
|
705
|
+
scripts: [],
|
|
706
|
+
submitTransactionSupported: true,
|
|
707
|
+
metadata: {
|
|
708
|
+
builderKey: "kcc20fee.create-account",
|
|
709
|
+
operation: "create-fee-account",
|
|
710
|
+
action: "genesis",
|
|
711
|
+
contract: "KCC20FeeEscrowV1",
|
|
712
|
+
network, walletAddress, tokenId, owner, accountId,
|
|
713
|
+
accountPrincipalSompi: principalSompi.toString(),
|
|
714
|
+
// Attach the broadcast txid to this hint before binding. The first
|
|
715
|
+
// spend reveals the script; until then the indexer sees only P2SH.
|
|
716
|
+
accountGenesisHint: {
|
|
717
|
+
vout: 0,
|
|
718
|
+
address: kw.addressFromScriptPublicKey(
|
|
719
|
+
payToScriptHashScript(initial.redeemScript), network,
|
|
720
|
+
).toString(),
|
|
721
|
+
covenantIdHex: accountId,
|
|
722
|
+
scriptHashHex: initial.scriptHash,
|
|
723
|
+
amountSompiExact: principalSompi.toString(),
|
|
724
|
+
state: { ...initial.state, roleName: "account" },
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
} finally {
|
|
729
|
+
await rpc.disconnect();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function buildFeeAccountBindPskt(input) {
|
|
734
|
+
const request = input.request || {};
|
|
735
|
+
const params = request.params || {};
|
|
736
|
+
// A genesis output has no revealed script yet. The caller keeps the
|
|
737
|
+
// creation hint, adds its accepted txid, then we check the live UTXO.
|
|
738
|
+
const indexed = params.accountGenesisHint
|
|
739
|
+
? { ...params.accountGenesisHint, status: "active", spentByTxidHex: null }
|
|
740
|
+
: params.activeAccountUtxo;
|
|
741
|
+
if (!indexed || !indexed.address) throw new Error("active unbound fee account UTXO and address are required");
|
|
742
|
+
const tokenId = requireHex32(params.tokenId, "fee account token id");
|
|
743
|
+
const accountId = requireHex32(params.accountId, "fee account id");
|
|
744
|
+
const marketId = requireHex32(params.marketId, "fee account market id");
|
|
745
|
+
const policyId = requireHex32(params.policyId, "fee account policy id");
|
|
746
|
+
const owner = requireHex32(request.owner?.kcc20Owner, "fee account owner");
|
|
747
|
+
if (String(indexed.state?.owner || "").toLowerCase() !== owner) {
|
|
748
|
+
throw new Error("fee account owner does not match connected wallet");
|
|
749
|
+
}
|
|
750
|
+
const network = request.network || env.KASPA_NETWORK || "testnet-10";
|
|
751
|
+
const walletAddress = requireKaspaAddress(request.owner?.walletAddress);
|
|
752
|
+
assertFeeAccountSchnorrWalletOwner(walletAddress, owner);
|
|
753
|
+
const accountAddress = requireKaspaAddress(indexed.address);
|
|
754
|
+
const priorityFee = parseU64(
|
|
755
|
+
env.KCC20_FEE_ACCOUNT_PRIORITY_FEE_SOMPI || DEFAULT_PRIORITY_FEE.toString(),
|
|
756
|
+
"priorityFee",
|
|
757
|
+
);
|
|
758
|
+
const computeBudget = parsePositiveNumber(
|
|
759
|
+
env.KCC20_FEE_ACCOUNT_COMPUTE_BUDGET || 500,
|
|
760
|
+
"computeBudget",
|
|
761
|
+
);
|
|
762
|
+
const artifactKey = "KCC20FeeEscrowV1.placeholder.json";
|
|
763
|
+
const supplied = options.artifacts?.[artifactKey] ?? await options.artifactProvider?.(artifactKey);
|
|
764
|
+
const artifact = supplied?.artifact ?? supplied;
|
|
765
|
+
if (!artifact) throw new Error("missing pinned fee escrow artifact");
|
|
766
|
+
|
|
767
|
+
const kw = kaspaWasm;
|
|
768
|
+
const { CovenantBinding, Encoding, Hash, RpcClient, Transaction,
|
|
769
|
+
TransactionOutput, payToAddressScript, payToScriptHashScript } = kw;
|
|
770
|
+
const rpc = new RpcClient({
|
|
771
|
+
url: env.KASPA_WRPC_URL || DEFAULT_WRPC_URL,
|
|
772
|
+
encoding: Encoding.Borsh,
|
|
773
|
+
networkId: network,
|
|
774
|
+
});
|
|
775
|
+
await rpc.connect();
|
|
776
|
+
try {
|
|
777
|
+
const [accountUtxos, walletUtxos] = await Promise.all([
|
|
778
|
+
getUtxosByAddresses(rpc, [accountAddress]),
|
|
779
|
+
getUtxosByAddresses(rpc, [walletAddress]),
|
|
780
|
+
]);
|
|
781
|
+
const txid = requireHex32(indexed.txidHex, "fee account txid");
|
|
782
|
+
const vout = parseVout(indexed.vout, "fee account vout");
|
|
783
|
+
const accountEntry = findUtxoEntry(accountUtxos.entries, txid, vout);
|
|
784
|
+
if (!accountEntry) throw new Error("unbound fee account UTXO is spent or unavailable");
|
|
785
|
+
requireMatchingCovenantId(accountEntry, accountId, "unbound fee account");
|
|
786
|
+
const spk = accountEntry.scriptPublicKey ??
|
|
787
|
+
accountEntry.entry?.scriptPublicKey ?? accountEntry.utxoEntry?.scriptPublicKey;
|
|
788
|
+
const scriptBytes = spk?.script;
|
|
789
|
+
if (!spk || !scriptBytes) throw new Error("unbound fee account has no script public key");
|
|
790
|
+
const scriptHex = typeof scriptBytes === "string" ? scriptBytes : bytesToHex(scriptBytes);
|
|
791
|
+
const plan = planKcc20FeeAccountBinding({
|
|
792
|
+
artifact,
|
|
793
|
+
indexed,
|
|
794
|
+
live: {
|
|
795
|
+
outpoint: { transactionId: txid, index: vout },
|
|
796
|
+
amountSompi: utxoAmountSompi(accountEntry),
|
|
797
|
+
covenantIdHex: accountId,
|
|
798
|
+
scriptPublicKey: { version: Number(spk.version), scriptHex },
|
|
799
|
+
},
|
|
800
|
+
tokenId, accountId, marketId, policyId,
|
|
801
|
+
});
|
|
802
|
+
assertScriptAddress(kw, plan.currentRedeemScript, accountAddress, network, "unbound fee account address");
|
|
803
|
+
const fundingEntry = selectFundingEntry(
|
|
804
|
+
walletUtxos.entries,
|
|
805
|
+
priorityFee + 10_000n,
|
|
806
|
+
[accountEntry.outpoint],
|
|
807
|
+
params.fundingOutpoint,
|
|
808
|
+
);
|
|
809
|
+
const fundingAmount = utxoAmountSompi(fundingEntry);
|
|
810
|
+
if (fundingAmount <= priorityFee + 10_000n) throw new Error("fee account binding funding change is too small");
|
|
811
|
+
const unsignedTx = new Transaction({
|
|
812
|
+
version: 1,
|
|
813
|
+
lockTime: 0n,
|
|
814
|
+
inputs: [
|
|
815
|
+
{ previousOutpoint: accountEntry.outpoint, utxo: accountEntry,
|
|
816
|
+
sequence: 0n, sigOpCount: 0, computeBudget },
|
|
817
|
+
{ previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
|
|
818
|
+
sequence: 0n, sigOpCount: 0, computeBudget: 30 },
|
|
819
|
+
],
|
|
820
|
+
outputs: [
|
|
821
|
+
new TransactionOutput(
|
|
822
|
+
plan.successorAmountSompi,
|
|
823
|
+
payToScriptHashScript(plan.successorRedeemScript),
|
|
824
|
+
new CovenantBinding(0, new Hash(accountId)),
|
|
825
|
+
),
|
|
826
|
+
new TransactionOutput(
|
|
827
|
+
fundingAmount - priorityFee,
|
|
828
|
+
payToAddressScript(walletAddress),
|
|
829
|
+
),
|
|
830
|
+
],
|
|
831
|
+
subnetworkId: SUBNETWORK_ID_NATIVE,
|
|
832
|
+
gas: 0n,
|
|
833
|
+
payload: "",
|
|
834
|
+
});
|
|
835
|
+
setVersionOneInputMassFields(unsignedTx, computeBudget);
|
|
836
|
+
unsignedTx.inputs[1].computeBudget = 30;
|
|
837
|
+
assertUniqueTransactionInputOutpoints(unsignedTx, "fee account binding");
|
|
838
|
+
const scripts = [{
|
|
839
|
+
inputIndex: 0,
|
|
840
|
+
scriptHex: bytesToHex(plan.currentRedeemScript),
|
|
841
|
+
signType: 1,
|
|
842
|
+
signatureScript: {
|
|
843
|
+
mode: "ordered-args",
|
|
844
|
+
args: [
|
|
845
|
+
dataTemplateArg(hexToBytes(marketId)),
|
|
846
|
+
dataTemplateArg(hexToBytes(policyId)),
|
|
847
|
+
{ type: "signature" },
|
|
848
|
+
i64TemplateArg(0),
|
|
849
|
+
dataTemplateArg(dispatchTagFor(artifact, "bindMarket")),
|
|
850
|
+
],
|
|
851
|
+
},
|
|
852
|
+
}];
|
|
853
|
+
const feeResult = transactionWithPredictedSignedFeeChange(
|
|
854
|
+
kw, unsignedTx, 1, network, "fee account binding", candidate => {
|
|
855
|
+
const serialized = candidate.serializeToSafeJSON();
|
|
856
|
+
if (typeof serialized !== "string") throw new Error("fee account mass preflight serialization failed");
|
|
857
|
+
const predictedJson = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
|
|
858
|
+
predictedJson.inputs[1].signatureScript = encodeP2pkSignatureScript(
|
|
859
|
+
bytesToHex(dummySignatureBytes()),
|
|
860
|
+
);
|
|
861
|
+
return typeof kw.Transaction.deserializeFromSafeJSON === "function"
|
|
862
|
+
? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predictedJson))
|
|
863
|
+
: kw.Transaction.deserializeFromJSON(JSON.stringify(predictedJson));
|
|
864
|
+
},
|
|
865
|
+
);
|
|
866
|
+
const nonContextualMass = calculateToccataV1NonContextualMass(
|
|
867
|
+
feeResult.predictedTransaction, "fee account binding",
|
|
868
|
+
).transactionMass;
|
|
869
|
+
const storageMass = calculateToccataStorageMass(
|
|
870
|
+
feeResult.predictedTransaction, "fee account binding",
|
|
871
|
+
);
|
|
872
|
+
if (nonContextualMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
|
|
873
|
+
storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
|
|
874
|
+
throw kcc20PsktBuilderError(
|
|
875
|
+
"fee account binding exceeds the signed transaction mass target",
|
|
876
|
+
"TRANSACTION_MASS_EXCEEDED",
|
|
877
|
+
{ nonContextualMass, storageMass, targetMass: CONSOLIDATION_TARGET_TRANSACTION_MASS },
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const transaction = feeResult.transaction;
|
|
881
|
+
const psktTransactionJson = transaction.serializeToSafeJSON();
|
|
882
|
+
if (typeof psktTransactionJson !== "string") throw new Error("fee account binding PSKT serialization failed");
|
|
883
|
+
return {
|
|
884
|
+
schema: OUTPUT_SCHEMA,
|
|
885
|
+
psktTransactionJson,
|
|
886
|
+
signInputs: [{ index: 1, sighashType: 1 }],
|
|
887
|
+
scripts,
|
|
888
|
+
submitTransactionSupported: true,
|
|
889
|
+
metadata: {
|
|
890
|
+
builderKey: "kcc20fee.bind-account",
|
|
891
|
+
operation: "bind-fee-account",
|
|
892
|
+
action: "bindMarket",
|
|
893
|
+
contract: "KCC20FeeEscrowV1",
|
|
894
|
+
network, walletAddress, tokenId, accountId, marketId, policyId,
|
|
895
|
+
accountInput: `${txid}:${vout}`,
|
|
896
|
+
accountPrincipalSompi: plan.successorAmountSompi.toString(),
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
} finally {
|
|
900
|
+
await rpc.disconnect();
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
609
904
|
async function buildFeeTicketRootUpdatePskt(input) {
|
|
610
905
|
const request = input.request || {};
|
|
611
906
|
const params = request.params || {};
|
|
@@ -13453,6 +13748,17 @@ export function createKcc20PsktBuilderEngine(
|
|
|
13453
13748
|
return address;
|
|
13454
13749
|
}
|
|
13455
13750
|
|
|
13751
|
+
function assertFeeAccountSchnorrWalletOwner(walletAddress, owner) {
|
|
13752
|
+
const script = String(kaspaWasm.payToAddressScript(walletAddress).script)
|
|
13753
|
+
.toLowerCase();
|
|
13754
|
+
const publicKey = /^20([0-9a-f]{64})ac$/.exec(script)?.[1];
|
|
13755
|
+
if (!publicKey || publicKey !== owner) {
|
|
13756
|
+
throw new Error(
|
|
13757
|
+
"fee account owner must match the connected Schnorr wallet address",
|
|
13758
|
+
);
|
|
13759
|
+
}
|
|
13760
|
+
}
|
|
13761
|
+
|
|
13456
13762
|
return {
|
|
13457
13763
|
build: buildInProcessPskt,
|
|
13458
13764
|
buildInProcessPskt,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { normalizeSilverScriptArtifact, templateParts } from "./abi.js";
|
|
2
|
+
import { KCC20_ESCROW_MAX_INT } from "./escrow-fee.js";
|
|
3
|
+
import type { Kcc20FeeAccountState } from "./escrow-claim-plan.js";
|
|
4
|
+
import {
|
|
5
|
+
buildKcc20FeeEscrowScript,
|
|
6
|
+
verifyKcc20UnboundFeeAccountForBinding,
|
|
7
|
+
type Kcc20IndexedFeeReceipt,
|
|
8
|
+
type Kcc20LiveFeeReceiptUtxo,
|
|
9
|
+
} from "./escrow-receipt.js";
|
|
10
|
+
|
|
11
|
+
const ZERO = "00".repeat(32);
|
|
12
|
+
const SECP256K1_FIELD = (1n << 256n) - (1n << 32n) - 977n;
|
|
13
|
+
|
|
14
|
+
function hex32(value: string, label: string): string {
|
|
15
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/i.test(value)) {
|
|
16
|
+
throw new Error(`${label} must be 32-byte hex`);
|
|
17
|
+
}
|
|
18
|
+
return value.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function principal(value: bigint): bigint {
|
|
22
|
+
if (typeof value !== "bigint" || value <= 0n || value > KCC20_ESCROW_MAX_INT) {
|
|
23
|
+
throw new Error("fee account principal must be a positive contract-range bigint");
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function modPow(base: bigint, exponent: bigint): bigint {
|
|
29
|
+
let result = 1n;
|
|
30
|
+
let factor = base % SECP256K1_FIELD;
|
|
31
|
+
for (let remaining = exponent; remaining > 0n; remaining >>= 1n) {
|
|
32
|
+
if (remaining & 1n) result = (result * factor) % SECP256K1_FIELD;
|
|
33
|
+
factor = (factor * factor) % SECP256K1_FIELD;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** BIP340 keys encode an x-coordinate that must lift to a secp256k1 point. */
|
|
39
|
+
function requireSchnorrXOnlyPublicKey(value: string): string {
|
|
40
|
+
const key = hex32(value, "owner Schnorr public key");
|
|
41
|
+
const x = BigInt(`0x${key}`);
|
|
42
|
+
if (x >= SECP256K1_FIELD) throw new Error("owner Schnorr public key is not a curve point");
|
|
43
|
+
const square = (x * x % SECP256K1_FIELD * x + 7n) % SECP256K1_FIELD;
|
|
44
|
+
const y = modPow(square, (SECP256K1_FIELD + 1n) / 4n);
|
|
45
|
+
if (y * y % SECP256K1_FIELD !== square) {
|
|
46
|
+
throw new Error("owner Schnorr public key is not a curve point");
|
|
47
|
+
}
|
|
48
|
+
return key;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Construct the initial account output. Its covenant lineage is assigned by
|
|
53
|
+
* the transaction; the market can bind that lineage only after it is known.
|
|
54
|
+
* This function does not select funding, sign, or estimate transaction mass.
|
|
55
|
+
*/
|
|
56
|
+
export function buildKcc20InitialFeeAccount(input: {
|
|
57
|
+
artifact: unknown;
|
|
58
|
+
tokenId: string;
|
|
59
|
+
ownerSchnorrPublicKey: string;
|
|
60
|
+
principalSompi: bigint;
|
|
61
|
+
}) {
|
|
62
|
+
const token = hex32(input.tokenId, "token");
|
|
63
|
+
const owner = requireSchnorrXOnlyPublicKey(input.ownerSchnorrPublicKey);
|
|
64
|
+
if (token === ZERO || owner === ZERO) throw new Error("fee account token and owner must be nonzero");
|
|
65
|
+
const amountSompi = principal(input.principalSompi);
|
|
66
|
+
const template = templateParts(normalizeSilverScriptArtifact(input.artifact));
|
|
67
|
+
const state: Kcc20FeeAccountState = {
|
|
68
|
+
role: "1", token, market: ZERO, policy: ZERO,
|
|
69
|
+
protocol_account: ZERO, creator_account: ZERO,
|
|
70
|
+
protocol_amount: "0", creator_amount: "0", owner,
|
|
71
|
+
owner_scheme: 0, guard_template: ZERO, guard_prefix: "0", guard_suffix: "0",
|
|
72
|
+
action_hash: ZERO,
|
|
73
|
+
self_template: toHex(template.hash),
|
|
74
|
+
self_prefix: String(template.prefixLength), self_suffix: String(template.suffixLength),
|
|
75
|
+
};
|
|
76
|
+
return { state, amountSompi, ...buildKcc20FeeEscrowScript(input.artifact, state as unknown as Record<string, unknown>) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toHex(bytes: Uint8Array): string {
|
|
80
|
+
return Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Verify the current unbound account output and plan the exact continuation.
|
|
85
|
+
* The owner signs bindMarket once; later claims use the account's authority.
|
|
86
|
+
*/
|
|
87
|
+
export function planKcc20FeeAccountBinding(input: {
|
|
88
|
+
artifact: unknown;
|
|
89
|
+
indexed: Kcc20IndexedFeeReceipt;
|
|
90
|
+
live: Kcc20LiveFeeReceiptUtxo;
|
|
91
|
+
tokenId: string;
|
|
92
|
+
accountId: string;
|
|
93
|
+
marketId: string;
|
|
94
|
+
policyId: string;
|
|
95
|
+
}) {
|
|
96
|
+
const market = hex32(input.marketId, "market");
|
|
97
|
+
const policy = hex32(input.policyId, "policy");
|
|
98
|
+
if (market === ZERO || policy === ZERO) throw new Error("fee account binding requires a market and policy");
|
|
99
|
+
const verified = verifyKcc20UnboundFeeAccountForBinding({
|
|
100
|
+
artifact: input.artifact,
|
|
101
|
+
indexed: input.indexed,
|
|
102
|
+
live: input.live,
|
|
103
|
+
expected: { tokenId: input.tokenId, accountId: input.accountId },
|
|
104
|
+
});
|
|
105
|
+
const successorState: Kcc20FeeAccountState = {
|
|
106
|
+
...verified.account.state,
|
|
107
|
+
market, policy,
|
|
108
|
+
};
|
|
109
|
+
const successor = buildKcc20FeeEscrowScript(
|
|
110
|
+
input.artifact,
|
|
111
|
+
successorState as unknown as Record<string, unknown>,
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
entrypoint: "bindMarket" as const,
|
|
115
|
+
account: verified.account,
|
|
116
|
+
currentRedeemScript: verified.redeemScript,
|
|
117
|
+
successorState,
|
|
118
|
+
successorRedeemScript: successor.redeemScript,
|
|
119
|
+
successorScriptPublicKey: successor.scriptPublicKey,
|
|
120
|
+
successorAmountSompi: verified.account.principalSompi,
|
|
121
|
+
requiresOwnerSchnorrSignature: true as const,
|
|
122
|
+
requiresSeparateNetworkFunding: true as const,
|
|
123
|
+
};
|
|
124
|
+
}
|
package/src/escrow-receipt.ts
CHANGED
|
@@ -87,6 +87,39 @@ function encodeState(state: Record<string, unknown>): Uint8Array {
|
|
|
87
87
|
return Uint8Array.from(encoded);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Render an escrow state using only the packaged, pinned v1 program. */
|
|
91
|
+
export function buildKcc20FeeEscrowScript(artifactInput: unknown, state: Record<string, unknown>) {
|
|
92
|
+
const artifactHash = Array.from(sha256(new TextEncoder().encode(JSON.stringify(artifactInput))),
|
|
93
|
+
byte => byte.toString(16).padStart(2, "0")).join("");
|
|
94
|
+
if (artifactHash !== ARTIFACT_CANONICAL_SHA256) throw new Error("fee escrow artifact metadata mismatch");
|
|
95
|
+
const artifact = normalizeSilverScriptArtifact(artifactInput);
|
|
96
|
+
assertKcc20ArtifactScriptHash(ARTIFACT_KEY, artifact);
|
|
97
|
+
if (artifact.contract_name !== "KCC20FeeEscrowV1" ||
|
|
98
|
+
artifact.state_layout?.start !== STATE_START || artifact.state_layout?.len !== STATE_LENGTH) {
|
|
99
|
+
throw new Error("unsupported fee escrow artifact layout");
|
|
100
|
+
}
|
|
101
|
+
const base = Uint8Array.from(artifact.script);
|
|
102
|
+
const prefix = base.slice(0, STATE_START);
|
|
103
|
+
const suffix = base.slice(STATE_START + STATE_LENGTH);
|
|
104
|
+
const hash = Array.from(templateHash(prefix, suffix), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
105
|
+
if (hex32(state.self_template, "self_template") !== hash ||
|
|
106
|
+
natural(state.self_prefix, "self_prefix") !== BigInt(prefix.length) ||
|
|
107
|
+
natural(state.self_suffix, "self_suffix") !== BigInt(suffix.length)) {
|
|
108
|
+
throw new Error("fee escrow self-template mismatch");
|
|
109
|
+
}
|
|
110
|
+
const redeemScript = new Uint8Array(prefix.length + STATE_LENGTH + suffix.length);
|
|
111
|
+
redeemScript.set(prefix);
|
|
112
|
+
redeemScript.set(encodeState(state), prefix.length);
|
|
113
|
+
redeemScript.set(suffix, prefix.length + STATE_LENGTH);
|
|
114
|
+
const scriptHash = Array.from(blake2b(redeemScript, { dkLen: 32 }), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
115
|
+
return {
|
|
116
|
+
redeemScript,
|
|
117
|
+
scriptHash,
|
|
118
|
+
scriptPublicKey: { version: 0 as const, scriptHex: `aa20${scriptHash}87` },
|
|
119
|
+
escrowTemplate: { hash, prefixLength: prefix.length, suffixLength: suffix.length },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
90
123
|
/**
|
|
91
124
|
* Check a receipt hint against the pinned program and a fresh node UTXO.
|
|
92
125
|
* The caller must fetch `live` from a qualified canonical node, authenticate
|
|
@@ -189,6 +222,30 @@ export function verifyKcc20FeeAccountForClaim(input: {
|
|
|
189
222
|
expected: { tokenId: string; marketId: string; policyId: string; accountId: string };
|
|
190
223
|
}): { account: Kcc20FeeClaimAccount; redeemScript: Uint8Array;
|
|
191
224
|
escrowTemplate: { hash: string; prefixLength: number; suffixLength: number } } {
|
|
225
|
+
return verifyFeeAccount(input, false);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Verify an unbound account before its one-time market binding. */
|
|
229
|
+
export function verifyKcc20UnboundFeeAccountForBinding(input: {
|
|
230
|
+
artifact: unknown;
|
|
231
|
+
indexed: Kcc20IndexedFeeReceipt;
|
|
232
|
+
live: Kcc20LiveFeeReceiptUtxo;
|
|
233
|
+
expected: { tokenId: string; accountId: string };
|
|
234
|
+
}): { account: Kcc20FeeClaimAccount; redeemScript: Uint8Array;
|
|
235
|
+
escrowTemplate: { hash: string; prefixLength: number; suffixLength: number } } {
|
|
236
|
+
return verifyFeeAccount({
|
|
237
|
+
...input,
|
|
238
|
+
expected: { ...input.expected, marketId: ZERO, policyId: ZERO },
|
|
239
|
+
}, true);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function verifyFeeAccount(input: {
|
|
243
|
+
artifact: unknown;
|
|
244
|
+
indexed: Kcc20IndexedFeeReceipt;
|
|
245
|
+
live: Kcc20LiveFeeReceiptUtxo;
|
|
246
|
+
expected: { tokenId: string; marketId: string; policyId: string; accountId: string };
|
|
247
|
+
}, requireUnbound: boolean): { account: Kcc20FeeClaimAccount; redeemScript: Uint8Array;
|
|
248
|
+
escrowTemplate: { hash: string; prefixLength: number; suffixLength: number } } {
|
|
192
249
|
const artifactHash = Array.from(sha256(new TextEncoder().encode(JSON.stringify(input.artifact))),
|
|
193
250
|
byte => byte.toString(16).padStart(2, "0")).join("");
|
|
194
251
|
if (artifactHash !== ARTIFACT_CANONICAL_SHA256) throw new Error("fee account artifact metadata mismatch");
|
|
@@ -207,7 +264,10 @@ export function verifyKcc20FeeAccountForClaim(input: {
|
|
|
207
264
|
const marketId = hex32(input.expected.marketId, "expected market");
|
|
208
265
|
const policyId = hex32(input.expected.policyId, "expected policy");
|
|
209
266
|
const accountId = hex32(input.expected.accountId, "expected account");
|
|
210
|
-
if (
|
|
267
|
+
if (tokenId === ZERO || accountId === ZERO ||
|
|
268
|
+
(requireUnbound ? (marketId !== ZERO || policyId !== ZERO) : (marketId === ZERO || policyId === ZERO))) {
|
|
269
|
+
throw new Error("invalid fee account identity");
|
|
270
|
+
}
|
|
211
271
|
for (const [name, expected] of [["token", tokenId], ["market", marketId], ["policy", policyId]] as const) {
|
|
212
272
|
if (hex32(state[name], name) !== expected) throw new Error(`fee account ${name} mismatch`);
|
|
213
273
|
}
|
|
@@ -231,6 +291,9 @@ export function verifyKcc20FeeAccountForClaim(input: {
|
|
|
231
291
|
throw new Error("invalid fee account guard state");
|
|
232
292
|
}
|
|
233
293
|
hex32(state.action_hash, "action_hash");
|
|
294
|
+
if (requireUnbound && (scheme !== 0 || state.action_hash !== ZERO)) {
|
|
295
|
+
throw new Error("unbound fee account must have its initial owner and action state");
|
|
296
|
+
}
|
|
234
297
|
const selfHash = Array.from(templateHash(prefix, suffix), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
235
298
|
if (hex32(state.self_template, "self_template") !== selfHash ||
|
|
236
299
|
natural(state.self_prefix, "self_prefix") !== BigInt(prefix.length) ||
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from "./escrow-policy.js";
|
|
|
23
23
|
export * from "./escrow-group.js";
|
|
24
24
|
export * from "./escrow-receipt.js";
|
|
25
25
|
export * from "./escrow-claim-plan.js";
|
|
26
|
+
export * from "./escrow-account-setup.js";
|
|
26
27
|
export * from "./trading-operation.js";
|
|
27
28
|
export * from "./protocol.js";
|
|
28
29
|
export * from "./runtime.js";
|
package/src/operations.ts
CHANGED
|
@@ -22,6 +22,8 @@ export const KCC20_BUILDER_KEYS = [
|
|
|
22
22
|
"fee-ticket.create-from-utility-burn",
|
|
23
23
|
"fee-ticket.burn",
|
|
24
24
|
"fee-ticket.transfer",
|
|
25
|
+
"kcc20fee.create-account",
|
|
26
|
+
"kcc20fee.bind-account",
|
|
25
27
|
"kcc20-snapshot.deploy-bootstrap",
|
|
26
28
|
"kcc20-snapshot.deploy-controller",
|
|
27
29
|
"kcc20-snapshot.start",
|