@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.
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/artifacts/KCC20.placeholder.json +77479 -0
- package/artifacts/KCC20FeeTicket.placeholder.json +16846 -0
- package/artifacts/KCC20Orderbook.placeholder.json +78001 -0
- package/artifacts/KCC20Vesting.placeholder.json +4298 -0
- package/artifacts/KCC20Wrapper.placeholder.json +12246 -0
- package/dist/abi.d.ts +44 -0
- package/dist/abi.d.ts.map +1 -0
- package/dist/abi.js +336 -0
- package/dist/abi.js.map +1 -0
- package/dist/action-source-resolver.d.ts +80 -0
- package/dist/action-source-resolver.d.ts.map +1 -0
- package/dist/action-source-resolver.js +412 -0
- package/dist/action-source-resolver.js.map +1 -0
- package/dist/artifacts.d.ts +14 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +44 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/builder.d.ts +12 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +18 -0
- package/dist/builder.js.map +1 -0
- package/dist/cjs/abi.d.ts +43 -0
- package/dist/cjs/abi.js +361 -0
- package/dist/cjs/abi.js.map +1 -0
- package/dist/cjs/action-source-resolver.d.ts +79 -0
- package/dist/cjs/action-source-resolver.js +435 -0
- package/dist/cjs/action-source-resolver.js.map +1 -0
- package/dist/cjs/artifacts.d.ts +13 -0
- package/dist/cjs/artifacts.js +51 -0
- package/dist/cjs/artifacts.js.map +1 -0
- package/dist/cjs/builder.d.ts +11 -0
- package/dist/cjs/builder.js +22 -0
- package/dist/cjs/builder.js.map +1 -0
- package/dist/cjs/deploy-operation.d.ts +103 -0
- package/dist/cjs/deploy-operation.js +335 -0
- package/dist/cjs/deploy-operation.js.map +1 -0
- package/dist/cjs/encoding.d.ts +5 -0
- package/dist/cjs/encoding.js +51 -0
- package/dist/cjs/encoding.js.map +1 -0
- package/dist/cjs/engine.d.ts +40 -0
- package/dist/cjs/engine.js +8972 -0
- package/dist/cjs/engine.js.map +1 -0
- package/dist/cjs/errors.d.ts +7 -0
- package/dist/cjs/errors.js +23 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/fee-ticket-operation.d.ts +204 -0
- package/dist/cjs/fee-ticket-operation.js +327 -0
- package/dist/cjs/fee-ticket-operation.js.map +1 -0
- package/dist/cjs/index.d.ts +27 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/ladder-limit-orders.d.ts +41 -0
- package/dist/cjs/ladder-limit-orders.js +129 -0
- package/dist/cjs/ladder-limit-orders.js.map +1 -0
- package/dist/cjs/mint-availability-operation.d.ts +56 -0
- package/dist/cjs/mint-availability-operation.js +95 -0
- package/dist/cjs/mint-availability-operation.js.map +1 -0
- package/dist/cjs/models.d.ts +90 -0
- package/dist/cjs/models.js +6 -0
- package/dist/cjs/models.js.map +1 -0
- package/dist/cjs/operations.d.ts +6 -0
- package/dist/cjs/operations.js +41 -0
- package/dist/cjs/operations.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/paid-mint-amount.d.ts +69 -0
- package/dist/cjs/paid-mint-amount.js +281 -0
- package/dist/cjs/paid-mint-amount.js.map +1 -0
- package/dist/cjs/protocol.d.ts +21 -0
- package/dist/cjs/protocol.js +25 -0
- package/dist/cjs/protocol.js.map +1 -0
- package/dist/cjs/runtime.d.ts +27 -0
- package/dist/cjs/runtime.js +3 -0
- package/dist/cjs/runtime.js.map +1 -0
- package/dist/cjs/sale-common.d.ts +287 -0
- package/dist/cjs/sale-common.js +928 -0
- package/dist/cjs/sale-common.js.map +1 -0
- package/dist/cjs/source-provider.d.ts +11 -0
- package/dist/cjs/source-provider.js +3 -0
- package/dist/cjs/source-provider.js.map +1 -0
- package/dist/cjs/source-validation.d.ts +9 -0
- package/dist/cjs/source-validation.js +33 -0
- package/dist/cjs/source-validation.js.map +1 -0
- package/dist/cjs/token-amount.d.ts +31 -0
- package/dist/cjs/token-amount.js +252 -0
- package/dist/cjs/token-amount.js.map +1 -0
- package/dist/cjs/token-operation.d.ts +135 -0
- package/dist/cjs/token-operation.js +236 -0
- package/dist/cjs/token-operation.js.map +1 -0
- package/dist/cjs/trade-fee.d.ts +60 -0
- package/dist/cjs/trade-fee.js +185 -0
- package/dist/cjs/trade-fee.js.map +1 -0
- package/dist/cjs/trading-operation.d.ts +212 -0
- package/dist/cjs/trading-operation.js +587 -0
- package/dist/cjs/trading-operation.js.map +1 -0
- package/dist/cjs/verify-operation.d.ts +79 -0
- package/dist/cjs/verify-operation.js +246 -0
- package/dist/cjs/verify-operation.js.map +1 -0
- package/dist/cjs/wallet-operation.d.ts +52 -0
- package/dist/cjs/wallet-operation.js +58 -0
- package/dist/cjs/wallet-operation.js.map +1 -0
- package/dist/cjs/wasm-provider.d.ts +15 -0
- package/dist/cjs/wasm-provider.js +20 -0
- package/dist/cjs/wasm-provider.js.map +1 -0
- package/dist/cjs/wrapper-operation.d.ts +196 -0
- package/dist/cjs/wrapper-operation.js +317 -0
- package/dist/cjs/wrapper-operation.js.map +1 -0
- package/dist/deploy-operation.d.ts +104 -0
- package/dist/deploy-operation.d.ts.map +1 -0
- package/dist/deploy-operation.js +327 -0
- package/dist/deploy-operation.js.map +1 -0
- package/dist/encoding.d.ts +6 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +44 -0
- package/dist/encoding.js.map +1 -0
- package/dist/engine.d.ts +41 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +8969 -0
- package/dist/engine.js.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/fee-ticket-operation.d.ts +205 -0
- package/dist/fee-ticket-operation.d.ts.map +1 -0
- package/dist/fee-ticket-operation.js +320 -0
- package/dist/fee-ticket-operation.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/ladder-limit-orders.d.ts +42 -0
- package/dist/ladder-limit-orders.d.ts.map +1 -0
- package/dist/ladder-limit-orders.js +121 -0
- package/dist/ladder-limit-orders.js.map +1 -0
- package/dist/mint-availability-operation.d.ts +57 -0
- package/dist/mint-availability-operation.d.ts.map +1 -0
- package/dist/mint-availability-operation.js +91 -0
- package/dist/mint-availability-operation.js.map +1 -0
- package/dist/models.d.ts +91 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +3 -0
- package/dist/models.js.map +1 -0
- package/dist/operations.d.ts +7 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +36 -0
- package/dist/operations.js.map +1 -0
- package/dist/paid-mint-amount.d.ts +70 -0
- package/dist/paid-mint-amount.d.ts.map +1 -0
- package/dist/paid-mint-amount.js +268 -0
- package/dist/paid-mint-amount.js.map +1 -0
- package/dist/protocol.d.ts +22 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +22 -0
- package/dist/protocol.js.map +1 -0
- package/dist/runtime.d.ts +28 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +2 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sale-common.d.ts +288 -0
- package/dist/sale-common.d.ts.map +1 -0
- package/dist/sale-common.js +844 -0
- package/dist/sale-common.js.map +1 -0
- package/dist/source-provider.d.ts +12 -0
- package/dist/source-provider.d.ts.map +1 -0
- package/dist/source-provider.js +2 -0
- package/dist/source-provider.js.map +1 -0
- package/dist/source-validation.d.ts +10 -0
- package/dist/source-validation.d.ts.map +1 -0
- package/dist/source-validation.js +28 -0
- package/dist/source-validation.js.map +1 -0
- package/dist/token-amount.d.ts +32 -0
- package/dist/token-amount.d.ts.map +1 -0
- package/dist/token-amount.js +237 -0
- package/dist/token-amount.js.map +1 -0
- package/dist/token-operation.d.ts +136 -0
- package/dist/token-operation.d.ts.map +1 -0
- package/dist/token-operation.js +231 -0
- package/dist/token-operation.js.map +1 -0
- package/dist/trade-fee.d.ts +61 -0
- package/dist/trade-fee.d.ts.map +1 -0
- package/dist/trade-fee.js +173 -0
- package/dist/trade-fee.js.map +1 -0
- package/dist/trading-operation.d.ts +213 -0
- package/dist/trading-operation.d.ts.map +1 -0
- package/dist/trading-operation.js +580 -0
- package/dist/trading-operation.js.map +1 -0
- package/dist/verify-operation.d.ts +80 -0
- package/dist/verify-operation.d.ts.map +1 -0
- package/dist/verify-operation.js +243 -0
- package/dist/verify-operation.js.map +1 -0
- package/dist/wallet-operation.d.ts +53 -0
- package/dist/wallet-operation.d.ts.map +1 -0
- package/dist/wallet-operation.js +55 -0
- package/dist/wallet-operation.js.map +1 -0
- package/dist/wasm-provider.d.ts +16 -0
- package/dist/wasm-provider.d.ts.map +1 -0
- package/dist/wasm-provider.js +17 -0
- package/dist/wasm-provider.js.map +1 -0
- package/dist/wrapper-operation.d.ts +197 -0
- package/dist/wrapper-operation.d.ts.map +1 -0
- package/dist/wrapper-operation.js +311 -0
- package/dist/wrapper-operation.js.map +1 -0
- package/index.d.ts +1 -0
- package/package.json +84 -0
- package/src/abi.ts +493 -0
- package/src/action-source-resolver.ts +660 -0
- package/src/artifacts.ts +87 -0
- package/src/builder.ts +37 -0
- package/src/deploy-operation.ts +458 -0
- package/src/encoding.ts +58 -0
- package/src/engine.ts +13226 -0
- package/src/errors.ts +35 -0
- package/src/fee-ticket-operation.ts +518 -0
- package/src/index.ts +27 -0
- package/src/ladder-limit-orders.ts +184 -0
- package/src/mint-availability-operation.ts +146 -0
- package/src/models.ts +103 -0
- package/src/operations.ts +46 -0
- package/src/paid-mint-amount.ts +402 -0
- package/src/protocol.ts +28 -0
- package/src/runtime.ts +29 -0
- package/src/sale-common.ts +1238 -0
- package/src/source-provider.ts +12 -0
- package/src/source-validation.ts +47 -0
- package/src/token-amount.ts +302 -0
- package/src/token-operation.ts +384 -0
- package/src/trade-fee.ts +264 -0
- package/src/trading-operation.ts +842 -0
- package/src/verify-operation.ts +400 -0
- package/src/wallet-operation.ts +86 -0
- package/src/wasm-provider.ts +32 -0
- 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
|
+
}
|