rain-sdk-v2 2.1.7 → 2.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/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to `rain-sdk-v2` are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [2.2.0] - Guardrails
7
+
8
+ Adds fail-fast validation and structured errors. These surface mistakes
9
+ synchronously (before broadcast) instead of as on-chain reverts or parsed
10
+ strings. Existing `catch (e)` blocks keep working — the new errors extend `Error`.
11
+
12
+ ### Added
13
+ - **Build-time parameter validation (F-03):** every transaction builder now
14
+ rejects the documented footguns before encoding — a `deadline` that looks like
15
+ a duration (`< 1_000_000_000`) or is already expired, a `slippageTolerance`
16
+ outside `0–100` (whole percent, not bps), and a 0-based `option`/`selectedOption`
17
+ (options are 1-based). Throws the new `RainValidationError`. Exported helpers:
18
+ `assertDeadline`, `assertSlippage`, `assertOption`.
19
+ - **Structured API errors (F-06):** non-2xx REST responses now throw
20
+ `RainApiError` carrying `status`, `endpoint`, `body`, and a stable `code`
21
+ (`NOT_FOUND` | `UNAUTHORIZED` | `FORBIDDEN` | `RATE_LIMITED` | `SERVER_ERROR` |
22
+ `UNKNOWN`) — branch on `err.code` instead of parsing message strings.
23
+
24
+ ### Changed
25
+ - **Empty states are no longer errors (F-06):** `getPnlByPoolId` (no position)
26
+ and `getTokenPrice` (non-whitelisted token) now return
27
+ `{ statusCode: 200, data: null }` on a 404 instead of throwing.
28
+ - **Deterministic default RPC (F-09):** omitting `rpcUrl` now resolves to a
29
+ single fixed public RPC (previously random per instance). In `production`
30
+ without an `rpcUrl`, the SDK emits a `console.warn` recommending a dedicated
31
+ endpoint.
32
+
33
+ ## [2.1.9] - Correctness patch
34
+
35
+ Addresses findings from the September 2026 independent technical review. No
36
+ behaviour change for correct callers.
37
+
38
+ ### Fixed
39
+ - **Missing type exports (F-10):** `SellOptionTxParams`, `EntrySharesResult`,
40
+ `SellProceedsResult`, and `OrderLevelInfo` are now exported from the package
41
+ root, so consumers can annotate their own functions with them.
42
+ - **`RainConfig` type honesty (F-04):** `alchemyApiKey` and `paymasterPolicyId`
43
+ are now required in the type — the `RainAA` constructor already threw without
44
+ them. The type now matches runtime behaviour.
45
+ - **`smartWalletAddress` optional (F-08):** `LoginParams.smartWalletAddress` is
46
+ now optional and defaults to `walletAddress`, so plain-EOA integrations no
47
+ longer have to pass the address twice.
48
+
49
+ ### Notes
50
+ - Two review findings (F-01 reverting order reads, F-02 REST type/wire
51
+ mismatches) require verification against the live dev API/contracts before any
52
+ change and are intentionally not addressed in this release.
package/README.md CHANGED
@@ -24,7 +24,7 @@ import { arbitrum } from 'viem/chains';
24
24
  // Initialize SDK
25
25
  const rain = new Rain({
26
26
  environment: 'development', // 'development' | 'stage' | 'production'
27
- rpcUrl: 'https://arb1.arbitrum.io/rpc', // optional, uses random public RPC if omitted
27
+ rpcUrl: 'https://arb1.arbitrum.io/rpc', // optional; falls back to a public RPC (pass your own in production)
28
28
  });
29
29
 
30
30
  // Get environment config
@@ -62,7 +62,7 @@ const rain = new Rain(config?: RainCoreConfig);
62
62
  | Parameter | Type | Default | Description |
63
63
  |-----------|------|---------|-------------|
64
64
  | `environment` | `'development' \| 'stage' \| 'production'` | `'development'` | Target environment |
65
- | `rpcUrl` | `string` | Random public RPC | Custom Arbitrum RPC URL |
65
+ | `rpcUrl` | `string` | Fixed public RPC | Custom Arbitrum RPC URL. Omitting it uses a single deterministic public RPC; in `production` this also emits a `console.warn` — pass a dedicated endpoint. |
66
66
  | `apiUrl` | `string` | From environment | Custom API URL |
67
67
 
68
68
  ---
