@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/RELEASING.md +16 -0
  3. package/artifacts/KCC20FeePolicyV1.manifest.json +4 -4
  4. package/artifacts/KCC20WrapperFeeArgentV1.placeholder.json +1816 -360
  5. package/artifacts/KCC20WrapperFeeV1.placeholder.json +1818 -362
  6. package/dist/artifacts.d.ts +5 -5
  7. package/dist/artifacts.js +5 -5
  8. package/dist/cjs/artifacts.d.ts +5 -5
  9. package/dist/cjs/artifacts.js +5 -5
  10. package/dist/cjs/engine.js +2770 -0
  11. package/dist/cjs/engine.js.map +1 -1
  12. package/dist/cjs/escrow-market-artifacts.d.ts +90 -0
  13. package/dist/cjs/escrow-market-artifacts.js +128 -0
  14. package/dist/cjs/escrow-market-artifacts.js.map +1 -0
  15. package/dist/cjs/escrow-market-setup.d.ts +429 -0
  16. package/dist/cjs/escrow-market-setup.js +140 -0
  17. package/dist/cjs/escrow-market-setup.js.map +1 -0
  18. package/dist/cjs/index.d.ts +2 -0
  19. package/dist/cjs/index.js +2 -0
  20. package/dist/cjs/index.js.map +1 -1
  21. package/dist/cjs/operations.d.ts +1 -1
  22. package/dist/cjs/operations.js +14 -0
  23. package/dist/cjs/operations.js.map +1 -1
  24. package/dist/cjs/sale-common.d.ts +1 -1
  25. package/dist/engine.d.ts.map +1 -1
  26. package/dist/engine.js +2770 -0
  27. package/dist/engine.js.map +1 -1
  28. package/dist/escrow-market-artifacts.d.ts +91 -0
  29. package/dist/escrow-market-artifacts.d.ts.map +1 -0
  30. package/dist/escrow-market-artifacts.js +124 -0
  31. package/dist/escrow-market-artifacts.js.map +1 -0
  32. package/dist/escrow-market-setup.d.ts +430 -0
  33. package/dist/escrow-market-setup.d.ts.map +1 -0
  34. package/dist/escrow-market-setup.js +133 -0
  35. package/dist/escrow-market-setup.js.map +1 -0
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/operations.d.ts +1 -1
  41. package/dist/operations.d.ts.map +1 -1
  42. package/dist/operations.js +14 -0
  43. package/dist/operations.js.map +1 -1
  44. package/dist/sale-common.d.ts +1 -1
  45. package/docs/COMPATIBILITY.json +1 -1
  46. package/package.json +3 -3
  47. package/src/artifacts.ts +5 -5
  48. package/src/engine.ts +2986 -0
  49. package/src/escrow-market-artifacts.ts +129 -0
  50. package/src/escrow-market-setup.ts +182 -0
  51. package/src/index.ts +2 -0
  52. package/src/operations.ts +14 -0
