@pulsepairs/sdk 0.2.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/README.md +264 -0
- package/dist/accountKit.d.ts +265 -0
- package/dist/accountKit.js +638 -0
- package/dist/approve.d.ts +46 -0
- package/dist/approve.js +69 -0
- package/dist/auth.d.ts +77 -0
- package/dist/auth.js +76 -0
- package/dist/eip712.d.ts +216 -0
- package/dist/eip712.js +229 -0
- package/dist/http.d.ts +46 -0
- package/dist/http.js +120 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +15 -0
- package/dist/types.d.ts +235 -0
- package/dist/types.js +20 -0
- package/dist/ws.d.ts +118 -0
- package/dist/ws.js +224 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# @pulsepairs/sdk
|
|
2
|
+
|
|
3
|
+
Standalone SDK for the **UpDown** (PulsePairs) up/down prediction markets —
|
|
4
|
+
BTC-USD / ETH-USD, UP or DOWN, on 5-minute / 15-minute / 1-hour cycles.
|
|
5
|
+
|
|
6
|
+
This is the package [rain.trade](https://rain.trade) integrates to surface
|
|
7
|
+
UpDown markets alongside Rain's own markets ("satellite" integration, Option A).
|
|
8
|
+
It is deliberately **segregated** from `rain-sdk-v2`: bump this package's version
|
|
9
|
+
independently, no release coupling.
|
|
10
|
+
|
|
11
|
+
It gives you three things:
|
|
12
|
+
|
|
13
|
+
1. **Matcher client** — typed REST + WebSocket client for the off-chain
|
|
14
|
+
order book (`UpDownHttpClient`, `UpDownWsClient`).
|
|
15
|
+
2. **EIP-712 + trade-math** — `buildOrderTypedData` / `buildCancelTypedData` /
|
|
16
|
+
`buildWsAuthTypedData`, plus fee/stake/price helpers that mirror the backend
|
|
17
|
+
and contract exactly.
|
|
18
|
+
3. **Account Kit signing** — `UpDownAccountKitSigner`: sign orders with an
|
|
19
|
+
Alchemy smart-contract account (the SAME wallet rain.trade uses), plus
|
|
20
|
+
gasless onboarding / approve / withdraw.
|
|
21
|
+
|
|
22
|
+
> **Live integration environment:** a public demo of the full stack (markets
|
|
23
|
+
> cycling 24/7 on Arbitrum One, mock oracle, mintable test USDT) runs at
|
|
24
|
+
> **https://demo-pulsepairs.rainwins.com** with the matcher API/WS at
|
|
25
|
+
> **`https://api.demo-pulsepairs.rainwins.com`** (`wss://…/stream`). Every
|
|
26
|
+
> example below targets it by default. Test funds: `POST /test/devmint`
|
|
27
|
+
> (10k cap, 1 mint per address per 5 min — mint to the **smart-account**
|
|
28
|
+
> address, not the owner EOA; it also seeds a little gas ETH).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @pulsepairs/sdk viem
|
|
36
|
+
# For Account Kit (smart-account) signing, also install the peer deps rain.trade
|
|
37
|
+
# already ships (skip if you only use the EOA / private-key path):
|
|
38
|
+
npm install @account-kit/wallet-client@^4.88 @account-kit/infra@^4.88 \
|
|
39
|
+
@account-kit/smart-contracts@^4.88 @aa-sdk/core@^4.88
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`viem` is a required peer. The four `@account-kit/*`/`@aa-sdk/*` packages are
|
|
43
|
+
**optional** peers — they are lazy-imported and only needed if you construct
|
|
44
|
+
`UpDownAccountKitSigner` (`smart-contracts` specifically backs the popup-less
|
|
45
|
+
order-session signing below). A pure-EOA bot never pulls any of them.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Two signing paths
|
|
50
|
+
|
|
51
|
+
The matcher and contract are signer-agnostic (on-chain verification is OZ
|
|
52
|
+
`SignatureChecker`, which accepts EOAs **and** ERC-1271 smart accounts). Pick:
|
|
53
|
+
|
|
54
|
+
| Path | `order.maker` | Sign with | Use for |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| **EOA** (`examples/simple-*`) | your EOA | `viem` `account.signTypedData` | bots, scripts, testing |
|
|
57
|
+
| **Account Kit** (`examples/account-kit-taker.ts`) | your **SCA** | `UpDownAccountKitSigner` | rain.trade / any smart-wallet UX |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Quickstart — EOA (bots)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createPublicClient, createWalletClient, http } from "viem";
|
|
65
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
66
|
+
import { arbitrum } from "viem/chains";
|
|
67
|
+
import {
|
|
68
|
+
UpDownHttpClient, buildOrderTypedData, ensureSettlementAllowance,
|
|
69
|
+
parseCompositeMarketKey, parseStake, OrderType, OrderSide, Option,
|
|
70
|
+
} from "@pulsepairs/sdk";
|
|
71
|
+
|
|
72
|
+
const api = new UpDownHttpClient("https://api.demo-pulsepairs.rainwins.com");
|
|
73
|
+
const cfg = await api.getConfig(); // never hardcode addresses
|
|
74
|
+
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
|
|
75
|
+
const [live] = (await api.getMarkets({ pair: "BTC-USD", timeframe: 300 }))
|
|
76
|
+
.filter((m) => m.status === "ACTIVE");
|
|
77
|
+
const { settlementAddress, marketId } = parseCompositeMarketKey(live.address)!;
|
|
78
|
+
|
|
79
|
+
const amount = parseStake("5");
|
|
80
|
+
const maxFee = (amount * BigInt(cfg.platformFeeBps + cfg.makerFeeBps)) / 10000n; // peak fee
|
|
81
|
+
const nonce = BigInt(Math.floor(Math.random() * 1e12));
|
|
82
|
+
const typedData = buildOrderTypedData({
|
|
83
|
+
cfg, settlementAddress,
|
|
84
|
+
message: { maker: account.address, market: BigInt(marketId), option: BigInt(Option.UP),
|
|
85
|
+
side: OrderSide.BUY, type: OrderType.MARKET, price: 0n, amount, maxFee, nonce,
|
|
86
|
+
expiry: BigInt(live.endTime) },
|
|
87
|
+
});
|
|
88
|
+
const signature = await account.signTypedData(typedData);
|
|
89
|
+
await api.postOrder({ maker: account.address, market: live.address, option: Option.UP,
|
|
90
|
+
side: OrderSide.BUY, type: OrderType.MARKET, price: 0, amount: amount.toString(),
|
|
91
|
+
maxFee: maxFee.toString(), nonce: Number(nonce), expiry: live.endTime, signature });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Full runnable scripts: `examples/simple-taker.ts` (MARKET), `examples/simple-maker.ts`
|
|
95
|
+
(LIMIT + authed WS), `examples/full-dmm-bot.ts` (two-sided quoting).
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Quickstart — Account Kit (rain.trade / smart wallets)
|
|
100
|
+
|
|
101
|
+
`UpDownAccountKitSigner` builds its own Alchemy smart-wallet client using the
|
|
102
|
+
**same config rain.trade's `RainAA` uses**, so the same owner EOA resolves to
|
|
103
|
+
the **same SCA address** → one wallet across both products.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { arbitrum } from "viem/chains";
|
|
107
|
+
import { UpDownAccountKitSigner, buildOrderTypedData, UpDownHttpClient } from "@pulsepairs/sdk";
|
|
108
|
+
|
|
109
|
+
const ak = new UpDownAccountKitSigner({
|
|
110
|
+
walletClient: window.ethereum, // owner EOA provider (or connector.getProvider())
|
|
111
|
+
alchemyApiKey: import.meta.env.VITE_ALCHEMY_API_KEY, // SAME as rain.trade
|
|
112
|
+
paymasterPolicyId: import.meta.env.VITE_PAYMASTER_POLICY_ID, // SAME as rain.trade
|
|
113
|
+
chain: arbitrum, // SAME as rain.trade (42161)
|
|
114
|
+
});
|
|
115
|
+
const sca = await ak.connect(); // deterministic; == rain.trade's SCA
|
|
116
|
+
|
|
117
|
+
// Deploy-before-fill: ONE UserOp deploys the SCA + approves USDT + installs
|
|
118
|
+
// the popup-less order-session key (see "Popup-less order signing" below).
|
|
119
|
+
await ak.onboard({ usdt: cfg.usdtAddress, settlement });
|
|
120
|
+
|
|
121
|
+
// Owner-key ERC-1271 order signature (6492 wrapper stripped automatically).
|
|
122
|
+
const signature = await ak.signTypedDataBare(buildOrderTypedData({ cfg, settlementAddress, message }));
|
|
123
|
+
|
|
124
|
+
// WS auth for orders:/balance: channels (keyed by the SCA):
|
|
125
|
+
ws.connectAuthed({ channels: [`orders:${sca.toLowerCase()}`], signAuth: () => ak.signWsAuth(cfg.chainId) });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
See `examples/account-kit-taker.ts` for the end-to-end flow.
|
|
129
|
+
|
|
130
|
+
### Already have rain.trade's smart-wallet client?
|
|
131
|
+
|
|
132
|
+
If the host app owns an Alchemy client (e.g. rain.trade's shared session), skip
|
|
133
|
+
`UpDownAccountKitSigner` and just make its output UpDown-correct:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { bareErc1271Signer } from "@pulsepairs/sdk";
|
|
137
|
+
const signOrder = bareErc1271Signer((td) => rainSmartWalletClient.signTypedData(td));
|
|
138
|
+
const signature = await signOrder(buildOrderTypedData({ cfg, settlementAddress, message }));
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Account Kit: the two rules you cannot break
|
|
144
|
+
|
|
145
|
+
Both come from the on-chain verifier (OZ `SignatureChecker` v5.6.1, no EIP-6492):
|
|
146
|
+
|
|
147
|
+
1. **Deploy the SCA before its first fill.** A counterfactual (undeployed)
|
|
148
|
+
account cannot be verified via ERC-1271 → the fill reverts. `onboard()` does
|
|
149
|
+
this (deploy + approve in one UserOp); `isDeployed(publicClient)` checks it.
|
|
150
|
+
2. **Bare ERC-1271, not 6492.** `signTypedData` on an undeployed account can
|
|
151
|
+
return a 6492-wrapped blob the settlement can't parse. Every signature this
|
|
152
|
+
SDK produces is run through `stripErc6492Wrapper(...)`, so a wrapped blob
|
|
153
|
+
never reaches the matcher. (Exposed standalone too, for the callback path.)
|
|
154
|
+
|
|
155
|
+
> **Confirmed live on Arbitrum One (2026-07-05/06).** A real SCA was deployed,
|
|
156
|
+
> `isValidSignature` over the full Order typed-data returned the `0x1626ba7e`
|
|
157
|
+
> magic on-chain, a real `enterPosition` fill settled with `maker = SCA`, and
|
|
158
|
+
> the negative control (undeployed SCA) reverted as expected. The MA-v2 /
|
|
159
|
+
> ERC-7739 question is resolved: sign the FULL typed-data through the account
|
|
160
|
+
> client (which this SDK does); a bare-`bytes32` flow does not validate.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Popup-less order signing (order sessions) — ON by default
|
|
165
|
+
|
|
166
|
+
`UpDownAccountKitSigner` distinguishes **two different session-key mechanisms**
|
|
167
|
+
— do not conflate them:
|
|
168
|
+
|
|
169
|
+
1. **Send session** (`grantSession()`, Alchemy `grantPermissions` root) —
|
|
170
|
+
authorizes gasless UserOp *execution* (`onboard`/`approve`/`withdraw`).
|
|
171
|
+
Its signatures pack the OWNER validation entity, so it is verified
|
|
172
|
+
**incapable** of ERC-1271 order signing.
|
|
173
|
+
2. **Order session** (default ON) — a locally-generated key installed on the
|
|
174
|
+
MA-v2 SCA as a **signature-validation-ONLY** entity
|
|
175
|
+
(`isSignatureValidation: true`, `isUserOpValidation: false`). It can only
|
|
176
|
+
answer `isValidSignature` for orders / cancels / WS-auth — it can **never
|
|
177
|
+
move funds or execute a UserOp**. `onboard()` batches the install into the
|
|
178
|
+
same single UserOp as deploy + approve (still exactly one owner signature),
|
|
179
|
+
after which orders sign with zero wallet popups.
|
|
180
|
+
|
|
181
|
+
Behavior and knobs:
|
|
182
|
+
|
|
183
|
+
- `signTypedDataBare` prefers the order-session key when one is active and
|
|
184
|
+
**falls back to the owner key automatically** on any failure — enabling this
|
|
185
|
+
feature can never break signing.
|
|
186
|
+
- `ensureOrderSession()` installs a session for already-onboarded accounts
|
|
187
|
+
(one owner-signed UserOp); `revokeOrderSession()` drops the local key;
|
|
188
|
+
`hasOrderSession` inspects state.
|
|
189
|
+
- Opt out entirely with `orderSessions: false` in the constructor — every
|
|
190
|
+
order then prompts the owner wallet (the pre-session behavior).
|
|
191
|
+
- Storage: browser `localStorage` (`updown:oskey:<sca>`) by default, an
|
|
192
|
+
in-memory map in Node, or bring your own via `orderSessionStorage`.
|
|
193
|
+
- **Known limitations** (fine for integration/demo, review before launch):
|
|
194
|
+
expiry is client-side only (24 h — not enforced on-chain), and
|
|
195
|
+
`revokeOrderSession()` does not uninstall the validation entity on-chain.
|
|
196
|
+
|
|
197
|
+
Validated in 4 live PoC stages before shipping (on-chain `0x1626ba7e` +
|
|
198
|
+
negative controls, live matcher order 201 + cancel 200, batched-install flow).
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## `maxFee` (F-2026-17731) — REQUIRED on every order
|
|
203
|
+
|
|
204
|
+
The signed `Order` includes `maxFee` (between `amount` and `nonce`). The
|
|
205
|
+
contract's `ORDER_TYPEHASH` includes it, so **omitting it produces a signature
|
|
206
|
+
the on-chain `SignatureChecker` rejects** — the fill reverts. Compute the peak
|
|
207
|
+
fee and pass the same value into both the typed-data and the POST body:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const maxFee = (amount * BigInt(cfg.platformFeeBps + cfg.makerFeeBps)) / 10000n;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Signing the peak means legitimate (probability-weighted) fills never revert with
|
|
214
|
+
`FeeExceedsTakerCap`; the contract enforces the *actual* cumulative fee under
|
|
215
|
+
this cap across partial fills.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## API surface
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
Clients UpDownHttpClient, UpDownWsClient, wsUrlFromHttpBase
|
|
223
|
+
EIP-712 ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES,
|
|
224
|
+
buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData,
|
|
225
|
+
freshSessionId, domainForSettlement, findPairBySettlement,
|
|
226
|
+
parseCompositeMarketKey
|
|
227
|
+
Trade-math centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic,
|
|
228
|
+
MIN_STAKE_ATOMIC, MAX_STAKE_ATOMIC
|
|
229
|
+
Approve (EOA) ensureSettlementAllowance, MAX_UINT256
|
|
230
|
+
Account Kit UpDownAccountKitSigner (connect, onboard, approve, withdraw,
|
|
231
|
+
signTypedDataBare, signWsAuth, isDeployed, grantSession,
|
|
232
|
+
ensureOrderSession, revokeOrderSession, hasOrderSession),
|
|
233
|
+
bareErc1271Signer, stripErc6492Wrapper, isErc6492Signature
|
|
234
|
+
L2 HMAC auth CLOB_AUTH_TYPES, buildClobAuthTypedData, buildHmacSignature, HMAC_HEADERS
|
|
235
|
+
Enums/types OrderType, OrderSide, Option, ApiConfig, PairConfig, PostOrderBody, …
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Field-name gotchas (live with them)
|
|
241
|
+
|
|
242
|
+
- `OrderRow.nonce` / `OrderRow.expiry` are JSON **strings** (uint256 precision).
|
|
243
|
+
- WS `order_update` uses `orderType`; REST `OrderRow` uses `type`. Same value.
|
|
244
|
+
- `Option` is `1 = UP`, `2 = DOWN` — not `0/1`.
|
|
245
|
+
- Market addresses are composite `<settlementAddress>-<marketId>`. The signed
|
|
246
|
+
`Order.market` is the **bare uint256 marketId**; the composite goes in the
|
|
247
|
+
POST body's `market` field for routing only.
|
|
248
|
+
- EIP-712 domain name is **`"UpDown Exchange"`**, `verifyingContract` = the
|
|
249
|
+
settlement contract, price in **bps** (not 1e18). WS-auth uses a distinct
|
|
250
|
+
domain (`"PulsePairs WebSocket Auth"`, `verifyingContract` = zero) so an order
|
|
251
|
+
signature can never replay as a WS-auth signature.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Reference
|
|
256
|
+
|
|
257
|
+
- **Wire shapes / REST / WS**: `docs/api.md` in the `updown-backend` repo
|
|
258
|
+
(branch `audit/hacken-remediation-v2`) — auth model, WS handshake, all
|
|
259
|
+
endpoints, error strings, rate limits.
|
|
260
|
+
- **On-chain spec**: `src/UpDownSettlement.sol` in the `updown-contracts` repo
|
|
261
|
+
(same branch) — `ORDER_TYPEHASH`, `enterPosition`, fee cap enforcement.
|
|
262
|
+
- **Schema drift guard**: `npm test` runs an EIP-712 golden-vector test whose
|
|
263
|
+
expected digest/signature were derived independently from the backend and
|
|
264
|
+
contract definitions.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Kit (Alchemy smart-account) signing for the UpDown satellite.
|
|
3
|
+
*
|
|
4
|
+
* ─── Why this exists ─────────────────────────────────────────────────────
|
|
5
|
+
* rain.trade custody is Alchemy Account Kit (a smart-contract account, "SCA").
|
|
6
|
+
* The UpDown settlement verifies order signatures on-chain with OpenZeppelin
|
|
7
|
+
* `SignatureChecker.isValidSignatureNow(maker, digest, sig)` (OZ 5.6.1). When
|
|
8
|
+
* `maker` is an SCA, that routes to `IERC1271(maker).isValidSignature(...)`.
|
|
9
|
+
*
|
|
10
|
+
* To make one wallet span BOTH products, this module builds its own Alchemy
|
|
11
|
+
* smart-wallet client using the EXACT same config rain.trade's `RainAA` uses
|
|
12
|
+
* (same `chain` + `alchemyApiKey` + `paymasterPolicyId` + owner EOA). Account
|
|
13
|
+
* Kit derives the SCA address deterministically from (owner, factory), so the
|
|
14
|
+
* same owner + same config ⇒ the SAME account address ⇒ one shared wallet.
|
|
15
|
+
* We deliberately do NOT import `RainAA` — it is a published, unpatchable
|
|
16
|
+
* package that exposes no `signTypedData`, and the meeting locked UpDown as a
|
|
17
|
+
* *segregated* package. We mirror its config, not its code.
|
|
18
|
+
*
|
|
19
|
+
* ─── The two hard constraints (see UPDOWN_SDK_MEETING_PREP_OPTION_A) ─────
|
|
20
|
+
* 1. BARE ERC-1271, not EIP-6492. OZ `SignatureChecker` v5.6.1 is NOT
|
|
21
|
+
* 6492-aware. `signTypedData` on an *undeployed* SCA can return a
|
|
22
|
+
* 6492-wrapped blob that the settlement cannot parse → `enterPosition`
|
|
23
|
+
* reverts. We therefore (a) enforce deploy-before-fill and (b) defensively
|
|
24
|
+
* `stripErc6492Wrapper(...)` every signature so a wrapped blob never
|
|
25
|
+
* reaches the matcher.
|
|
26
|
+
* 2. Two DIFFERENT session-key mechanisms — don't conflate them:
|
|
27
|
+
* a. RainAA-style "send session" (`grantSession`, Alchemy
|
|
28
|
+
* `grantPermissions({type:'root'})`): authorizes UserOp EXECUTION only.
|
|
29
|
+
* Its signatures pack the OWNER validation entity, so it is verified
|
|
30
|
+
* INCAPABLE of ERC-1271 order signing (isValidSignature → 0xffffffff;
|
|
31
|
+
* see updown-demo/POC_SESSION_KEY_ORDERS_2026-07-06.md). Used only for
|
|
32
|
+
* gasless *sends* (onboard / approve / withdraw), mirroring RainAA.
|
|
33
|
+
* b. "ORDER session" (2026-07-06, PoC-validated on-chain + live API): a
|
|
34
|
+
* locally-generated key installed on the MA-v2 SCA as a
|
|
35
|
+
* SingleSignerValidationModule entity with isSignatureValidation=true
|
|
36
|
+
* and isUserOpValidation=false — it can ONLY answer `isValidSignature`
|
|
37
|
+
* (orders / cancels / ws-auth, popup-less) and can never execute a
|
|
38
|
+
* UserOp. Installed inside the onboarding UserOp (one owner popup
|
|
39
|
+
* total) or via `ensureOrderSession()`. Orders sign with it when
|
|
40
|
+
* active; the OWNER client is the automatic fallback. Client-side 24h
|
|
41
|
+
* expiry only (on-chain expiry needs a time-range hook — follow-up);
|
|
42
|
+
* `revokeOrderSession()` drops the local key.
|
|
43
|
+
*
|
|
44
|
+
* ─── MUST be confirmed with a fork test before trusting on mainnet ───────
|
|
45
|
+
* deploy a real Alchemy SCA on the target chain → `approve(settlement,USDT)`
|
|
46
|
+
* → sign a real `Order` via `signTypedDataBare(...)` → call `enterPosition`
|
|
47
|
+
* as the relayer → assert NO revert. Negative control: an *undeployed* SCA
|
|
48
|
+
* and/or a 6492-wrapped signature MUST revert. This resolves the open
|
|
49
|
+
* Light-Account-v2-vs-Modular-Account-v2 (ERC-7739) question: for MA v2 only
|
|
50
|
+
* the account-level `signTypedData` over the FULL typed-data validates 1271
|
|
51
|
+
* (handing the SCA a bare `bytes32` silently fails). `signTypedDataBare`
|
|
52
|
+
* below signs the full typed-data, which is the correct path for both.
|
|
53
|
+
*/
|
|
54
|
+
import { type Address, type Chain, type Hex, type PublicClient, type TypedDataDefinition } from "viem";
|
|
55
|
+
import type { WsAuthCredentials } from "./ws.js";
|
|
56
|
+
/** True if `signature` is an EIP-6492-wrapped blob (ends with the magic suffix). */
|
|
57
|
+
export declare function isErc6492Signature(signature: Hex): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Return the BARE inner signature from an EIP-6492-wrapped blob, or the input
|
|
60
|
+
* unchanged if it is not wrapped. The 6492 layout is
|
|
61
|
+
* `abi.encode(address factory, bytes factoryCalldata, bytes signature)` with
|
|
62
|
+
* the 32-byte magic suffix appended. Once the SCA is deployed the bare inner
|
|
63
|
+
* signature is exactly what OZ `SignatureChecker` needs.
|
|
64
|
+
*/
|
|
65
|
+
export declare function stripErc6492Wrapper(signature: Hex): Hex;
|
|
66
|
+
export type RawTypedDataSigner = (typedData: TypedDataDefinition) => Promise<Hex>;
|
|
67
|
+
/**
|
|
68
|
+
* Wrap ANY raw typed-data signer so its output is a BARE ERC-1271 signature
|
|
69
|
+
* the UpDown settlement can verify. Use this when the host app already owns an
|
|
70
|
+
* Alchemy smart-wallet client (e.g. rain.trade's shared session) and just
|
|
71
|
+
* wants UpDown-correct order signatures without a second signer:
|
|
72
|
+
*
|
|
73
|
+
* const signOrder = bareErc1271Signer((td) => rainSmartWalletClient.signTypedData(td));
|
|
74
|
+
* const sig = await signOrder(buildOrderTypedData({ cfg, settlementAddress, message }));
|
|
75
|
+
*
|
|
76
|
+
* The SCA MUST be deployed on-chain before the signature is used in a fill.
|
|
77
|
+
*/
|
|
78
|
+
export declare function bareErc1271Signer(raw: RawTypedDataSigner): RawTypedDataSigner;
|
|
79
|
+
/** EIP-1193 provider for the OWNER EOA (e.g. `window.ethereum`, or the
|
|
80
|
+
* provider from a wagmi connector via `connector.getProvider()`). */
|
|
81
|
+
export type Eip1193Provider = {
|
|
82
|
+
request: (args: {
|
|
83
|
+
method: string;
|
|
84
|
+
params?: unknown[] | object;
|
|
85
|
+
}) => Promise<unknown>;
|
|
86
|
+
};
|
|
87
|
+
export type UpDownAccountKitConfig = {
|
|
88
|
+
/** Provider for the owner EOA. Signs the owner ERC-1271 order signatures and
|
|
89
|
+
* the one-time `grantSession` permission. */
|
|
90
|
+
walletClient: Eip1193Provider;
|
|
91
|
+
/** Alchemy API key (same one rain.trade uses — `VITE_ALCHEMY_API_KEY`). */
|
|
92
|
+
alchemyApiKey: string;
|
|
93
|
+
/**
|
|
94
|
+
* Alchemy Gas Manager policy id (`VITE_PAYMASTER_POLICY_ID`).
|
|
95
|
+
* - **Sponsorship-type** policy: pass the id alone — sends are app-sponsored.
|
|
96
|
+
* - **ERC-20-type** policy: also pass `gasToken` (the token the USER pays gas
|
|
97
|
+
* in; must be on the policy's allowlist and held by the SCA).
|
|
98
|
+
* - **Omitted**: UserOps are SELF-PAID — the SCA must hold ETH for gas.
|
|
99
|
+
*/
|
|
100
|
+
paymasterPolicyId?: string;
|
|
101
|
+
/** Gas token for an ERC-20-type Gas Manager policy (e.g. USDC). Ignored when
|
|
102
|
+
* `paymasterPolicyId` is unset. */
|
|
103
|
+
gasToken?: {
|
|
104
|
+
tokenAddress: Address;
|
|
105
|
+
};
|
|
106
|
+
/** viem `Chain`. MUST equal rain.trade's RainAA chain (Arbitrum One, 42161)
|
|
107
|
+
* to derive the SAME SCA address and share one wallet. */
|
|
108
|
+
chain: Chain;
|
|
109
|
+
/** Optional node RPC override; defaults to Alchemy's RPC for `chain`. */
|
|
110
|
+
rpcUrl?: string;
|
|
111
|
+
/** Popup-less ORDER-session signing (header §2b). Default true; set false to
|
|
112
|
+
* force owner-key signing per order (the pre-2026-07-06 behavior). */
|
|
113
|
+
orderSessions?: boolean;
|
|
114
|
+
/** Where the order-session key persists. Defaults to `globalThis.localStorage`
|
|
115
|
+
* when available (browser), else an in-memory map (Node — session lives for
|
|
116
|
+
* the process). Keys are namespaced `updown:oskey:<sca>`. */
|
|
117
|
+
orderSessionStorage?: {
|
|
118
|
+
getItem: (k: string) => string | null;
|
|
119
|
+
setItem: (k: string, v: string) => void;
|
|
120
|
+
removeItem: (k: string) => void;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
export type GrantSessionResult = {
|
|
124
|
+
privateKey: Hex;
|
|
125
|
+
sessionKeyAddress: Address;
|
|
126
|
+
context: Hex;
|
|
127
|
+
expirySec: number;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Owner-key ERC-1271 order signer + gasless custody sends on an Alchemy SCA.
|
|
131
|
+
*
|
|
132
|
+
* Lifecycle:
|
|
133
|
+
* const ak = new UpDownAccountKitSigner({ walletClient, alchemyApiKey, paymasterPolicyId, chain });
|
|
134
|
+
* const sca = await ak.connect(); // SCA address (order.maker)
|
|
135
|
+
* await ak.onboard({ usdt, settlement }); // deploy SCA + approve USDT (one UserOp)
|
|
136
|
+
* const sig = await ak.signTypedDataBare(orderTypedData); // owner ERC-1271, bare
|
|
137
|
+
*
|
|
138
|
+
* All Account Kit packages are lazy-imported peer deps (mirrors RainAA), so
|
|
139
|
+
* importing this module never pulls `@account-kit/*` unless you call `connect`.
|
|
140
|
+
*/
|
|
141
|
+
export declare class UpDownAccountKitSigner {
|
|
142
|
+
private readonly config;
|
|
143
|
+
private _client;
|
|
144
|
+
private _sessionClient;
|
|
145
|
+
private _account;
|
|
146
|
+
private _address;
|
|
147
|
+
private _ownerEoa;
|
|
148
|
+
private _sessionContext;
|
|
149
|
+
private _sessionKeyAddress;
|
|
150
|
+
private _sessionPrivateKey;
|
|
151
|
+
private _sessionExpirySec;
|
|
152
|
+
private _orderSession;
|
|
153
|
+
private _orderSessionClient;
|
|
154
|
+
private _mods;
|
|
155
|
+
constructor(config: UpDownAccountKitConfig);
|
|
156
|
+
/**
|
|
157
|
+
* Create the owner smart-wallet client and resolve the SCA address. Does NOT
|
|
158
|
+
* prompt for a session — call `grantSession()` (or `onboard`, which grants
|
|
159
|
+
* as needed) before gasless sends. Order signing works immediately (owner key).
|
|
160
|
+
*/
|
|
161
|
+
connect(): Promise<Address>;
|
|
162
|
+
/** The SCA address — this is `order.maker`, the deposit address, and the
|
|
163
|
+
* ERC-1271 signer. Identical to rain.trade's SCA for the same owner. */
|
|
164
|
+
get address(): Address;
|
|
165
|
+
/** Owner EOA address (signs the ERC-1271 order signatures under the hood). */
|
|
166
|
+
get ownerEoa(): Address;
|
|
167
|
+
/** The owner smart-wallet client. Exposes `signTypedData`, `sendCalls`, etc. */
|
|
168
|
+
get ownerClient(): any;
|
|
169
|
+
get hasActiveSession(): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* EIP-712 typed data as a BARE ERC-1271 signature — the ONLY signing path
|
|
172
|
+
* for orders, cancels, and ws-auth. ORDER-session-signed (popup-less) when
|
|
173
|
+
* an order session is active (header §2b); owner-signed with the 6492
|
|
174
|
+
* wrapper stripped otherwise. Requires `connect()`; the SCA should be
|
|
175
|
+
* deployed (`onboard`) before the signature is used in a fill.
|
|
176
|
+
*/
|
|
177
|
+
signTypedDataBare(typedData: TypedDataDefinition): Promise<Hex>;
|
|
178
|
+
private get orderSessionsEnabled();
|
|
179
|
+
private get orderSessionStore();
|
|
180
|
+
/** True iff an unexpired order session exists for the connected SCA. */
|
|
181
|
+
get hasOrderSession(): boolean;
|
|
182
|
+
private readOrderSession;
|
|
183
|
+
private persistOrderSession;
|
|
184
|
+
/** Drop the local order-session key (no on-chain uninstall — follow-up). */
|
|
185
|
+
revokeOrderSession(): void;
|
|
186
|
+
/**
|
|
187
|
+
* Sign typed data with the order-session key (MA-v2 entity signature, bare
|
|
188
|
+
* by construction). Returns null when no session is active or anything
|
|
189
|
+
* fails — `signTypedDataBare` falls back to the owner path.
|
|
190
|
+
*/
|
|
191
|
+
private orderSessionSign;
|
|
192
|
+
/** Lazily build the MA-v2 client bound to the order-session key's entity. */
|
|
193
|
+
private orderSessionAccount;
|
|
194
|
+
/**
|
|
195
|
+
* Make sure an order session exists: no-op when one is active (or the
|
|
196
|
+
* feature is off), otherwise install a fresh session key via a one-time
|
|
197
|
+
* owner UserOp. Fresh users get the install batched into `onboard()` and
|
|
198
|
+
* never hit the UserOp here. Throws if the owner rejects; callers should
|
|
199
|
+
* treat that as non-fatal (owner-key signing keeps working).
|
|
200
|
+
*/
|
|
201
|
+
ensureOrderSession(): Promise<"disabled" | "active" | "installed">;
|
|
202
|
+
/**
|
|
203
|
+
* Build the `installValidation` self-call that registers a fresh session
|
|
204
|
+
* key on the SCA as a signature-validation-ONLY entity (validated flow:
|
|
205
|
+
* updown-frontend/scripts/poc-session-key-fe-flow.mjs).
|
|
206
|
+
*/
|
|
207
|
+
private buildOrderSessionInstallCall;
|
|
208
|
+
/** The @account-kit/infra chain (Alchemy RPC config baked in) for MA-v2 clients. */
|
|
209
|
+
private infraChain;
|
|
210
|
+
/**
|
|
211
|
+
* Build + owner-sign a WS-auth handshake and package it as `WsAuthCredentials`
|
|
212
|
+
* for `UpDownWsClient.connectAuthed({ signAuth })`. `wallet` is the SCA
|
|
213
|
+
* address (private channels are keyed by the SCA, the trading identity).
|
|
214
|
+
*/
|
|
215
|
+
signWsAuth(chainId: number): Promise<WsAuthCredentials>;
|
|
216
|
+
/** True iff the SCA has bytecode on-chain (deploy-before-fill precondition). */
|
|
217
|
+
isDeployed(publicClient: PublicClient): Promise<boolean>;
|
|
218
|
+
/**
|
|
219
|
+
* Grant a 24h session key (one MetaMask popup) so subsequent sends are
|
|
220
|
+
* gasless + popup-less. Mirrors RainAA's `grantSession`; safe to call when a
|
|
221
|
+
* session is already active (returns the existing one without prompting).
|
|
222
|
+
*/
|
|
223
|
+
grantSession(): Promise<GrantSessionResult>;
|
|
224
|
+
/**
|
|
225
|
+
* One-time onboarding: DEPLOY the SCA, `approve(settlement, USDT, MAX)` AND
|
|
226
|
+
* install the order-session key (header §2b), all in a single UserOp —
|
|
227
|
+
* still exactly one owner signature. Satisfies deploy-before-fill + the
|
|
228
|
+
* allowance `enterPosition` needs, and makes subsequent order signatures
|
|
229
|
+
* popup-less. Order-session prep failures degrade to plain deploy+approve
|
|
230
|
+
* (owner-key signing per order). Uses the owner client so it works before
|
|
231
|
+
* any session exists; deployment is carried by the account init-code.
|
|
232
|
+
*/
|
|
233
|
+
onboard(args: {
|
|
234
|
+
usdt: Address;
|
|
235
|
+
settlement: Address;
|
|
236
|
+
}): Promise<Hex>;
|
|
237
|
+
/** Idempotent USDT approve to the settlement (gasless UserOp). */
|
|
238
|
+
approve(args: {
|
|
239
|
+
usdt: Address;
|
|
240
|
+
settlement: Address;
|
|
241
|
+
}): Promise<Hex>;
|
|
242
|
+
/** Transfer USDT out of the SCA to `to` (gasless UserOp). */
|
|
243
|
+
withdraw(args: {
|
|
244
|
+
usdt: Address;
|
|
245
|
+
to: Address;
|
|
246
|
+
amount: bigint;
|
|
247
|
+
}): Promise<Hex>;
|
|
248
|
+
/**
|
|
249
|
+
* Send a single call from the SCA as a UserOp. Gas is paid per the config:
|
|
250
|
+
* sponsorship policy → app-sponsored; ERC-20 policy + `gasToken` → user pays
|
|
251
|
+
* in that token; no policy → SELF-PAID (SCA must hold ETH). Prefers the
|
|
252
|
+
* session client (no popup) when a session is active; otherwise falls back
|
|
253
|
+
* to the owner client (one signature). Returns the on-chain tx hash.
|
|
254
|
+
*/
|
|
255
|
+
sendCall(call: {
|
|
256
|
+
to: Address;
|
|
257
|
+
data: Hex;
|
|
258
|
+
value?: bigint;
|
|
259
|
+
}): Promise<Hex>;
|
|
260
|
+
/** Send one UserOp batching `calls` in order (same gas/session semantics as
|
|
261
|
+
* `sendCall`); returns the on-chain tx hash. */
|
|
262
|
+
private sendCalls;
|
|
263
|
+
/** Clear in-memory state (persisted sessions, if any, are left intact). */
|
|
264
|
+
disconnect(): void;
|
|
265
|
+
}
|