rain-sdk-v2 2.3.0 → 2.5.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 CHANGED
@@ -9,6 +9,68 @@ APIs slated for removal are marked `@deprecated` in the type declarations for at
9
9
  least one minor release before they are removed, with the replacement named in
10
10
  the deprecation notice. Breaking removals land only in a major version.
11
11
 
12
+ ## [2.5.0] - Sell slippage + create-market validation
13
+
14
+ ### Fixed
15
+ - **Unprotected sells (critical):** `buildSellOptionTx` hard-coded `minAmountOut = 0`
16
+ and silently ignored slippage — every SDK sell shipped with zero MEV/slippage
17
+ protection despite the docs. It now quotes `getSellProceeds` on-chain and applies
18
+ `slippageTolerance` (default 5%) when `minAmountOut` is omitted. Verified against
19
+ live Arbitrum: the quote is the correct (slightly conservative) `minAmountOut`
20
+ basis. Pass `minAmountOut: 0n` to opt out explicitly.
21
+ - **Silent market-odds corruption:** `createMarket` now validates bar values and
22
+ option arrays and **throws** instead of silently redistributing the rounding
23
+ remainder onto the last option (which turned `[60,60]` into a 60/40 market).
24
+ Enforced: `no_of_options ≥ 2`; `marketOptions` / `barValues` / `initialYesPrices`
25
+ lengths equal `no_of_options`; each bar value in `0–100`; bar values sum to 100;
26
+ each `initialYesPrice` in `(0, 1e18)`.
27
+ - **`split` / `merge` option guard:** both now reject `option: 0n` (options are
28
+ 1-based) before any network call — the last two builders missing the guard.
29
+
30
+ ### Changed (breaking)
31
+ - **`buildSellOptionTx` is now async** (`Promise<RawTransaction>`) because it makes
32
+ an on-chain quote — this matches what the README already documented. Callers must
33
+ `await` it. `SellOptionTxParams` gains `slippageTolerance?`.
34
+
35
+ ### Added
36
+ - Test coverage grew to cover sell slippage derivation (mocked quote), create-market
37
+ validation, and the split/merge option guards.
38
+
39
+ ### Note for integrators
40
+ - Confirm the fee treatment of `getSellProceeds` vs. `sellOption` with the contract
41
+ team; the SDK uses it as the contract's own sell quote and applies the slippage
42
+ discount to it (empirically a conservative, safe floor).
43
+
44
+ ## [2.4.0] - Production readiness
45
+
46
+ No breaking changes to the public API.
47
+
48
+ ### Fixed
49
+ - **Docs correctness:** the `deadline` examples and parameter tables in the README
50
+ no longer show `deadline: 600n` (a duration), which the builders reject since
51
+ 2.2.0. They now show an absolute unix timestamp and note the default 10-min window.
52
+
53
+ ### Changed
54
+ - **viem-only (dropped `ethers`):** `utils/helpers.ts` (allowance + decimals reads,
55
+ `parseUnits`, RPC liveness) is ported to viem. `ethers` is removed from
56
+ dependencies — one web3 stack, smaller install/bundle. The redundant
57
+ per-call `getNetwork()` RPC round-trip before allowance checks is gone.
58
+ - **`RainAA` browser-only, documented + guarded:** session methods now throw a
59
+ clear error when `indexedDB` is unavailable (Node/SSR) instead of a cryptic
60
+ `ReferenceError`. Documented in the README and class JSDoc. Use `Rain` for
61
+ server-side flows.
62
+
63
+ ### Added
64
+ - **Test suite (vitest):** unit tests for the validators and encoders, plus a
65
+ gated Arbitrum integration test (`npm run test:integration`) exercising the
66
+ on-chain read path. Scripts: `test`, `test:watch`, `test:integration`.
67
+ - **CI:** GitHub Actions — build + test on push/PR, and publish-on-release
68
+ (`.github/workflows/`).
69
+
70
+ ### Removed
71
+ - Dead `getRandomRpc` export (superseded by `getDefaultRpc` in 2.3.0).
72
+ - Committed `*.tgz` tarball removed from the repo and gitignored.
73
+
12
74
  ## [2.3.0] - Developer experience
