@pulsepairs/sdk 0.9.1 → 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 +9 -2
- package/README.md +59 -4
- 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 +5 -4
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.
|
|
@@ -43,10 +43,14 @@ returns, what unit a number is in, or why a signature is being rejected.
|
|
|
43
43
|
## 1. What this SDK is
|
|
44
44
|
|
|
45
45
|
UpDown is a binary prediction market: *will BTC-USD (or ETH-USD) be UP or DOWN at
|
|
46
|
-
the end of this 5-minute
|
|
46
|
+
the end of this 5-minute or 15-minute cycle?* Trading is an **off-chain
|
|
47
47
|
central limit order book** (the "matcher"); settlement is **on-chain** on
|
|
48
48
|
Arbitrum.
|
|
49
49
|
|
|
50
|
+
60-minute cycles were retired on 2026-08-18 and no new ones are created. The
|
|
51
|
+
`3600` timeframe is still accepted and still returns the historical rounds — see
|
|
52
|
+
`getMarkets` under [§5 `UpDownHttpClient`](#5-updownhttpclient--rest-client).
|
|
53
|
+
|
|
50
54
|
The SDK gives you three layers, usable independently:
|
|
51
55
|
|
|
52
56
|
| Layer | Modules | What it does |
|
|
@@ -725,7 +729,10 @@ fresh nonce. Single-writer bots may prefer a monotonic counter — seed it from
|
|
|
725
729
|
centsToBps(cents: number): number // 55 → 5500; 49.5 → 4950. Throws outside (0,100)
|
|
726
730
|
bpsToCents(bps: number): number // 5500 → 55. Throws outside (0,10000)
|
|
727
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.
|
|
728
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).
|
|
729
736
|
feeAtomic(notionalAtomic, priceBps, cfg): bigint
|
|
730
737
|
|
|
731
738
|
MIN_STAKE_ATOMIC = 1_000_000n // $1 of share FACE value (BUY only)
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @pulsepairs/sdk
|
|
2
2
|
|
|
3
3
|
Standalone SDK for the **UpDown** up/down prediction markets —
|
|
4
|
-
BTC-USD / ETH-USD, UP or DOWN, on 5-minute
|
|
4
|
+
BTC-USD / ETH-USD, UP or DOWN, on 5-minute and 15-minute cycles.
|
|
5
5
|
|
|
6
6
|
> **On the name:** the product is **UpDown**. "PulsePairs" is a retired name that
|
|
7
7
|
> survives in two places for compatibility reasons only — this npm package's name
|
|
@@ -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
|
|
@@ -456,6 +485,32 @@ for (const f of bulkFailures(res)) {
|
|
|
456
485
|
|
|
457
486
|
`index` is the position in the array you sent — results are positional.
|
|
458
487
|
|
|
488
|
+
**On success the order is NESTED, and the single routes spread it.** This is
|
|
489
|
+
the one asymmetry that bites, because nothing about the call site hints at it:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
const single = await api.amendOrder({ cancelOrderId, order });
|
|
493
|
+
single.id; // flat — the replacement's id
|
|
494
|
+
|
|
495
|
+
const bulk = await api.amendOrdersBulk({ amends });
|
|
496
|
+
for (const r of bulk.results) {
|
|
497
|
+
if (!r.ok) continue; // narrow first; failures have no `order`
|
|
498
|
+
r.order.id; // NESTED — `r.id` is undefined
|
|
499
|
+
r.replaced; // the id that was retired
|
|
500
|
+
r.priorityKept; // false when queue position was forfeited
|
|
501
|
+
r.remaining; // atomic USDT carried to the replacement
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
`postOrdersBulk` nests the same way: `results[i].order`, never `results[i].id`.
|
|
506
|
+
|
|
507
|
+
Reading `results[i].id` yields `undefined` rather than throwing, so the mistake
|
|
508
|
+
survives to wherever that id is used — usually a later cancel that silently
|
|
509
|
+
matches nothing. SDK **0.9.0 typed the bulk result flat** and shipped that
|
|
510
|
+
mistake into the types themselves; 0.9.1 corrected them. The wire shape never
|
|
511
|
+
changed, so no server behaviour depends on which version you are on — but on
|
|
512
|
+
0.9.0 the compiler will agree with you while you read the wrong field.
|
|
513
|
+
|
|
459
514
|
Two behavioural differences worth knowing, because they change how latency
|
|
460
515
|
scales:
|
|
461
516
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulsepairs/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Standalone SDK for the UpDown (PulsePairs) up/down prediction markets
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -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",
|