@@ -852,7 +852,7 @@ const signature = await signLoginMessage(walletClient, '0x...' as `0x${string}`)
852
852
  const result = await rain.login({
853
853
  signature, // from signLoginMessage
854
854
  walletAddress: '0x...', // EOA address
855
- smartWalletAddress: '0x...', // Smart account address
855
+ smartWalletAddress: '0x...', // optional — defaults to walletAddress for plain-EOA integrations
856
856
  referredBy: 'CODE', // optional referral code
857
857
  });
858
858
 
@@ -1544,6 +1544,54 @@ socket.disconnect();
1544
1544
 
1545
1545
  ---
1546
1546
 
1547
+ ## Error Handling & Validation
1548
+
1549
+ ### Build-time validation
1550
+
1551
+ Transaction builders validate their inputs synchronously — before any encoding
1552
+ or broadcast — and throw `RainValidationError` on the common footguns:
1553
+
1554
+ - `deadline` must be an **absolute unix timestamp** (seconds), not a duration.
1555
+ `deadline: 600n` is rejected; use `BigInt(Math.floor(Date.now()/1000) + 600)`.
1556
+ - `slippageTolerance` is a **whole percent** (`5n` = 5%), and must be `0–100`.
1557
+ - `option` / `selectedOption` are **1-based** (the first option is `1`, never `0`).
1558
+
1559
+ ```typescript
1560
+ import { RainValidationError } from 'rain-sdk-v2';
1561
+
1562
+ try {
1563
+ await rain.buildEnterOptionTx({ /* ... */, deadline: 600n });
1564
+ } catch (err) {
1565
+ if (err instanceof RainValidationError) {
1566
+ console.error(err.field, err.message); // 'deadline', 'Invalid deadline: ...'
1567
+ }
1568
+ }
1569
+ ```
1570
+
1571
+ ### API errors
1572
+
1573
+ REST methods throw `RainApiError` (a subclass of `Error`) on non-2xx responses,
1574
+ carrying structured fields so you can branch on `code` instead of parsing strings:
1575
+
1576
+ ```typescript
1577
+ import { RainApiError } from 'rain-sdk-v2';
1578
+
1579
+ try {
1580
+ await rain.getSomeData(/* ... */);
1581
+ } catch (err) {
1582
+ if (err instanceof RainApiError) {
1583
+ console.error(err.status, err.code, err.endpoint, err.body);
1584
+ // code: 'NOT_FOUND' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'RATE_LIMITED' | 'SERVER_ERROR' | 'UNKNOWN'
1585
+ }
1586
+ }
1587
+ ```
1588
+
1589
+ Legitimate empty states are **not** errors: `getPnlByPoolId` (no position) and
1590
+ `getTokenPrice` (non-whitelisted token) return `{ statusCode: 200, data: null }`
1591
+ rather than throwing on a 404.
1592
+
1593
+ ---
1594
+
1547
1595
  ## License
1548
1596
 
1549
1597
  MIT