13
75
 
14
76
  Additive, non-breaking. Fills in the untyped core data model and adds the
package/README.md CHANGED
@@ -71,6 +71,8 @@ const rain = new Rain(config?: RainCoreConfig);
71
71
 
72
72
  Stateful class for smart account (Account Abstraction) management with Alchemy gas sponsorship.
73
73
 
74
+ > ⚠️ **Browser-only.** `RainAA` persists session keys in `indexedDB`, so it is **not supported in Node.js or SSR** — session methods throw a descriptive error in those environments. For server-side flows use the stateless `Rain` class (transaction building and on-chain reads), which runs anywhere.
75
+
74
76
  #### Constructor
75
77
 
76
78
  ```typescript
@@ -197,6 +199,8 @@ const txsRain = await rain.buildCreateMarketTx({
197
199
  | `tradingModel` | `TradingModel` | `AMM (0)` or `OrderBook (1)` |
198
200
  | `marketImage` | `string` | Market image URL (required) |
199
201
 
202
+ > **Constraints (validated, throws if violated):** `no_of_options ≥ 2`; `marketOptions`, `barValues`, and (if provided) `initialYesPrices` must each have length `no_of_options`; every `barValue` must be within `0–100` and the set must **sum to 100**; each `initialYesPrice` must be within `(0, 1e18)` exclusive. Previously out-of-spec bar values were silently redistributed onto the last option — they are now rejected.
203
+
200
204
  ---
201
205
 
202
206
  ### Trading
@@ -213,7 +217,7 @@ const txs = await rain.buildEnterOptionTx({
213
217
  buyAmountInWei: parseUnits('5', 6), // 5 USDT (or parseUnits('5', 18) for RAIN)
214
218
  walletAddress: '0x...', // user's wallet address
215
219
  slippageTolerance: 5n, // optional, default 5% — percentage tolerance for minSharesOut
216
- deadline: 600n, // optional, default 600 (10 min) — duration in seconds
220
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 600), // optional — absolute unix timestamp; omit for a default 10-min window
217
221
  });
218
222
  // Returns [approveTx?, enterOptionTx]
219
223
  ```
