@pulsepairs/sdk 0.9.2 → 0.10.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/CHANGELOG.md +52 -0
- package/DOCUMENTATION.md +4 -1
- package/README.md +32 -3
- package/dist/http.d.ts +33 -0
- package/dist/http.js +77 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/units.d.ts +53 -0
- package/dist/units.js +137 -0
- package/package.json +4 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.10.0 — 2026-09-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Collateral unit helpers**, so an integrator no longer hand-rolls them. The
|
|
8
|
+
venue's decimals are `usdtDecimals` on `GET /config`: **18 on dev (USDR), 6 on
|
|
9
|
+
prod (USDT0)**. Every amount the API sends or accepts is in base units at that
|
|
10
|
+
scale.
|
|
11
|
+
- `client.ensureUsdtDecimals(): Promise<number>` — reads `usdtDecimals` from
|
|
12
|
+
`/config` once per client and caches it; concurrent callers share one
|
|
13
|
+
request. An invalid or missing value (anything but an integer 0–36) rejects
|
|
14
|
+
and is not cached, so the caller can retry.
|
|
15
|
+
- `client.getUsdtDecimals(): number` — the cached value, synchronously. Before
|
|
16
|
+
it is loaded it returns 6 and warns once (a caller bug: await
|
|
17
|
+
`ensureUsdtDecimals()` first).
|
|
18
|
+
- `client.setUsdtDecimals(d)` — stores `d` if valid, e.g. from a `getConfig()`
|
|
19
|
+
response the caller already has.
|
|
20
|
+
- `usdToAtomic(usd, decimals?)` — whole tokens → base units, through a decimal
|
|
21
|
+
string (`parseUnits`), never multiplication, which is inexact at 18 decimals.
|
|
22
|
+
- `atomicToUsd(atomic, decimals?)` — base units → whole tokens via
|
|
23
|
+
`formatUnits`; `null`/`""` → 0; never throws.
|
|
24
|
+
- `roundTripToleranceAtomic(decimals?)` — `10n ** max(0, decimals − 6)`, for
|
|
25
|
+
snapping a MAX sell onto the exact owned balance.
|
|
26
|
+
- `isValidUsdtDecimals(d)` — the validity check the above use.
|
|
27
|
+
|
|
28
|
+
The three standalone helpers default to the decimals the client loaded.
|
|
29
|
+
- **Client-bound forms** — `client.usdToAtomic`, `client.atomicToUsd`,
|
|
30
|
+
`client.roundTripToleranceAtomic` — at that client's own decimals, for a
|
|
31
|
+
process that talks to more than one venue (e.g. dev at 18 and prod at 6).
|
|
32
|
+
Once two clients load different decimals the standalone helpers' shared
|
|
33
|
+
default is ambiguous: `usdToAtomic` and `roundTripToleranceAtomic` then throw
|
|
34
|
+
rather than sign at a guessed scale, and `atomicToUsd` keeps its never-throw
|
|
35
|
+
contract (warns once, returns 0). Explicit `decimals` always works.
|
|
36
|
+
A client only ever uses its OWN decimals: before it has loaded, it answers
|
|
37
|
+
6 and warns once per client, never another client's value. At worst an
|
|
38
|
+
unloaded client under-signs (the venue's $1 minimum rejects it); it can never
|
|
39
|
+
over-sign at another venue's scale.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- The README order example and the five example scripts (`simple-taker`,
|
|
44
|
+
`simple-maker`, `rain-taker`, `account-kit-taker`, `full-dmm-bot`) signed
|
|
45
|
+
amounts with `parseStake`, which is 6-decimal only, so on the 18-decimal dev
|
|
46
|
+
venue they signed 10^12 too little. They now load `usdtDecimals` from the
|
|
47
|
+
config they already fetch and use `client.usdToAtomic`, and check the $1
|
|
48
|
+
minimum against `client.usdToAtomic(1)` instead of `assertStakeBounds`, whose
|
|
49
|
+
minimum is 6-decimal. `parseStake` and `assertStakeBounds` themselves are
|
|
50
|
+
unchanged and documented as 6-decimal only.
|
|
51
|
+
|
|
52
|
+
No existing method changed.
|
package/DOCUMENTATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@pulsepairs/sdk` — Reference Documentation
|
|
2
2
|
|
|
3
|
-
**Version:** 0.
|
|
3
|
+
**Version:** 0.10.0 · **License:** UNLICENSED · **Runtime:** Node ≥ 18 or a modern browser
|
|
4
4
|
|
|
5
5
|
Complete reference for the UpDown TypeScript SDK: every export, its
|
|
6
6
|
units, its failure modes, and the protocol it speaks.
|
|
@@ -729,7 +729,10 @@ fresh nonce. Single-writer bots may prefer a monotonic counter — seed it from
|
|
|
729
729
|
centsToBps(cents: number): number // 55 → 5500; 49.5 → 4950. Throws outside (0,100)
|
|
730
730
|
bpsToCents(bps: number): number // 5500 → 55. Throws outside (0,10000)
|
|
731
731
|
parseStake(usd: string | number): bigint // "5.50" → 5_500_000n. Rejects negative/non-finite
|
|
732
|
+
// 6-DECIMAL ONLY (prod USDT0). For the venue's scale use usdToAtomic.
|
|
732
733
|
assertStakeBounds(amountAtomic: bigint, side?: "BUY" | "SELL"): void
|
|
734
|
+
// 6-DECIMAL ONLY: its $1 minimum is 1_000_000. On other
|
|
735
|
+
// scales compare against client.usdToAtomic(1).
|
|
733
736
|
feeAtomic(notionalAtomic, priceBps, cfg): bigint
|
|
734
737
|
|
|
735
738
|
MIN_STAKE_ATOMIC = 1_000_000n // $1 of share FACE value (BUY only)
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ import { privateKeyToAccount } from "viem/accounts";
|
|
|
83
83
|
import { arbitrum } from "viem/chains";
|
|
84
84
|
import {
|
|
85
85
|
UpDownHttpClient, buildOrderTypedData, ensureSettlementAllowance, freshNonce,
|
|
86
|
-
parseCompositeMarketKey,
|
|
86
|
+
parseCompositeMarketKey, OrderType, OrderSide, Option,
|
|
87
87
|
} from "@pulsepairs/sdk";
|
|
88
88
|
|
|
89
89
|
// Point the bot at a matcher YOU chose. Don't hardcode ours into a process
|
|
@@ -96,7 +96,10 @@ const [live] = (await api.getMarkets({ pair: "BTC-USD", timeframe: 300 }))
|
|
|
96
96
|
.filter((m) => m.status === "ACTIVE");
|
|
97
97
|
const { settlementAddress, marketId } = parseCompositeMarketKey(live.address)!;
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
// Amounts are base units at the venue's decimals (18 on dev, 6 on prod) — see
|
|
100
|
+
// "Collateral units" below. `parseStake` is 6-decimal only (prod USDT0).
|
|
101
|
+
api.setUsdtDecimals(cfg.usdtDecimals); // or: await api.ensureUsdtDecimals()
|
|
102
|
+
const amount = api.usdToAtomic(5); // 5 × 10^decimals
|
|
100
103
|
|
|
101
104
|
// BOUNDED approve. `settlementAddress` is server-supplied (it came from the
|
|
102
105
|
// matcher's market list), so cap its reach at what you actually intend to
|
|
@@ -395,7 +398,7 @@ EIP-712 ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES,
|
|
|
395
398
|
buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData,
|
|
396
399
|
freshSessionId, freshNonce, domainForSettlement,
|
|
397
400
|
findPairBySettlement, parseCompositeMarketKey
|
|
398
|
-
Trade-math centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic,
|
|
401
|
+
Trade-math centsToBps, bpsToCents, parseStake (6-decimal only), assertStakeBounds (6-decimal only), feeAtomic,
|
|
399
402
|
MIN_STAKE_ATOMIC, MAX_STAKE_ATOMIC
|
|
400
403
|
PnL (v0.5.0) UpDownHttpClient.getPnl, positionPnl, portfolioPnl,
|
|
401
404
|
computePortfolioPnl, markValueAtomic, midBps, markForOption,
|
|
@@ -413,6 +416,32 @@ Enums/types OrderType, OrderSide, Option, ApiConfig, PairConfig, PostOrderB
|
|
|
413
416
|
|
|
414
417
|
---
|
|
415
418
|
|
|
419
|
+
## Collateral units (v0.10.0)
|
|
420
|
+
|
|
421
|
+
Amounts are base units at the venue's collateral decimals — `usdtDecimals` on
|
|
422
|
+
`GET /config`: **18 on dev (USDR), 6 on prod (USDT0)**. Load them once, then
|
|
423
|
+
convert with the helpers; never hardcode 6.
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
import { UpDownHttpClient, usdToAtomic, atomicToUsd } from "@pulsepairs/sdk";
|
|
427
|
+
|
|
428
|
+
const client = new UpDownHttpClient("https://dev-api-updown.rain.trade");
|
|
429
|
+
await client.ensureUsdtDecimals(); // 18 on dev; one request, cached
|
|
430
|
+
|
|
431
|
+
usdToAtomic(50); // 50000000000000000000n — sign this as `amount`
|
|
432
|
+
atomicToUsd("101010101010101010101"); // 101.01… — render this
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`usdToAtomic` goes through a decimal string (`parseUnits`), never
|
|
436
|
+
`usd * 10 ** decimals`, which is inexact at 18 decimals. Reading the decimals
|
|
437
|
+
before `ensureUsdtDecimals()` resolves falls back to 6 with a one-time warning.
|
|
438
|
+
|
|
439
|
+
**Talking to more than one venue** (e.g. dev and prod in one process)? Use the
|
|
440
|
+
client-bound forms — `client.usdToAtomic`, `client.atomicToUsd`,
|
|
441
|
+
`client.roundTripToleranceAtomic` — which use that client's own decimals. The
|
|
442
|
+
standalone helpers share one default; once two clients load different decimals
|
|
443
|
+
it is ambiguous, and `usdToAtomic` throws rather than sign at a guessed scale.
|
|
444
|
+
|
|
416
445
|
## Cancel-and-replace, and bulk writes (v0.9.0)
|
|
417
446
|
|
|
418
447
|
**`amendOrder` replaces a resting order in one request.** Doing it as
|
package/dist/http.d.ts
CHANGED
|
@@ -2,7 +2,40 @@ import type { AmendOrderBody, AmendOrderResult, ApiConfig, Balance, BulkAmendBod
|
|
|
2
2
|
import { type PortfolioPnl } from "./pnl.js";
|
|
3
3
|
export declare class UpDownHttpClient {
|
|
4
4
|
private readonly baseUrl;
|
|
5
|
+
/** `usdtDecimals` from /config, once loaded. See `ensureUsdtDecimals`. */
|
|
6
|
+
private usdtDecimals;
|
|
7
|
+
/** The one /config request concurrent `ensureUsdtDecimals` callers share. */
|
|
8
|
+
private usdtDecimalsInflight;
|
|
9
|
+
/** getUsdtDecimals() warns once per client when read before loading. */
|
|
10
|
+
private warnedUnloaded;
|
|
5
11
|
constructor(baseUrl: string);
|
|
12
|
+
/**
|
|
13
|
+
* The venue's collateral decimals (`usdtDecimals` on `GET /config`: 18 on dev,
|
|
14
|
+
* 6 on prod), fetched once per client and cached. Concurrent callers share one
|
|
15
|
+
* request. An invalid or missing value REJECTS rather than being guessed at, and
|
|
16
|
+
* a rejection is not cached, so the caller can retry. Also becomes the default
|
|
17
|
+
* for `usdToAtomic` / `atomicToUsd` / `roundTripToleranceAtomic`.
|
|
18
|
+
*/
|
|
19
|
+
ensureUsdtDecimals(): Promise<number>;
|
|
20
|
+
/**
|
|
21
|
+
* This client's decimals, synchronously. Called before `ensureUsdtDecimals`
|
|
22
|
+
* has resolved, it answers 6 and warns once — that is a bug in the caller. It
|
|
23
|
+
* never reads another client's value.
|
|
24
|
+
*/
|
|
25
|
+
getUsdtDecimals(): number;
|
|
26
|
+
/**
|
|
27
|
+
* `usdToAtomic` / `atomicToUsd` / `roundTripToleranceAtomic` at THIS client's
|
|
28
|
+
* decimals. Use these when a process talks to more than one venue (dev at 18,
|
|
29
|
+
* prod at 6): the standalone helpers share one default, which is ambiguous
|
|
30
|
+
* then. Before this client's `ensureUsdtDecimals` resolves they use 6 and
|
|
31
|
+
* warn once, like `getUsdtDecimals` — never another client's decimals.
|
|
32
|
+
*/
|
|
33
|
+
usdToAtomic(usd: number): bigint;
|
|
34
|
+
atomicToUsd(atomic: bigint | string | number | null | undefined): number;
|
|
35
|
+
roundTripToleranceAtomic(): bigint;
|
|
36
|
+
/** Cache `d` if it is a valid decimal count (integer 0–36); otherwise ignored.
|
|
37
|
+
* Lets a `getConfig()` response the caller already has populate the cache. */
|
|
38
|
+
setUsdtDecimals(d: unknown): void;
|
|
6
39
|
getVersion(): Promise<Version>;
|
|
7
40
|
getHealth(): Promise<{
|
|
8
41
|
status: string;
|
package/dist/http.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computePortfolioPnl, feesPaidByWallet } from "./pnl.js";
|
|
2
|
+
import { atomicToUsd, isValidUsdtDecimals, publishUsdtDecimals, roundTripToleranceAtomic, usdToAtomic, } from "./units.js";
|
|
2
3
|
function buildUrl(base, path, query) {
|
|
3
4
|
const b = base.replace(/\/$/, "");
|
|
4
5
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -31,9 +32,85 @@ async function parseJson(res) {
|
|
|
31
32
|
}
|
|
32
33
|
export class UpDownHttpClient {
|
|
33
34
|
baseUrl;
|
|
35
|
+
/** `usdtDecimals` from /config, once loaded. See `ensureUsdtDecimals`. */
|
|
36
|
+
usdtDecimals;
|
|
37
|
+
/** The one /config request concurrent `ensureUsdtDecimals` callers share. */
|
|
38
|
+
usdtDecimalsInflight;
|
|
39
|
+
/** getUsdtDecimals() warns once per client when read before loading. */
|
|
40
|
+
warnedUnloaded = false;
|
|
34
41
|
constructor(baseUrl) {
|
|
35
42
|
this.baseUrl = baseUrl;
|
|
36
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The venue's collateral decimals (`usdtDecimals` on `GET /config`: 18 on dev,
|
|
46
|
+
* 6 on prod), fetched once per client and cached. Concurrent callers share one
|
|
47
|
+
* request. An invalid or missing value REJECTS rather than being guessed at, and
|
|
48
|
+
* a rejection is not cached, so the caller can retry. Also becomes the default
|
|
49
|
+
* for `usdToAtomic` / `atomicToUsd` / `roundTripToleranceAtomic`.
|
|
50
|
+
*/
|
|
51
|
+
async ensureUsdtDecimals() {
|
|
52
|
+
if (this.usdtDecimals !== undefined)
|
|
53
|
+
return this.usdtDecimals;
|
|
54
|
+
if (!this.usdtDecimalsInflight) {
|
|
55
|
+
this.usdtDecimalsInflight = this.getConfig()
|
|
56
|
+
.then((cfg) => {
|
|
57
|
+
const d = cfg.usdtDecimals;
|
|
58
|
+
if (!isValidUsdtDecimals(d)) {
|
|
59
|
+
throw new Error(`[@pulsepairs/sdk] /config returned an invalid usdtDecimals: ${String(d)}`);
|
|
60
|
+
}
|
|
61
|
+
this.setUsdtDecimals(d);
|
|
62
|
+
return d;
|
|
63
|
+
})
|
|
64
|
+
.finally(() => {
|
|
65
|
+
this.usdtDecimalsInflight = undefined;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return this.usdtDecimalsInflight;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* This client's decimals, synchronously. Called before `ensureUsdtDecimals`
|
|
72
|
+
* has resolved, it answers 6 and warns once — that is a bug in the caller. It
|
|
73
|
+
* never reads another client's value.
|
|
74
|
+
*/
|
|
75
|
+
getUsdtDecimals() {
|
|
76
|
+
if (this.usdtDecimals !== undefined)
|
|
77
|
+
return this.usdtDecimals;
|
|
78
|
+
// THIS client's value only — never another client's shared default. A prod
|
|
79
|
+
// client that has not loaded would otherwise inherit a dev client's 18 and
|
|
80
|
+
// sign a $5 order 10^12 too large (PR #437 re-review). Unloaded, it answers
|
|
81
|
+
// the documented 6 and warns once per client: at worst it under-signs, which
|
|
82
|
+
// the venue's $1 minimum rejects, and it can never over-sign.
|
|
83
|
+
if (!this.warnedUnloaded) {
|
|
84
|
+
this.warnedUnloaded = true;
|
|
85
|
+
console.warn("[@pulsepairs/sdk] this client's usdtDecimals is not loaded — using 6. " +
|
|
86
|
+
"Await ensureUsdtDecimals() first (the venue may use 18).");
|
|
87
|
+
}
|
|
88
|
+
return 6;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `usdToAtomic` / `atomicToUsd` / `roundTripToleranceAtomic` at THIS client's
|
|
92
|
+
* decimals. Use these when a process talks to more than one venue (dev at 18,
|
|
93
|
+
* prod at 6): the standalone helpers share one default, which is ambiguous
|
|
94
|
+
* then. Before this client's `ensureUsdtDecimals` resolves they use 6 and
|
|
95
|
+
* warn once, like `getUsdtDecimals` — never another client's decimals.
|
|
96
|
+
*/
|
|
97
|
+
usdToAtomic(usd) {
|
|
98
|
+
return usdToAtomic(usd, this.getUsdtDecimals());
|
|
99
|
+
}
|
|
100
|
+
atomicToUsd(atomic) {
|
|
101
|
+
return atomicToUsd(atomic, this.getUsdtDecimals());
|
|
102
|
+
}
|
|
103
|
+
roundTripToleranceAtomic() {
|
|
104
|
+
return roundTripToleranceAtomic(this.getUsdtDecimals());
|
|
105
|
+
}
|
|
106
|
+
/** Cache `d` if it is a valid decimal count (integer 0–36); otherwise ignored.
|
|
107
|
+
* Lets a `getConfig()` response the caller already has populate the cache. */
|
|
108
|
+
setUsdtDecimals(d) {
|
|
109
|
+
if (!isValidUsdtDecimals(d))
|
|
110
|
+
return;
|
|
111
|
+
this.usdtDecimals = d;
|
|
112
|
+
publishUsdtDecimals(d);
|
|
113
|
+
}
|
|
37
114
|
async getVersion() {
|
|
38
115
|
const res = await fetch(buildUrl(this.baseUrl, "/version"));
|
|
39
116
|
return parseJson(res);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { UpDownHttpClient, wsUrlFromHttpBase, bulkFailures } from "./http.js";
|
|
2
|
+
export { usdToAtomic, atomicToUsd, roundTripToleranceAtomic, isValidUsdtDecimals } from "./units.js";
|
|
2
3
|
export { UpDownWsClient, type UpDownWsMessage, type SubscribePayload, type WsAuthCredentials, type ConnectAuthedOptions, } from "./ws.js";
|
|
3
4
|
export { ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES, buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData, freshSessionId, freshNonce, domainForSettlement, findPairBySettlement, parseCompositeMarketKey, centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic, MIN_STAKE_ATOMIC, type OrderSignMessage, type CancelSignMessage, type WsAuthMessage, type ParsedComposite, } from "./eip712.js";
|
|
4
5
|
export { CLOB_AUTH_TYPES, CLOB_AUTH_MESSAGE, buildClobAuthTypedData, buildHmacSignature, HMAC_HEADERS, type ClobAuthDomain, type ClobAuthMessage, } from "./auth.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// HTTP + WS clients
|
|
2
2
|
export { UpDownHttpClient, wsUrlFromHttpBase, bulkFailures } from "./http.js";
|
|
3
|
+
// Collateral units (usdtDecimals from /config: 18 on dev USDR, 6 on prod USDT0)
|
|
4
|
+
export { usdToAtomic, atomicToUsd, roundTripToleranceAtomic, isValidUsdtDecimals } from "./units.js";
|
|
3
5
|
export { UpDownWsClient, } from "./ws.js";
|
|
4
6
|
// EIP-712 helpers
|
|
5
7
|
export { ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES, buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData, freshSessionId, freshNonce, domainForSettlement, findPairBySettlement, parseCompositeMarketKey, centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic,
|
package/dist/units.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collateral unit helpers — whole-token ("USD") amounts ↔ on-chain base units.
|
|
3
|
+
*
|
|
4
|
+
* The venue's collateral decimals are published as `usdtDecimals` on
|
|
5
|
+
* `GET /config`: 18 on dev (USDR), 6 on prod (USDT0). Every amount the API
|
|
6
|
+
* sends or accepts is in base units at that scale, so a client must read it
|
|
7
|
+
* rather than assume 6 — a 6-decimal assumption against an 18-decimal venue is
|
|
8
|
+
* off by 10^12 on every order and every displayed amount.
|
|
9
|
+
*
|
|
10
|
+
* `UpDownHttpClient.ensureUsdtDecimals()` fetches the value and caches it here;
|
|
11
|
+
* the helpers below default to that cached value.
|
|
12
|
+
*/
|
|
13
|
+
/** Integer 0–36: the range a real ERC-20 uses. Anything else is not guessed at. */
|
|
14
|
+
export declare function isValidUsdtDecimals(d: unknown): d is number;
|
|
15
|
+
/** @internal Store a validated value as the default for the helpers below. */
|
|
16
|
+
export declare function publishUsdtDecimals(d: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* @internal The decimals the helpers default to. Before the value is loaded it
|
|
19
|
+
* answers 6 and warns ONCE: reading it early is a bug in the caller (it should
|
|
20
|
+
* have awaited `ensureUsdtDecimals()`), and 6 is wrong on an 18-decimal venue.
|
|
21
|
+
*/
|
|
22
|
+
export declare function defaultUsdtDecimals(): number;
|
|
23
|
+
/** @internal tests only. */
|
|
24
|
+
export declare function _resetUsdtDecimalsForTests(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Whole tokens → base units, e.g. `usdToAtomic(0.1, 18) === 100000000000000000n`.
|
|
27
|
+
*
|
|
28
|
+
* Goes through a decimal STRING (`parseUnits(usd.toFixed(min(decimals, 12)))`),
|
|
29
|
+
* never `usd * 10 ** decimals`: at 18 decimals that product is past 2^53 and
|
|
30
|
+
* float error lands in the integer (1.1 * 1e18 is 1100000000000000128).
|
|
31
|
+
* `toFixed` is capped at 12 places, well beyond any display precision.
|
|
32
|
+
* Non-finite input → 0n.
|
|
33
|
+
*
|
|
34
|
+
* Without `decimals`, THROWS if clients in this process loaded different
|
|
35
|
+
* decimals: signing at a guessed scale is the failure this refuses. Use
|
|
36
|
+
* `client.usdToAtomic` in a multi-venue process.
|
|
37
|
+
*/
|
|
38
|
+
export declare function usdToAtomic(usd: number, decimals?: number): bigint;
|
|
39
|
+
/**
|
|
40
|
+
* Base units → whole tokens, e.g. `atomicToUsd("5000000", 6) === 5`.
|
|
41
|
+
*
|
|
42
|
+
* Integer input (bigint, integer string or integer number) goes through
|
|
43
|
+
* `formatUnits`, so 18-decimal amounts convert without float loss. A
|
|
44
|
+
* non-integer number falls back to `atomic / 10 ** decimals`. `null`,
|
|
45
|
+
* `undefined` and `""` → 0. Never throws; a non-finite result → 0.
|
|
46
|
+
*/
|
|
47
|
+
export declare function atomicToUsd(atomic: bigint | string | number | null | undefined, decimals?: number): number;
|
|
48
|
+
/**
|
|
49
|
+
* The slack, in base units, for snapping a MAX sell onto the exact owned
|
|
50
|
+
* balance after a round trip through a 6-decimal display value:
|
|
51
|
+
* `10n ** BigInt(max(0, decimals - 6))`. 1n at 6 decimals, 10^12 at 18.
|
|
52
|
+
*/
|
|
53
|
+
export declare function roundTripToleranceAtomic(decimals?: number): bigint;
|
package/dist/units.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { formatUnits, parseUnits } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* Collateral unit helpers — whole-token ("USD") amounts ↔ on-chain base units.
|
|
4
|
+
*
|
|
5
|
+
* The venue's collateral decimals are published as `usdtDecimals` on
|
|
6
|
+
* `GET /config`: 18 on dev (USDR), 6 on prod (USDT0). Every amount the API
|
|
7
|
+
* sends or accepts is in base units at that scale, so a client must read it
|
|
8
|
+
* rather than assume 6 — a 6-decimal assumption against an 18-decimal venue is
|
|
9
|
+
* off by 10^12 on every order and every displayed amount.
|
|
10
|
+
*
|
|
11
|
+
* `UpDownHttpClient.ensureUsdtDecimals()` fetches the value and caches it here;
|
|
12
|
+
* the helpers below default to that cached value.
|
|
13
|
+
*/
|
|
14
|
+
/** Integer 0–36: the range a real ERC-20 uses. Anything else is not guessed at. */
|
|
15
|
+
export function isValidUsdtDecimals(d) {
|
|
16
|
+
return typeof d === "number" && Number.isInteger(d) && d >= 0 && d <= 36;
|
|
17
|
+
}
|
|
18
|
+
/** The fallback before the value is loaded — prod's USDT0. */
|
|
19
|
+
const FALLBACK_DECIMALS = 6;
|
|
20
|
+
let cachedDecimals;
|
|
21
|
+
let warnedUnloaded = false;
|
|
22
|
+
/**
|
|
23
|
+
* Set once two clients have loaded DIFFERENT decimals (a process talking to dev
|
|
24
|
+
* at 18 and prod at 6). The shared default is then ambiguous: it would be
|
|
25
|
+
* whichever loaded last, and a signed amount at the wrong scale is 10^12 off.
|
|
26
|
+
* From then on the standalone helpers refuse to guess — see defaultUsdtDecimals.
|
|
27
|
+
* Per-client conversion stays exact: `client.usdToAtomic` etc.
|
|
28
|
+
*/
|
|
29
|
+
let conflictingDecimals = false;
|
|
30
|
+
/** @internal Store a validated value as the default for the helpers below. */
|
|
31
|
+
export function publishUsdtDecimals(d) {
|
|
32
|
+
if (cachedDecimals !== undefined && cachedDecimals !== d)
|
|
33
|
+
conflictingDecimals = true;
|
|
34
|
+
cachedDecimals = d;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @internal The decimals the helpers default to. Before the value is loaded it
|
|
38
|
+
* answers 6 and warns ONCE: reading it early is a bug in the caller (it should
|
|
39
|
+
* have awaited `ensureUsdtDecimals()`), and 6 is wrong on an 18-decimal venue.
|
|
40
|
+
*/
|
|
41
|
+
export function defaultUsdtDecimals() {
|
|
42
|
+
if (conflictingDecimals) {
|
|
43
|
+
throw new Error("[@pulsepairs/sdk] clients in this process loaded different usdtDecimals " +
|
|
44
|
+
"(e.g. dev 18 and prod 6), so there is no single default. Pass `decimals` " +
|
|
45
|
+
"explicitly, or use client.usdToAtomic / client.atomicToUsd.");
|
|
46
|
+
}
|
|
47
|
+
if (cachedDecimals !== undefined)
|
|
48
|
+
return cachedDecimals;
|
|
49
|
+
if (!warnedUnloaded) {
|
|
50
|
+
warnedUnloaded = true;
|
|
51
|
+
console.warn("[@pulsepairs/sdk] usdtDecimals read before it was loaded — using 6. " +
|
|
52
|
+
"Await client.ensureUsdtDecimals() first (the venue may use 18).");
|
|
53
|
+
}
|
|
54
|
+
return FALLBACK_DECIMALS;
|
|
55
|
+
}
|
|
56
|
+
/** @internal tests only. */
|
|
57
|
+
export function _resetUsdtDecimalsForTests() {
|
|
58
|
+
cachedDecimals = undefined;
|
|
59
|
+
warnedUnloaded = false;
|
|
60
|
+
conflictingDecimals = false;
|
|
61
|
+
warnedAmbiguous = false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whole tokens → base units, e.g. `usdToAtomic(0.1, 18) === 100000000000000000n`.
|
|
65
|
+
*
|
|
66
|
+
* Goes through a decimal STRING (`parseUnits(usd.toFixed(min(decimals, 12)))`),
|
|
67
|
+
* never `usd * 10 ** decimals`: at 18 decimals that product is past 2^53 and
|
|
68
|
+
* float error lands in the integer (1.1 * 1e18 is 1100000000000000128).
|
|
69
|
+
* `toFixed` is capped at 12 places, well beyond any display precision.
|
|
70
|
+
* Non-finite input → 0n.
|
|
71
|
+
*
|
|
72
|
+
* Without `decimals`, THROWS if clients in this process loaded different
|
|
73
|
+
* decimals: signing at a guessed scale is the failure this refuses. Use
|
|
74
|
+
* `client.usdToAtomic` in a multi-venue process.
|
|
75
|
+
*/
|
|
76
|
+
export function usdToAtomic(usd, decimals = defaultUsdtDecimals()) {
|
|
77
|
+
if (!Number.isFinite(usd))
|
|
78
|
+
return 0n;
|
|
79
|
+
return parseUnits(usd.toFixed(Math.min(decimals, 12)), decimals);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Base units → whole tokens, e.g. `atomicToUsd("5000000", 6) === 5`.
|
|
83
|
+
*
|
|
84
|
+
* Integer input (bigint, integer string or integer number) goes through
|
|
85
|
+
* `formatUnits`, so 18-decimal amounts convert without float loss. A
|
|
86
|
+
* non-integer number falls back to `atomic / 10 ** decimals`. `null`,
|
|
87
|
+
* `undefined` and `""` → 0. Never throws; a non-finite result → 0.
|
|
88
|
+
*/
|
|
89
|
+
export function atomicToUsd(atomic, decimals) {
|
|
90
|
+
try {
|
|
91
|
+
if (atomic === null || atomic === undefined || atomic === "")
|
|
92
|
+
return 0;
|
|
93
|
+
// Resolved inside the try: an ambiguous default (clients disagree) must not
|
|
94
|
+
// make this throw — it warns once and answers 0 instead.
|
|
95
|
+
if (decimals === undefined) {
|
|
96
|
+
try {
|
|
97
|
+
decimals = defaultUsdtDecimals();
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
warnAmbiguousOnce(e);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let out;
|
|
105
|
+
if (typeof atomic === "bigint") {
|
|
106
|
+
out = Number(formatUnits(atomic, decimals));
|
|
107
|
+
}
|
|
108
|
+
else if (typeof atomic === "string") {
|
|
109
|
+
const s = atomic.trim();
|
|
110
|
+
out = /^-?\d+$/.test(s) ? Number(formatUnits(BigInt(s), decimals)) : Number(s) / 10 ** decimals;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
out = Number.isInteger(atomic)
|
|
114
|
+
? Number(formatUnits(BigInt(atomic), decimals))
|
|
115
|
+
: atomic / 10 ** decimals;
|
|
116
|
+
}
|
|
117
|
+
return Number.isFinite(out) ? out : 0;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
let warnedAmbiguous = false;
|
|
124
|
+
function warnAmbiguousOnce(e) {
|
|
125
|
+
if (warnedAmbiguous)
|
|
126
|
+
return;
|
|
127
|
+
warnedAmbiguous = true;
|
|
128
|
+
console.warn(String(e instanceof Error ? e.message : e));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The slack, in base units, for snapping a MAX sell onto the exact owned
|
|
132
|
+
* balance after a round trip through a 6-decimal display value:
|
|
133
|
+
* `10n ** BigInt(max(0, decimals - 6))`. 1n at 6 decimals, 10^12 at 18.
|
|
134
|
+
*/
|
|
135
|
+
export function roundTripToleranceAtomic(decimals = defaultUsdtDecimals()) {
|
|
136
|
+
return 10n ** BigInt(Math.max(0, decimals - 6));
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulsepairs/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Standalone SDK for the UpDown (PulsePairs) up/down prediction markets — matcher REST/WS client, EIP-712 order signing, trade-math, and Alchemy Account Kit (smart-account) order signing for rain.trade integration.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
30
|
"README.md",
|
|
31
|
-
"DOCUMENTATION.md"
|
|
31
|
+
"DOCUMENTATION.md",
|
|
32
|
+
"CHANGELOG.md"
|
|
32
33
|
],
|
|
33
34
|
"sideEffects": false,
|
|
34
35
|
"scripts": {
|
|
35
36
|
"clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
|
|
36
37
|
"build": "npm run clean && tsc",
|
|
37
|
-
"test": "node scripts/eip712-golden.test.mjs && node scripts/rawtx-tier.test.mjs && node scripts/hot-key-safety.test.mjs && node scripts/reconcile.test.mjs && node scripts/pnl.test.mjs && node scripts/ws-subscribe.test.mjs && node scripts/ws-onerror.test.mjs && node scripts/amend-wrappers.test.mjs && node scripts/amend-integration.test.mjs",
|
|
38
|
+
"test": "node scripts/eip712-golden.test.mjs && node scripts/rawtx-tier.test.mjs && node scripts/hot-key-safety.test.mjs && node scripts/reconcile.test.mjs && node scripts/pnl.test.mjs && node scripts/ws-subscribe.test.mjs && node scripts/ws-onerror.test.mjs && node scripts/amend-wrappers.test.mjs && node scripts/amend-integration.test.mjs && node scripts/usdt-units.test.mjs",
|
|
38
39
|
"test:integration": "UPND_INTEGRATION=1 node scripts/amend-integration.test.mjs",
|
|
39
40
|
"prepublishOnly": "npm run build && npm test",
|
|
40
41
|
"example:taker": "npx tsx examples/simple-taker.ts",
|