@kaspacom/kcc20-tx-builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/artifacts/KCC20.placeholder.json +77479 -0
  4. package/artifacts/KCC20FeeTicket.placeholder.json +16846 -0
  5. package/artifacts/KCC20Orderbook.placeholder.json +78001 -0
  6. package/artifacts/KCC20Vesting.placeholder.json +4298 -0
  7. package/artifacts/KCC20Wrapper.placeholder.json +12246 -0
  8. package/dist/abi.d.ts +44 -0
  9. package/dist/abi.d.ts.map +1 -0
  10. package/dist/abi.js +336 -0
  11. package/dist/abi.js.map +1 -0
  12. package/dist/action-source-resolver.d.ts +80 -0
  13. package/dist/action-source-resolver.d.ts.map +1 -0
  14. package/dist/action-source-resolver.js +412 -0
  15. package/dist/action-source-resolver.js.map +1 -0
  16. package/dist/artifacts.d.ts +14 -0
  17. package/dist/artifacts.d.ts.map +1 -0
  18. package/dist/artifacts.js +44 -0
  19. package/dist/artifacts.js.map +1 -0
  20. package/dist/builder.d.ts +12 -0
  21. package/dist/builder.d.ts.map +1 -0
  22. package/dist/builder.js +18 -0
  23. package/dist/builder.js.map +1 -0
  24. package/dist/cjs/abi.d.ts +43 -0
  25. package/dist/cjs/abi.js +361 -0
  26. package/dist/cjs/abi.js.map +1 -0
  27. package/dist/cjs/action-source-resolver.d.ts +79 -0
  28. package/dist/cjs/action-source-resolver.js +435 -0
  29. package/dist/cjs/action-source-resolver.js.map +1 -0
  30. package/dist/cjs/artifacts.d.ts +13 -0
  31. package/dist/cjs/artifacts.js +51 -0
  32. package/dist/cjs/artifacts.js.map +1 -0
  33. package/dist/cjs/builder.d.ts +11 -0
  34. package/dist/cjs/builder.js +22 -0
  35. package/dist/cjs/builder.js.map +1 -0
  36. package/dist/cjs/deploy-operation.d.ts +103 -0
  37. package/dist/cjs/deploy-operation.js +335 -0
  38. package/dist/cjs/deploy-operation.js.map +1 -0
  39. package/dist/cjs/encoding.d.ts +5 -0
  40. package/dist/cjs/encoding.js +51 -0
  41. package/dist/cjs/encoding.js.map +1 -0
  42. package/dist/cjs/engine.d.ts +40 -0
  43. package/dist/cjs/engine.js +8972 -0
  44. package/dist/cjs/engine.js.map +1 -0
  45. package/dist/cjs/errors.d.ts +7 -0
  46. package/dist/cjs/errors.js +23 -0
  47. package/dist/cjs/errors.js.map +1 -0
  48. package/dist/cjs/fee-ticket-operation.d.ts +204 -0
  49. package/dist/cjs/fee-ticket-operation.js +327 -0
  50. package/dist/cjs/fee-ticket-operation.js.map +1 -0
  51. package/dist/cjs/index.d.ts +27 -0
  52. package/dist/cjs/index.js +45 -0
  53. package/dist/cjs/index.js.map +1 -0
  54. package/dist/cjs/ladder-limit-orders.d.ts +41 -0
  55. package/dist/cjs/ladder-limit-orders.js +129 -0
  56. package/dist/cjs/ladder-limit-orders.js.map +1 -0
  57. package/dist/cjs/mint-availability-operation.d.ts +56 -0
  58. package/dist/cjs/mint-availability-operation.js +95 -0
  59. package/dist/cjs/mint-availability-operation.js.map +1 -0
  60. package/dist/cjs/models.d.ts +90 -0
  61. package/dist/cjs/models.js +6 -0
  62. package/dist/cjs/models.js.map +1 -0
  63. package/dist/cjs/operations.d.ts +6 -0
  64. package/dist/cjs/operations.js +41 -0
  65. package/dist/cjs/operations.js.map +1 -0
  66. package/dist/cjs/package.json +1 -0
  67. package/dist/cjs/paid-mint-amount.d.ts +69 -0
  68. package/dist/cjs/paid-mint-amount.js +281 -0
  69. package/dist/cjs/paid-mint-amount.js.map +1 -0
  70. package/dist/cjs/protocol.d.ts +21 -0
  71. package/dist/cjs/protocol.js +25 -0
  72. package/dist/cjs/protocol.js.map +1 -0
  73. package/dist/cjs/runtime.d.ts +27 -0
  74. package/dist/cjs/runtime.js +3 -0
  75. package/dist/cjs/runtime.js.map +1 -0
  76. package/dist/cjs/sale-common.d.ts +287 -0
  77. package/dist/cjs/sale-common.js +928 -0
  78. package/dist/cjs/sale-common.js.map +1 -0
  79. package/dist/cjs/source-provider.d.ts +11 -0
  80. package/dist/cjs/source-provider.js +3 -0
  81. package/dist/cjs/source-provider.js.map +1 -0
  82. package/dist/cjs/source-validation.d.ts +9 -0
  83. package/dist/cjs/source-validation.js +33 -0
  84. package/dist/cjs/source-validation.js.map +1 -0
  85. package/dist/cjs/token-amount.d.ts +31 -0
  86. package/dist/cjs/token-amount.js +252 -0
  87. package/dist/cjs/token-amount.js.map +1 -0
  88. package/dist/cjs/token-operation.d.ts +135 -0
  89. package/dist/cjs/token-operation.js +236 -0
  90. package/dist/cjs/token-operation.js.map +1 -0
  91. package/dist/cjs/trade-fee.d.ts +60 -0
  92. package/dist/cjs/trade-fee.js +185 -0
  93. package/dist/cjs/trade-fee.js.map +1 -0
  94. package/dist/cjs/trading-operation.d.ts +212 -0
  95. package/dist/cjs/trading-operation.js +587 -0
  96. package/dist/cjs/trading-operation.js.map +1 -0
  97. package/dist/cjs/verify-operation.d.ts +79 -0
  98. package/dist/cjs/verify-operation.js +246 -0
  99. package/dist/cjs/verify-operation.js.map +1 -0
  100. package/dist/cjs/wallet-operation.d.ts +52 -0
  101. package/dist/cjs/wallet-operation.js +58 -0
  102. package/dist/cjs/wallet-operation.js.map +1 -0
  103. package/dist/cjs/wasm-provider.d.ts +15 -0
  104. package/dist/cjs/wasm-provider.js +20 -0
  105. package/dist/cjs/wasm-provider.js.map +1 -0
  106. package/dist/cjs/wrapper-operation.d.ts +196 -0
  107. package/dist/cjs/wrapper-operation.js +317 -0
  108. package/dist/cjs/wrapper-operation.js.map +1 -0
  109. package/dist/deploy-operation.d.ts +104 -0
  110. package/dist/deploy-operation.d.ts.map +1 -0
  111. package/dist/deploy-operation.js +327 -0
  112. package/dist/deploy-operation.js.map +1 -0
  113. package/dist/encoding.d.ts +6 -0
  114. package/dist/encoding.d.ts.map +1 -0
  115. package/dist/encoding.js +44 -0
  116. package/dist/encoding.js.map +1 -0
  117. package/dist/engine.d.ts +41 -0
  118. package/dist/engine.d.ts.map +1 -0
  119. package/dist/engine.js +8969 -0
  120. package/dist/engine.js.map +1 -0
  121. package/dist/errors.d.ts +8 -0
  122. package/dist/errors.d.ts.map +1 -0
  123. package/dist/errors.js +18 -0
  124. package/dist/errors.js.map +1 -0
  125. package/dist/fee-ticket-operation.d.ts +205 -0
  126. package/dist/fee-ticket-operation.d.ts.map +1 -0
  127. package/dist/fee-ticket-operation.js +320 -0
  128. package/dist/fee-ticket-operation.js.map +1 -0
  129. package/dist/index.d.ts +28 -0
  130. package/dist/index.d.ts.map +1 -0
  131. package/dist/index.js +28 -0
  132. package/dist/index.js.map +1 -0
  133. package/dist/ladder-limit-orders.d.ts +42 -0
  134. package/dist/ladder-limit-orders.d.ts.map +1 -0
  135. package/dist/ladder-limit-orders.js +121 -0
  136. package/dist/ladder-limit-orders.js.map +1 -0
  137. package/dist/mint-availability-operation.d.ts +57 -0
  138. package/dist/mint-availability-operation.d.ts.map +1 -0
  139. package/dist/mint-availability-operation.js +91 -0
  140. package/dist/mint-availability-operation.js.map +1 -0
  141. package/dist/models.d.ts +91 -0
  142. package/dist/models.d.ts.map +1 -0
  143. package/dist/models.js +3 -0
  144. package/dist/models.js.map +1 -0
  145. package/dist/operations.d.ts +7 -0
  146. package/dist/operations.d.ts.map +1 -0
  147. package/dist/operations.js +36 -0
  148. package/dist/operations.js.map +1 -0
  149. package/dist/paid-mint-amount.d.ts +70 -0
  150. package/dist/paid-mint-amount.d.ts.map +1 -0
  151. package/dist/paid-mint-amount.js +268 -0
  152. package/dist/paid-mint-amount.js.map +1 -0
  153. package/dist/protocol.d.ts +22 -0
  154. package/dist/protocol.d.ts.map +1 -0
  155. package/dist/protocol.js +22 -0
  156. package/dist/protocol.js.map +1 -0
  157. package/dist/runtime.d.ts +28 -0
  158. package/dist/runtime.d.ts.map +1 -0
  159. package/dist/runtime.js +2 -0
  160. package/dist/runtime.js.map +1 -0
  161. package/dist/sale-common.d.ts +288 -0
  162. package/dist/sale-common.d.ts.map +1 -0
  163. package/dist/sale-common.js +844 -0
  164. package/dist/sale-common.js.map +1 -0
  165. package/dist/source-provider.d.ts +12 -0
  166. package/dist/source-provider.d.ts.map +1 -0
  167. package/dist/source-provider.js +2 -0
  168. package/dist/source-provider.js.map +1 -0
  169. package/dist/source-validation.d.ts +10 -0
  170. package/dist/source-validation.d.ts.map +1 -0
  171. package/dist/source-validation.js +28 -0
  172. package/dist/source-validation.js.map +1 -0
  173. package/dist/token-amount.d.ts +32 -0
  174. package/dist/token-amount.d.ts.map +1 -0
  175. package/dist/token-amount.js +237 -0
  176. package/dist/token-amount.js.map +1 -0
  177. package/dist/token-operation.d.ts +136 -0
  178. package/dist/token-operation.d.ts.map +1 -0
  179. package/dist/token-operation.js +231 -0
  180. package/dist/token-operation.js.map +1 -0
  181. package/dist/trade-fee.d.ts +61 -0
  182. package/dist/trade-fee.d.ts.map +1 -0
  183. package/dist/trade-fee.js +173 -0
  184. package/dist/trade-fee.js.map +1 -0
  185. package/dist/trading-operation.d.ts +213 -0
  186. package/dist/trading-operation.d.ts.map +1 -0
  187. package/dist/trading-operation.js +580 -0
  188. package/dist/trading-operation.js.map +1 -0
  189. package/dist/verify-operation.d.ts +80 -0
  190. package/dist/verify-operation.d.ts.map +1 -0
  191. package/dist/verify-operation.js +243 -0
  192. package/dist/verify-operation.js.map +1 -0
  193. package/dist/wallet-operation.d.ts +53 -0
  194. package/dist/wallet-operation.d.ts.map +1 -0
  195. package/dist/wallet-operation.js +55 -0
  196. package/dist/wallet-operation.js.map +1 -0
  197. package/dist/wasm-provider.d.ts +16 -0
  198. package/dist/wasm-provider.d.ts.map +1 -0
  199. package/dist/wasm-provider.js +17 -0
  200. package/dist/wasm-provider.js.map +1 -0
  201. package/dist/wrapper-operation.d.ts +197 -0
  202. package/dist/wrapper-operation.d.ts.map +1 -0
  203. package/dist/wrapper-operation.js +311 -0
  204. package/dist/wrapper-operation.js.map +1 -0
  205. package/index.d.ts +1 -0
  206. package/package.json +84 -0
  207. package/src/abi.ts +493 -0
  208. package/src/action-source-resolver.ts +660 -0
  209. package/src/artifacts.ts +87 -0
  210. package/src/builder.ts +37 -0
  211. package/src/deploy-operation.ts +458 -0
  212. package/src/encoding.ts +58 -0
  213. package/src/engine.ts +13226 -0
  214. package/src/errors.ts +35 -0
  215. package/src/fee-ticket-operation.ts +518 -0
  216. package/src/index.ts +27 -0
  217. package/src/ladder-limit-orders.ts +184 -0
  218. package/src/mint-availability-operation.ts +146 -0
  219. package/src/models.ts +103 -0
  220. package/src/operations.ts +46 -0
  221. package/src/paid-mint-amount.ts +402 -0
  222. package/src/protocol.ts +28 -0
  223. package/src/runtime.ts +29 -0
  224. package/src/sale-common.ts +1238 -0
  225. package/src/source-provider.ts +12 -0
  226. package/src/source-validation.ts +47 -0
  227. package/src/token-amount.ts +302 -0
  228. package/src/token-operation.ts +384 -0
  229. package/src/trade-fee.ts +264 -0
  230. package/src/trading-operation.ts +842 -0
  231. package/src/verify-operation.ts +400 -0
  232. package/src/wallet-operation.ts +86 -0
  233. package/src/wasm-provider.ts +32 -0
  234. package/src/wrapper-operation.ts +531 -0