@@ -227,7 +231,7 @@ const txs = await rain.buildEnterOptionTx({
227
231
  | `walletAddress` | `0x${string}` | User's wallet address (for allowance check) |
228
232
  | `minSharesOut` | `bigint` | *(Optional)* Minimum shares to receive. Auto-calculated from `getEntryShares` with slippage if not set |
229
233
  | `slippageTolerance` | `bigint` | *(Optional)* Slippage percentage (e.g. `5n` = 5%). Default: 5% |
230
- | `deadline` | `bigint` | *(Optional)* Duration in seconds (e.g. `600n` = 10 min). Default: 600 |
234
+ | `deadline` | `bigint` | *(Optional)* Absolute unix timestamp in seconds, e.g. `BigInt(Math.floor(Date.now()/1000) + 600)`. Omit for a default 10-min window. A small duration like `600n` is **rejected** (`RainValidationError`). |
231
235
 
232
236
  > **Note:** Approval is handled automatically. The SDK reads `baseToken` from the market contract and checks allowance before building transactions. Slippage protection is auto-calculated: the SDK calls `getEntryShares` on-chain to get expected shares, then applies the slippage tolerance.
233
237
 
@@ -235,7 +239,7 @@ const txs = await rain.buildEnterOptionTx({
235
239
 
236
240
  #### `buildSellOptionTx(params): Promise<RawTransaction>`
237
241
 
238
- Market-sell shares of an option into the resting buy order book. No approval needed (you're selling shares, not tokens). Slippage protection is auto-calculated from on-chain `getCurrentPrice`.
242
+ Market-sell shares of an option into the resting buy order book. No approval needed (you're selling shares, not tokens). Slippage protection is auto-calculated from on-chain `getSellProceeds` (default 5%) when `minAmountOut` is omitted. Pass `minAmountOut: 0n` to opt out explicitly.
239
243
 
240
244
  ```typescript
241
245
  const tx = await rain.buildSellOptionTx({
@@ -244,7 +248,7 @@ const tx = await rain.buildSellOptionTx({
244
248
  optionSide: OptionSide.Yes, // Yes = 1, No = 2
245
249
  sharesAmount: parseUnits('10', 6), // shares to sell
246
250
  slippageTolerance: 5n, // optional, default 5%
247
- deadline: 600n, // optional, default 600 (10 min) — duration in seconds
251
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 600), // optional — absolute unix timestamp; omit for a default 10-min window
248
252
  });
249
253
  // Returns a single RawTransaction (no approval needed)
250
254
  ```
@@ -255,9 +259,9 @@ const tx = await rain.buildSellOptionTx({
255
259
  | `selectedOption` | `bigint` | Option index (1-based) |
256
260
  | `optionSide` | `OptionSide` | `Yes (1)` or `No (2)` |
257
261
  | `sharesAmount` | `bigint` | Number of shares to sell |
258
- | `minAmountOut` | `bigint` | *(Optional)* Minimum base tokens to receive. Auto-calculated from `getCurrentPrice` with slippage if not set |
262
+ | `minAmountOut` | `bigint` | *(Optional)* Minimum base tokens to receive. Auto-calculated from `getSellProceeds` with slippage if not set. Pass `0n` to opt out |
259
263
  | `slippageTolerance` | `bigint` | *(Optional)* Slippage percentage (e.g. `5n` = 5%). Default: 5% |
260
- | `deadline` | `bigint` | *(Optional)* Duration in seconds (e.g. `600n` = 10 min). Default: 600 |
264
+ | `deadline` | `bigint` | *(Optional)* Absolute unix timestamp in seconds, e.g. `BigInt(Math.floor(Date.now()/1000) + 600)`. Omit for a default 10-min window. A small duration like `600n` is **rejected** (`RainValidationError`). |
261
265
 
262
266
  ---
263
267
 
@@ -308,7 +312,7 @@ const txs = await rain.buildAddLiquidityTx({
308
312
  totalAmountInWei: parseUnits('10', 6), // 10 USDT
309
313
  walletAddress: '0x...', // user's wallet address
310
314
  slippageTolerance: 5n, // optional, default 5%
311
- deadline: 600n, // optional, default 600 (10 min) — duration in seconds
315
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 600), // optional — absolute unix timestamp; omit for a default 10-min window
312
316
  });
313
317
  // Returns [approveTx?, addLiquidityTx]
314
318
  ```
@@ -322,7 +326,7 @@ const txs = await rain.buildAddLiquidityTx({
322
326
  | `minYesToDeposit` | `bigint` | *(Optional)* Min yes tokens to deposit. Auto-calculated from reserves if not set |
323
327
  | `minNoToDeposit` | `bigint` | *(Optional)* Min no tokens to deposit. Auto-calculated from reserves if not set |
324
328
  | `slippageTolerance` | `bigint` | *(Optional)* Slippage percentage (e.g. `5n` = 5%). Default: 5% |
325
- | `deadline` | `bigint` | *(Optional)* Duration in seconds (e.g. `600n` = 10 min). Default: 600 |
329
+ | `deadline` | `bigint` | *(Optional)* Absolute unix timestamp in seconds, e.g. `BigInt(Math.floor(Date.now()/1000) + 600)`. Omit for a default 10-min window. A small duration like `600n` is **rejected** (`RainValidationError`). |
326
330
 
327
331
  > **Note:** Approval is handled automatically. Slippage protection is auto-calculated from `ammYesReserve`/`ammNoReserve` proportionally.
328
332
 
@@ -342,7 +346,7 @@ const tx = await rain.buildRemoveLiquidityTx({
342
346
  option: 1n,
343
347
  lpShares, // raw LP shares amount
344
348
  slippageTolerance: 5n, // optional, default 5%
345
- deadline: 600n, // optional, default 600 (10 min) — duration in seconds
349
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 600), // optional — absolute unix timestamp; omit for a default 10-min window
346
350
  });
347
351
  ```
348
352
 
@@ -354,7 +358,7 @@ const tx = await rain.buildRemoveLiquidityTx({
354
358
  | `minYesOut` | `bigint` | *(Optional)* Min yes tokens to receive. Auto-calculated from `getRemovedLiquidity` if not set |
355
359
  | `minNoOut` | `bigint` | *(Optional)* Min no tokens to receive. Auto-calculated if not set |
356
360
  | `slippageTolerance` | `bigint` | *(Optional)* Slippage percentage (e.g. `5n` = 5%). Default: 5% |
357
- | `deadline` | `bigint` | *(Optional)* Duration in seconds (e.g. `600n` = 10 min). Default: 600 |
361
+ | `deadline` | `bigint` | *(Optional)* Absolute unix timestamp in seconds, e.g. `BigInt(Math.floor(Date.now()/1000) + 600)`. Omit for a default 10-min window. A small duration like `600n` is **rejected** (`RainValidationError`). |
358
362
 
359
363
  ---
360
364
 
package/dist/Rain.d.ts CHANGED
@@ -56,7 +56,7 @@ export declare class Rain {
56
56
  buildEnterOptionTx(params: EnterOptionTxParams & {
57
57
  walletAddress: `0x${string}`;
58
58
  }): Promise<RawTransaction[]>;
59
- buildSellOptionTx(params: SellOptionTxParams): RawTransaction;
59
+ buildSellOptionTx(params: SellOptionTxParams): Promise<RawTransaction>;
60
60
  buildAddLiquidityTx(params: AddLiquidityTxParams & {
61
61
  walletAddress: `0x${string}`;
62
62
  }): Promise<RawTransaction[]>;
package/dist/Rain.js CHANGED
@@ -164,8 +164,8 @@ export class Rain {
164
164
  async buildEnterOptionTx(params) {
165
165
  return buildEnterOptionRawTx({ ...params, rpcUrl: this.rpcUrl });
166
166
  }
167
- buildSellOptionTx(params) {
168
- return buildSellOptionRawTx(params);
167
+ async buildSellOptionTx(params) {
168
+ return buildSellOptionRawTx({ ...params, rpcUrl: this.rpcUrl });
169
169
  }
170
170
  async buildAddLiquidityTx(params) {
171
171
  return buildAddLiquidityRawTx({ ...params, rpcUrl: this.rpcUrl });
package/dist/RainAA.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { RainConfig } from './types.js';
2
2
  import { RawTransaction } from './tx/types.js';
3
+ /**
4
+ * Smart-account (Account Abstraction) manager with Alchemy gas sponsorship.
5
+ *
6
+ * ⚠️ **Browser-only.** Session persistence uses `indexedDB`, so `RainAA` is not
7
+ * supported in Node.js or SSR — session methods throw a descriptive error there.
8
+ * The stateless `Rain` class works in any environment; use it for server-side flows.
9
+ */
3
10
  export declare class RainAA {
4
11
  private config;
5
12
  private _client;
package/dist/RainAA.js CHANGED
@@ -3,6 +3,10 @@ const SESSION_DURATION_SEC = 60 * 60 * 24; // 24 hours
3
3
  const DB_NAME = 'RainSDKV2';
4
4
  const STORE_NAME = 'sessions';
5
5
  function openSessionDB() {
6
+ if (typeof indexedDB === 'undefined') {
7
+ return Promise.reject(new Error('RainAA requires a browser environment: session persistence uses indexedDB, ' +
8
+ 'which is unavailable in Node.js/SSR. RainAA is browser-only.'));
9
+ }
6
10
  return new Promise((resolve, reject) => {
7
11
  const req = indexedDB.open(DB_NAME, 1);
8
12
  req.onupgradeneeded = () => {
@@ -42,6 +46,13 @@ async function deleteSession(key) {
42
46
  tx.onerror = () => reject(tx.error);
43
47
  });
44
48
  }
49
+ /**
50
+ * Smart-account (Account Abstraction) manager with Alchemy gas sponsorship.
51
+ *
52
+ * ⚠️ **Browser-only.** Session persistence uses `indexedDB`, so `RainAA` is not
53
+ * supported in Node.js or SSR — session methods throw a descriptive error there.
54
+ * The stateless `Rain` class works in any environment; use it for server-side flows.
55
+ */
45
56
  export class RainAA {
46
57
  config;
47
58
  _client = null; // EOA-signed client (for grantPermissions)
@@ -1,15 +1,12 @@
1
- /** Decode the `exp` (unix seconds) claim from a JWT without verifying it. Cross-environment (browser + Node). */
1
+ /** Decode the `exp` (unix seconds) claim from a JWT without verifying it. Uses `atob` (browser + Node 18+). */
2
2
  function decodeJwtExp(token) {
3
3
  try {
4
4
  const part = token.split('.')[1];
5
- if (!part)
5
+ if (!part || typeof atob !== 'function')
6
6
  return undefined;
7
7
  const b64 = part.replace(/-/g, '+').replace(/_/g, '/');
8
8
  const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
9
- const json = typeof atob === 'function'
10
- ? atob(padded)
11
- : Buffer.from(padded, 'base64').toString('binary');
12
- const payload = JSON.parse(json);
9
+ const payload = JSON.parse(atob(padded));
13
10
  return typeof payload?.exp === 'number' ? payload.exp : undefined;
14
11
  }
15
12
  catch {
@@ -1,6 +1,5 @@
1
1
  export declare const ALLOWED_ENVIRONMENTS: readonly ["development", "stage", "production"];
2
2
  export declare const DEFAULT_RPCS: string[];
3
- export declare function getRandomRpc(): string;
4
3
  /** Deterministic default RPC — every instance resolves to the same endpoint. */
5
4
  export declare function getDefaultRpc(): string;
6
5
  export declare const USDT_SYMBOL_DEV = "USDTm";
@@ -4,10 +4,6 @@ export const DEFAULT_RPCS = [
4
4
  "https://arbitrum-one.publicnode.com",
5
5
  "https://rpc.sentio.xyz/arbitrum-one"
6
6
  ];
7
- export function getRandomRpc() {
8
- const index = Math.floor(Math.random() * DEFAULT_RPCS.length);
9
- return DEFAULT_RPCS[index];
10
- }
11
7
  /** Deterministic default RPC — every instance resolves to the same endpoint. */
12
8
  export function getDefaultRpc() {
13
9
  return DEFAULT_RPCS[0];
@@ -1,5 +1,5 @@
1
1
  export function validateCreateMarketParams(params) {
2
- const { marketQuestion, marketOptions, marketTags, marketDescription, isPublic, isPublicPoolResolverAi, creator, startTime, endTime, no_of_options, inputAmountWei, barValues, baseToken, factoryContractAddress, tokenDecimals, } = params;
2
+ const { marketQuestion, marketOptions, marketTags, marketDescription, isPublic, isPublicPoolResolverAi, creator, startTime, endTime, no_of_options, inputAmountWei, barValues, baseToken, factoryContractAddress, tokenDecimals, initialYesPrices, } = params;
3
3
  // Required field validations
4
4
  if (typeof isPublic !== "boolean")
5
5
  throw new Error("isPublic is required and must be a boolean");
@@ -48,5 +48,33 @@ export function validateCreateMarketParams(params) {
48
48
  }
49
49
  if (startTime >= endTime)
50
50
  throw new Error("startTime must be earlier than endTime");
51
+ // Cross-field validation — these previously slipped through and were silently
52
+ // "fixed" by normalizeBarValues (which dumped the rounding remainder onto the
53
+ // last option, producing odds the caller never asked for). Reject loudly instead.
54
+ const optionCount = Number(no_of_options);
55
+ if (optionCount < 2)
56
+ throw new Error("no_of_options must be at least 2");
57
+ if (marketOptions.length !== optionCount) {
58
+ throw new Error(`marketOptions length (${marketOptions.length}) must equal no_of_options (${optionCount})`);
59
+ }
60
+ if (barValues.length !== optionCount) {
61
+ throw new Error(`barValues length (${barValues.length}) must equal no_of_options (${optionCount})`);
62
+ }
63
+ if (barValues.some(v => typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 100)) {
64
+ throw new Error("each barValue must be a number between 0 and 100");
65
+ }
66
+ const barSum = barValues.reduce((s, v) => s + v, 0);
67
+ if (Math.abs(barSum - 100) > 0.01) {
68
+ throw new Error(`barValues must sum to 100 (got ${barSum})`);
69
+ }
70
+ if (initialYesPrices !== undefined) {
71
+ if (initialYesPrices.length !== optionCount) {
72
+ throw new Error(`initialYesPrices length (${initialYesPrices.length}) must equal no_of_options (${optionCount})`);
73
+ }
74
+ const ONE = 10n ** 18n;
75
+ if (initialYesPrices.some(p => p <= 0n || p >= ONE)) {
76
+ throw new Error("each initialYesPrice must be within (0, 1e18) exclusive");
77
+ }
78
+ }
51
79
  return true;
52
80
  }
@@ -1,6 +1,7 @@
1
1
  import { encodeFunctionData } from "viem";
2
2
  import { MarketsAbi } from "../abi/MarketsAbi.js";
3
3
  import { MERGE } from "../constants/contractmethods.js";
4
+ import { assertOption } from "./validation.js";
4
5
  export function buildMergeRawTx(params) {
5
6
  const { marketContractAddress, option, amount } = params;
6
7
  if (!marketContractAddress)
@@ -9,6 +10,7 @@ export function buildMergeRawTx(params) {
9
10
  throw new Error("option is required");
10
11
  if (!amount || amount <= 0n)
11
12
  throw new Error("amount must be greater than 0");
13
+ assertOption(option);
12
14
  return {
13
15
  to: marketContractAddress,
14
16
  data: encodeFunctionData({
@@ -1,2 +1,4 @@
1
1
  import { SellOptionTxParams, RawTransaction } from "./types.js";
2
- export declare function buildSellOptionRawTx(params: SellOptionTxParams): RawTransaction;
2
+ export declare function buildSellOptionRawTx(params: SellOptionTxParams & {
3
+ rpcUrl: string;
4
+ }): Promise<RawTransaction>;
@@ -1,10 +1,12 @@
1
1
  import { encodeFunctionData } from "viem";
2
2
  import { MarketsAbi } from "../abi/MarketsAbi.js";
3
3
  import { SELL_OPTION } from "../constants/contractmethods.js";
4
- import { assertOption, assertDeadline } from "./validation.js";
4
+ import { assertOption, assertSlippage, assertDeadline } from "./validation.js";
5
+ import { getSellProceeds } from "../markets/getSellProceeds.js";
6
+ const DEFAULT_SLIPPAGE = 5n; // 5%
5
7
  const DEFAULT_DEADLINE_DURATION = 600; // 10 minutes
6
- export function buildSellOptionRawTx(params) {
7
- const { marketContractAddress, selectedOption, optionSide, sharesAmount, minAmountOut, deadline } = params;
8
+ export async function buildSellOptionRawTx(params) {
9
+ const { marketContractAddress, selectedOption, optionSide, sharesAmount, minAmountOut, slippageTolerance, deadline, rpcUrl } = params;
8
10
  const effectiveDeadline = deadline ?? BigInt(Math.floor(Date.now() / 1000) + DEFAULT_DEADLINE_DURATION);
9
11
  if (!marketContractAddress)
10
12
  throw new Error("marketContractAddress is required");
@@ -17,13 +19,29 @@ export function buildSellOptionRawTx(params) {
17
19
  if (sharesAmount <= 0n)
18
20
  throw new Error("sharesAmount must be greater than 0");
19
21
  assertOption(selectedOption, "selectedOption");
22
+ assertSlippage(slippageTolerance);
20
23
  assertDeadline(deadline);
24
+ // Slippage protection: when the caller does not pin minAmountOut, quote the
25
+ // expected proceeds on-chain (getSellProceeds — a conservative quote) and
26
+ // apply the slippage tolerance. Pass minAmountOut: 0n to opt out explicitly.
27
+ let effectiveMinAmountOut = minAmountOut;
28
+ if (effectiveMinAmountOut === undefined || effectiveMinAmountOut === null) {
29
+ const { proceeds } = await getSellProceeds({
30
+ marketContractAddress,
31
+ option: selectedOption,
32
+ optionSide,
33
+ shares: sharesAmount,
34
+ rpcUrl,
35
+ });
36
+ const slippage = slippageTolerance ?? DEFAULT_SLIPPAGE;
37
+ effectiveMinAmountOut = proceeds * (100n - slippage) / 100n;
38
+ }
21
39
  return {
22
40
  to: marketContractAddress,
23
41
  data: encodeFunctionData({
24
42
  abi: MarketsAbi,
25
43
  functionName: SELL_OPTION,
26
- args: [selectedOption, optionSide, sharesAmount, minAmountOut ?? 0n, effectiveDeadline],
44
+ args: [selectedOption, optionSide, sharesAmount, effectiveMinAmountOut, effectiveDeadline],
27
45
  }),
28
46
  value: 0n,
29
47
  };
@@ -3,6 +3,7 @@ import { MarketsAbi } from "../abi/MarketsAbi.js";
3
3
  import { SPLIT } from "../constants/contractmethods.js";
4
4
  import { checkMarketTokenAllowance } from "../utils/helpers.js";
5
5
  import { buildApproveRawTx } from "./buildApprovalRawTx.js";
6
+ import { assertOption } from "./validation.js";
6
7
  export async function buildSplitRawTx(params) {
7
8
  const { marketContractAddress, option, amount, walletAddress, rpcUrl } = params;
8
9
  if (!marketContractAddress)
@@ -11,6 +12,7 @@ export async function buildSplitRawTx(params) {
11
12
  throw new Error("option is required");
12
13
  if (!amount || amount <= 0n)
13
14
  throw new Error("amount must be greater than 0");
15
+ assertOption(option);
14
16
  const { allowance, baseToken } = await checkMarketTokenAllowance({ marketContractAddress, owner: walletAddress, rpcUrl });
15
17
  const txs = [];
16
18
  if (allowance < amount) {
@@ -31,6 +31,7 @@ export interface SellOptionTxParams {
31
31
  optionSide: OptionSide;
32
32
  sharesAmount: bigint;
33
33
  minAmountOut?: bigint;
34
+ slippageTolerance?: bigint;
34
35
  deadline?: bigint;
35
36
  }
36
37
  export interface PlaceBuyOrderTxParams {
@@ -1,7 +1,8 @@
1
1
  import { CreateMarketTxParams } from "../tx/types.js";
2
2
  export declare const convertToWeiEthers: (value: string | bigint, decimals: number) => bigint;
3
+ /** Lightweight RPC liveness check via viem `getChainId`. Kept for callers that want a pre-flight probe. */
3
4
  export declare function isRpcValid(rpcUrl: string | undefined): Promise<boolean>;
4
- export declare function getUserAllowance(params: CreateMarketTxParams): Promise<number>;
5
+ export declare function getUserAllowance(params: CreateMarketTxParams): Promise<bigint>;
5
6
  /**
6
7
  * Checks allowance for a market's base token.
7
8
  * Reads baseToken from the market contract, then checks the ERC20 allowance.
@@ -1,31 +1,36 @@
1
- import { ethers, JsonRpcProvider, Contract } from "ethers";
1
+ import { parseUnits, createPublicClient, http } from "viem";
2
+ import { arbitrum } from "viem/chains";
2
3
  import { ERC20Abi } from "../abi/ERC20Abi.js";
3
4
  import { getMarketBaseToken } from "../markets/getResolverBondAmount.js";
5
+ const erc20Abi = ERC20Abi;
4
6
  export const convertToWeiEthers = (value, decimals) => {
5
- return ethers.parseUnits(value.toString(), decimals);
7
+ return parseUnits(value.toString(), decimals);
6
8
  };
9
+ /** Lightweight RPC liveness check via viem `getChainId`. Kept for callers that want a pre-flight probe. */
7
10
  export async function isRpcValid(rpcUrl) {
8
11
  if (!rpcUrl)
9
12
  return false;
10
- const provider = new JsonRpcProvider(rpcUrl);
11
13
  try {
12
- await provider.getNetwork();
14
+ const client = createPublicClient({ chain: arbitrum, transport: http(rpcUrl) });
15
+ await client.getChainId();
13
16
  return true;
14
17
  }
15
- catch (error) {
18
+ catch {
16
19
  return false;
17
20
  }
18
21
  }
19
22
  export async function getUserAllowance(params) {
20
23
  const { factoryContractAddress, baseToken, creator, rpcUrl } = params;
21
- const isRpcWorking = await isRpcValid(rpcUrl);
22
- if (!rpcUrl || !isRpcWorking) {
24
+ if (!rpcUrl)
23
25
  throw new Error("Provided RPC URL is not valid or not working");
24
- }
25
- const provider = new JsonRpcProvider(rpcUrl);
26
- const ERC20ApprovalContract = new Contract(baseToken, ERC20Abi, provider);
27
- const userAllowance = await ERC20ApprovalContract.allowance(creator, factoryContractAddress);
28
- return userAllowance;
26
+ const client = createPublicClient({ chain: arbitrum, transport: http(rpcUrl) });
27
+ const allowance = await client.readContract({
28
+ address: baseToken,
29
+ abi: erc20Abi,
30
+ functionName: 'allowance',
31
+ args: [creator, factoryContractAddress],
32
+ });
33
+ return allowance;
29
34
  }
30
35
  /**
31
36
  * Checks allowance for a market's base token.
@@ -34,19 +39,16 @@ export async function getUserAllowance(params) {
34
39
  */
35
40
  export async function checkMarketTokenAllowance(params) {
36
41
  const { marketContractAddress, owner, rpcUrl } = params;
37
- const isRpcWorking = await isRpcValid(rpcUrl);
38
- if (!rpcUrl || !isRpcWorking) {
42
+ if (!rpcUrl)
39
43
  throw new Error("Provided RPC URL is not valid or not working");
40
- }
41
44
  const baseToken = await getMarketBaseToken({ marketContractAddress, rpcUrl });
42
- const provider = new JsonRpcProvider(rpcUrl);
43
- const tokenContract = new Contract(baseToken, ERC20Abi, provider);
45
+ const client = createPublicClient({ chain: arbitrum, transport: http(rpcUrl) });
44
46
  const [userAllowance, tokenDecimals] = await Promise.all([
45
- tokenContract.allowance(owner, marketContractAddress),
46
- tokenContract.decimals(),
47
+ client.readContract({ address: baseToken, abi: erc20Abi, functionName: 'allowance', args: [owner, marketContractAddress] }),
48
+ client.readContract({ address: baseToken, abi: erc20Abi, functionName: 'decimals' }),
47
49
  ]);
48
50
  return {
49
- allowance: BigInt(userAllowance),
51
+ allowance: userAllowance,
50
52
  baseToken,
51
53
  decimals: Number(tokenDecimals),
52
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rain-sdk-v2",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
5
  "description": "Rain SDK V2 — TypeScript SDK for Rain prediction markets on Arbitrum. Market creation, trading, liquidity, order book, split/merge, dispute, and smart account support.",
6
6
  "main": "dist/index.js",
@@ -19,6 +19,9 @@
19
19
  "scripts": {
20
20
  "build": "tsc",
21
21
  "dev": "tsc -w",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "test:integration": "RUN_INTEGRATION=1 vitest run test/integration.test.ts",
22
25
  "prepublishOnly": "npm run build"
23
26
  },
24
27
  "repository": {
@@ -57,11 +60,11 @@
57
60
  }
58
61
  },
59
62
  "dependencies": {
60
- "ethers": "^6.16.0",
61
63
  "socket.io-client": "^4.8.3"
62
64
  },
63
65
  "devDependencies": {
64
66
  "typescript": "^5.9.3",
65
- "viem": "^2.48.8"
67
+ "viem": "^2.48.8",
68
+ "vitest": "^5.0.1"
66
69
  }
67
70
  }