@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,660 @@
|
|
|
1
|
+
export interface Kcc20IndexedCovenantUtxo {
|
|
2
|
+
[field: string]: unknown;
|
|
3
|
+
txidHex: string;
|
|
4
|
+
vout: number;
|
|
5
|
+
outpoint?: string;
|
|
6
|
+
address: string;
|
|
7
|
+
amountSompi: string;
|
|
8
|
+
action?: string;
|
|
9
|
+
state: Record<string, unknown> | null;
|
|
10
|
+
/** Native covenant id, when the host enriched it from Kaspa RPC. */
|
|
11
|
+
covenantId?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Kcc20HolderUtxoSummary {
|
|
15
|
+
requestedAmount: string;
|
|
16
|
+
aggregateAmount: string;
|
|
17
|
+
maxSingleUtxoAmount: string;
|
|
18
|
+
utxoCount: number;
|
|
19
|
+
fragmented: boolean;
|
|
20
|
+
maxSingleUtxoOutpoint?: string;
|
|
21
|
+
recommendedAction?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Kcc20WrapperSourceInfo {
|
|
25
|
+
wrapperId: string;
|
|
26
|
+
marketId: string;
|
|
27
|
+
canonicalTokenId?: string | null;
|
|
28
|
+
contractCanonicalTokenId?: string | null;
|
|
29
|
+
activeCovenantId?: string | null;
|
|
30
|
+
wrapperAddress?: string | null;
|
|
31
|
+
feeTicketId?: string | null;
|
|
32
|
+
feeTicketSupported?: boolean;
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
claimedCanonicalTokenId?: string | null;
|
|
35
|
+
pendingReveal?: boolean;
|
|
36
|
+
activeUtxos?: number | null;
|
|
37
|
+
buildable?: boolean;
|
|
38
|
+
buildableReason?: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Kcc20ActiveWrapperResolution {
|
|
42
|
+
selected?: Kcc20IndexedCovenantUtxo;
|
|
43
|
+
candidates: Kcc20IndexedCovenantUtxo[];
|
|
44
|
+
ambiguous: boolean;
|
|
45
|
+
activeCovenantId?: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Kcc20FeeTicketOutpointLike {
|
|
49
|
+
outpoint: string;
|
|
50
|
+
amountSompi: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const KCC20_MIN_CONSOLIDATION_INPUTS = 2;
|
|
54
|
+
export const KCC20_MAX_CONSOLIDATION_INPUTS = 8;
|
|
55
|
+
|
|
56
|
+
export function kcc20IndexedUtxoOutpoint(
|
|
57
|
+
utxo: Pick<Kcc20IndexedCovenantUtxo, "txidHex" | "vout" | "outpoint">,
|
|
58
|
+
): string {
|
|
59
|
+
return (utxo.outpoint ?? `${utxo.txidHex}:${utxo.vout}`).toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function normalizeKcc20IndexedUtxos(
|
|
63
|
+
values: readonly unknown[],
|
|
64
|
+
): Kcc20IndexedCovenantUtxo[] {
|
|
65
|
+
const byOutpoint = new Map<string, Kcc20IndexedCovenantUtxo>();
|
|
66
|
+
for (const value of values) {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
68
|
+
const row = value as Record<string, unknown>;
|
|
69
|
+
const txidHex = hex32(row["txidHex"]);
|
|
70
|
+
const vout = Number(row["vout"]);
|
|
71
|
+
const address = typeof row["address"] === "string" ? row["address"] : "";
|
|
72
|
+
const amountSompi = unsigned(row["amountSompi"]);
|
|
73
|
+
if (
|
|
74
|
+
!txidHex ||
|
|
75
|
+
!Number.isInteger(vout) ||
|
|
76
|
+
vout < 0 ||
|
|
77
|
+
!address ||
|
|
78
|
+
amountSompi === null
|
|
79
|
+
) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const rawState = record(row["state"]);
|
|
83
|
+
const state = rawState ? normalizeState(rawState) : null;
|
|
84
|
+
const covenantId = hex32(row["covenantId"]);
|
|
85
|
+
const normalized: Kcc20IndexedCovenantUtxo = {
|
|
86
|
+
txidHex,
|
|
87
|
+
vout,
|
|
88
|
+
outpoint: `${txidHex}:${vout}`,
|
|
89
|
+
address,
|
|
90
|
+
amountSompi,
|
|
91
|
+
...(typeof row["action"] === "string" ? { action: row["action"] } : {}),
|
|
92
|
+
state,
|
|
93
|
+
...(covenantId ? { covenantId } : {}),
|
|
94
|
+
};
|
|
95
|
+
const key = kcc20IndexedUtxoOutpoint(normalized);
|
|
96
|
+
const existing = byOutpoint.get(key);
|
|
97
|
+
if (!existing || (!existing.state && normalized.state))
|
|
98
|
+
byOutpoint.set(key, normalized);
|
|
99
|
+
}
|
|
100
|
+
return [...byOutpoint.values()].sort((a, b) =>
|
|
101
|
+
kcc20IndexedUtxoOutpoint(a).localeCompare(kcc20IndexedUtxoOutpoint(b)),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function selectActiveMinterUtxo(
|
|
106
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
107
|
+
requestedAmount: string,
|
|
108
|
+
): Kcc20IndexedCovenantUtxo | null {
|
|
109
|
+
const requested = requiredAmount(requestedAmount);
|
|
110
|
+
return (
|
|
111
|
+
utxos.find((utxo) => {
|
|
112
|
+
const state = utxo.state;
|
|
113
|
+
const remaining = amount(state?.["remainingSupply"]);
|
|
114
|
+
return (
|
|
115
|
+
state?.["isMintAuthority"] === true &&
|
|
116
|
+
remaining !== null &&
|
|
117
|
+
remaining >= requested
|
|
118
|
+
);
|
|
119
|
+
}) ?? null
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function ownerNativeHolderUtxos(
|
|
124
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
125
|
+
owner: string,
|
|
126
|
+
): Kcc20IndexedCovenantUtxo[] {
|
|
127
|
+
const normalizedOwner = owner.toLowerCase();
|
|
128
|
+
return utxos.filter((utxo) => {
|
|
129
|
+
const state = utxo.state;
|
|
130
|
+
const value = amount(state?.["amount"]);
|
|
131
|
+
return (
|
|
132
|
+
value !== null &&
|
|
133
|
+
value > 0n &&
|
|
134
|
+
state?.["isMintAuthority"] !== true &&
|
|
135
|
+
numberValue(state?.["ownerScheme"] ?? state?.["identifierType"]) === 0 &&
|
|
136
|
+
stringValue(
|
|
137
|
+
state?.["ownerIdentifier"] ?? state?.["owner"],
|
|
138
|
+
)?.toLowerCase() === normalizedOwner
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function selectOwnerNativeHolderUtxo(
|
|
144
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
145
|
+
owner: string,
|
|
146
|
+
requestedAmount: string,
|
|
147
|
+
outpoint?: string,
|
|
148
|
+
): Kcc20IndexedCovenantUtxo | null {
|
|
149
|
+
const requested = requiredAmount(requestedAmount);
|
|
150
|
+
const requestedOutpoint = outpoint?.toLowerCase();
|
|
151
|
+
return (
|
|
152
|
+
ownerNativeHolderUtxos(utxos, owner)
|
|
153
|
+
.filter(
|
|
154
|
+
(utxo) =>
|
|
155
|
+
!requestedOutpoint ||
|
|
156
|
+
kcc20IndexedUtxoOutpoint(utxo) === requestedOutpoint,
|
|
157
|
+
)
|
|
158
|
+
.filter((utxo) => amount(utxo.state?.["amount"])! >= requested)
|
|
159
|
+
.sort(compareStateAmount)[0] ?? null
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function ownerWrappedHolderUtxos(
|
|
164
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
165
|
+
owner: string,
|
|
166
|
+
canonicalTokenId: string,
|
|
167
|
+
): Kcc20IndexedCovenantUtxo[] {
|
|
168
|
+
const normalizedOwner = owner.toLowerCase();
|
|
169
|
+
const canonical = canonicalTokenId.toLowerCase();
|
|
170
|
+
return utxos.filter((utxo) => {
|
|
171
|
+
const state = utxo.state;
|
|
172
|
+
const value = amount(state?.["amount"]);
|
|
173
|
+
return (
|
|
174
|
+
value !== null &&
|
|
175
|
+
value > 0n &&
|
|
176
|
+
numberValue(state?.["mode"] ?? state?.["stateMode"]) === 0 &&
|
|
177
|
+
numberValue(state?.["identifierType"] ?? state?.["ownerScheme"]) === 0 &&
|
|
178
|
+
stringValue(
|
|
179
|
+
state?.["ownerIdentifier"] ?? state?.["owner"],
|
|
180
|
+
)?.toLowerCase() === normalizedOwner &&
|
|
181
|
+
stringValue(
|
|
182
|
+
state?.["canonicalTokenId"] ?? state?.["tokenId"],
|
|
183
|
+
)?.toLowerCase() === canonical
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function selectOwnerWrappedHolderUtxo(
|
|
189
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
190
|
+
owner: string,
|
|
191
|
+
requestedAmount: string,
|
|
192
|
+
canonicalTokenId: string,
|
|
193
|
+
largest = false,
|
|
194
|
+
): Kcc20IndexedCovenantUtxo | null {
|
|
195
|
+
const requested = requiredAmount(requestedAmount);
|
|
196
|
+
const candidates = ownerWrappedHolderUtxos(utxos, owner, canonicalTokenId)
|
|
197
|
+
.filter((utxo) => amount(utxo.state?.["amount"])! >= requested)
|
|
198
|
+
.sort(compareStateAmount);
|
|
199
|
+
return (largest ? candidates.at(-1) : candidates[0]) ?? null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function selectOwnerWrappedHolderUtxosForSweep(
|
|
203
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
204
|
+
owner: string,
|
|
205
|
+
requestedAmounts: readonly string[],
|
|
206
|
+
canonicalTokenId: string,
|
|
207
|
+
): Kcc20IndexedCovenantUtxo[] {
|
|
208
|
+
const candidates = ownerWrappedHolderUtxos(
|
|
209
|
+
utxos,
|
|
210
|
+
owner,
|
|
211
|
+
canonicalTokenId,
|
|
212
|
+
).sort(compareStateAmount);
|
|
213
|
+
const selected: Kcc20IndexedCovenantUtxo[] = [];
|
|
214
|
+
const used = new Set<string>();
|
|
215
|
+
|
|
216
|
+
for (const requestedText of requestedAmounts) {
|
|
217
|
+
const requested = amount(requestedText);
|
|
218
|
+
if (requested === null) return selected;
|
|
219
|
+
const match = candidates.find(
|
|
220
|
+
(utxo) =>
|
|
221
|
+
!used.has(kcc20IndexedUtxoOutpoint(utxo)) &&
|
|
222
|
+
amount(utxo.state?.["amount"])! >= requested,
|
|
223
|
+
);
|
|
224
|
+
if (!match) return selected;
|
|
225
|
+
used.add(kcc20IndexedUtxoOutpoint(match));
|
|
226
|
+
selected.push(match);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return selected;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function selectWrappedRootUtxo(
|
|
233
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
234
|
+
canonicalTokenId: string,
|
|
235
|
+
): Kcc20IndexedCovenantUtxo | null {
|
|
236
|
+
const canonical = canonicalTokenId.toLowerCase();
|
|
237
|
+
return (
|
|
238
|
+
utxos.find(
|
|
239
|
+
(utxo) =>
|
|
240
|
+
numberValue(utxo.state?.["mode"] ?? utxo.state?.["stateMode"]) === 1 &&
|
|
241
|
+
stringValue(
|
|
242
|
+
utxo.state?.["canonicalTokenId"] ?? utxo.state?.["tokenId"],
|
|
243
|
+
)?.toLowerCase() === canonical,
|
|
244
|
+
) ?? null
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function selectWrappedOrderUtxo(
|
|
249
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
250
|
+
input: {
|
|
251
|
+
orderId: string;
|
|
252
|
+
mode: 2 | 3;
|
|
253
|
+
canonicalTokenId: string;
|
|
254
|
+
requestedAmount?: string;
|
|
255
|
+
unitPriceSompi?: string;
|
|
256
|
+
owner?: string;
|
|
257
|
+
},
|
|
258
|
+
): Kcc20IndexedCovenantUtxo | null {
|
|
259
|
+
const canonical = input.canonicalTokenId.toLowerCase();
|
|
260
|
+
const requested = input.requestedAmount
|
|
261
|
+
? requiredAmount(input.requestedAmount)
|
|
262
|
+
: null;
|
|
263
|
+
const owner = input.owner?.toLowerCase();
|
|
264
|
+
return (
|
|
265
|
+
utxos.find((utxo) => {
|
|
266
|
+
const state = utxo.state;
|
|
267
|
+
const value = amount(state?.["amount"]);
|
|
268
|
+
return (
|
|
269
|
+
kcc20IndexedUtxoOutpoint(utxo) === input.orderId.toLowerCase() &&
|
|
270
|
+
numberValue(state?.["mode"] ?? state?.["stateMode"]) === input.mode &&
|
|
271
|
+
stringValue(
|
|
272
|
+
state?.["canonicalTokenId"] ?? state?.["tokenId"],
|
|
273
|
+
)?.toLowerCase() === canonical &&
|
|
274
|
+
(!input.unitPriceSompi ||
|
|
275
|
+
stringValue(state?.["unitPriceSompi"]) === input.unitPriceSompi) &&
|
|
276
|
+
(requested === null || (value !== null && value >= requested)) &&
|
|
277
|
+
(!owner ||
|
|
278
|
+
stringValue(
|
|
279
|
+
state?.["ownerIdentifier"] ?? state?.["owner"],
|
|
280
|
+
)?.toLowerCase() === owner)
|
|
281
|
+
);
|
|
282
|
+
}) ?? null
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function wrapperReserveUtxos(
|
|
287
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
288
|
+
wrapperIds: readonly string[],
|
|
289
|
+
): Kcc20IndexedCovenantUtxo[] {
|
|
290
|
+
const ids = new Set(
|
|
291
|
+
wrapperIds
|
|
292
|
+
.map((id) => id?.toLowerCase())
|
|
293
|
+
.filter((id) => /^[a-f0-9]{64}$/.test(id)),
|
|
294
|
+
);
|
|
295
|
+
return utxos
|
|
296
|
+
.filter((utxo) => {
|
|
297
|
+
const state = utxo.state;
|
|
298
|
+
const value = amount(state?.["amount"]);
|
|
299
|
+
return (
|
|
300
|
+
value !== null &&
|
|
301
|
+
value > 0n &&
|
|
302
|
+
state?.["isMintAuthority"] !== true &&
|
|
303
|
+
numberValue(state?.["ownerScheme"] ?? state?.["identifierType"]) ===
|
|
304
|
+
4 &&
|
|
305
|
+
ids.has(
|
|
306
|
+
stringValue(
|
|
307
|
+
state?.["ownerIdentifier"] ?? state?.["owner"],
|
|
308
|
+
)?.toLowerCase() ?? "",
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
})
|
|
312
|
+
.sort(compareStateAmount);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function selectWrapperReserveUtxo(
|
|
316
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
317
|
+
wrapperIds: readonly string[],
|
|
318
|
+
requestedAmount: string,
|
|
319
|
+
): Kcc20IndexedCovenantUtxo | null {
|
|
320
|
+
const requested = requiredAmount(requestedAmount);
|
|
321
|
+
return (
|
|
322
|
+
wrapperReserveUtxos(utxos, wrapperIds).find(
|
|
323
|
+
(utxo) => amount(utxo.state?.["amount"])! >= requested,
|
|
324
|
+
) ?? null
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function resolveActiveWrapperUtxo(
|
|
329
|
+
wrapper: Kcc20WrapperSourceInfo,
|
|
330
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
331
|
+
): Kcc20ActiveWrapperResolution {
|
|
332
|
+
const address = wrapper.wrapperAddress?.toLowerCase();
|
|
333
|
+
const candidates = utxos.filter(
|
|
334
|
+
(utxo) => !address || utxo.address.toLowerCase() === address,
|
|
335
|
+
);
|
|
336
|
+
const activeId = wrapper.activeCovenantId?.toLowerCase();
|
|
337
|
+
const byNativeId = activeId
|
|
338
|
+
? candidates.filter((utxo) => utxo.covenantId?.toLowerCase() === activeId)
|
|
339
|
+
: [];
|
|
340
|
+
const selectable = byNativeId.length ? byNativeId : candidates;
|
|
341
|
+
if (!selectable.length)
|
|
342
|
+
throw new Error("wrapped market has no active wrapper UTXO");
|
|
343
|
+
if (selectable.length > 1) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
"wrapped market has multiple active wrapper UTXOs; refresh the indexer snapshot",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
selected: selectable[0],
|
|
350
|
+
candidates: selectable,
|
|
351
|
+
ambiguous: false,
|
|
352
|
+
...(activeId || selectable[0].covenantId
|
|
353
|
+
? { activeCovenantId: activeId ?? selectable[0].covenantId }
|
|
354
|
+
: {}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function kcc20WrapperBelongsToToken(
|
|
359
|
+
wrapper: Kcc20WrapperSourceInfo,
|
|
360
|
+
canonicalTokenId: string,
|
|
361
|
+
): boolean {
|
|
362
|
+
const expected = canonicalTokenId.toLowerCase();
|
|
363
|
+
const appCanonicalIds = [
|
|
364
|
+
wrapper.canonicalTokenId,
|
|
365
|
+
wrapper.claimedCanonicalTokenId,
|
|
366
|
+
]
|
|
367
|
+
.filter((value): value is string => Boolean(value))
|
|
368
|
+
.map((value) => value.toLowerCase());
|
|
369
|
+
return !appCanonicalIds.length || appCanonicalIds.includes(expected);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function isKcc20WrapperReadyToBuild(
|
|
373
|
+
wrapper: Kcc20WrapperSourceInfo | null | undefined,
|
|
374
|
+
): boolean {
|
|
375
|
+
if (!wrapper?.enabled) return false;
|
|
376
|
+
if (wrapper.pendingReveal === true)
|
|
377
|
+
return wrapper.buildable === true && (wrapper.activeUtxos ?? 0) > 0;
|
|
378
|
+
return wrapper.buildable !== false && Boolean(wrapper.activeCovenantId);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function kcc20WrapperNotReadyReason(
|
|
382
|
+
wrapper: Kcc20WrapperSourceInfo | null | undefined,
|
|
383
|
+
): string {
|
|
384
|
+
if (!wrapper) return "No wrapper is ready to wrap.";
|
|
385
|
+
if (!wrapper.enabled) return "Wrapped trading is disabled for this market.";
|
|
386
|
+
if (wrapper.buildable === false)
|
|
387
|
+
return wrapper.buildableReason?.trim() || "Wrapper is not buildable yet.";
|
|
388
|
+
return "No wrapper is ready to wrap.";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function selectOwnerFeeTicketUtxos(
|
|
392
|
+
utxos: readonly Kcc20IndexedCovenantUtxo[],
|
|
393
|
+
owner: string,
|
|
394
|
+
outpoints?: readonly string[],
|
|
395
|
+
): Kcc20IndexedCovenantUtxo[] {
|
|
396
|
+
const normalizedOwner = hex32(owner);
|
|
397
|
+
if (!normalizedOwner) return [];
|
|
398
|
+
const requested = outpoints
|
|
399
|
+
? new Set(outpoints.map((item) => item.toLowerCase()))
|
|
400
|
+
: null;
|
|
401
|
+
const selected = utxos
|
|
402
|
+
.filter((utxo) => {
|
|
403
|
+
const state = utxo.state;
|
|
404
|
+
return (
|
|
405
|
+
numberValue(state?.["mode"] ?? state?.["stateMode"]) === 2 &&
|
|
406
|
+
feeTicketStateOwner(state) === normalizedOwner &&
|
|
407
|
+
(!requested || requested.has(kcc20IndexedUtxoOutpoint(utxo)))
|
|
408
|
+
);
|
|
409
|
+
})
|
|
410
|
+
.sort((a, b) =>
|
|
411
|
+
kcc20IndexedUtxoOutpoint(a).localeCompare(kcc20IndexedUtxoOutpoint(b)),
|
|
412
|
+
);
|
|
413
|
+
if (requested && selected.length !== requested.size) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
"One or more selected FeeTicket UTXOs are not active for this wallet",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return selected;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function feeTicketStateOwner(
|
|
422
|
+
state: Record<string, unknown> | null,
|
|
423
|
+
): string | null {
|
|
424
|
+
if (!state) return null;
|
|
425
|
+
for (const key of [
|
|
426
|
+
"ownerIdentifier",
|
|
427
|
+
"stateOwner",
|
|
428
|
+
"ticketOwner",
|
|
429
|
+
"owner",
|
|
430
|
+
"recipientOwner",
|
|
431
|
+
"holderOwner",
|
|
432
|
+
]) {
|
|
433
|
+
const owner = hex32(state[key]);
|
|
434
|
+
if (owner) return owner;
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function selectFeeTicketOutpoints(
|
|
440
|
+
tickets: readonly Kcc20FeeTicketOutpointLike[],
|
|
441
|
+
quantity: number,
|
|
442
|
+
options: {
|
|
443
|
+
maxQuantity?: number;
|
|
444
|
+
shortageAction?: string;
|
|
445
|
+
} = {},
|
|
446
|
+
): string[] {
|
|
447
|
+
const maxQuantity = options.maxQuantity ?? 10;
|
|
448
|
+
const shortageAction = options.shortageAction ?? "use";
|
|
449
|
+
if (
|
|
450
|
+
!Number.isInteger(maxQuantity) ||
|
|
451
|
+
maxQuantity < 1 ||
|
|
452
|
+
!Number.isInteger(quantity) ||
|
|
453
|
+
quantity < 1 ||
|
|
454
|
+
quantity > maxQuantity
|
|
455
|
+
) {
|
|
456
|
+
throw new Error(`Coupon quantity must be between 1 and ${maxQuantity}.`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const normalized = tickets.map((ticket) => {
|
|
460
|
+
const outpoint = ticket.outpoint.trim().toLowerCase();
|
|
461
|
+
if (
|
|
462
|
+
!/^[0-9a-f]{64}:\d+$/.test(outpoint) ||
|
|
463
|
+
unsigned(ticket.amountSompi) === null
|
|
464
|
+
) {
|
|
465
|
+
throw new Error("Coupon availability is invalid. Refresh and try again.");
|
|
466
|
+
}
|
|
467
|
+
return { outpoint, collateral: BigInt(ticket.amountSompi) };
|
|
468
|
+
});
|
|
469
|
+
const unique = new Set(normalized.map((ticket) => ticket.outpoint));
|
|
470
|
+
if (unique.size !== normalized.length)
|
|
471
|
+
throw new Error("Coupon availability contains duplicate outpoints.");
|
|
472
|
+
if (normalized.length < quantity) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`Not enough active coupons are available to ${shortageAction}.`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return normalized
|
|
479
|
+
.sort((left, right) =>
|
|
480
|
+
left.collateral < right.collateral
|
|
481
|
+
? -1
|
|
482
|
+
: left.collateral > right.collateral
|
|
483
|
+
? 1
|
|
484
|
+
: left.outpoint.localeCompare(right.outpoint),
|
|
485
|
+
)
|
|
486
|
+
.slice(0, quantity)
|
|
487
|
+
.map((ticket) => ticket.outpoint);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function selectConsolidationBatch(
|
|
491
|
+
candidates: readonly Kcc20IndexedCovenantUtxo[],
|
|
492
|
+
sourceOutpoints?: readonly string[],
|
|
493
|
+
maximum = KCC20_MAX_CONSOLIDATION_INPUTS,
|
|
494
|
+
order: "smallest" | "largest" = "smallest",
|
|
495
|
+
minimum = 1,
|
|
496
|
+
): Kcc20IndexedCovenantUtxo[] {
|
|
497
|
+
const sorted = [...candidates].sort((left, right) =>
|
|
498
|
+
order === "largest"
|
|
499
|
+
? compareStateAmount(right, left)
|
|
500
|
+
: compareStateAmount(left, right),
|
|
501
|
+
);
|
|
502
|
+
if (!sourceOutpoints?.length) return sorted.slice(0, maximum);
|
|
503
|
+
const requested = new Set(
|
|
504
|
+
sourceOutpoints.map((value) => value.toLowerCase()),
|
|
505
|
+
);
|
|
506
|
+
if (sourceOutpoints.length < minimum)
|
|
507
|
+
throw new Error(`at least ${minimum} holder UTXOs must be selected`);
|
|
508
|
+
if (requested.size !== sourceOutpoints.length)
|
|
509
|
+
throw new Error("distinct source outpoints are required");
|
|
510
|
+
const byOutpoint = new Map(
|
|
511
|
+
candidates.map((utxo) => [kcc20IndexedUtxoOutpoint(utxo), utxo]),
|
|
512
|
+
);
|
|
513
|
+
const selected = sourceOutpoints.flatMap((outpoint) => {
|
|
514
|
+
const match = byOutpoint.get(outpoint.toLowerCase());
|
|
515
|
+
return match ? [match] : [];
|
|
516
|
+
});
|
|
517
|
+
if (selected.length !== requested.size)
|
|
518
|
+
throw new Error("selected holder UTXOs are not available");
|
|
519
|
+
if (selected.length > maximum)
|
|
520
|
+
throw new Error(`at most ${maximum} holder UTXOs can be consolidated`);
|
|
521
|
+
return selected;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function summarizeHolderUtxos(
|
|
525
|
+
candidates: readonly Kcc20IndexedCovenantUtxo[],
|
|
526
|
+
requestedAmount: string,
|
|
527
|
+
): Kcc20HolderUtxoSummary {
|
|
528
|
+
const requested = requiredAmount(requestedAmount);
|
|
529
|
+
const amounts = candidates.map(
|
|
530
|
+
(utxo) => amount(utxo.state?.["amount"]) ?? 0n,
|
|
531
|
+
);
|
|
532
|
+
const aggregate = amounts.reduce((sum, value) => sum + value, 0n);
|
|
533
|
+
let max = 0n;
|
|
534
|
+
let maxIndex = -1;
|
|
535
|
+
amounts.forEach((value, index) => {
|
|
536
|
+
if (value > max) {
|
|
537
|
+
max = value;
|
|
538
|
+
maxIndex = index;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
const fragmented = aggregate >= requested && max < requested;
|
|
542
|
+
return {
|
|
543
|
+
requestedAmount: requested.toString(),
|
|
544
|
+
aggregateAmount: aggregate.toString(),
|
|
545
|
+
maxSingleUtxoAmount: max.toString(),
|
|
546
|
+
utxoCount: candidates.length,
|
|
547
|
+
fragmented,
|
|
548
|
+
...(maxIndex >= 0
|
|
549
|
+
? {
|
|
550
|
+
maxSingleUtxoOutpoint: kcc20IndexedUtxoOutpoint(candidates[maxIndex]),
|
|
551
|
+
}
|
|
552
|
+
: {}),
|
|
553
|
+
...(fragmented ? { recommendedAction: "consolidate" } : {}),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function normalizeState(
|
|
558
|
+
state: Record<string, unknown>,
|
|
559
|
+
): Record<string, unknown> {
|
|
560
|
+
const extension = record(state["extension"]);
|
|
561
|
+
const owner = hex32(state["owner"] ?? state["ownerIdentifier"]);
|
|
562
|
+
const ownerScheme = numberValue(
|
|
563
|
+
state["ownerScheme"] ?? state["identifierType"],
|
|
564
|
+
);
|
|
565
|
+
const normalized: Record<string, unknown> = { ...state };
|
|
566
|
+
if (owner) {
|
|
567
|
+
normalized["owner"] = owner;
|
|
568
|
+
normalized["ownerIdentifier"] = owner;
|
|
569
|
+
}
|
|
570
|
+
if (ownerScheme !== null) {
|
|
571
|
+
normalized["ownerScheme"] = ownerScheme;
|
|
572
|
+
normalized["identifierType"] = ownerScheme;
|
|
573
|
+
}
|
|
574
|
+
const amountValue = unsigned(state["amount"]);
|
|
575
|
+
if (amountValue !== null) normalized["amount"] = amountValue;
|
|
576
|
+
const remaining = unsigned(
|
|
577
|
+
state["remainingSupply"] ?? extension?.["remainingSupply"],
|
|
578
|
+
);
|
|
579
|
+
if (remaining !== null) normalized["remainingSupply"] = remaining;
|
|
580
|
+
const canonical = hex32(state["canonicalTokenId"] ?? state["tokenId"]);
|
|
581
|
+
if (canonical) normalized["canonicalTokenId"] = canonical;
|
|
582
|
+
const mode = numberValue(state["mode"] ?? state["stateMode"]);
|
|
583
|
+
if (mode !== null) normalized["mode"] = mode;
|
|
584
|
+
const unitPrice = unsigned(
|
|
585
|
+
state["unitPriceSompi"] ?? state["marketUnitPriceSompi"],
|
|
586
|
+
);
|
|
587
|
+
if (unitPrice !== null) normalized["unitPriceSompi"] = unitPrice;
|
|
588
|
+
const feeTicketId = hex32(state["feeTicketId"]);
|
|
589
|
+
if (feeTicketId) normalized["feeTicketId"] = feeTicketId;
|
|
590
|
+
const displayScale = unsigned(
|
|
591
|
+
state["displayScale"] ??
|
|
592
|
+
state["priceScale"] ??
|
|
593
|
+
state["tokenDisplayScale"] ??
|
|
594
|
+
extension?.["displayScale"],
|
|
595
|
+
);
|
|
596
|
+
if (displayScale !== null) {
|
|
597
|
+
normalized["displayScale"] = displayScale;
|
|
598
|
+
normalized["priceScale"] = displayScale;
|
|
599
|
+
normalized["tokenDisplayScale"] = displayScale;
|
|
600
|
+
}
|
|
601
|
+
return normalized;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function compareStateAmount(
|
|
605
|
+
left: Kcc20IndexedCovenantUtxo,
|
|
606
|
+
right: Kcc20IndexedCovenantUtxo,
|
|
607
|
+
): number {
|
|
608
|
+
const a = amount(left.state?.["amount"]) ?? 0n;
|
|
609
|
+
const b = amount(right.state?.["amount"]) ?? 0n;
|
|
610
|
+
return a < b
|
|
611
|
+
? -1
|
|
612
|
+
: a > b
|
|
613
|
+
? 1
|
|
614
|
+
: kcc20IndexedUtxoOutpoint(left).localeCompare(
|
|
615
|
+
kcc20IndexedUtxoOutpoint(right),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function amount(value: unknown): bigint | null {
|
|
620
|
+
const normalized = unsigned(value);
|
|
621
|
+
return normalized === null ? null : BigInt(normalized);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function requiredAmount(value: unknown): bigint {
|
|
625
|
+
const normalized = amount(value);
|
|
626
|
+
if (normalized === null)
|
|
627
|
+
throw new Error("indexed covenant amount is invalid");
|
|
628
|
+
return normalized;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function unsigned(value: unknown): string | null {
|
|
632
|
+
if (typeof value === "bigint") return value >= 0n ? value.toString() : null;
|
|
633
|
+
if (typeof value === "number")
|
|
634
|
+
return Number.isSafeInteger(value) && value >= 0 ? value.toString() : null;
|
|
635
|
+
return typeof value === "string" && /^(0|[1-9]\d*)$/.test(value)
|
|
636
|
+
? value
|
|
637
|
+
: null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function hex32(value: unknown): string | null {
|
|
641
|
+
if (typeof value !== "string") return null;
|
|
642
|
+
const normalized = value.trim().toLowerCase().replace(/^0x/, "");
|
|
643
|
+
return /^[a-f0-9]{64}$/.test(normalized) ? normalized : null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function record(value: unknown): Record<string, unknown> | null {
|
|
647
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
648
|
+
? (value as Record<string, unknown>)
|
|
649
|
+
: null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function stringValue(value: unknown): string | null {
|
|
653
|
+
return typeof value === "string" ? value : null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function numberValue(value: unknown): number | null {
|
|
657
|
+
const parsed =
|
|
658
|
+
typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
|
|
659
|
+
return typeof parsed === "number" && Number.isInteger(parsed) ? parsed : null;
|
|
660
|
+
}
|