@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,384 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ownerNativeHolderUtxos,
|
|
3
|
+
selectActiveMinterUtxo,
|
|
4
|
+
selectConsolidationBatch,
|
|
5
|
+
selectOwnerNativeHolderUtxo,
|
|
6
|
+
summarizeHolderUtxos,
|
|
7
|
+
type Kcc20IndexedCovenantUtxo,
|
|
8
|
+
} from "./action-source-resolver.js";
|
|
9
|
+
import {
|
|
10
|
+
kcc20DisplayScaleForDecimals,
|
|
11
|
+
normalizeKcc20Decimals,
|
|
12
|
+
normalizeKcc20OwnerIdentifier,
|
|
13
|
+
parseKcc20DisplayAmountToBaseUnits,
|
|
14
|
+
} from "./token-amount.js";
|
|
15
|
+
import { kcc20ValidatePaidMintAmount } from "./paid-mint-amount.js";
|
|
16
|
+
import {
|
|
17
|
+
KCC20_PUBLIC_MINT_MAX_DISPLAY_TOKENS,
|
|
18
|
+
KCC20_PUBLIC_MINT_MAX_DISPLAY_TOKENS_LABEL,
|
|
19
|
+
} from "./protocol.js";
|
|
20
|
+
import { buildKcc20WalletOperationFromPlan } from "./wallet-operation.js";
|
|
21
|
+
|
|
22
|
+
export interface Kcc20ActionWalletInfo {
|
|
23
|
+
walletAddress: string;
|
|
24
|
+
kcc20Owner: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Kcc20ActionTokenInfo {
|
|
28
|
+
covenantId: string;
|
|
29
|
+
ticker?: string | null;
|
|
30
|
+
tokenName?: string | null;
|
|
31
|
+
decimals?: number | null;
|
|
32
|
+
mintPolicy?: {
|
|
33
|
+
mintable?: boolean;
|
|
34
|
+
policy?: string;
|
|
35
|
+
publicMintActive?: boolean | null;
|
|
36
|
+
remainingSupply?: string | null;
|
|
37
|
+
mintPricePerTokenSompi?: string | null;
|
|
38
|
+
treasuryRecipient?: string | null;
|
|
39
|
+
protocolFeeRecipient?: string | null;
|
|
40
|
+
protocolFeeBps?: number | null;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Kcc20TokenOperationOptions {
|
|
45
|
+
network?: string;
|
|
46
|
+
createdAt?: string;
|
|
47
|
+
requestId?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildKcc20MintTokenOperation(
|
|
51
|
+
wallet: Kcc20ActionWalletInfo,
|
|
52
|
+
input: {
|
|
53
|
+
token: Kcc20ActionTokenInfo;
|
|
54
|
+
tokenAmount: string;
|
|
55
|
+
recipientOwner?: string;
|
|
56
|
+
activeUtxos: readonly Kcc20IndexedCovenantUtxo[];
|
|
57
|
+
publicRequest?: boolean;
|
|
58
|
+
},
|
|
59
|
+
options: Kcc20TokenOperationOptions = {},
|
|
60
|
+
) {
|
|
61
|
+
const covenantId = requireHex32(input.token.covenantId, "covenantId");
|
|
62
|
+
const policy = input.token.mintPolicy?.policy;
|
|
63
|
+
if (
|
|
64
|
+
!input.token.mintPolicy?.mintable ||
|
|
65
|
+
(policy !== "public" && policy !== "controlled")
|
|
66
|
+
) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Unsupported KCC20 mint policy: ${policy ?? "not-mintable"}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const pausedPublicMint =
|
|
72
|
+
policy === "public" && input.token.mintPolicy.publicMintActive === false;
|
|
73
|
+
if (pausedPublicMint && input.publicRequest !== false) {
|
|
74
|
+
throw new Error("Public mint is paused by the token creator");
|
|
75
|
+
}
|
|
76
|
+
const decimals = normalizeKcc20Decimals(input.token.decimals);
|
|
77
|
+
const priceScale = kcc20DisplayScaleForDecimals(decimals);
|
|
78
|
+
const tokenAmount = parseKcc20DisplayAmountToBaseUnits(
|
|
79
|
+
input.tokenAmount,
|
|
80
|
+
decimals,
|
|
81
|
+
"tokenAmount",
|
|
82
|
+
);
|
|
83
|
+
if (
|
|
84
|
+
policy === "public" &&
|
|
85
|
+
BigInt(tokenAmount) > KCC20_PUBLIC_MINT_MAX_DISPLAY_TOKENS * priceScale
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Public mint amount exceeds the maximum per mint transaction of ${KCC20_PUBLIC_MINT_MAX_DISPLAY_TOKENS_LABEL} tokens`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const mintPrice = input.token.mintPolicy.mintPricePerTokenSompi;
|
|
92
|
+
if (mintPrice && /^\d+$/.test(mintPrice) && BigInt(mintPrice) > 0n) {
|
|
93
|
+
kcc20ValidatePaidMintAmount({
|
|
94
|
+
tokenAmount: BigInt(tokenAmount),
|
|
95
|
+
unitPriceSompi: BigInt(mintPrice),
|
|
96
|
+
priceScale,
|
|
97
|
+
protocolFeeBps: input.token.mintPolicy.protocolFeeBps,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const minter = selectActiveMinterUtxo(input.activeUtxos, tokenAmount);
|
|
101
|
+
if (!minter?.state)
|
|
102
|
+
throw new Error(
|
|
103
|
+
"No active KCC20 mint-authority UTXO has enough remaining supply",
|
|
104
|
+
);
|
|
105
|
+
const walletOwner = normalizeKcc20OwnerIdentifier(
|
|
106
|
+
wallet.kcc20Owner,
|
|
107
|
+
"wallet owner",
|
|
108
|
+
);
|
|
109
|
+
const authorityOwner = String(
|
|
110
|
+
minter.state["ownerIdentifier"] ?? minter.state["owner"] ?? "",
|
|
111
|
+
).toLowerCase();
|
|
112
|
+
const authorityScheme = Number(
|
|
113
|
+
minter.state["ownerScheme"] ?? minter.state["identifierType"],
|
|
114
|
+
);
|
|
115
|
+
if (
|
|
116
|
+
(policy === "controlled" || pausedPublicMint) &&
|
|
117
|
+
(authorityScheme !== 0 || authorityOwner !== walletOwner)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
policy === "controlled"
|
|
121
|
+
? "Controlled mint requires the active mint-authority owner"
|
|
122
|
+
: "Paused public mint requires the active minter owner",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const recipientOwner = input.recipientOwner
|
|
126
|
+
? normalizeKcc20OwnerIdentifier(input.recipientOwner, "recipientOwner")
|
|
127
|
+
: walletOwner;
|
|
128
|
+
if (
|
|
129
|
+
recipientOwner !== walletOwner &&
|
|
130
|
+
policy !== "controlled" &&
|
|
131
|
+
!pausedPublicMint
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"Mint recipient override requires controlled mint authority",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const mintPath =
|
|
138
|
+
policy === "controlled" || authorityOwner === walletOwner
|
|
139
|
+
? "owner"
|
|
140
|
+
: "public";
|
|
141
|
+
return buildKcc20WalletOperationFromPlan(
|
|
142
|
+
wallet,
|
|
143
|
+
{
|
|
144
|
+
builderKey: "kcc20.mint",
|
|
145
|
+
operation: "mint-token",
|
|
146
|
+
contract: "KCC20",
|
|
147
|
+
action: "mint",
|
|
148
|
+
description: "Build a KCC20 mint transaction request.",
|
|
149
|
+
params: {
|
|
150
|
+
covenantId,
|
|
151
|
+
tokenAmount,
|
|
152
|
+
tokenDisplayScale: priceScale.toString(),
|
|
153
|
+
priceScale: priceScale.toString(),
|
|
154
|
+
recipientOwner,
|
|
155
|
+
recipientOwnerScheme: 0,
|
|
156
|
+
mintPath,
|
|
157
|
+
ticker: input.token.ticker ?? null,
|
|
158
|
+
tokenName: input.token.tokenName ?? null,
|
|
159
|
+
mintPolicy: input.token.mintPolicy ?? null,
|
|
160
|
+
activeMinterUtxo: minter,
|
|
161
|
+
},
|
|
162
|
+
sourceRequirements: [
|
|
163
|
+
{
|
|
164
|
+
type: "kcc20-mint-authority-utxo",
|
|
165
|
+
covenantId,
|
|
166
|
+
outpoint: minter.outpoint,
|
|
167
|
+
remainingSupply: minter.state["remainingSupply"],
|
|
168
|
+
purpose: "authorize mint supply reduction and recipient output",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: "kcc20-mint-recipient-owner",
|
|
172
|
+
owner: recipientOwner,
|
|
173
|
+
ownerType: "pubkey",
|
|
174
|
+
purpose: "create minted holder output for the intended recipient",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
type: "kaspa-funding-utxo",
|
|
178
|
+
ownerAddress: wallet.walletAddress,
|
|
179
|
+
purpose: "fund output dust and network fee",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
warnings: [
|
|
183
|
+
"Wallet must enforce remaining-supply accounting before signing.",
|
|
184
|
+
...(mintPath === "owner"
|
|
185
|
+
? [
|
|
186
|
+
"Owner-authorized mint requires the wallet to sign the mint-authority covenant input.",
|
|
187
|
+
]
|
|
188
|
+
: []),
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
options,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function buildKcc20TransferTokenOperation(
|
|
196
|
+
wallet: Kcc20ActionWalletInfo,
|
|
197
|
+
input: {
|
|
198
|
+
token: Kcc20ActionTokenInfo;
|
|
199
|
+
tokenAmount: string;
|
|
200
|
+
recipientOwner: string;
|
|
201
|
+
activeUtxos: readonly Kcc20IndexedCovenantUtxo[];
|
|
202
|
+
holderOutpoint?: string;
|
|
203
|
+
},
|
|
204
|
+
options: Kcc20TokenOperationOptions = {},
|
|
205
|
+
) {
|
|
206
|
+
const covenantId = requireHex32(input.token.covenantId, "covenantId");
|
|
207
|
+
const decimals = normalizeKcc20Decimals(input.token.decimals);
|
|
208
|
+
const tokenAmount = parseKcc20DisplayAmountToBaseUnits(
|
|
209
|
+
input.tokenAmount,
|
|
210
|
+
decimals,
|
|
211
|
+
"tokenAmount",
|
|
212
|
+
);
|
|
213
|
+
const owner = normalizeKcc20OwnerIdentifier(
|
|
214
|
+
wallet.kcc20Owner,
|
|
215
|
+
"wallet owner",
|
|
216
|
+
);
|
|
217
|
+
const recipientOwner = normalizeKcc20OwnerIdentifier(
|
|
218
|
+
input.recipientOwner,
|
|
219
|
+
"recipientOwner",
|
|
220
|
+
);
|
|
221
|
+
const holder = selectOwnerNativeHolderUtxo(
|
|
222
|
+
input.activeUtxos,
|
|
223
|
+
owner,
|
|
224
|
+
tokenAmount,
|
|
225
|
+
input.holderOutpoint,
|
|
226
|
+
);
|
|
227
|
+
const ownerUtxos = ownerNativeHolderUtxos(input.activeUtxos, owner);
|
|
228
|
+
const summary = summarizeHolderUtxos(ownerUtxos, tokenAmount);
|
|
229
|
+
if (!holder) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
summary.fragmented
|
|
232
|
+
? "No single KCC20 holder UTXO is large enough; consolidate the token balance first"
|
|
233
|
+
: "No single KCC20 holder UTXO is available for this wallet and amount",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const nativeCovenantId = holder.covenantId ?? covenantId;
|
|
237
|
+
return buildKcc20WalletOperationFromPlan(
|
|
238
|
+
wallet,
|
|
239
|
+
{
|
|
240
|
+
builderKey: "kcc20.transfer",
|
|
241
|
+
operation: "transfer-token",
|
|
242
|
+
contract: "KCC20",
|
|
243
|
+
action: "transfer",
|
|
244
|
+
description: "Build a KCC20 holder transfer transaction request.",
|
|
245
|
+
params: {
|
|
246
|
+
covenantId,
|
|
247
|
+
appCanonicalCovenantId: covenantId,
|
|
248
|
+
activeHolderNativeCovenantId: nativeCovenantId,
|
|
249
|
+
tokenAmount,
|
|
250
|
+
recipientOwner,
|
|
251
|
+
recipientOwnerScheme: 0,
|
|
252
|
+
ticker: input.token.ticker ?? null,
|
|
253
|
+
tokenName: input.token.tokenName ?? null,
|
|
254
|
+
activeHolderUtxo: holder,
|
|
255
|
+
},
|
|
256
|
+
sourceRequirements: [
|
|
257
|
+
{
|
|
258
|
+
type: "kcc20-holder-utxo",
|
|
259
|
+
covenantId,
|
|
260
|
+
activeHolderNativeCovenantId: nativeCovenantId,
|
|
261
|
+
owner,
|
|
262
|
+
minimumTokenAmount: tokenAmount,
|
|
263
|
+
...summary,
|
|
264
|
+
outpoint: holder.outpoint,
|
|
265
|
+
amount: holder.state?.["amount"],
|
|
266
|
+
purpose:
|
|
267
|
+
"spend native holder balance into recipient output and optional owner change output",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
type: "kcc20-transfer-recipient-owner",
|
|
271
|
+
owner: recipientOwner,
|
|
272
|
+
ownerType: "pubkey",
|
|
273
|
+
purpose:
|
|
274
|
+
"create transferred holder output for the intended recipient",
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
type: "kaspa-funding-utxo",
|
|
278
|
+
ownerAddress: wallet.walletAddress,
|
|
279
|
+
purpose: "fund output dust and network fee",
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
warnings: [
|
|
283
|
+
`This transfers ${input.tokenAmount} KCC20 balance to the selected recipient owner.`,
|
|
284
|
+
"Only pubkey-owned holder outputs can be transferred directly from this wallet flow.",
|
|
285
|
+
"Transfer spends one KCC20 holder UTXO; fragmented balances require consolidation.",
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
options,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function buildKcc20NativeConsolidationOperation(
|
|
293
|
+
wallet: Kcc20ActionWalletInfo,
|
|
294
|
+
input: {
|
|
295
|
+
token: Kcc20ActionTokenInfo;
|
|
296
|
+
activeUtxos: readonly Kcc20IndexedCovenantUtxo[];
|
|
297
|
+
sourceOutpoints?: readonly string[];
|
|
298
|
+
},
|
|
299
|
+
options: Kcc20TokenOperationOptions = {},
|
|
300
|
+
) {
|
|
301
|
+
const covenantId = requireHex32(input.token.covenantId, "covenantId");
|
|
302
|
+
const owner = normalizeKcc20OwnerIdentifier(
|
|
303
|
+
wallet.kcc20Owner,
|
|
304
|
+
"wallet owner",
|
|
305
|
+
);
|
|
306
|
+
const compatible = ownerNativeHolderUtxos(input.activeUtxos, owner);
|
|
307
|
+
const selected = selectConsolidationBatch(
|
|
308
|
+
compatible,
|
|
309
|
+
input.sourceOutpoints,
|
|
310
|
+
8,
|
|
311
|
+
);
|
|
312
|
+
if (selected.length < 2)
|
|
313
|
+
throw new Error(
|
|
314
|
+
"at least two compatible KCC20 holder UTXOs are required for consolidation",
|
|
315
|
+
);
|
|
316
|
+
const nativeIds = new Set(
|
|
317
|
+
selected.map((utxo) => utxo.covenantId ?? covenantId),
|
|
318
|
+
);
|
|
319
|
+
if (nativeIds.size !== 1)
|
|
320
|
+
throw new Error(
|
|
321
|
+
"selected holder UTXOs belong to different active covenant ids",
|
|
322
|
+
);
|
|
323
|
+
const nativeCovenantId = [...nativeIds][0];
|
|
324
|
+
const total = selected
|
|
325
|
+
.reduce(
|
|
326
|
+
(sum, utxo) => sum + BigInt(String(utxo.state?.["amount"] ?? "0")),
|
|
327
|
+
0n,
|
|
328
|
+
)
|
|
329
|
+
.toString();
|
|
330
|
+
const summary = summarizeHolderUtxos(compatible, "0");
|
|
331
|
+
return buildKcc20WalletOperationFromPlan(
|
|
332
|
+
wallet,
|
|
333
|
+
{
|
|
334
|
+
builderKey: "kcc20.consolidate-holders",
|
|
335
|
+
operation: "consolidate-native-holders",
|
|
336
|
+
contract: "KCC20",
|
|
337
|
+
action: "consolidate",
|
|
338
|
+
description: "Build a KCC20 holder consolidation transaction request.",
|
|
339
|
+
params: {
|
|
340
|
+
covenantId,
|
|
341
|
+
appCanonicalCovenantId: covenantId,
|
|
342
|
+
activeHolderNativeCovenantId: nativeCovenantId,
|
|
343
|
+
tokenAmount: total,
|
|
344
|
+
ticker: input.token.ticker ?? null,
|
|
345
|
+
tokenName: input.token.tokenName ?? null,
|
|
346
|
+
activeHolderUtxos: selected,
|
|
347
|
+
},
|
|
348
|
+
sourceRequirements: [
|
|
349
|
+
{
|
|
350
|
+
type: "kcc20-holder-utxo-batch",
|
|
351
|
+
covenantId,
|
|
352
|
+
activeHolderNativeCovenantId: nativeCovenantId,
|
|
353
|
+
owner,
|
|
354
|
+
selectedOutpoints: selected.map((utxo) => utxo.outpoint),
|
|
355
|
+
selectedAmounts: selected.map((utxo) => utxo.state?.["amount"]),
|
|
356
|
+
aggregateAmount: summary.aggregateAmount,
|
|
357
|
+
utxoCount: summary.utxoCount,
|
|
358
|
+
purpose:
|
|
359
|
+
"merge wallet-owned KCC20 holder outputs into one holder output",
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
type: "kaspa-funding-utxo",
|
|
363
|
+
ownerAddress: wallet.walletAddress,
|
|
364
|
+
purpose: "fund network fee for holder consolidation",
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
warnings: [
|
|
368
|
+
"Consolidation merges between two and eight wallet-owned holder UTXOs into one larger holder UTXO.",
|
|
369
|
+
"Repeat consolidation when more compatible holder UTXOs remain.",
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
options,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function requireHex32(value: string, field: string): string {
|
|
377
|
+
const normalized = String(value ?? "")
|
|
378
|
+
.trim()
|
|
379
|
+
.toLowerCase()
|
|
380
|
+
.replace(/^0x/, "");
|
|
381
|
+
if (!/^[a-f0-9]{64}$/.test(normalized))
|
|
382
|
+
throw new Error(`${field} must be 64 hex characters`);
|
|
383
|
+
return normalized;
|
|
384
|
+
}
|
package/src/trade-fee.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
kcc20DecimalsFromDisplayScale,
|
|
3
|
+
kcc20DisplayScaleForDecimals,
|
|
4
|
+
} from "./token-amount.js";
|
|
5
|
+
import {
|
|
6
|
+
KCC20_DEFAULT_PRICE_SCALE,
|
|
7
|
+
KCC20_DEFAULT_PROTOCOL_FEE_BPS,
|
|
8
|
+
KCC20_MIN_PROTOCOL_FEE_SOMPI,
|
|
9
|
+
} from "./protocol.js";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
KCC20_DEFAULT_PRICE_SCALE,
|
|
13
|
+
KCC20_DEFAULT_PROTOCOL_FEE_BPS,
|
|
14
|
+
KCC20_MIN_PROTOCOL_FEE_SOMPI,
|
|
15
|
+
} from "./protocol.js";
|
|
16
|
+
|
|
17
|
+
export interface Kcc20TradeFeeQuote {
|
|
18
|
+
priceScale: string;
|
|
19
|
+
grossSompi: string;
|
|
20
|
+
protocolFeeBps: number;
|
|
21
|
+
minimumProtocolFeeSompi: string;
|
|
22
|
+
estimatedProtocolFeeSompi: string;
|
|
23
|
+
protocolFeeSompi: string;
|
|
24
|
+
feeTicketApplied: boolean;
|
|
25
|
+
feePayer: "buyer" | "seller" | "matcher" | "none";
|
|
26
|
+
feeTiming: "order-creation" | "fill" | "cross-match";
|
|
27
|
+
buyerPaysSompi?: string;
|
|
28
|
+
buyerEscrowSompi?: string;
|
|
29
|
+
buyerTradeEscrowSompi?: string;
|
|
30
|
+
temporaryKasDepositSompi?: string;
|
|
31
|
+
sellerReceivesSompi?: string;
|
|
32
|
+
matcherPaysSompi?: string;
|
|
33
|
+
buyerSurplusRefundSompi?: string;
|
|
34
|
+
note: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildCreateOrderFeeQuote(input: {
|
|
38
|
+
side: "buy" | "sell";
|
|
39
|
+
tokenAmount: string;
|
|
40
|
+
unitPriceSompi: string;
|
|
41
|
+
priceScale?: unknown;
|
|
42
|
+
temporaryKasDepositSompi?: string;
|
|
43
|
+
}): Kcc20TradeFeeQuote {
|
|
44
|
+
const priceScale = normalizePriceScale(input.priceScale);
|
|
45
|
+
const gross = exactGross(input.tokenAmount, input.unitPriceSompi, priceScale);
|
|
46
|
+
const estimatedFee = protocolFee(gross);
|
|
47
|
+
const deposit =
|
|
48
|
+
input.side === "buy"
|
|
49
|
+
? unsigned(
|
|
50
|
+
input.temporaryKasDepositSompi ?? "0",
|
|
51
|
+
"temporaryKasDepositSompi",
|
|
52
|
+
)
|
|
53
|
+
: 0n;
|
|
54
|
+
return {
|
|
55
|
+
priceScale: priceScale.toString(),
|
|
56
|
+
grossSompi: gross.toString(),
|
|
57
|
+
protocolFeeBps: Number(KCC20_DEFAULT_PROTOCOL_FEE_BPS),
|
|
58
|
+
minimumProtocolFeeSompi: KCC20_MIN_PROTOCOL_FEE_SOMPI.toString(),
|
|
59
|
+
estimatedProtocolFeeSompi: estimatedFee.toString(),
|
|
60
|
+
protocolFeeSompi: "0",
|
|
61
|
+
feeTicketApplied: false,
|
|
62
|
+
feePayer: "none",
|
|
63
|
+
feeTiming: "order-creation",
|
|
64
|
+
...(input.side === "buy"
|
|
65
|
+
? {
|
|
66
|
+
buyerEscrowSompi: (gross + deposit).toString(),
|
|
67
|
+
buyerTradeEscrowSompi: gross.toString(),
|
|
68
|
+
temporaryKasDepositSompi: deposit.toString(),
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
note:
|
|
72
|
+
input.side === "buy"
|
|
73
|
+
? "Buy order creation escrows gross KAS plus a Temporary KAS deposit that follows the buyer holder and is returned when that holder lifecycle ends; the protocol fee is paid at settlement."
|
|
74
|
+
: "Sell order creation locks wrapped tokens; protocol fee is paid at settlement.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildStandaloneFillFeeQuote(input: {
|
|
79
|
+
side: "buy" | "sell";
|
|
80
|
+
tokenAmount: string;
|
|
81
|
+
unitPriceSompi: string;
|
|
82
|
+
priceScale?: unknown;
|
|
83
|
+
feeTicketApplied: boolean;
|
|
84
|
+
}): Kcc20TradeFeeQuote {
|
|
85
|
+
const priceScale = normalizePriceScale(input.priceScale);
|
|
86
|
+
const gross = exactGross(input.tokenAmount, input.unitPriceSompi, priceScale);
|
|
87
|
+
const estimatedFee = protocolFee(gross);
|
|
88
|
+
const fee = input.feeTicketApplied ? 0n : estimatedFee;
|
|
89
|
+
if (input.side === "buy") {
|
|
90
|
+
return {
|
|
91
|
+
priceScale: priceScale.toString(),
|
|
92
|
+
grossSompi: gross.toString(),
|
|
93
|
+
protocolFeeBps: Number(KCC20_DEFAULT_PROTOCOL_FEE_BPS),
|
|
94
|
+
minimumProtocolFeeSompi: KCC20_MIN_PROTOCOL_FEE_SOMPI.toString(),
|
|
95
|
+
estimatedProtocolFeeSompi: estimatedFee.toString(),
|
|
96
|
+
protocolFeeSompi: fee.toString(),
|
|
97
|
+
feeTicketApplied: input.feeTicketApplied,
|
|
98
|
+
feePayer: "buyer",
|
|
99
|
+
feeTiming: "fill",
|
|
100
|
+
buyerPaysSompi: (gross + fee).toString(),
|
|
101
|
+
sellerReceivesSompi: gross.toString(),
|
|
102
|
+
note: "Buying an ask pays seller gross KAS plus protocol fee unless a FeeTicket is burned.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
priceScale: priceScale.toString(),
|
|
107
|
+
grossSompi: gross.toString(),
|
|
108
|
+
protocolFeeBps: Number(KCC20_DEFAULT_PROTOCOL_FEE_BPS),
|
|
109
|
+
minimumProtocolFeeSompi: KCC20_MIN_PROTOCOL_FEE_SOMPI.toString(),
|
|
110
|
+
estimatedProtocolFeeSompi: estimatedFee.toString(),
|
|
111
|
+
protocolFeeSompi: fee.toString(),
|
|
112
|
+
feeTicketApplied: input.feeTicketApplied,
|
|
113
|
+
feePayer: "seller",
|
|
114
|
+
feeTiming: "fill",
|
|
115
|
+
buyerPaysSompi: gross.toString(),
|
|
116
|
+
sellerReceivesSompi: (gross > fee ? gross - fee : 0n).toString(),
|
|
117
|
+
note: "Selling into a bid receives gross KAS minus protocol fee unless a FeeTicket is burned.",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function calculateKcc20ProtocolFeeSompi(
|
|
122
|
+
grossSompi: bigint,
|
|
123
|
+
feeBps = Number(KCC20_DEFAULT_PROTOCOL_FEE_BPS),
|
|
124
|
+
minimumFeeSompi = KCC20_MIN_PROTOCOL_FEE_SOMPI,
|
|
125
|
+
): bigint {
|
|
126
|
+
if (grossSompi <= 0n || feeBps <= 0) return 0n;
|
|
127
|
+
if (!Number.isInteger(feeBps) || feeBps > 10_000) {
|
|
128
|
+
throw new Error("feeBps must be an integer from 0 to 10000");
|
|
129
|
+
}
|
|
130
|
+
if (minimumFeeSompi < 0n) {
|
|
131
|
+
throw new Error("minimumFeeSompi must not be negative");
|
|
132
|
+
}
|
|
133
|
+
const percentage = (grossSompi * BigInt(feeBps)) / 10_000n;
|
|
134
|
+
return percentage < minimumFeeSompi ? minimumFeeSompi : percentage;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Largest trade gross that fits a buyer budget after the protocol fee and a
|
|
139
|
+
* host-selected network reserve. All values are integer sompi.
|
|
140
|
+
*/
|
|
141
|
+
export function calculateKcc20MaximumBuyerGrossSompi(input: {
|
|
142
|
+
availableSompi: bigint;
|
|
143
|
+
networkReserveSompi?: bigint;
|
|
144
|
+
protocolFeeBps?: number;
|
|
145
|
+
minimumProtocolFeeSompi?: bigint;
|
|
146
|
+
}): bigint {
|
|
147
|
+
const reserve = input.networkReserveSompi ?? 0n;
|
|
148
|
+
const feeBps = input.protocolFeeBps ?? Number(KCC20_DEFAULT_PROTOCOL_FEE_BPS);
|
|
149
|
+
const minimumFee =
|
|
150
|
+
input.minimumProtocolFeeSompi ?? KCC20_MIN_PROTOCOL_FEE_SOMPI;
|
|
151
|
+
if (input.availableSompi < 0n) {
|
|
152
|
+
throw new Error("availableSompi must not be negative");
|
|
153
|
+
}
|
|
154
|
+
if (reserve < 0n) {
|
|
155
|
+
throw new Error("networkReserveSompi must not be negative");
|
|
156
|
+
}
|
|
157
|
+
// Validate even when no balance is available.
|
|
158
|
+
calculateKcc20ProtocolFeeSompi(1n, feeBps, minimumFee);
|
|
159
|
+
const budget =
|
|
160
|
+
input.availableSompi > reserve ? input.availableSompi - reserve : 0n;
|
|
161
|
+
let low = 0n;
|
|
162
|
+
let high = budget;
|
|
163
|
+
while (low < high) {
|
|
164
|
+
const middle = low + (high - low + 1n) / 2n;
|
|
165
|
+
const fee = calculateKcc20ProtocolFeeSompi(middle, feeBps, minimumFee);
|
|
166
|
+
if (middle + fee <= budget) low = middle;
|
|
167
|
+
else high = middle - 1n;
|
|
168
|
+
}
|
|
169
|
+
return low;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Returns the largest base-unit token amount affordable by a buyer. */
|
|
173
|
+
export function calculateKcc20MaximumBuyTokenBaseUnits(input: {
|
|
174
|
+
availableSompi: bigint;
|
|
175
|
+
unitPriceSompi: bigint;
|
|
176
|
+
priceScale: bigint;
|
|
177
|
+
networkReserveSompi?: bigint;
|
|
178
|
+
protocolFeeBps?: number;
|
|
179
|
+
minimumProtocolFeeSompi?: bigint;
|
|
180
|
+
}): bigint {
|
|
181
|
+
if (input.unitPriceSompi <= 0n) return 0n;
|
|
182
|
+
if (input.priceScale <= 0n) {
|
|
183
|
+
throw new Error("priceScale must be greater than zero");
|
|
184
|
+
}
|
|
185
|
+
const gross = calculateKcc20MaximumBuyerGrossSompi(input);
|
|
186
|
+
return (gross * input.priceScale) / input.unitPriceSompi;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function buildCrossMatchFeeQuote(input: {
|
|
190
|
+
tokenAmount: string;
|
|
191
|
+
askUnitPriceSompi: string;
|
|
192
|
+
bidUnitPriceSompi: string;
|
|
193
|
+
priceScale?: unknown;
|
|
194
|
+
}): Kcc20TradeFeeQuote {
|
|
195
|
+
const priceScale = normalizePriceScale(input.priceScale);
|
|
196
|
+
const askUnitPrice = unsigned(input.askUnitPriceSompi, "askUnitPriceSompi");
|
|
197
|
+
const bidUnitPrice = unsigned(input.bidUnitPriceSompi, "bidUnitPriceSompi");
|
|
198
|
+
const gross = exactGross(
|
|
199
|
+
input.tokenAmount,
|
|
200
|
+
input.askUnitPriceSompi,
|
|
201
|
+
priceScale,
|
|
202
|
+
);
|
|
203
|
+
const fee = protocolFee(gross);
|
|
204
|
+
const surplus =
|
|
205
|
+
bidUnitPrice > askUnitPrice
|
|
206
|
+
? exactGross(
|
|
207
|
+
input.tokenAmount,
|
|
208
|
+
(bidUnitPrice - askUnitPrice).toString(),
|
|
209
|
+
priceScale,
|
|
210
|
+
)
|
|
211
|
+
: 0n;
|
|
212
|
+
return {
|
|
213
|
+
priceScale: priceScale.toString(),
|
|
214
|
+
grossSompi: gross.toString(),
|
|
215
|
+
protocolFeeBps: Number(KCC20_DEFAULT_PROTOCOL_FEE_BPS),
|
|
216
|
+
minimumProtocolFeeSompi: KCC20_MIN_PROTOCOL_FEE_SOMPI.toString(),
|
|
217
|
+
estimatedProtocolFeeSompi: fee.toString(),
|
|
218
|
+
protocolFeeSompi: fee.toString(),
|
|
219
|
+
feeTicketApplied: false,
|
|
220
|
+
feePayer: "matcher",
|
|
221
|
+
feeTiming: "cross-match",
|
|
222
|
+
buyerPaysSompi: (gross + surplus).toString(),
|
|
223
|
+
sellerReceivesSompi: gross.toString(),
|
|
224
|
+
matcherPaysSompi: fee.toString(),
|
|
225
|
+
buyerSurplusRefundSompi: surplus.toString(),
|
|
226
|
+
note: "Crossed settlement currently pays seller gross and refunds all buyer surplus; the backend matcher funds the protocol fee under the deployed covenant artifact.",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizePriceScale(value: unknown): bigint {
|
|
231
|
+
if (value === undefined || value === null) return KCC20_DEFAULT_PRICE_SCALE;
|
|
232
|
+
const decimals = kcc20DecimalsFromDisplayScale(value);
|
|
233
|
+
if (decimals !== null) return kcc20DisplayScaleForDecimals(decimals);
|
|
234
|
+
throw new Error(
|
|
235
|
+
"priceScale must be an exact power-of-ten display scale for decimals 0-8",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function exactGross(
|
|
240
|
+
amountValue: string,
|
|
241
|
+
priceValue: string,
|
|
242
|
+
scale: bigint,
|
|
243
|
+
): bigint {
|
|
244
|
+
const product =
|
|
245
|
+
unsigned(amountValue, "tokenAmount") *
|
|
246
|
+
unsigned(priceValue, "unitPriceSompi");
|
|
247
|
+
if (product % scale !== 0n)
|
|
248
|
+
throw new Error("trade gross amount is not an exact sompi value");
|
|
249
|
+
return product / scale;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function protocolFee(gross: bigint): bigint {
|
|
253
|
+
const percentage = (gross * KCC20_DEFAULT_PROTOCOL_FEE_BPS) / 10_000n;
|
|
254
|
+
return percentage < KCC20_MIN_PROTOCOL_FEE_SOMPI
|
|
255
|
+
? KCC20_MIN_PROTOCOL_FEE_SOMPI
|
|
256
|
+
: percentage;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function unsigned(value: unknown, field: string): bigint {
|
|
260
|
+
const raw = String(value ?? "").trim();
|
|
261
|
+
if (!/^\d+$/.test(raw))
|
|
262
|
+
throw new Error(`${field} must be an unsigned integer string`);
|
|
263
|
+
return BigInt(raw);
|
|
264
|
+
}
|