package/dist/Rain.d.ts CHANGED
@@ -21,7 +21,8 @@ export declare class Rain {
21
21
  buildApprovalTx(params: ApproveTxParams): RawTransaction;
22
22
  getCreateMarketFees(tokenAddress: `0x${string}`, inputAmountWei: bigint): Promise<{
23
23
  oracleFeePerOption: bigint;
24
- liquidityFeeBps: bigint;
24
+ liquidityFee: bigint;
25
+ feeMagnification: bigint;
25
26
  useBufferApproval: boolean;
26
27
  perOptionBuffer: bigint;
27
28
  }>;
package/dist/Rain.js CHANGED
@@ -14,7 +14,7 @@ import { buildCancelBuyOrdersRawTx, buildCancelSellOrdersRawTx } from './tx/buil
14
14
  import { buildCalculateWinnerRawTx } from './tx/buildCalculateWinnerRawTx.js';
15
15
  import { buildExtendTimeRawTx } from './tx/buildExtendTimeRawTx.js';
16
16
  import { getUserActiveBuyOrders, getUserActiveSellOrders, getFirstBuyOrderPrice, getFirstSellOrderPrice, getBuyOrdersAtPrice, getSellOrdersAtPrice, checkOrderExists } from './markets/getOrderInfo.js';
17
- import { ALLOWED_ENVIRONMENTS, ENV_CONFIG, getRandomRpc } from './config/environments.js';
17
+ import { ALLOWED_ENVIRONMENTS, ENV_CONFIG, getDefaultRpc } from './config/environments.js';
18
18
  import { loginUser } from './auth/login.js';
19
19
  import { getUserOptionLPShares } from './markets/getUserOptionLPShares.js';
20
20
  import { getUserOptionShares } from './markets/getUserOptionShares.js';
@@ -51,6 +51,12 @@ const factoryViewAbi = parseAbi([
51
51
  'function liquidityFee() view returns (uint256)',
52
52
  'function baseToken() view returns (address)',
53
53
  ]);
54
+ // Scale of the factory's liquidityFee(). Matches the market contracts'
55
+ // FEE_MAGNIFICATION constant (verified on-chain = 1000 on dev and production
56
+ // markets; the factory itself does not expose it). liquidityFee() = 12 means
57
+ // a 1.2% fee — the unit is per-mille, NOT basis points: dividing by 10000
58
+ // computes every fee 10x too small.
59
+ const FEE_MAGNIFICATION = 1000n;
54
60
  export class Rain {
55
61
  environment;
56
62
  marketFactory;
@@ -66,7 +72,11 @@ export class Rain {
66
72
  throw new Error(`Invalid environment "${environment}". Allowed values: ${ALLOWED_ENVIRONMENTS.join(", ")}`);
67
73
  }
68
74
  this.environment = environment;
69
- this.rpcUrl = rpcUrl ?? getRandomRpc();
75
+ if (!rpcUrl && environment === "production") {
76
+ console.warn("[rain-sdk-v2] No rpcUrl provided in production; falling back to a public Arbitrum RPC. " +
77
+ "Public RPCs have shared rate limits and varying reliability — pass a dedicated rpcUrl.");
78
+ }
79
+ this.rpcUrl = rpcUrl ?? getDefaultRpc();
70
80
  const envConfig = ENV_CONFIG[this.environment];
71
81
  this.marketFactory = envConfig.market_factory_address;
72
82
  this.apiUrl = apiUrl ?? envConfig.apiUrl;
@@ -90,26 +100,26 @@ export class Rain {
90
100
  }
91
101
  async getCreateMarketFees(tokenAddress, inputAmountWei) {
92
102
  const pc = createPublicClient({ chain: arbitrum, transport: http(this.rpcUrl) });
93
- const [oracleFeeRaw, liquidityFeeBps, factoryBaseToken] = await Promise.all([
103
+ const [oracleFeeRaw, liquidityFee, factoryBaseToken] = await Promise.all([
94
104
  pc.readContract({ address: this.marketFactory, abi: factoryViewAbi, functionName: 'oracleFixedFee' }),
95
105
  pc.readContract({ address: this.marketFactory, abi: factoryViewAbi, functionName: 'liquidityFee' }),
96
106
  pc.readContract({ address: this.marketFactory, abi: factoryViewAbi, functionName: 'baseToken' }),
97
107
  ]);
98
108
  if (tokenAddress.toLowerCase() === factoryBaseToken.toLowerCase()) {
99
- return { oracleFeePerOption: oracleFeeRaw, liquidityFeeBps: liquidityFeeBps, useBufferApproval: false, perOptionBuffer: 0n };
109
+ return { oracleFeePerOption: oracleFeeRaw, liquidityFee: liquidityFee, feeMagnification: FEE_MAGNIFICATION, useBufferApproval: false, perOptionBuffer: 0n };
100
110
  }
101
111
  // For non-base tokens (e.g. RAIN): approval = initialLiquidity + (numberOfOptions * 20% of initialLiquidity)
102
112
  const perOptionBuffer = inputAmountWei * 20n / 100n;
103
- return { oracleFeePerOption: 0n, liquidityFeeBps: liquidityFeeBps, useBufferApproval: true, perOptionBuffer };
113
+ return { oracleFeePerOption: 0n, liquidityFee: liquidityFee, feeMagnification: FEE_MAGNIFICATION, useBufferApproval: true, perOptionBuffer };
104
114
  }
105
115
  async buildCreateMarketTx(params) {
106
116
  const tokenConfig = this.getTokenConfig(params.baseToken);
107
117
  const tokenDecimals = params.tokenDecimals ?? tokenConfig?.decimals ?? 6;
108
- const { oracleFeePerOption, liquidityFeeBps, useBufferApproval, perOptionBuffer } = await this.getCreateMarketFees(params.baseToken, params.inputAmountWei);
118
+ const { oracleFeePerOption, liquidityFee, feeMagnification, useBufferApproval, perOptionBuffer } = await this.getCreateMarketFees(params.baseToken, params.inputAmountWei);
109
119
  if (useBufferApproval) {
110
120
  return buildCreateMarketRawTx({ ...params, tokenDecimals, factoryContractAddress: this.marketFactory, apiUrl: this.apiUrl, rpcUrl: this.rpcUrl, disputeTimer: this.distute_initial_timer, oracleFixedFeePerOption: perOptionBuffer });
111
121
  }
112
- const liquidityFeeAmount = params.inputAmountWei * liquidityFeeBps / 10000n;
122
+ const liquidityFeeAmount = params.inputAmountWei * liquidityFee / feeMagnification;
113
123
  const totalOracleFee = oracleFeePerOption * BigInt(params.no_of_options) + liquidityFeeAmount;
114
124
  return buildCreateMarketRawTx({ ...params, tokenDecimals, factoryContractAddress: this.marketFactory, apiUrl: this.apiUrl, rpcUrl: this.rpcUrl, disputeTimer: this.distute_initial_timer, oracleFixedFeePerOption: totalOracleFee / BigInt(params.no_of_options) + 1n });
115
125
  }
@@ -1,4 +1,18 @@
1
1
  import { ApiConfig } from './types.js';
2
2
  export declare function buildHeaders(config: ApiConfig, contentType?: string): Record<string, string>;
3
3
  export declare function buildQuery(params: Record<string, string | number | boolean | undefined | null>): string;
4
- export declare function handleResponse<T>(res: Response): Promise<T>;
4
+ export type RainApiErrorCode = 'NOT_FOUND' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'RATE_LIMITED' | 'SERVER_ERROR' | 'UNKNOWN';
5
+ /**
6
+ * Structured error thrown by every REST wrapper on a non-2xx response.
7
+ * Extends `Error` (so existing `catch` blocks keep working) but adds
8
+ * `status`, `endpoint`, `body`, and a stable `code` to branch on instead of
9
+ * parsing message strings.
10
+ */
11
+ export declare class RainApiError extends Error {
12
+ readonly status: number;
13
+ readonly body: unknown;
14
+ readonly endpoint?: string | undefined;
15
+ readonly code: RainApiErrorCode;
16
+ constructor(status: number, body: unknown, endpoint?: string | undefined, code?: RainApiErrorCode);
17
+ }
18
+ export declare function handleResponse<T>(res: Response, endpoint?: string): Promise<T>;
@@ -17,10 +17,44 @@ export function buildQuery(params) {
17
17
  return '';
18
18
  return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
19
19
  }
20
- export async function handleResponse(res) {
20
+ /**
21
+ * Structured error thrown by every REST wrapper on a non-2xx response.
22
+ * Extends `Error` (so existing `catch` blocks keep working) but adds
23
+ * `status`, `endpoint`, `body`, and a stable `code` to branch on instead of
24
+ * parsing message strings.
25
+ */
26
+ export class RainApiError extends Error {
27
+ status;
28
+ body;
29
+ endpoint;
30
+ code;
31
+ constructor(status, body, endpoint, code = 'UNKNOWN') {
32
+ super(`Rain API error ${status}${endpoint ? ` on ${endpoint}` : ''}`);
33
+ this.status = status;
34
+ this.body = body;
35
+ this.endpoint = endpoint;
36
+ this.code = code;
37
+ this.name = 'RainApiError';
38
+ }
39
+ }
40
+ function codeForStatus(status) {
41
+ switch (status) {
42
+ case 401: return 'UNAUTHORIZED';
43
+ case 403: return 'FORBIDDEN';
44
+ case 404: return 'NOT_FOUND';
45
+ case 429: return 'RATE_LIMITED';
46
+ default: return status >= 500 ? 'SERVER_ERROR' : 'UNKNOWN';
47
+ }
48
+ }
49
+ export async function handleResponse(res, endpoint) {
21
50
  if (!res.ok) {
22
51
  const text = await res.text().catch(() => res.statusText);
23
- throw new Error(`API error (${res.status}): ${text}`);
52
+ let body = text;
53
+ try {
54
+ body = JSON.parse(text);
55
+ }
56
+ catch { /* not JSON — keep raw text */ }
57
+ throw new RainApiError(res.status, body, endpoint, codeForStatus(res.status));
24
58
  }
25
59
  return res.json();
26
60
  }
@@ -91,11 +91,16 @@ export async function calculateUserPNLPerPool(config) {
91
91
  return handleResponse(res);
92
92
  }
93
93
  export async function getPnlByPoolId(params, config) {
94
- const res = await fetch(`${config.apiUrl}/investments/pnl/${encodeURIComponent(params.poolId)}`, {
94
+ const endpoint = `/investments/pnl/${encodeURIComponent(params.poolId)}`;
95
+ const res = await fetch(`${config.apiUrl}${endpoint}`, {
95
96
  method: 'GET',
96
97
  headers: buildHeaders(config),
97
98
  });
98
- return handleResponse(res);
99
+ // No position in this pool is a legitimate empty state, not an error.
100
+ if (res.status === 404) {
101
+ return { statusCode: 200, message: 'No position for this pool', data: null };
102
+ }
103
+ return handleResponse(res, endpoint);
99
104
  }
100
105
  export async function getTopWinnersLosers(params, config) {
101
106
  const qs = buildQuery({ timeFilter: params.timeFilter });
@@ -1,8 +1,13 @@
1
1
  import { buildHeaders, handleResponse } from './helpers.js';
2
2
  export async function getTokenPrice(tokenAddress, config) {
3
- const res = await fetch(`${config.apiUrl}/whitelisted-tokens/get-token-price?tokenAddress=${encodeURIComponent(tokenAddress)}`, {
3
+ const endpoint = '/whitelisted-tokens/get-token-price';
4
+ const res = await fetch(`${config.apiUrl}${endpoint}?tokenAddress=${encodeURIComponent(tokenAddress)}`, {
4
5
  method: 'GET',
5
6
  headers: buildHeaders(config),
6
7
  });
7
- return handleResponse(res);
8
+ // A non-whitelisted token is a legitimate empty state, not an error.
9
+ if (res.status === 404) {
10
+ return { statusCode: 200, message: 'Token not whitelisted or no price available', data: null };
11
+ }
12
+ return handleResponse(res, endpoint);
8
13
  }
@@ -1,5 +1,7 @@
1
1
  export async function loginUser(params) {
2
2
  const { signature, walletAddress, smartWalletAddress, referredBy, apiUrl } = params;
3
+ // Plain-EOA integrations may omit smartWalletAddress; fall back to the EOA.
4
+ const resolvedSmartWalletAddress = smartWalletAddress ?? walletAddress;
3
5
  const res = await fetch(`${apiUrl}/auth/login-or-register-with-walletAddress`, {
4
6
  method: 'POST',
5
7
  headers: {
@@ -9,7 +11,7 @@ export async function loginUser(params) {
9
11
  body: JSON.stringify({
10
12
  sign: signature,
11
13
  walletAddress,
12
- userSmartAddress: smartWalletAddress,
14
+ userSmartAddress: resolvedSmartWalletAddress,
13
15
  ...(referredBy ? { referredBy } : {}),
14
16
  }),
15
17
  });
@@ -3,8 +3,8 @@ export interface LoginParams {
3
3
  signature: string;
4
4
  /** EOA wallet address */
5
5
  walletAddress: string;
6
- /** Smart account / AA wallet address */
7
- smartWalletAddress: string;
6
+ /** Smart account / AA wallet address. Defaults to `walletAddress` for plain-EOA integrations. */
7
+ smartWalletAddress?: string;
8
8
  /** Optional referral code */
9
9
  referredBy?: string;
10
10
  }
@@ -1,6 +1,8 @@
1
1
  export declare const ALLOWED_ENVIRONMENTS: readonly ["development", "stage", "production"];
2
2
  export declare const DEFAULT_RPCS: string[];
3
3
  export declare function getRandomRpc(): string;
4
+ /** Deterministic default RPC — every instance resolves to the same endpoint. */
5
+ export declare function getDefaultRpc(): string;
4
6
  export declare const USDT_SYMBOL_DEV = "USDTm";
5
7
  export interface TokenConfig {
6
8
  address: `0x${string}`;
@@ -8,6 +8,10 @@ export function getRandomRpc() {
8
8
  const index = Math.floor(Math.random() * DEFAULT_RPCS.length);
9
9
  return DEFAULT_RPCS[index];
10
10
  }
11
+ /** Deterministic default RPC — every instance resolves to the same endpoint. */
12
+ export function getDefaultRpc() {
13
+ return DEFAULT_RPCS[0];
14
+ }
11
15
  export const USDT_SYMBOL_DEV = "USDTm";
12
16
  export const ENV_CONFIG = {
13
17
  development: {
package/dist/index.d.ts CHANGED
@@ -2,7 +2,11 @@ export { Rain } from './Rain.js';
2
2
  export { RainAA } from './RainAA.js';
3
3
  export * from './types.js';
4
4
  export { TradingModel, OptionSide } from './tx/types.js';
5
- export type { CreateMarketTxParams, EnterOptionTxParams, AddLiquidityTxParams, RemoveLiquidityTxParams, SplitTxParams, MergeTxParams, ClosePoolAITxParams, ClosePoolManualTxParams, ChooseWinnerTxParams, PlaceBuyOrderTxParams, PlaceSellOrderTxParams, OpenDisputeTxParams, ClaimTxParams, CancelBuyOrdersTxParams, CancelSellOrdersTxParams, RawTransaction, ApproveTxParams } from './tx/types.js';
5
+ export type { CreateMarketTxParams, EnterOptionTxParams, AddLiquidityTxParams, RemoveLiquidityTxParams, SplitTxParams, MergeTxParams, ClosePoolAITxParams, ClosePoolManualTxParams, ChooseWinnerTxParams, PlaceBuyOrderTxParams, PlaceSellOrderTxParams, OpenDisputeTxParams, ClaimTxParams, CancelBuyOrdersTxParams, CancelSellOrdersTxParams, RawTransaction, ApproveTxParams, SellOptionTxParams } from './tx/types.js';
6
+ export type { EntrySharesResult } from './markets/getEntryShares.js';
7
+ export type { SellProceedsResult } from './markets/getSellProceeds.js';
8
+ export type { OrderLevelInfo } from './markets/getOrderInfo.js';
9
+ export { RainValidationError, assertDeadline, assertSlippage, assertOption } from './tx/validation.js';
6
10
  export type { LoginParams, LoginResult } from './auth/types.js';
7
11
  export { signLoginMessage } from './auth/signMessage.js';
8
12
  export type { ExtendTimeTxParams } from './tx/buildExtendTimeRawTx.js';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { Rain } from './Rain.js';
2
2
  export { RainAA } from './RainAA.js';
3
3
  export * from './types.js';
4
4
  export { TradingModel, OptionSide } from './tx/types.js';
5
+ export { RainValidationError, assertDeadline, assertSlippage, assertOption } from './tx/validation.js';
5
6
  export { signLoginMessage } from './auth/signMessage.js';
6
7
  export { USDRAbi } from './abi/USDRAbi.js';
7
8
  export * from './api/index.js';
@@ -4,6 +4,7 @@ import { MarketsAbi } from "../abi/MarketsAbi.js";
4
4
  import { ENTER_LIQUIDITY } from "../constants/contractmethods.js";
5
5
  import { checkMarketTokenAllowance } from "../utils/helpers.js";
6
6
  import { buildApproveRawTx } from "./buildApprovalRawTx.js";
7
+ import { assertOption, assertSlippage, assertDeadline } from "./validation.js";
7
8
  const DEFAULT_SLIPPAGE = 5n; // 5%
8
9
  const DEFAULT_DEADLINE_DURATION = 600; // 10 minutes
9
10
  export async function buildAddLiquidityRawTx(params) {
@@ -16,6 +17,9 @@ export async function buildAddLiquidityRawTx(params) {
16
17
  throw new Error("totalAmountInWei is required");
17
18
  if (totalAmountInWei <= 0n)
18
19
  throw new Error("totalAmountInWei must be greater than 0");
20
+ assertOption(option);
21
+ assertSlippage(slippageTolerance);
22
+ assertDeadline(deadline);
19
23
  const { allowance, baseToken } = await checkMarketTokenAllowance({ marketContractAddress, owner: walletAddress, rpcUrl });
20
24
  const txs = [];
21
25
  if (allowance < totalAmountInWei) {
@@ -4,6 +4,7 @@ import { MarketsAbi } from "../abi/MarketsAbi.js";
4
4
  import { ENTER_OPTION } from "../constants/contractmethods.js";
5
5
  import { checkMarketTokenAllowance } from "../utils/helpers.js";
6
6
  import { buildApproveRawTx } from "./buildApprovalRawTx.js";
7
+ import { assertOption, assertSlippage, assertDeadline } from "./validation.js";
7
8
  const DEFAULT_SLIPPAGE = 5n; // 5%
8
9
  const DEFAULT_DEADLINE_DURATION = 600; // 10 minutes
9
10
  export async function buildEnterOptionRawTx(params) {
@@ -18,6 +19,9 @@ export async function buildEnterOptionRawTx(params) {
18
19
  throw new Error("buyAmountInWei is required");
19
20
  if (buyAmountInWei <= 0n)
20
21
  throw new Error("buyAmountInWei must be greater than 0");
22
+ assertOption(selectedOption, "selectedOption");
23
+ assertSlippage(slippageTolerance);
24
+ assertDeadline(deadline);
21
25
  const { allowance, baseToken } = await checkMarketTokenAllowance({ marketContractAddress, owner: walletAddress, rpcUrl });
22
26
  const txs = [];
23
27
  if (allowance < buyAmountInWei) {
@@ -3,6 +3,7 @@ import { MarketsAbi } from "../abi/MarketsAbi.js";
3
3
  import { PLACE_BUY_ORDER, PLACE_SELL_ORDER } 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 buildPlaceBuyOrderRawTx(params) {
7
8
  const { marketContractAddress, option, optionSide, price, amount, postOnly = false, walletAddress, rpcUrl } = params;
8
9
  if (!marketContractAddress)
@@ -15,6 +16,7 @@ export async function buildPlaceBuyOrderRawTx(params) {
15
16
  throw new Error("price must be greater than 0");
16
17
  if (!amount || amount <= 0n)
17
18
  throw new Error("amount must be greater than 0");
19
+ assertOption(option);
18
20
  const { allowance, baseToken } = await checkMarketTokenAllowance({ marketContractAddress, owner: walletAddress, rpcUrl });
19
21
  const txs = [];
20
22
  if (allowance < amount) {
@@ -43,6 +45,7 @@ export function buildPlaceSellOrderRawTx(params) {
43
45
  throw new Error("price must be greater than 0");
44
46
  if (!shares || shares <= 0n)
45
47
  throw new Error("shares must be greater than 0");
48
+ assertOption(option);
46
49
  return {
47
50
  to: marketContractAddress,
48
51
  data: encodeFunctionData({
@@ -2,6 +2,7 @@ import { encodeFunctionData, createPublicClient, http } from "viem";
2
2
  import { arbitrum } from "viem/chains";
3
3
  import { MarketsAbi } from "../abi/MarketsAbi.js";
4
4
  import { REMOVE_LIQUIDITY } from "../constants/contractmethods.js";
5
+ import { assertOption, assertSlippage, assertDeadline } from "./validation.js";
5
6
  const DEFAULT_SLIPPAGE = 5n; // 5%
6
7
  const DEFAULT_DEADLINE_DURATION = 600; // 10 minutes
7
8
  export async function buildRemoveLiquidityRawTx(params) {
@@ -14,6 +15,9 @@ export async function buildRemoveLiquidityRawTx(params) {
14
15
  throw new Error("lpShares is required");
15
16
  if (lpShares <= 0n)
16
17
  throw new Error("lpShares must be greater than 0");
18
+ assertOption(option);
19
+ assertSlippage(slippageTolerance);
20
+ assertDeadline(deadline);
17
21
  let effectiveMinYes = minYesOut;
18
22
  let effectiveMinNo = minNoOut;
19
23
  if ((effectiveMinYes === undefined || effectiveMinYes === null) && (effectiveMinNo === undefined || effectiveMinNo === null)) {
@@ -1,6 +1,7 @@
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
5
  const DEFAULT_DEADLINE_DURATION = 600; // 10 minutes
5
6
  export function buildSellOptionRawTx(params) {
6
7
  const { marketContractAddress, selectedOption, optionSide, sharesAmount, minAmountOut, deadline } = params;
@@ -15,6 +16,8 @@ export function buildSellOptionRawTx(params) {
15
16
  throw new Error("sharesAmount is required");
16
17
  if (sharesAmount <= 0n)
17
18
  throw new Error("sharesAmount must be greater than 0");
19
+ assertOption(selectedOption, "selectedOption");
20
+ assertDeadline(deadline);
18
21
  return {
19
22
  to: marketContractAddress,
20
23
  data: encodeFunctionData({
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Synchronous, build-time parameter guards shared across the transaction
3
+ * builders. These convert documented footguns (durations passed as deadlines,
4
+ * slippage in the wrong unit, 0-based option indices) into immediate, descriptive
5
+ * errors — before any encoding or broadcast.
6
+ */
7
+ export declare class RainValidationError extends Error {
8
+ readonly field: string;
9
+ constructor(field: string, message: string);
10
+ }
11
+ /** Deadlines are absolute unix timestamps (seconds), not durations. Optional — skipped when omitted. */
12
+ export declare function assertDeadline(deadline?: bigint | null): void;
13
+ /** slippageTolerance is a whole percent (e.g. 5 = 5%), not basis points. Optional — skipped when omitted. */
14
+ export declare function assertSlippage(slippageTolerance?: bigint | null): void;
15
+ /** Option indices are 1-based across the SDK (the first option is 1, never 0). */
16
+ export declare function assertOption(option: bigint | undefined | null, field?: string): void;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Synchronous, build-time parameter guards shared across the transaction
3
+ * builders. These convert documented footguns (durations passed as deadlines,
4
+ * slippage in the wrong unit, 0-based option indices) into immediate, descriptive
5
+ * errors — before any encoding or broadcast.
6
+ */
7
+ export class RainValidationError extends Error {
8
+ field;
9
+ constructor(field, message) {
10
+ super(`Invalid ${field}: ${message}`);
11
+ this.field = field;
12
+ this.name = 'RainValidationError';
13
+ }
14
+ }
15
+ // Any value below this looks like a duration (seconds) rather than an absolute
16
+ // unix timestamp. 1_000_000_000 = 2001-09-09; any real deadline is far larger.
17
+ const MIN_PLAUSIBLE_TIMESTAMP = 1000000000n;
18
+ /** Deadlines are absolute unix timestamps (seconds), not durations. Optional — skipped when omitted. */
19
+ export function assertDeadline(deadline) {
20
+ if (deadline === undefined || deadline === null)
21
+ return;
22
+ if (deadline < MIN_PLAUSIBLE_TIMESTAMP) {
23
+ throw new RainValidationError('deadline', `${deadline} looks like a duration; pass an absolute unix timestamp in seconds`);
24
+ }
25
+ const now = BigInt(Math.floor(Date.now() / 1000));
26
+ if (deadline <= now) {
27
+ throw new RainValidationError('deadline', `${deadline} is already in the past`);
28
+ }
29
+ }
30
+ /** slippageTolerance is a whole percent (e.g. 5 = 5%), not basis points. Optional — skipped when omitted. */
31
+ export function assertSlippage(slippageTolerance) {
32
+ if (slippageTolerance === undefined || slippageTolerance === null)
33
+ return;
34
+ if (slippageTolerance < 0n || slippageTolerance > 100n) {
35
+ throw new RainValidationError('slippageTolerance', `${slippageTolerance} must be between 0 and 100 (whole percent, not basis points)`);
36
+ }
37
+ }
38
+ /** Option indices are 1-based across the SDK (the first option is 1, never 0). */
39
+ export function assertOption(option, field = 'option') {
40
+ if (option === undefined || option === null)
41
+ return;
42
+ if (option < 1n) {
43
+ throw new RainValidationError(field, `${option} is invalid; options are 1-based (the first option is 1)`);
44
+ }
45
+ }
package/dist/types.d.ts CHANGED
@@ -2,8 +2,10 @@ import { WalletClient } from 'viem';
2
2
  import { Chain } from 'viem/chains';
3
3
  export interface RainConfig {
4
4
  walletClient: WalletClient;
5
- alchemyApiKey?: string;
6
- paymasterPolicyId?: string;
5
+ /** Required: the RainAA constructor throws without it (gas-sponsored AA). */
6
+ alchemyApiKey: string;
7
+ /** Required: the RainAA constructor throws without it (paymaster policy). */
8
+ paymasterPolicyId: string;
7
9
  chain: Chain;
8
10
  rpcUrl?: string;
9
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rain-sdk-v2",
3
- "version": "2.1.7",
3
+ "version": "2.2.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",
@@ -13,7 +13,8 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
- "README.md"
16
+ "README.md",
17
+ "CHANGELOG.md"
17
18
  ],
18
19
  "scripts": {
19
20
  "build": "tsc",
@@ -63,4 +64,4 @@
63
64
  "typescript": "^5.9.3",
64
65
  "viem": "^2.48.8"
65
66
  }
66
- }
67
+ }