@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,12 @@
|
|
|
1
|
+
import type { BuildSources, Kcc20PsktBuildRequest } from "./models.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application-specific source preparation belongs outside the protocol core.
|
|
5
|
+
*
|
|
6
|
+
* The backend implementation can satisfy this interface from its indexer/RPC
|
|
7
|
+
* providers. The frontend implementation can satisfy it from API snapshots
|
|
8
|
+
* and wallet-owned UTXOs. The builder itself only consumes the returned data.
|
|
9
|
+
*/
|
|
10
|
+
export interface Kcc20SourceProvider {
|
|
11
|
+
getSources(request: Kcc20PsktBuildRequest): Promise<BuildSources>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Kcc20BuilderError } from "./errors.js";
|
|
2
|
+
import type { BuildSources, TransactionOutpoint } from "./models.js";
|
|
3
|
+
|
|
4
|
+
export interface SourceSnapshotMetadata {
|
|
5
|
+
snapshotId: string;
|
|
6
|
+
expiresAt?: string;
|
|
7
|
+
revision?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function outpointKey(outpoint: TransactionOutpoint): string {
|
|
11
|
+
return `${outpoint.txidHex.toLowerCase()}:${outpoint.vout}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function assertUniqueSources(sources: BuildSources): void {
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
const all = [
|
|
17
|
+
...(sources.covenantUtxos ?? []),
|
|
18
|
+
...sources.walletUtxos,
|
|
19
|
+
...(sources.optionalFeeTicketUtxos ?? []),
|
|
20
|
+
];
|
|
21
|
+
for (const source of all) {
|
|
22
|
+
const key = outpointKey(source);
|
|
23
|
+
if (seen.has(key)) {
|
|
24
|
+
throw new Kcc20BuilderError(
|
|
25
|
+
"INVALID_INPUT",
|
|
26
|
+
`duplicate source outpoint: ${key}`,
|
|
27
|
+
{ outpoint: key },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
seen.add(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function assertSnapshotFresh(
|
|
35
|
+
metadata: SourceSnapshotMetadata | undefined,
|
|
36
|
+
now = Date.now(),
|
|
37
|
+
): void {
|
|
38
|
+
if (!metadata?.expiresAt) return;
|
|
39
|
+
const expiresAt = Date.parse(metadata.expiresAt);
|
|
40
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
41
|
+
throw new Kcc20BuilderError(
|
|
42
|
+
"STALE_SOURCE",
|
|
43
|
+
"source snapshot is missing a valid future expiry",
|
|
44
|
+
{ snapshotId: metadata.snapshotId },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
export const KCC20_FIXED_DECIMALS = 8;
|
|
2
|
+
export const KCC20_MAX_DECIMALS = KCC20_FIXED_DECIMALS;
|
|
3
|
+
export const KCC20_LEGACY_DECIMALS = 0;
|
|
4
|
+
export const KCC20_U64_MAX = (1n << 64n) - 1n;
|
|
5
|
+
export const KCC20_DISPLAY_AMOUNT_RE = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
6
|
+
export const KCC20_POSITIVE_DISPLAY_AMOUNT_RE =
|
|
7
|
+
/^(?:(?:[1-9]\d*)(?:\.\d+)?|0\.\d*[1-9]\d*)$/;
|
|
8
|
+
|
|
9
|
+
export function parseKcc20Decimals(value: unknown): number | null {
|
|
10
|
+
const normalized =
|
|
11
|
+
typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
12
|
+
return typeof normalized === "number" &&
|
|
13
|
+
Number.isInteger(normalized) &&
|
|
14
|
+
normalized >= 0 &&
|
|
15
|
+
normalized <= KCC20_MAX_DECIMALS
|
|
16
|
+
? normalized
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeKcc20Decimals(value: unknown): number {
|
|
21
|
+
return parseKcc20Decimals(value) ?? KCC20_LEGACY_DECIMALS;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function kcc20DisplayScaleForDecimals(decimals: number): bigint {
|
|
25
|
+
if (
|
|
26
|
+
!Number.isInteger(decimals) ||
|
|
27
|
+
decimals < 0 ||
|
|
28
|
+
decimals > KCC20_MAX_DECIMALS
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`unsupported KCC20 decimals ${decimals}`);
|
|
31
|
+
}
|
|
32
|
+
return 10n ** BigInt(decimals);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function kcc20SupportedDisplayScales(): bigint[] {
|
|
36
|
+
return Array.from({ length: KCC20_MAX_DECIMALS + 1 }, (_, decimals) =>
|
|
37
|
+
kcc20DisplayScaleForDecimals(decimals),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function kcc20DecimalsFromDisplayScale(value: unknown): number | null {
|
|
42
|
+
const raw =
|
|
43
|
+
typeof value === "bigint" ||
|
|
44
|
+
typeof value === "number" ||
|
|
45
|
+
typeof value === "string"
|
|
46
|
+
? String(value).trim()
|
|
47
|
+
: "";
|
|
48
|
+
if (!/^(0|[1-9]\d*)$/.test(raw)) return null;
|
|
49
|
+
const scale = BigInt(raw);
|
|
50
|
+
for (let decimals = 0; decimals <= KCC20_MAX_DECIMALS; decimals += 1) {
|
|
51
|
+
if (scale === 10n ** BigInt(decimals)) return decimals;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isSupportedKcc20DisplayScale(value: unknown): boolean {
|
|
57
|
+
return kcc20DecimalsFromDisplayScale(value) !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseKcc20DisplayAmountToBaseUnits(
|
|
61
|
+
value: string,
|
|
62
|
+
decimals: number,
|
|
63
|
+
field: string,
|
|
64
|
+
options: { allowZero?: boolean } = {},
|
|
65
|
+
): string {
|
|
66
|
+
const raw = String(value ?? "").trim();
|
|
67
|
+
if (!KCC20_DISPLAY_AMOUNT_RE.test(raw)) {
|
|
68
|
+
throw new Error(`${field} must be a non-negative decimal token amount`);
|
|
69
|
+
}
|
|
70
|
+
if (
|
|
71
|
+
!Number.isInteger(decimals) ||
|
|
72
|
+
decimals < 0 ||
|
|
73
|
+
decimals > KCC20_MAX_DECIMALS
|
|
74
|
+
) {
|
|
75
|
+
throw new Error(`${field} has unsupported token decimals`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const [whole, fraction = ""] = raw.split(".");
|
|
79
|
+
if (decimals === 0 && fraction.length > 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`${field} must be an unsigned integer string for legacy tokens`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (fraction.length > decimals) {
|
|
85
|
+
throw new Error(`${field} supports at most ${decimals} decimal places`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const scale = kcc20DisplayScaleForDecimals(decimals);
|
|
89
|
+
const base =
|
|
90
|
+
BigInt(whole) * scale +
|
|
91
|
+
BigInt((fraction + "0".repeat(decimals)).slice(0, decimals) || "0");
|
|
92
|
+
if (!options.allowZero && base <= 0n) {
|
|
93
|
+
throw new Error(`${field} must be greater than zero`);
|
|
94
|
+
}
|
|
95
|
+
if (base > KCC20_U64_MAX) {
|
|
96
|
+
throw new Error(`${field} exceeds u64 max`);
|
|
97
|
+
}
|
|
98
|
+
return base.toString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function formatKcc20BaseUnitsForDisplay(
|
|
102
|
+
value: string,
|
|
103
|
+
decimals: number,
|
|
104
|
+
): string {
|
|
105
|
+
const raw = String(value ?? "").trim();
|
|
106
|
+
if (!/^\d+$/.test(raw)) return value;
|
|
107
|
+
if (decimals <= 0) return raw;
|
|
108
|
+
const scale = 10n ** BigInt(decimals);
|
|
109
|
+
const amount = BigInt(raw);
|
|
110
|
+
const whole = amount / scale;
|
|
111
|
+
const fraction = (amount % scale)
|
|
112
|
+
.toString()
|
|
113
|
+
.padStart(decimals, "0")
|
|
114
|
+
.replace(/0+$/, "");
|
|
115
|
+
return fraction ? `${whole}.${fraction}` : whole.toString();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface Kcc20OrderTotalSompi {
|
|
119
|
+
tokenAmountBaseUnits: string;
|
|
120
|
+
roundedTokenAmountBaseUnits: string;
|
|
121
|
+
totalSompi: string;
|
|
122
|
+
roundedDown: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Gross KAS value for an indexed fill amount expressed in token base units. */
|
|
126
|
+
export function calculateKcc20FillTotalSompi(
|
|
127
|
+
tokenAmountBaseUnits: unknown,
|
|
128
|
+
unitPriceSompi: unknown,
|
|
129
|
+
decimals: number = KCC20_FIXED_DECIMALS,
|
|
130
|
+
): string | undefined {
|
|
131
|
+
const amount = parseUnsigned(tokenAmountBaseUnits);
|
|
132
|
+
const price = parseUnsigned(unitPriceSompi);
|
|
133
|
+
if (amount === undefined || amount <= 0n || price === undefined) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const scale = kcc20DisplayScaleForDecimals(normalizeKcc20Decimals(decimals));
|
|
137
|
+
return ((amount * price) / scale).toString();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Exact-sompi order calculation. Amount is rounded down to the nearest token
|
|
142
|
+
* base-unit step whose product with unit price is divisible by price scale.
|
|
143
|
+
*/
|
|
144
|
+
export function calculateKcc20OrderTotalSompi(
|
|
145
|
+
tokenAmount: string,
|
|
146
|
+
unitPriceSompi: string,
|
|
147
|
+
decimals: number,
|
|
148
|
+
): Kcc20OrderTotalSompi | undefined {
|
|
149
|
+
let baseUnits: bigint;
|
|
150
|
+
try {
|
|
151
|
+
baseUnits = BigInt(
|
|
152
|
+
parseKcc20DisplayAmountToBaseUnits(
|
|
153
|
+
tokenAmount,
|
|
154
|
+
normalizeKcc20Decimals(decimals),
|
|
155
|
+
"tokenAmount",
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
const price = parseUnsigned(unitPriceSompi);
|
|
162
|
+
if (price === undefined || price <= 0n) return undefined;
|
|
163
|
+
|
|
164
|
+
const priceScale = kcc20DisplayScaleForDecimals(
|
|
165
|
+
normalizeKcc20Decimals(decimals),
|
|
166
|
+
);
|
|
167
|
+
const step = priceScale / gcd(priceScale, price);
|
|
168
|
+
const roundedBaseUnits = baseUnits - (baseUnits % step);
|
|
169
|
+
if (roundedBaseUnits <= 0n) return undefined;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
tokenAmountBaseUnits: baseUnits.toString(),
|
|
173
|
+
roundedTokenAmountBaseUnits: roundedBaseUnits.toString(),
|
|
174
|
+
totalSompi: ((roundedBaseUnits * price) / priceScale).toString(),
|
|
175
|
+
roundedDown: roundedBaseUnits !== baseUnits,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function requirePositiveU64(value: unknown, field: string): string {
|
|
180
|
+
const raw = String(value ?? "").trim();
|
|
181
|
+
if (!/^[1-9]\d*$/.test(raw)) {
|
|
182
|
+
throw new Error(`${field} must be greater than zero`);
|
|
183
|
+
}
|
|
184
|
+
if (BigInt(raw) > KCC20_U64_MAX) {
|
|
185
|
+
throw new Error(`${field} exceeds u64 max`);
|
|
186
|
+
}
|
|
187
|
+
return raw;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function normalizeKcc20OwnerIdentifier(
|
|
191
|
+
value: string,
|
|
192
|
+
field = "owner",
|
|
193
|
+
): string {
|
|
194
|
+
const normalized = String(value ?? "")
|
|
195
|
+
.trim()
|
|
196
|
+
.toLowerCase();
|
|
197
|
+
if (/^[a-f0-9]{64}$/.test(normalized)) {
|
|
198
|
+
return normalized;
|
|
199
|
+
}
|
|
200
|
+
const decoded = ownerFromKaspaAddress(normalized);
|
|
201
|
+
if (decoded) return decoded;
|
|
202
|
+
throw new Error(`${field} must be a 64 hex owner or Kaspa P2PK address`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const KASPA_ADDRESS_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
206
|
+
const KASPA_ADDRESS_GENERATORS = [
|
|
207
|
+
0x98f2bc8e61n,
|
|
208
|
+
0x79b76d99e2n,
|
|
209
|
+
0xf33e5fb3c4n,
|
|
210
|
+
0xae2eabe2a8n,
|
|
211
|
+
0x1e4f43e470n,
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
function ownerFromKaspaAddress(value: string): string | undefined {
|
|
215
|
+
const [prefix, payload, extra] = value.split(":");
|
|
216
|
+
if (!prefix || !payload || extra !== undefined) return undefined;
|
|
217
|
+
const data: number[] = [];
|
|
218
|
+
for (const char of payload) {
|
|
219
|
+
const index = KASPA_ADDRESS_CHARSET.indexOf(char);
|
|
220
|
+
if (index < 0) return undefined;
|
|
221
|
+
data.push(index);
|
|
222
|
+
}
|
|
223
|
+
if (
|
|
224
|
+
data.length <= 8 ||
|
|
225
|
+
kaspaAddressPolymod(kaspaAddressPrefixExpand(prefix).concat(data)) !== 1n
|
|
226
|
+
) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const bytes = convertBits(data.slice(0, -8), 5, 8, false);
|
|
230
|
+
if (!bytes || bytes.length !== 33 || (bytes[0] !== 0 && bytes[0] !== 1)) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
return bytes
|
|
234
|
+
.slice(1)
|
|
235
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
236
|
+
.join("");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function kaspaAddressPrefixExpand(prefix: string): number[] {
|
|
240
|
+
return [...prefix].map((char) => char.charCodeAt(0) & 0x1f).concat([0]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function kaspaAddressPolymod(values: number[]): bigint {
|
|
244
|
+
let checksum = 1n;
|
|
245
|
+
for (const value of values) {
|
|
246
|
+
const top = checksum >> 35n;
|
|
247
|
+
checksum = ((checksum & 0x07ffffffffn) << 5n) ^ BigInt(value);
|
|
248
|
+
for (let index = 0; index < KASPA_ADDRESS_GENERATORS.length; index += 1) {
|
|
249
|
+
if (((top >> BigInt(index)) & 1n) === 1n) {
|
|
250
|
+
checksum ^= KASPA_ADDRESS_GENERATORS[index];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return checksum;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function convertBits(
|
|
258
|
+
data: number[],
|
|
259
|
+
fromBits: number,
|
|
260
|
+
toBits: number,
|
|
261
|
+
pad: boolean,
|
|
262
|
+
): number[] | undefined {
|
|
263
|
+
let accumulator = 0;
|
|
264
|
+
let bits = 0;
|
|
265
|
+
const result: number[] = [];
|
|
266
|
+
const maxValue = (1 << toBits) - 1;
|
|
267
|
+
const maxAccumulator = (1 << (fromBits + toBits - 1)) - 1;
|
|
268
|
+
for (const value of data) {
|
|
269
|
+
if (value < 0 || value >> fromBits) return undefined;
|
|
270
|
+
accumulator = ((accumulator << fromBits) | value) & maxAccumulator;
|
|
271
|
+
bits += fromBits;
|
|
272
|
+
while (bits >= toBits) {
|
|
273
|
+
bits -= toBits;
|
|
274
|
+
result.push((accumulator >> bits) & maxValue);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (pad && bits > 0) {
|
|
278
|
+
result.push((accumulator << (toBits - bits)) & maxValue);
|
|
279
|
+
} else if (
|
|
280
|
+
!pad &&
|
|
281
|
+
(bits >= fromBits || ((accumulator << (toBits - bits)) & maxValue) !== 0)
|
|
282
|
+
) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseUnsigned(value: unknown): bigint | undefined {
|
|
289
|
+
const raw = String(value ?? "").trim();
|
|
290
|
+
return /^\d+$/.test(raw) ? BigInt(raw) : undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function gcd(left: bigint, right: bigint): bigint {
|
|
294
|
+
let a = left < 0n ? -left : left;
|
|
295
|
+
let b = right < 0n ? -right : right;
|
|
296
|
+
while (b !== 0n) {
|
|
297
|
+
const remainder = a % b;
|
|
298
|
+
a = b;
|
|
299
|
+
b = remainder;
|
|
300
|
+
}
|
|
301
|
+
return a;
|
|
302
|
+
}
|