@@ -0,0 +1,842 @@
1
+ import {
2
+ ownerWrappedHolderUtxos,
3
+ selectConsolidationBatch,
4
+ selectOwnerFeeTicketUtxos,
5
+ selectOwnerWrappedHolderUtxo,
6
+ selectWrappedOrderUtxo,
7
+ selectWrappedRootUtxo,
8
+ summarizeHolderUtxos,
9
+ type Kcc20IndexedCovenantUtxo,
10
+ type Kcc20WrapperSourceInfo,
11
+ } from "./action-source-resolver.js";
12
+ import {
13
+ buildCreateOrderFeeQuote,
14
+ buildStandaloneFillFeeQuote,
15
+ } from "./trade-fee.js";
16
+ import {
17
+ kcc20DisplayScaleForDecimals,
18
+ normalizeKcc20Decimals,
19
+ parseKcc20DisplayAmountToBaseUnits,
20
+ } from "./token-amount.js";
21
+ import { KCC20_ORDERBOOK_MAX_EXPANDED_SWEEP_BID_LEGS } from "./sale-common.js";
22
+ import { buildKcc20WalletOperationFromPlan } from "./wallet-operation.js";
23
+
24
+ export interface Kcc20TradingWalletInfo {
25
+ walletAddress: string;
26
+ kcc20Owner: string;
27
+ }
28
+
29
+ export interface Kcc20TradingOperationContext {
30
+ canonicalCovenantId: string;
31
+ tokenDecimals?: number | null;
32
+ wrapper: Kcc20WrapperSourceInfo;
33
+ wrappedTokenUtxos: readonly Kcc20IndexedCovenantUtxo[];
34
+ feeTicketUtxos?: readonly Kcc20IndexedCovenantUtxo[];
35
+ }
36
+
37
+ export interface Kcc20TradingOperationOptions {
38
+ network?: string;
39
+ createdAt?: string;
40
+ requestId?: string;
41
+ }
42
+
43
+ export interface Kcc20OrderDraft {
44
+ wrappedMarketId: string;
45
+ side: "buy" | "sell";
46
+ tokenAmount: string;
47
+ unitPriceSompi: string;
48
+ feeTicketId?: string;
49
+ }
50
+
51
+ export function buildKcc20OrderOperation(
52
+ wallet: Kcc20TradingWalletInfo,
53
+ context: Kcc20TradingOperationContext,
54
+ draft: Kcc20OrderDraft,
55
+ options: Kcc20TradingOperationOptions = {},
56
+ ) {
57
+ requireTradableWrapper(context.wrapper, draft.wrappedMarketId);
58
+ const canonicalId = contractCanonicalId(context);
59
+ const marketId = requireHex32(context.wrapper.marketId, "wrappedMarketId");
60
+ const amount = displayAmount(draft.tokenAmount, context.tokenDecimals);
61
+ const priceScale = priceScaleFor(context.tokenDecimals);
62
+ const feeTicketId = validatedWrapperFeeTicket(
63
+ context.wrapper,
64
+ draft.feeTicketId,
65
+ );
66
+ const root =
67
+ draft.side === "buy"
68
+ ? selectWrappedRootUtxo(context.wrappedTokenUtxos, canonicalId)
69
+ : null;
70
+ const holders =
71
+ draft.side === "sell"
72
+ ? ownerWrappedHolderUtxos(
73
+ context.wrappedTokenUtxos,
74
+ wallet.kcc20Owner,
75
+ canonicalId,
76
+ )
77
+ : [];
78
+ const holder =
79
+ draft.side === "sell"
80
+ ? selectOwnerWrappedHolderUtxo(
81
+ context.wrappedTokenUtxos,
82
+ wallet.kcc20Owner,
83
+ amount,
84
+ canonicalId,
85
+ true,
86
+ )
87
+ : null;
88
+ const summary =
89
+ draft.side === "sell" ? summarizeHolderUtxos(holders, amount) : null;
90
+ if (draft.side === "buy" && !root)
91
+ throw new Error("Wrapped market root UTXO is not available");
92
+ if (draft.side === "sell" && !holder) {
93
+ throw new Error(
94
+ summary?.fragmented
95
+ ? "No single wrapped holder UTXO is large enough; consolidate the wrapped balance first"
96
+ : "Sell order requires a wrapped holder UTXO owned by this wallet",
97
+ );
98
+ }
99
+ const buying = draft.side === "buy";
100
+ return buildKcc20WalletOperationFromPlan(
101
+ wallet,
102
+ {
103
+ builderKey: buying
104
+ ? "kcc20orderbook.create-bid"
105
+ : "kcc20orderbook.create-ask",
106
+ operation: buying ? "create-buy-order" : "create-sell-order",
107
+ contract: "KCC20Orderbook",
108
+ action: buying ? "createBid" : "createAsk",
109
+ description: `Build a KCC20 DEX Orderbook V1 ${draft.side} order request.`,
110
+ params: {
111
+ canonicalCovenantId: canonicalId,
112
+ wrappedMarketId: marketId,
113
+ side: draft.side,
114
+ tokenAmount: amount,
115
+ unitPriceSompi: requireUnsigned(draft.unitPriceSompi, "unitPriceSompi"),
116
+ feeTicketId,
117
+ feeTicketSupported: Boolean(feeTicketId),
118
+ feeQuote: buildCreateOrderFeeQuote({
119
+ side: draft.side,
120
+ tokenAmount: amount,
121
+ unitPriceSompi: draft.unitPriceSompi,
122
+ priceScale,
123
+ temporaryKasDepositSompi: buying ? "200000000" : "0",
124
+ }),
125
+ ...(context.wrapper.activeCovenantId
126
+ ? { activeWrapperCovenantId: context.wrapper.activeCovenantId }
127
+ : {}),
128
+ activeWrappedRootUtxo: root,
129
+ activeWrappedHolderUtxo: holder,
130
+ },
131
+ sourceRequirements: buying
132
+ ? [
133
+ {
134
+ type: "kaspa-funding-utxo",
135
+ ownerAddress: wallet.walletAddress,
136
+ minimumGrossSompiExpression:
137
+ "tokenAmount * unitPriceSompi / priceScale + temporary holder deposit + network fee",
138
+ },
139
+ {
140
+ type: "kcc20-orderbook-root-utxo",
141
+ covenantId: marketId,
142
+ outpoint: root?.outpoint,
143
+ amount: root?.state?.["amount"],
144
+ purpose: "authorize bid output creation",
145
+ },
146
+ ]
147
+ : [
148
+ {
149
+ type: "kcc20-orderbook-holder-utxo",
150
+ covenantId: marketId,
151
+ owner: wallet.kcc20Owner,
152
+ minimumTokenAmount: amount,
153
+ ...(summary ?? {}),
154
+ outpoint: holder?.outpoint,
155
+ amount: holder?.state?.["amount"],
156
+ },
157
+ ],
158
+ warnings: [
159
+ "Crossed orders are settled by the backend matcher after normal bid/ask creation.",
160
+ "Fee discount tickets apply only to supported fill paths.",
161
+ ],
162
+ },
163
+ options,
164
+ );
165
+ }
166
+
167
+ export interface Kcc20FillDraft extends Kcc20OrderDraft {
168
+ targetOrderId: string;
169
+ useFeeTicket?: boolean;
170
+ }
171
+
172
+ export function buildKcc20FillOperation(
173
+ wallet: Kcc20TradingWalletInfo,
174
+ context: Kcc20TradingOperationContext,
175
+ draft: Kcc20FillDraft,
176
+ options: Kcc20TradingOperationOptions = {},
177
+ ) {
178
+ requireTradableWrapper(context.wrapper, draft.wrappedMarketId);
179
+ const canonicalId = contractCanonicalId(context);
180
+ const marketId = requireHex32(context.wrapper.marketId, "wrappedMarketId");
181
+ const amount = displayAmount(draft.tokenAmount, context.tokenDecimals);
182
+ const fillsAsk = draft.side === "buy";
183
+ const order = selectWrappedOrderUtxo(context.wrappedTokenUtxos, {
184
+ orderId: requireOrderId(draft.targetOrderId),
185
+ mode: fillsAsk ? 2 : 3,
186
+ canonicalTokenId: canonicalId,
187
+ requestedAmount: amount,
188
+ unitPriceSompi: requireUnsigned(draft.unitPriceSompi, "unitPriceSompi"),
189
+ });
190
+ if (!order)
191
+ throw new Error(
192
+ `Selected ${fillsAsk ? "ask" : "bid"} order is not open or does not match fill terms`,
193
+ );
194
+ const holders = fillsAsk
195
+ ? []
196
+ : ownerWrappedHolderUtxos(
197
+ context.wrappedTokenUtxos,
198
+ wallet.kcc20Owner,
199
+ canonicalId,
200
+ );
201
+ const holder = fillsAsk
202
+ ? null
203
+ : selectOwnerWrappedHolderUtxo(
204
+ context.wrappedTokenUtxos,
205
+ wallet.kcc20Owner,
206
+ amount,
207
+ canonicalId,
208
+ true,
209
+ );
210
+ const summary = fillsAsk ? null : summarizeHolderUtxos(holders, amount);
211
+ if (!fillsAsk && !holder)
212
+ throw new Error(
213
+ "Fill bid requires a wrapped holder UTXO owned by the seller",
214
+ );
215
+ const requestedTicket = draft.useFeeTicket || draft.feeTicketId;
216
+ const orderTicketId = normalizeOptionalHex32(order.state?.["feeTicketId"]);
217
+ const feeTicketId = requestedTicket
218
+ ? validatedFillFeeTicket(context.wrapper, orderTicketId, draft.feeTicketId)
219
+ : null;
220
+ const ticket = feeTicketId
221
+ ? selectOwnerFeeTicketUtxos(
222
+ context.feeTicketUtxos ?? [],
223
+ wallet.kcc20Owner,
224
+ )[0]
225
+ : undefined;
226
+ if (feeTicketId && !ticket)
227
+ throw new Error("No active FeeTicket UTXO is available for this wallet");
228
+ const feeQuote = buildStandaloneFillFeeQuote({
229
+ side: draft.side,
230
+ tokenAmount: amount,
231
+ unitPriceSompi: draft.unitPriceSompi,
232
+ priceScale: String(
233
+ order.state?.["priceScale"] ??
234
+ order.state?.["tokenDisplayScale"] ??
235
+ priceScaleFor(context.tokenDecimals),
236
+ ),
237
+ feeTicketApplied: Boolean(ticket),
238
+ });
239
+ const operation = fillsAsk ? "fill-ask-order" : "fill-bid-order";
240
+ return buildKcc20WalletOperationFromPlan(
241
+ wallet,
242
+ {
243
+ builderKey: fillsAsk
244
+ ? "kcc20orderbook.fill-ask"
245
+ : "kcc20orderbook.fill-bid",
246
+ operation,
247
+ contract: "KCC20Orderbook",
248
+ action: fillsAsk ? "fillAsk" : "fillBid",
249
+ description: `Build a KCC20 DEX Orderbook V1 ${draft.side} fill request.`,
250
+ params: {
251
+ canonicalCovenantId: canonicalId,
252
+ wrappedMarketId: marketId,
253
+ side: draft.side,
254
+ targetOrderId: draft.targetOrderId.toLowerCase(),
255
+ tokenAmount: amount,
256
+ unitPriceSompi: draft.unitPriceSompi,
257
+ feeTicketId,
258
+ feeTicketSupported: Boolean(feeTicketId),
259
+ feeQuote,
260
+ ...(context.wrapper.activeCovenantId
261
+ ? { activeWrapperCovenantId: context.wrapper.activeCovenantId }
262
+ : {}),
263
+ activeTargetOrderUtxo: order,
264
+ activeWrappedHolderUtxo: holder,
265
+ activeFeeTicketUtxo: ticket ?? null,
266
+ },
267
+ sourceRequirements: [
268
+ {
269
+ type: fillsAsk
270
+ ? "kcc20-orderbook-ask-order-utxo"
271
+ : "kcc20-orderbook-bid-order-utxo",
272
+ orderId: draft.targetOrderId,
273
+ covenantId: marketId,
274
+ outpoint: order.outpoint,
275
+ amount: order.state?.["amount"],
276
+ unitPriceSompi: order.state?.["unitPriceSompi"],
277
+ },
278
+ ...(fillsAsk
279
+ ? [
280
+ {
281
+ type: "kaspa-funding-utxo",
282
+ ownerAddress: wallet.walletAddress,
283
+ minimumGrossSompiExpression:
284
+ "tokenAmount * unitPriceSompi / priceScale + protocol fee + network fee",
285
+ },
286
+ ]
287
+ : [
288
+ {
289
+ type: "kcc20-orderbook-holder-utxo",
290
+ covenantId: marketId,
291
+ owner: wallet.kcc20Owner,
292
+ minimumTokenAmount: amount,
293
+ ...(summary ?? {}),
294
+ outpoint: holder?.outpoint,
295
+ amount: holder?.state?.["amount"],
296
+ },
297
+ ]),
298
+ ...(ticket
299
+ ? [
300
+ {
301
+ type: "fee-ticket-utxo",
302
+ rootId: feeTicketId,
303
+ owner: wallet.kcc20Owner,
304
+ outpoint: ticket.outpoint,
305
+ amountSompi: ticket.amountSompi,
306
+ address: ticket.address,
307
+ purpose: "burn one FeeTicket for this standalone fill discount",
308
+ },
309
+ ]
310
+ : []),
311
+ ],
312
+ warnings: [
313
+ ticket
314
+ ? "This fill burns one selected FeeTicket UTXO for the protocol-fee discount."
315
+ : "No FeeTicket was selected; this fill pays the normal KAS protocol fee.",
316
+ ],
317
+ },
318
+ options,
319
+ );
320
+ }
321
+
322
+ export interface Kcc20SweepFill {
323
+ orderId: string;
324
+ tokenAmount: string;
325
+ unitPriceSompi: string;
326
+ totalPriceSompi?: string;
327
+ availableAmount?: string | null;
328
+ minFillAmount?: string | null;
329
+ }
330
+
331
+ export interface Kcc20SweepDraft {
332
+ wrappedMarketId: string;
333
+ side: "buy" | "sell";
334
+ tokenAmount: string;
335
+ mode: "market" | "limit";
336
+ limitUnitPriceSompi?: string;
337
+ feeTicketId?: string;
338
+ expectedFills: Kcc20SweepFill[];
339
+ grossSompi: string;
340
+ protocolFeeSompi: string;
341
+ buyerPaysSompi: string;
342
+ sellerReceivesSompi: string;
343
+ averageUnitPriceSompi?: string | null;
344
+ maxBuyerPaysSompi?: string;
345
+ minSellerReceivesSompi?: string;
346
+ }
347
+
348
+ export function buildKcc20SweepOperation(
349
+ wallet: Kcc20TradingWalletInfo,
350
+ context: Kcc20TradingOperationContext,
351
+ draft: Kcc20SweepDraft,
352
+ options: Kcc20TradingOperationOptions = {},
353
+ ) {
354
+ requireTradableWrapper(context.wrapper, draft.wrappedMarketId);
355
+ if (draft.expectedFills.length < 2)
356
+ throw new Error("Multi-order sweep requires at least two fill legs");
357
+ const canonicalId = contractCanonicalId(context);
358
+ const marketId = requireHex32(context.wrapper.marketId, "wrappedMarketId");
359
+ const tokenAmount = displayAmount(draft.tokenAmount, context.tokenDecimals);
360
+ const sweepsAsks = draft.side === "buy";
361
+ const orders = draft.expectedFills.flatMap((fill) => {
362
+ const selected = selectWrappedOrderUtxo(context.wrappedTokenUtxos, {
363
+ orderId: fill.orderId,
364
+ mode: sweepsAsks ? 2 : 3,
365
+ canonicalTokenId: canonicalId,
366
+ requestedAmount: fill.tokenAmount,
367
+ unitPriceSompi: fill.unitPriceSompi,
368
+ });
369
+ return selected ? [selected] : [];
370
+ });
371
+ if (orders.length !== draft.expectedFills.length)
372
+ throw new Error("One or more matched order UTXOs are no longer open");
373
+ const sweepAmount = draft.expectedFills.reduce(
374
+ (sum, fill) => sum + BigInt(fill.tokenAmount),
375
+ 0n,
376
+ );
377
+ const holders = sweepsAsks
378
+ ? []
379
+ : ownerWrappedHolderUtxos(
380
+ context.wrappedTokenUtxos,
381
+ wallet.kcc20Owner,
382
+ canonicalId,
383
+ );
384
+ const singleHolder = sweepsAsks
385
+ ? null
386
+ : selectOwnerWrappedHolderUtxo(
387
+ context.wrappedTokenUtxos,
388
+ wallet.kcc20Owner,
389
+ sweepAmount.toString(),
390
+ canonicalId,
391
+ );
392
+ const pairedHolders: Kcc20IndexedCovenantUtxo[] = [];
393
+ if (!sweepsAsks && !singleHolder) {
394
+ const used = new Set<string>();
395
+ for (const fill of draft.expectedFills) {
396
+ const match = holders
397
+ .filter((candidate) => !used.has(candidate.outpoint!))
398
+ .sort((a, b) =>
399
+ BigInt(String(a.state?.["amount"])) <
400
+ BigInt(String(b.state?.["amount"]))
401
+ ? -1
402
+ : 1,
403
+ )
404
+ .find(
405
+ (candidate) =>
406
+ BigInt(String(candidate.state?.["amount"])) >=
407
+ BigInt(fill.tokenAmount),
408
+ );
409
+ if (match) {
410
+ used.add(match.outpoint!);
411
+ pairedHolders.push(match);
412
+ }
413
+ }
414
+ }
415
+ const hasSingleHolder =
416
+ !sweepsAsks &&
417
+ draft.expectedFills.length <= KCC20_ORDERBOOK_MAX_EXPANDED_SWEEP_BID_LEGS &&
418
+ Boolean(singleHolder);
419
+ if (
420
+ !sweepsAsks &&
421
+ !hasSingleHolder &&
422
+ pairedHolders.length !== draft.expectedFills.length
423
+ ) {
424
+ throw new Error(
425
+ "Sweep bid orders requires one holder covering the full amount or one distinct holder per bid leg",
426
+ );
427
+ }
428
+ const feeTicketId = draft.feeTicketId
429
+ ? requireHex32(draft.feeTicketId, "feeTicketId")
430
+ : null;
431
+ if (feeTicketId && context.wrapper.feeTicketId?.toLowerCase() !== feeTicketId)
432
+ throw new Error(
433
+ "Requested FeeTicket root does not match this wrapped market",
434
+ );
435
+ if (
436
+ feeTicketId &&
437
+ orders.some(
438
+ (order) =>
439
+ normalizeOptionalHex32(order.state?.["feeTicketId"]) !== feeTicketId,
440
+ )
441
+ ) {
442
+ throw new Error(
443
+ "One or more sweep orders are not bound to the requested FeeTicket root",
444
+ );
445
+ }
446
+ const ticket = feeTicketId
447
+ ? selectOwnerFeeTicketUtxos(
448
+ context.feeTicketUtxos ?? [],
449
+ wallet.kcc20Owner,
450
+ )[0]
451
+ : undefined;
452
+ if (feeTicketId && !ticket)
453
+ throw new Error("FeeTicket sweep requires one active ticket UTXO");
454
+ const gross = requireUnsigned(draft.grossSompi, "grossSompi");
455
+ const protocolFee = ticket
456
+ ? "0"
457
+ : requireUnsigned(draft.protocolFeeSompi, "protocolFeeSompi");
458
+ const buyerPays = ticket
459
+ ? gross
460
+ : requireUnsigned(draft.buyerPaysSompi, "buyerPaysSompi");
461
+ const sellerReceives = ticket
462
+ ? gross
463
+ : requireUnsigned(draft.sellerReceivesSompi, "sellerReceivesSompi");
464
+ if (
465
+ draft.maxBuyerPaysSompi &&
466
+ BigInt(buyerPays) > BigInt(draft.maxBuyerPaysSompi)
467
+ )
468
+ throw new Error("The market buy cost is higher than the approved quote");
469
+ if (
470
+ draft.minSellerReceivesSompi &&
471
+ BigInt(sellerReceives) < BigInt(draft.minSellerReceivesSompi)
472
+ )
473
+ throw new Error(
474
+ "The market sell proceeds are lower than the approved quote",
475
+ );
476
+ const summary = sweepsAsks
477
+ ? null
478
+ : summarizeHolderUtxos(holders, sweepAmount.toString());
479
+ return buildKcc20WalletOperationFromPlan(
480
+ wallet,
481
+ {
482
+ builderKey: sweepsAsks
483
+ ? "kcc20orderbook.sweep-asks"
484
+ : "kcc20orderbook.sweep-bids",
485
+ operation: sweepsAsks ? "sweep-ask-orders" : "sweep-bid-orders",
486
+ contract: "KCC20Orderbook",
487
+ action: sweepsAsks ? "sweepAsks" : "sweepBids",
488
+ description: `Build a KCC20 DEX Orderbook V1 ${draft.side} multi-order sweep request.`,
489
+ params: {
490
+ canonicalCovenantId: canonicalId,
491
+ wrappedMarketId: marketId,
492
+ side: draft.side,
493
+ tokenAmount,
494
+ mode: draft.mode,
495
+ limitUnitPriceSompi: draft.limitUnitPriceSompi ?? null,
496
+ fills: draft.expectedFills,
497
+ grossSompi: gross,
498
+ protocolFeeSompi: protocolFee,
499
+ quotedProtocolFeeSompi: ticket ? draft.protocolFeeSompi : null,
500
+ buyerPaysSompi: buyerPays,
501
+ sellerReceivesSompi: sellerReceives,
502
+ averageUnitPriceSompi: draft.averageUnitPriceSompi ?? null,
503
+ feeTicketId,
504
+ activeFeeTicketUtxos: ticket ? [ticket] : [],
505
+ ...(context.wrapper.activeCovenantId
506
+ ? { activeWrapperCovenantId: context.wrapper.activeCovenantId }
507
+ : {}),
508
+ activeTargetOrderUtxos: orders,
509
+ activeWrappedHolderUtxo: singleHolder,
510
+ activeWrappedHolderUtxos: hasSingleHolder ? [] : pairedHolders,
511
+ },
512
+ sourceRequirements: [
513
+ ...draft.expectedFills.map((fill, index) => ({
514
+ type: sweepsAsks
515
+ ? "kcc20-orderbook-ask-order-utxo"
516
+ : "kcc20-orderbook-bid-order-utxo",
517
+ orderId: fill.orderId,
518
+ covenantId: marketId,
519
+ tokenAmount: fill.tokenAmount,
520
+ unitPriceSompi: fill.unitPriceSompi,
521
+ outpoint: orders[index]?.outpoint,
522
+ amount: orders[index]?.state?.["amount"],
523
+ })),
524
+ ...(!sweepsAsks && singleHolder
525
+ ? [
526
+ {
527
+ type: "kcc20-orderbook-holder-utxo",
528
+ covenantId: marketId,
529
+ owner: wallet.kcc20Owner,
530
+ minimumTokenAmount: sweepAmount.toString(),
531
+ outpoint: singleHolder.outpoint,
532
+ amount: singleHolder.state?.["amount"],
533
+ },
534
+ ]
535
+ : []),
536
+ ...(!sweepsAsks && !singleHolder
537
+ ? pairedHolders.map((holder, index) => ({
538
+ type: "kcc20-orderbook-holder-utxo",
539
+ covenantId: marketId,
540
+ owner: wallet.kcc20Owner,
541
+ legIndex: index,
542
+ minimumTokenAmount: draft.expectedFills[index]?.tokenAmount,
543
+ outpoint: holder.outpoint,
544
+ amount: holder.state?.["amount"],
545
+ }))
546
+ : []),
547
+ ...(ticket
548
+ ? [
549
+ {
550
+ type: "kcc20-fee-ticket-utxo",
551
+ covenantId: feeTicketId,
552
+ purpose: "burn one ticket for the sweep transaction",
553
+ outpoint: ticket.outpoint,
554
+ amountSompi: ticket.amountSompi,
555
+ },
556
+ ]
557
+ : []),
558
+ ...(!sweepsAsks
559
+ ? [
560
+ {
561
+ type: "kcc20-orderbook-holder-utxo-summary",
562
+ covenantId: marketId,
563
+ owner: wallet.kcc20Owner,
564
+ minimumTokenAmount: sweepAmount.toString(),
565
+ ...(summary ?? {}),
566
+ },
567
+ ]
568
+ : []),
569
+ {
570
+ type: "kaspa-funding-utxo",
571
+ ownerAddress: wallet.walletAddress,
572
+ purpose: "fund sweep settlement outputs and network fee",
573
+ },
574
+ ],
575
+ warnings: [
576
+ "This sweep spends multiple matched orders atomically in one transaction.",
577
+ ticket
578
+ ? "This sweep burns one FeeTicket UTXO."
579
+ : "This sweep pays the normal KAS protocol fee.",
580
+ ],
581
+ },
582
+ options,
583
+ );
584
+ }
585
+
586
+ export interface Kcc20CancelDraft {
587
+ wrappedMarketId: string;
588
+ side: "ask" | "bid";
589
+ targetOrderId: string;
590
+ }
591
+
592
+ export function buildKcc20CancelOperation(
593
+ wallet: Kcc20TradingWalletInfo,
594
+ context: Kcc20TradingOperationContext,
595
+ draft: Kcc20CancelDraft,
596
+ options: Kcc20TradingOperationOptions = {},
597
+ ) {
598
+ const canonicalId = contractCanonicalId(context);
599
+ const marketId = requireHex32(context.wrapper.marketId, "wrappedMarketId");
600
+ const orderId = requireOrderId(draft.targetOrderId);
601
+ const cancelsAsk = draft.side === "ask";
602
+ const order = selectWrappedOrderUtxo(context.wrappedTokenUtxos, {
603
+ orderId,
604
+ mode: cancelsAsk ? 2 : 3,
605
+ canonicalTokenId: canonicalId,
606
+ owner: wallet.kcc20Owner,
607
+ });
608
+ if (!order)
609
+ throw new Error(
610
+ `Selected ${draft.side} order is not open or not owned by this wallet`,
611
+ );
612
+ return buildKcc20WalletOperationFromPlan(
613
+ wallet,
614
+ {
615
+ builderKey: cancelsAsk
616
+ ? "kcc20orderbook.cancel-ask"
617
+ : "kcc20orderbook.cancel-bid",
618
+ operation: cancelsAsk ? "cancel-ask-order" : "cancel-bid-order",
619
+ contract: "KCC20Orderbook",
620
+ action: cancelsAsk ? "cancelAsk" : "cancelBid",
621
+ description: `Build a KCC20 DEX Orderbook V1 ${draft.side} cancel request.`,
622
+ params: {
623
+ canonicalCovenantId: canonicalId,
624
+ wrappedMarketId: marketId,
625
+ side: draft.side,
626
+ targetOrderId: orderId,
627
+ ...(context.wrapper.activeCovenantId
628
+ ? { activeWrapperCovenantId: context.wrapper.activeCovenantId }
629
+ : {}),
630
+ activeTargetOrderUtxo: order,
631
+ },
632
+ sourceRequirements: [
633
+ {
634
+ type: cancelsAsk
635
+ ? "kcc20-orderbook-ask-order-utxo"
636
+ : "kcc20-orderbook-bid-order-utxo",
637
+ orderId,
638
+ covenantId: marketId,
639
+ owner: wallet.kcc20Owner,
640
+ outpoint: order.outpoint,
641
+ amount: order.state?.["amount"],
642
+ unitPriceSompi: order.state?.["unitPriceSompi"],
643
+ purpose:
644
+ "consume the open order UTXO and return remaining assets to owner",
645
+ },
646
+ {
647
+ type: "kaspa-funding-utxo",
648
+ ownerAddress: wallet.walletAddress,
649
+ purpose: "fund output dust and network fee",
650
+ },
651
+ ],
652
+ warnings: [
653
+ "Cancel spends the selected open order UTXO and returns remaining assets to the order owner.",
654
+ "Indexer order status remains source of truth after broadcast.",
655
+ ],
656
+ },
657
+ options,
658
+ );
659
+ }
660
+
661
+ export function buildKcc20WrappedConsolidationOperation(
662
+ wallet: Kcc20TradingWalletInfo,
663
+ context: Kcc20TradingOperationContext,
664
+ sourceOutpoints?: readonly string[],
665
+ options: Kcc20TradingOperationOptions = {},
666
+ ) {
667
+ const canonicalId = contractCanonicalId(context);
668
+ const marketId = requireHex32(context.wrapper.marketId, "wrappedMarketId");
669
+ const compatible = ownerWrappedHolderUtxos(
670
+ context.wrappedTokenUtxos,
671
+ wallet.kcc20Owner,
672
+ canonicalId,
673
+ );
674
+ const selected = selectConsolidationBatch(compatible, sourceOutpoints, 8);
675
+ if (selected.length < 2)
676
+ throw new Error(
677
+ "at least two compatible wrapped holder UTXOs are required for consolidation",
678
+ );
679
+ const total = selected
680
+ .reduce(
681
+ (sum, utxo) => sum + BigInt(String(utxo.state?.["amount"] ?? "0")),
682
+ 0n,
683
+ )
684
+ .toString();
685
+ const summary = summarizeHolderUtxos(compatible, "0");
686
+ return buildKcc20WalletOperationFromPlan(
687
+ wallet,
688
+ {
689
+ builderKey: "kcc20orderbook.consolidate-holders",
690
+ operation: "consolidate-orderbook-holders",
691
+ contract: "KCC20Orderbook",
692
+ action: "consolidate",
693
+ description:
694
+ "Build a KCC20 DEX Orderbook V1 holder consolidation transaction request.",
695
+ params: {
696
+ canonicalCovenantId: canonicalId,
697
+ appCanonicalCovenantId: context.canonicalCovenantId.toLowerCase(),
698
+ wrappedMarketId: marketId,
699
+ activeWrapperCovenantId: context.wrapper.activeCovenantId,
700
+ tokenAmount: total,
701
+ activeWrappedHolderUtxos: selected,
702
+ },
703
+ sourceRequirements: [
704
+ {
705
+ type: "kcc20-orderbook-holder-utxo-batch",
706
+ covenantId: canonicalId,
707
+ wrappedMarketId: marketId,
708
+ activeWrapperCovenantId: context.wrapper.activeCovenantId,
709
+ owner: wallet.kcc20Owner,
710
+ selectedOutpoints: selected.map((utxo) => utxo.outpoint),
711
+ selectedAmounts: selected.map((utxo) => utxo.state?.["amount"]),
712
+ aggregateAmount: summary.aggregateAmount,
713
+ utxoCount: summary.utxoCount,
714
+ purpose:
715
+ "merge wallet-owned KCC20 DEX Orderbook V1 holder outputs into one holder output",
716
+ },
717
+ {
718
+ type: "kaspa-funding-utxo",
719
+ ownerAddress: wallet.walletAddress,
720
+ purpose: "fund network fee for wrapped holder consolidation",
721
+ },
722
+ ],
723
+ warnings: [
724
+ "Consolidation merges between two and eight wallet-owned holder UTXOs into one larger holder UTXO.",
725
+ "Repeat consolidation when more compatible holder UTXOs remain.",
726
+ ],
727
+ },
728
+ options,
729
+ );
730
+ }
731
+
732
+ function contractCanonicalId(context: Kcc20TradingOperationContext): string {
733
+ return requireHex32(
734
+ context.wrapper.contractCanonicalTokenId ??
735
+ context.wrapper.canonicalTokenId ??
736
+ context.canonicalCovenantId,
737
+ "canonicalCovenantId",
738
+ );
739
+ }
740
+
741
+ function requireTradableWrapper(
742
+ wrapper: Kcc20WrapperSourceInfo,
743
+ requestedMarketId: string,
744
+ ): void {
745
+ if (wrapper.enabled === false)
746
+ throw new Error("Wrapped trading is disabled for this market");
747
+ if (wrapper.marketId.toLowerCase() !== requestedMarketId.toLowerCase())
748
+ throw new Error(
749
+ "Requested wrapped market does not match the selected wrapper",
750
+ );
751
+ }
752
+
753
+ function validatedWrapperFeeTicket(
754
+ wrapper: Kcc20WrapperSourceInfo,
755
+ requested?: string,
756
+ ): string | null {
757
+ const configured = normalizeOptionalHex32(wrapper.feeTicketId);
758
+ const normalized = normalizeOptionalHex32(requested);
759
+ if (requested && !normalized) throw new Error("FeeTicket root id is invalid");
760
+ if (normalized && !configured)
761
+ throw new Error("FeeTicket is not enabled for this wrapped market");
762
+ if (normalized && configured !== normalized)
763
+ throw new Error(
764
+ "Requested FeeTicket root does not match this wrapped market",
765
+ );
766
+ if (wrapper.feeTicketSupported && !configured)
767
+ throw new Error("Wrapped market FeeTicket root is invalid");
768
+ return configured;
769
+ }
770
+
771
+ function validatedFillFeeTicket(
772
+ wrapper: Kcc20WrapperSourceInfo,
773
+ orderTicketId: string | null,
774
+ requested?: string,
775
+ ): string {
776
+ if (!orderTicketId)
777
+ throw new Error("Selected order is not bound to a FeeTicket root");
778
+ const explicit = requested
779
+ ? requireHex32(requested, "feeTicketId")
780
+ : orderTicketId;
781
+ if (explicit !== orderTicketId)
782
+ throw new Error(
783
+ "Selected order is not bound to the requested FeeTicket root",
784
+ );
785
+ const active = normalizeOptionalHex32(wrapper.feeTicketId);
786
+ if (!active || active !== explicit)
787
+ throw new Error(
788
+ "Selected order FeeTicket root is not the active platform root",
789
+ );
790
+ return explicit;
791
+ }
792
+
793
+ function displayAmount(
794
+ value: string,
795
+ decimals: number | null | undefined,
796
+ ): string {
797
+ return parseKcc20DisplayAmountToBaseUnits(
798
+ value,
799
+ normalizeKcc20Decimals(decimals),
800
+ "tokenAmount",
801
+ );
802
+ }
803
+
804
+ function priceScaleFor(decimals: number | null | undefined): string {
805
+ return kcc20DisplayScaleForDecimals(
806
+ normalizeKcc20Decimals(decimals),
807
+ ).toString();
808
+ }
809
+
810
+ function requireHex32(value: unknown, field: string): string {
811
+ const normalized = String(value ?? "")
812
+ .trim()
813
+ .toLowerCase()
814
+ .replace(/^0x/, "");
815
+ if (!/^[a-f0-9]{64}$/.test(normalized))
816
+ throw new Error(`${field} must be 64 hex characters`);
817
+ return normalized;
818
+ }
819
+
820
+ function normalizeOptionalHex32(value: unknown): string | null {
821
+ if (value === undefined || value === null || value === "") return null;
822
+ const normalized = String(value).trim().toLowerCase().replace(/^0x/, "");
823
+ return /^[a-f0-9]{64}$/.test(normalized) && !/^0{64}$/.test(normalized)
824
+ ? normalized
825
+ : null;
826
+ }
827
+
828
+ function requireUnsigned(value: unknown, field: string): string {
829
+ const raw = String(value ?? "").trim();
830
+ if (!/^\d+$/.test(raw))
831
+ throw new Error(`${field} must be an unsigned integer string`);
832
+ return raw;
833
+ }
834
+
835
+ function requireOrderId(value: string): string {
836
+ const normalized = String(value ?? "")
837
+ .trim()
838
+ .toLowerCase();
839
+ if (!/^[a-f0-9]{64}:\d+$/.test(normalized))
840
+ throw new Error("targetOrderId must be txid:vout");
841
+ return normalized;
842
+ }