@@ -0,0 +1,129 @@
1
+ import { sha256 } from "@noble/hashes/sha256";
2
+ import { blake2b } from "@noble/hashes/blake2b";
3
+ import { normalizeSilverScriptArtifact, templateParts } from "./abi.js";
4
+ import { assertKcc20ArtifactScriptHash } from "./artifacts.js";
5
+ import { encodeKcc20EscrowPolicy, type Kcc20EscrowPolicy } from "./escrow-policy.js";
6
+
7
+ const PINS = {
8
+ receipt: ["KCC20FeeEscrowV1.placeholder.json", "KCC20FeeEscrowV1", "2ff79d6e8ca7176d6346ab7523c72b789f7238865cd9d3b056c83a8af1b5b2cd"],
9
+ root: ["KCC20OrderbookFeeRootV1.placeholder.json", "KCC20OrderbookFeeRootV1", "4f913975eebb0342272912573b9901f84551d9f421b80608c0b872a42479c011"],
10
+ holder: ["KCC20OrderbookFeeHolderV1.placeholder.json", "KCC20OrderbookFeeHolderV1", "78a3fe1ef7a4d7ae142ab78a3a71cef2c1a70ea638cd695e89ea891dd17eec67"],
11
+ ask: ["KCC20OrderbookFeeAskV1.placeholder.json", "KCC20OrderbookFeeAskV1", "b60465af84c75121820323aa83155b6ddcfaae4bff2c64b06740058f5ebc0f95"],
12
+ bid: ["KCC20OrderbookFeeBidV1.placeholder.json", "KCC20OrderbookFeeBidV1", "98d7c97319bbe1ca954c7f4f6a02467e90a7f3e84c14c91db650f8ef5887ee0e"],
13
+ ticket: ["KCC20FeeTicket.placeholder.json", "KCC20FeeTicket", "289f3f74914deb383d2cccc11128187a77e4bcea90e61c83e05d86c5c9d63bc5"],
14
+ wrapper: ["KCC20WrapperFeeV1.placeholder.json", "KCC20WrapperFeeV1", "84c3844493efd4bdf6b2e3435e1ac511f5b5a7f58b5b6c093c91e2edcae1a2e0"],
15
+ argentWrapper: ["KCC20WrapperFeeArgentV1.placeholder.json", "KCC20WrapperFeeArgentV1", "130f25462e070523821579d28809577fac6d010f8d5deabc66ad2de1c11d2c73"],
16
+ } as const;
17
+
18
+ export type Kcc20FeeArtifactRole = keyof typeof PINS;
19
+ export type Kcc20FeeArtifactSet = { [K in Kcc20FeeArtifactRole]: unknown };
20
+ const POLICY_ROLES = ["receipt", "root", "holder", "ask", "bid", "ticket"] as const;
21
+
22
+ function hex(bytes: Uint8Array): string {
23
+ return Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("");
24
+ }
25
+
26
+ function fixedBytes(value: unknown, width: number, field: string): Uint8Array {
27
+ if (typeof value !== "string" || !new RegExp(`^[0-9a-fA-F]{${width * 2}}$`).test(value)) {
28
+ throw new Error(`${field} must be ${width}-byte hex`);
29
+ }
30
+ return Uint8Array.from(value.match(/../g)!, part => Number.parseInt(part, 16));
31
+ }
32
+
33
+ /** Render a pinned versioned role with exact SilverScript runtime-state encoding. */
34
+ export function buildKcc20FeeMarketRoleScript(input: {
35
+ artifact: unknown;
36
+ role: "root" | "holder" | "ask" | "bid" | "wrapper" | "argentWrapper";
37
+ state: Record<string, unknown>;
38
+ }) {
39
+ const [key, name, metadataHash] = PINS[input.role];
40
+ const raw = input.artifact as {
41
+ contracts?: Record<string, { runtime_state?: { fields?: Array<{ name: string; type: { kind: string; len?: number } }> } }>;
42
+ };
43
+ if (!raw || typeof raw !== "object" ||
44
+ hex(sha256(new TextEncoder().encode(JSON.stringify(raw)))) !== metadataHash) {
45
+ throw new Error(`${input.role} fee-market artifact metadata mismatch`);
46
+ }
47
+ const artifact = normalizeSilverScriptArtifact(raw);
48
+ if (artifact.contract_name !== name) throw new Error(`${input.role} contract mismatch`);
49
+ assertKcc20ArtifactScriptHash(key, artifact);
50
+ const fields = Object.values(raw.contracts ?? {})[0]?.runtime_state?.fields;
51
+ if (!fields || Object.keys(input.state).length !== fields.length ||
52
+ fields.some(field => !Object.hasOwn(input.state, field.name))) {
53
+ throw new Error(`${input.role} runtime state fields mismatch`);
54
+ }
55
+ const encoded: number[] = [];
56
+ for (const field of fields) {
57
+ const value = input.state[field.name];
58
+ const { kind, len } = field.type;
59
+ let bytes: Uint8Array;
60
+ if (kind === "fixed_bytes" && Number.isInteger(len) && len! > 0) {
61
+ bytes = fixedBytes(value, len!, field.name);
62
+ } else if (kind === "bool" && typeof value === "boolean") {
63
+ bytes = Uint8Array.of(value ? 1 : 0);
64
+ } else if (kind === "byte" && Number.isInteger(value) && Number(value) >= 0 && Number(value) <= 255) {
65
+ bytes = Uint8Array.of(Number(value));
66
+ } else if (kind === "int" && typeof value === "string" && /^(0|[1-9]\d*)$/.test(value) &&
67
+ BigInt(value) <= (1n << 63n) - 1n) {
68
+ bytes = new Uint8Array(8);
69
+ new DataView(bytes.buffer).setBigUint64(0, BigInt(value), true);
70
+ } else {
71
+ throw new Error(`invalid ${input.role} state field ${field.name}`);
72
+ }
73
+ if (bytes.length <= 75) encoded.push(bytes.length, ...bytes);
74
+ else if (bytes.length <= 65535) encoded.push(77, bytes.length & 255, bytes.length >> 8, ...bytes);
75
+ else throw new Error(`${field.name} exceeds script push size`);
76
+ }
77
+ const layout = artifact.state_layout;
78
+ if (!layout || encoded.length !== layout.len) throw new Error(`${input.role} state width mismatch`);
79
+ const parts = templateParts(artifact);
80
+ const redeemScript = Uint8Array.from([...parts.prefix, ...encoded, ...parts.suffix]);
81
+ const scriptHash = hex(blake2b(redeemScript, { dkLen: 32 }));
82
+ return {
83
+ redeemScript,
84
+ scriptHash,
85
+ scriptPublicKey: { version: 0 as const, scriptHex: `aa20${scriptHash}87` },
86
+ template: { hash: hex(parts.hash), prefixLength: parts.prefixLength, suffixLength: parts.suffixLength },
87
+ };
88
+ }
89
+
90
+ /** Check the compiled programs and immutable policy before any fee-market PSKT is built. */
91
+ export function verifyKcc20FeeMarketArtifacts(input: {
92
+ policy: Kcc20EscrowPolicy;
93
+ artifacts: Kcc20FeeArtifactSet;
94
+ wrapperProfile: "native" | "argent";
95
+ }) {
96
+ if (encodeKcc20EscrowPolicy(input.policy).length !== 361) {
97
+ throw new Error("invalid fee-market policy width");
98
+ }
99
+ const verified = {} as Record<Kcc20FeeArtifactRole, ReturnType<typeof templateParts>>;
100
+ for (const role of [...POLICY_ROLES, input.wrapperProfile === "native" ? "wrapper" : "argentWrapper"] as Kcc20FeeArtifactRole[]) {
101
+ const artifact = input.artifacts[role];
102
+ const [key, name, metadataHash] = PINS[role];
103
+ if (!artifact || typeof artifact !== "object") throw new Error(`missing ${role} fee-market artifact`);
104
+ const actualMetadataHash = hex(sha256(new TextEncoder().encode(JSON.stringify(artifact))));
105
+ if (actualMetadataHash !== metadataHash) throw new Error(`${role} fee-market artifact metadata mismatch`);
106
+ const normalized = normalizeSilverScriptArtifact(artifact);
107
+ if (normalized.contract_name !== name) throw new Error(`${role} fee-market artifact contract mismatch`);
108
+ assertKcc20ArtifactScriptHash(key, normalized);
109
+ verified[role] = templateParts(normalized);
110
+ }
111
+ for (const role of POLICY_ROLES) {
112
+ const actual = verified[role];
113
+ const expected = input.policy[`${role}Template`];
114
+ if (hex(actual.hash) !== expected.hash.toLowerCase() ||
115
+ actual.prefixLength !== expected.prefixLength ||
116
+ actual.suffixLength !== expected.suffixLength) {
117
+ throw new Error(`${role} fee-market policy template mismatch`);
118
+ }
119
+ }
120
+ return {
121
+ wrapper: input.wrapperProfile === "native" ? verified.wrapper : verified.argentWrapper,
122
+ receipt: verified.receipt,
123
+ root: verified.root,
124
+ holder: verified.holder,
125
+ ask: verified.ask,
126
+ bid: verified.bid,
127
+ ticket: verified.ticket,
128
+ };
129
+ }
@@ -0,0 +1,182 @@
1
+ import { encodeKcc20EscrowPolicy, kcc20EscrowPolicyId, type Kcc20EscrowPolicy } from "./escrow-policy.js";
2
+ import {
3
+ buildKcc20FeeMarketRoleScript,
4
+ verifyKcc20FeeMarketArtifacts,
5
+ type Kcc20FeeArtifactSet,
6
+ } from "./escrow-market-artifacts.js";
7
+
8
+ const ZERO = "00".repeat(32);
9
+ const MAX = (1n << 63n) - 1n;
10
+
11
+ function hex32(value: unknown, label: string, allowZero = false): string {
12
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/i.test(value) ||
13
+ (!allowZero && value.toLowerCase() === ZERO)) {
14
+ throw new Error(`${label} must be a ${allowZero ? "valid" : "nonzero"} 32-byte hex value`);
15
+ }
16
+ return value.toLowerCase();
17
+ }
18
+
19
+ export interface Kcc20FeeMarketGenesisInput {
20
+ tokenId: string;
21
+ feeTicketId: string;
22
+ priceScale: bigint;
23
+ policy: Kcc20EscrowPolicy;
24
+ wrapperProfile: "native" | "argent";
25
+ artifacts: Kcc20FeeArtifactSet;
26
+ }
27
+
28
+ /** Produce the only genesis role: an unbound wrapper with no orderbook root. */
29
+ export function prepareKcc20FeeMarketGenesis(input: Kcc20FeeMarketGenesisInput) {
30
+ const tokenId = hex32(input.tokenId, "token ID");
31
+ const feeTicketId = hex32(input.feeTicketId, "FeeTicket ID", true);
32
+ if (typeof input.priceScale !== "bigint" || input.priceScale <= 0n || input.priceScale > MAX) {
33
+ throw new Error("market price scale is outside the contract range");
34
+ }
35
+ if (input.wrapperProfile !== "native" && input.wrapperProfile !== "argent") {
36
+ throw new Error("unsupported fee-market wrapper profile");
37
+ }
38
+ if (input.wrapperProfile === "argent" && input.policy.creatorFeeBps !== 0) {
39
+ throw new Error("Argent token profile cannot authorize creator fees");
40
+ }
41
+ verifyKcc20FeeMarketArtifacts(input);
42
+ const feePolicy = Array.from(encodeKcc20EscrowPolicy(input.policy),
43
+ byte => byte.toString(16).padStart(2, "0")).join("");
44
+ const policyId = kcc20EscrowPolicyId({ tokenId, feeTicketId, priceScale: input.priceScale, policy: input.policy });
45
+ const state = {
46
+ canonicalTokenId: tokenId, enabled: false, priceScale: input.priceScale.toString(),
47
+ feeTicketId, feePolicy, authenticatedMarketId: ZERO,
48
+ };
49
+ const role = input.wrapperProfile === "native" ? "wrapper" : "argentWrapper";
50
+ const wrapper = buildKcc20FeeMarketRoleScript({ artifact: input.artifacts[role], role, state });
51
+ return { tokenId, feeTicketId, policyId, state, wrapper };
52
+ }
53
+
54
+ /** Render both bound outputs after the accepted genesis has fixed market ID. */
55
+ export function prepareKcc20FeeMarketBinding(input: Kcc20FeeMarketGenesisInput & {
56
+ marketId: string;
57
+ rootOwner: string;
58
+ rootOwnerScheme: number;
59
+ rootPayoutScriptHash: string;
60
+ }) {
61
+ const genesis = prepareKcc20FeeMarketGenesis(input);
62
+ const marketId = hex32(input.marketId, "market ID");
63
+ const rootOwner = hex32(input.rootOwner, "root owner");
64
+ const rootPayoutScriptHash = hex32(input.rootPayoutScriptHash, "root payout script hash");
65
+ if (!Number.isInteger(input.rootOwnerScheme) || input.rootOwnerScheme < 0 || input.rootOwnerScheme > 4) {
66
+ throw new Error("unsupported root owner scheme");
67
+ }
68
+ const role = input.wrapperProfile === "native" ? "wrapper" : "argentWrapper";
69
+ const wrapperState = { ...genesis.state, enabled: true, authenticatedMarketId: marketId };
70
+ const wrapper = buildKcc20FeeMarketRoleScript({ artifact: input.artifacts[role], role, state: wrapperState });
71
+ const rootState = {
72
+ canonicalTokenId: genesis.tokenId, owner: rootOwner, owner_scheme: input.rootOwnerScheme,
73
+ amount: "0", mode: "1", unitPrice: "0", feeTicketId: genesis.feeTicketId,
74
+ priceScale: input.priceScale.toString(), feePolicy: genesis.state.feePolicy,
75
+ payout_script_hash: rootPayoutScriptHash, wrapper_spk_hash: wrapper.scriptHash,
76
+ };
77
+ const root = buildKcc20FeeMarketRoleScript({ artifact: input.artifacts.root, role: "root", state: rootState });
78
+ return { genesis, marketId, wrapperState, wrapper, rootState, root };
79
+ }
80
+
81
+ /** Render a wrapped holder against the exact enabled wrapper for this market. */
82
+ export function prepareKcc20FeeMarketWrappedHolder(input: Kcc20FeeMarketGenesisInput & {
83
+ marketId: string;
84
+ recipientOwner: string;
85
+ recipientOwnerScheme: number;
86
+ recipientPayoutScriptHash: string;
87
+ tokenAmount: bigint;
88
+ }) {
89
+ const genesis = prepareKcc20FeeMarketGenesis(input);
90
+ const marketId = hex32(input.marketId, "market ID");
91
+ const recipientOwner = hex32(input.recipientOwner, "wrapped recipient");
92
+ const payoutScriptHash = hex32(input.recipientPayoutScriptHash, "wrapped payout script hash");
93
+ if (!Number.isInteger(input.recipientOwnerScheme) ||
94
+ input.recipientOwnerScheme < 0 || input.recipientOwnerScheme > 4) {
95
+ throw new Error("unsupported wrapped recipient scheme");
96
+ }
97
+ if (typeof input.tokenAmount !== "bigint" || input.tokenAmount <= 0n || input.tokenAmount > MAX) {
98
+ throw new Error("wrapped token amount is outside the contract range");
99
+ }
100
+ const wrapperRole = input.wrapperProfile === "native" ? "wrapper" : "argentWrapper";
101
+ const wrapperState = { ...genesis.state, enabled: true, authenticatedMarketId: marketId };
102
+ const wrapper = buildKcc20FeeMarketRoleScript({
103
+ artifact: input.artifacts[wrapperRole], role: wrapperRole, state: wrapperState,
104
+ });
105
+ const holderState = {
106
+ canonicalTokenId: genesis.tokenId,
107
+ owner: recipientOwner,
108
+ owner_scheme: input.recipientOwnerScheme,
109
+ amount: input.tokenAmount.toString(),
110
+ mode: "0",
111
+ unitPrice: "0",
112
+ feeTicketId: genesis.feeTicketId,
113
+ priceScale: input.priceScale.toString(),
114
+ feePolicy: genesis.state.feePolicy,
115
+ payout_script_hash: payoutScriptHash,
116
+ wrapper_spk_hash: wrapper.scriptHash,
117
+ };
118
+ const holder = buildKcc20FeeMarketRoleScript({
119
+ artifact: input.artifacts.holder, role: "holder", state: holderState,
120
+ });
121
+ return { marketId, wrapperState, wrapper, holderState, holder };
122
+ }
123
+
124
+ /** Render an ask and any holder continuation against a pinned market policy. */
125
+ export function prepareKcc20FeeMarketAsk(input: Kcc20FeeMarketGenesisInput & {
126
+ marketId: string;
127
+ owner: string;
128
+ ownerScheme: number;
129
+ payoutScriptHash: string;
130
+ holderAmount: bigint;
131
+ askAmount: bigint;
132
+ askUnitPrice: bigint;
133
+ }) {
134
+ if (input.askAmount <= 0n || input.askAmount > input.holderAmount ||
135
+ input.askUnitPrice <= 0n || input.askUnitPrice > MAX) {
136
+ throw new Error("fee-market ask amount or price is invalid");
137
+ }
138
+ const holder = prepareKcc20FeeMarketWrappedHolder({ ...input,
139
+ recipientOwner: input.owner, recipientOwnerScheme: input.ownerScheme,
140
+ recipientPayoutScriptHash: input.payoutScriptHash,
141
+ tokenAmount: input.holderAmount,
142
+ });
143
+ const askState = { ...holder.holderState, amount: input.askAmount.toString(),
144
+ mode: "2", unitPrice: input.askUnitPrice.toString() };
145
+ const ask = buildKcc20FeeMarketRoleScript({ artifact: input.artifacts.ask,
146
+ role: "ask", state: askState });
147
+ const remaining = input.holderAmount - input.askAmount;
148
+ const change = remaining > 0n ? prepareKcc20FeeMarketWrappedHolder({ ...input,
149
+ recipientOwner: input.owner, recipientOwnerScheme: input.ownerScheme,
150
+ recipientPayoutScriptHash: input.payoutScriptHash, tokenAmount: remaining,
151
+ }) : null;
152
+ return { holder, askState, ask, remaining, change };
153
+ }
154
+
155
+ /** Render a bid while preserving the single market root unchanged. */
156
+ export function prepareKcc20FeeMarketBid(input: Kcc20FeeMarketGenesisInput & {
157
+ marketId: string;
158
+ rootOwner: string;
159
+ rootOwnerScheme: number;
160
+ rootPayoutScriptHash: string;
161
+ buyer: string;
162
+ buyerScheme: number;
163
+ buyerPayoutScriptHash: string;
164
+ bidAmount: bigint;
165
+ bidUnitPrice: bigint;
166
+ }) {
167
+ if (input.bidAmount <= 0n || input.bidAmount > MAX ||
168
+ input.bidUnitPrice <= 0n || input.bidUnitPrice > MAX ||
169
+ !Number.isInteger(input.buyerScheme) || input.buyerScheme < 0 ||
170
+ input.buyerScheme > 4) {
171
+ throw new Error("fee-market bid amount, price or buyer scheme is invalid");
172
+ }
173
+ const binding = prepareKcc20FeeMarketBinding(input);
174
+ const bidState = { ...binding.rootState,
175
+ owner: hex32(input.buyer, "bid buyer"), owner_scheme: input.buyerScheme,
176
+ amount: input.bidAmount.toString(), mode: "3",
177
+ unitPrice: input.bidUnitPrice.toString(),
178
+ payout_script_hash: hex32(input.buyerPayoutScriptHash, "bid payout hash") };
179
+ const bid = buildKcc20FeeMarketRoleScript({ artifact: input.artifacts.bid,
180
+ role: "bid", state: bidState });
181
+ return { binding, bidState, bid };
182
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ export * from "./token-operation.js";
20
20
  export * from "./trade-fee.js";
21
21
  export * from "./escrow-fee.js";
22
22
  export * from "./escrow-policy.js";
23
+ export * from "./escrow-market-artifacts.js";
24
+ export * from "./escrow-market-setup.js";
23
25
  export * from "./escrow-group.js";
24
26
  export * from "./escrow-receipt.js";
25
27
  export * from "./escrow-claim-plan.js";
package/src/operations.ts CHANGED
@@ -24,6 +24,20 @@ export const KCC20_BUILDER_KEYS = [
24
24
  "fee-ticket.transfer",
25
25
  "kcc20fee.create-account",
26
26
  "kcc20fee.bind-account",
27
+ "kcc20fee.deploy-market-genesis",
28
+ "kcc20fee.bind-market",
29
+ "kcc20fee.wrap",
30
+ "kcc20fee.unwrap",
31
+ "kcc20fee.merge-holders",
32
+ "kcc20fee.create-ask",
33
+ "kcc20fee.cancel-ask",
34
+ "kcc20fee.create-bid",
35
+ "kcc20fee.cancel-bid",
36
+ "kcc20fee.buy-ask",
37
+ "kcc20fee.buy-two-asks",
38
+ "kcc20fee.sell-into-bids",
39
+ "kcc20fee.claim",
40
+ "kcc20fee.rotate-account",
27
41
  "kcc20-snapshot.deploy-bootstrap",
28
42
  "kcc20-snapshot.deploy-controller",
29
43
  "kcc20-snapshot.start",