liquid-sdk 1.5.2 → 1.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liquid-sdk",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "TypeScript SDK to deploy ERC-20 tokens with Uniswap V4 liquidity on Base — zero API keys, one dependency (viem)",
5
5
  "author": "Liquid Protocol",
6
6
  "homepage": "https://github.com/craigbots/liquid-sdk#readme",
@@ -8,8 +8,8 @@ When a new token is deployed on Liquid Protocol, a **sniper auction** activates
8
8
 
9
9
  1. **Fee decay**: The auction starts with an **80% fee** on swaps and decays linearly to **40% over 32 seconds**
10
10
  2. **Gas price bidding**: Bidders compete by setting their transaction gas price **above the pool's gas peg**. The difference between your gas price and the gas peg determines your bid amount
11
- 3. **Rounds**: The auction runs in discrete rounds, each lasting a configurable number of blocks. Each round has its own auction window
12
- 4. **Winner takes the swap**: The highest gas-price transaction in each block wins the right to swap at the current fee rate
11
+ 3. **Rounds**: The auction runs in discrete rounds every 2 blocks (5 rounds max). Each round is valid for exactly **one block** (`nextAuctionBlock`)
12
+ 4. **Winner takes the swap**: The highest gas-price transaction in the auction block wins the right to swap at the current fee rate
13
13
  5. **Revenue distribution**: Auction revenue (bid amounts) flows to the protocol and LP holders
14
14
 
15
15
  The auction is **not** a separate step from trading — it's a modified swap where your gas price encodes your bid.
@@ -21,7 +21,7 @@ npm install liquid-sdk viem
21
21
  ```
22
22
 
23
23
  You need:
24
- - A **private key** with ETH on Base
24
+ - A **private key** with ETH on Base (enough for gas + bid + swap amount)
25
25
  - The **token address** or **pool ID** of a recently launched token
26
26
  - An **RPC endpoint** for Base mainnet
27
27
 
@@ -31,7 +31,7 @@ You need:
31
31
  import { createPublicClient, createWalletClient, http, parseEther } from "viem";
32
32
  import { base } from "viem/chains";
33
33
  import { privateKeyToAccount } from "viem/accounts";
34
- import { LiquidSDK } from "liquid-sdk";
34
+ import { LiquidSDK, EXTERNAL } from "liquid-sdk";
35
35
 
36
36
  const account = privateKeyToAccount(PRIVATE_KEY);
37
37
  const publicClient = createPublicClient({ chain: base, transport: http(RPC_URL) });
@@ -39,6 +39,31 @@ const walletClient = createWalletClient({ account, chain: base, transport: http(
39
39
  const sdk = new LiquidSDK({ publicClient, walletClient });
40
40
  ```
41
41
 
42
+ ## Critical Concepts
43
+
44
+ Before bidding, understand these mechanics:
45
+
46
+ ### Two Separate ETH Costs
47
+ - **`bidAmount`** (msg.value): ETH sent to the auction as your bid. Goes to protocol/LP.
48
+ - **`amountIn`** (WETH transfer): The actual swap input. Pulled from your WETH balance via `transferFrom`. **This is separate from the bid.**
49
+
50
+ The SDK **automatically wraps ETH → WETH and approves the SniperUtilV2** if your WETH balance or allowance is insufficient. You just need enough total ETH.
51
+
52
+ ### Gas Price = Bid Encoding
53
+ The bid amount is encoded in the transaction's gas price: `bidAmount = (tx.gasprice - gasPeg) × paymentPerGasUnit`. Both `maxFeePerGas` **and** `maxPriorityFeePerGas` must be set to the calculated value, otherwise Base's EIP-1559 will compute a lower effective gas price.
54
+
55
+ ### Gas Estimation Must Be Skipped
56
+ `eth_estimateGas` simulates at `baseFee` (~5M wei on Base), which is below the `gasPeg` (~6.3M wei). This causes the auction check to fail during estimation. The SDK sets `gas: 800_000n` manually.
57
+
58
+ ### Block Timing is Critical
59
+ The auction is valid at **exactly** `nextAuctionBlock` — not before, not after. Submit your transaction ~1 block early (when `currentBlock === nextAuctionBlock - 1`) to land in the target block. Base has ~2s block time.
60
+
61
+ ### zeroForOne Depends on Token Sort Order
62
+ Do **not** hardcode `zeroForOne: true`. Determine it from the pool key:
63
+ ```typescript
64
+ const zeroForOne = poolKey.currency0.toLowerCase() === EXTERNAL.WETH.toLowerCase();
65
+ ```
66
+
42
67
  ## Step-by-Step: Bidding in an Auction
43
68
 
44
69
  ### Step 1: Get the Auction State
@@ -60,47 +85,27 @@ console.log("Current fee:", auction.currentFee); // in uniBps (800000 = 80%)
60
85
  **Key fields:**
61
86
  | Field | Type | Description |
62
87
  |-------|------|-------------|
63
- | `nextAuctionBlock` | `bigint` | Block number when next auction round starts |
88
+ | `nextAuctionBlock` | `bigint` | The ONE block where bids are valid |
64
89
  | `round` | `bigint` | Current round number (must match when bidding) |
65
90
  | `gasPeg` | `bigint` | Base gas price reference — you bid by exceeding this |
66
91
  | `currentFee` | `number` | Current MEV fee in uniBps (decays from 800000→400000) |
67
92
 
68
- ### Step 2: Check Auction Fee Config
93
+ ### Step 2: Get Pool Key and Determine Swap Direction
69
94
 
70
95
  ```typescript
71
- const feeConfig = await sdk.getAuctionFeeConfig(poolId);
72
-
73
- console.log("Starting fee:", feeConfig.startingFee); // 800000 (80%)
74
- console.log("Ending fee:", feeConfig.endingFee); // 400000 (40%)
75
- console.log("Seconds to decay:", feeConfig.secondsToDecay); // 32n
76
- ```
77
-
78
- ### Step 3: Check Timing
79
-
80
- ```typescript
81
- // When did fee decay start?
82
- const decayStart = await sdk.getAuctionDecayStartTime(poolId);
83
- const now = BigInt(Math.floor(Date.now() / 1000));
84
- const elapsed = now - decayStart;
85
-
86
- console.log("Seconds since decay started:", elapsed);
87
- // If elapsed > secondsToDecay, fee is at the floor (endingFee)
88
-
89
- // How many rounds total?
90
- const maxRounds = await sdk.getAuctionMaxRounds();
91
- console.log("Max auction rounds:", maxRounds);
96
+ const rewards = await sdk.getTokenRewards(tokenAddress);
97
+ const poolKey = rewards.poolKey;
92
98
 
93
- // Is this round still active?
94
- const currentBlock = await publicClient.getBlockNumber();
95
- console.log("Current block:", currentBlock);
96
- console.log("Next auction block:", auction.nextAuctionBlock);
97
- // If currentBlock < nextAuctionBlock, the current round is still active
99
+ // Determine swap direction from token sort order
100
+ const zeroForOne = poolKey.currency0.toLowerCase() === EXTERNAL.WETH.toLowerCase();
101
+ // true = WETH is currency0, buying token (most common)
102
+ // false = token is currency0, buying with WETH from currency1 side
98
103
  ```
99
104
 
100
- ### Step 4: Calculate Gas Price for Your Bid
105
+ ### Step 3: Calculate Gas Price for Your Bid
101
106
 
102
107
  ```typescript
103
- const desiredBidAmount = parseEther("0.01"); // How much ETH you want to bid
108
+ const desiredBidAmount = parseEther("0.001"); // How much ETH you want to bid
104
109
 
105
110
  // SDK calculates the exact gas price needed
106
111
  const requiredGasPrice = await sdk.getAuctionGasPriceForBid(
@@ -109,44 +114,50 @@ const requiredGasPrice = await sdk.getAuctionGasPriceForBid(
109
114
  );
110
115
 
111
116
  console.log("Required gas price:", requiredGasPrice);
112
- // This is the maxFeePerGas you must set on your transaction
113
117
  ```
114
118
 
115
- **The formula:** `bidAmount = (txGasPrice - gasPeg) * paymentPerGasUnit`. The utility contract solves for `txGasPrice` given your desired `bidAmount`.
119
+ **The formula:** `bidAmount = (txGasPrice - gasPeg) * paymentPerGasUnit` where `paymentPerGasUnit = 0.0001 ETH (1e14 wei)`. The utility contract solves for `txGasPrice` given your desired `bidAmount`.
116
120
 
117
- ### Step 5: Get the Pool Key
121
+ ### Step 4: Wait for the Auction Block
118
122
 
119
123
  ```typescript
120
- // The pool key identifies the Uniswap V4 pool for the swap
121
- const rewards = await sdk.getTokenRewards(tokenAddress);
122
- const poolKey = rewards.poolKey;
124
+ // Poll until we're one block before the auction
125
+ while (true) {
126
+ const currentBlock = await publicClient.getBlockNumber();
127
+ const gap = Number(auction.nextAuctionBlock - currentBlock);
128
+
129
+ if (gap <= 0) {
130
+ console.log("Missed this round");
131
+ break;
132
+ }
133
+ if (gap === 1) {
134
+ console.log("Next block is auction — fire!");
135
+ break;
136
+ }
123
137
 
124
- // poolKey contains:
125
- // .currency0 lower-sorted token (WETH or the Liquid token)
126
- // .currency1 — higher-sorted token
127
- // .fee — fee tier
128
- // .tickSpacing — tick spacing (200)
129
- // .hooks — hook contract address
138
+ // Wait ~2s (Base block time)
139
+ await new Promise(r => setTimeout(r, gap > 2 ? 500 : 200));
140
+ }
130
141
  ```
131
142
 
132
- ### Step 6: Execute the Bid
143
+ ### Step 5: Execute the Bid
133
144
 
134
145
  ```typescript
146
+ // The SDK handles WETH wrapping + approval automatically
135
147
  const result = await sdk.bidInAuction(
136
148
  {
137
149
  poolKey: rewards.poolKey,
138
- zeroForOne: true, // true = buying tokens with ETH
139
- amountIn: parseEther("0.1"), // amount of ETH to swap
140
- amountOutMinimum: 0n, // set slippage protection (0 = no minimum)
141
- round: auction.round, // must match current on-chain round
142
- bidAmount: desiredBidAmount, // ETH bid (sent as msg.value)
150
+ zeroForOne, // determined in step 2
151
+ amountIn: parseEther("0.001"), // WETH to swap (auto-wrapped from ETH)
152
+ amountOutMinimum: 0n, // set slippage protection in production!
153
+ round: auction.round, // must match current on-chain round
154
+ bidAmount: desiredBidAmount, // ETH bid (sent as msg.value)
143
155
  },
144
- requiredGasPrice, // calculated gas price from step 4
156
+ requiredGasPrice, // from step 3
145
157
  );
146
158
 
147
159
  console.log("Bid tx:", result.txHash);
148
160
 
149
- // Wait for confirmation
150
161
  const receipt = await publicClient.waitForTransactionReceipt({ hash: result.txHash });
151
162
  console.log("Status:", receipt.status); // "success" or "reverted"
152
163
  ```
@@ -157,7 +168,7 @@ console.log("Status:", receipt.status); // "success" or "reverted"
157
168
  import { createPublicClient, createWalletClient, http, parseEther, formatEther } from "viem";
158
169
  import { base } from "viem/chains";
159
170
  import { privateKeyToAccount } from "viem/accounts";
160
- import { LiquidSDK } from "liquid-sdk";
171
+ import { LiquidSDK, EXTERNAL, ERC20Abi } from "liquid-sdk";
161
172
 
162
173
  async function snipeToken(tokenAddress: `0x${string}`, bidETH: string, swapETH: string) {
163
174
  const account = privateKeyToAccount(PRIVATE_KEY);
@@ -183,21 +194,31 @@ async function snipeToken(tokenAddress: `0x${string}`, bidETH: string, swapETH:
183
194
  return;
184
195
  }
185
196
 
186
- // 4. Get pool key for the swap
197
+ // 4. Get pool key and determine swap direction
187
198
  const rewards = await sdk.getTokenRewards(tokenAddress);
199
+ const zeroForOne = rewards.poolKey.currency0.toLowerCase() === EXTERNAL.WETH.toLowerCase();
188
200
 
189
201
  // 5. Calculate gas price for desired bid
190
202
  const bidAmount = parseEther(bidETH);
191
203
  const gasPrice = await sdk.getAuctionGasPriceForBid(auction.gasPeg, bidAmount);
192
204
 
193
- console.log(`Bid amount: ${formatEther(bidAmount)} ETH`);
194
- console.log(`Required gas price: ${gasPrice}`);
205
+ console.log(`Bid: ${formatEther(bidAmount)} ETH | Swap: ${swapETH} ETH`);
206
+ console.log(`Gas price: ${gasPrice} (peg: ${auction.gasPeg})`);
195
207
 
196
- // 6. Execute the bid
208
+ // 6. Wait for the auction block
209
+ while (true) {
210
+ const currentBlock = await publicClient.getBlockNumber();
211
+ const gap = Number(auction.nextAuctionBlock - currentBlock);
212
+ if (gap <= 0) { console.log("Missed this round"); return; }
213
+ if (gap === 1) break; // next block is auction — fire!
214
+ await new Promise(r => setTimeout(r, gap > 2 ? 500 : 200));
215
+ }
216
+
217
+ // 7. Execute the bid (SDK auto-wraps WETH + approves SniperUtil)
197
218
  const result = await sdk.bidInAuction(
198
219
  {
199
220
  poolKey: rewards.poolKey,
200
- zeroForOne: true,
221
+ zeroForOne,
201
222
  amountIn: parseEther(swapETH),
202
223
  amountOutMinimum: 0n, // In production, calculate proper slippage!
203
224
  round: auction.round,
@@ -208,19 +229,43 @@ async function snipeToken(tokenAddress: `0x${string}`, bidETH: string, swapETH:
208
229
 
209
230
  const receipt = await publicClient.waitForTransactionReceipt({ hash: result.txHash });
210
231
  console.log(`Bid ${receipt.status === "success" ? "WON" : "FAILED"}: ${result.txHash}`);
232
+
233
+ if (receipt.status === "success") {
234
+ const tokenBal = await publicClient.readContract({
235
+ address: tokenAddress,
236
+ abi: ERC20Abi,
237
+ functionName: "balanceOf",
238
+ args: [account.address],
239
+ });
240
+ console.log(`Tokens received: ${formatEther(tokenBal as bigint)}`);
241
+ }
211
242
  }
212
243
 
213
- // Usage
214
- await snipeToken("0x...", "0.005", "0.1"); // bid 0.005 ETH, swap 0.1 ETH
244
+ // Usage: bid 0.001 ETH, swap 0.001 ETH for tokens
245
+ await snipeToken("0x...", "0.001", "0.001");
215
246
  ```
216
247
 
248
+ ## The Working Bid Flow (Summary)
249
+
250
+ 1. Get auction state (`getAuctionState`) — need `gasPeg`, `round`, `nextAuctionBlock`
251
+ 2. Get pool key (`getTokenRewards`) — need `poolKey` for the swap
252
+ 3. Determine `zeroForOne` from pool key token sort order
253
+ 4. Calculate gas price (`getAuctionGasPriceForBid`)
254
+ 5. Wait until `currentBlock === nextAuctionBlock - 1`
255
+ 6. Call `sdk.bidInAuction(params, gasPrice)` — SDK handles:
256
+ - Auto-wrapping ETH → WETH for `amountIn`
257
+ - Auto-approving SniperUtilV2 for WETH
258
+ - Setting `gas: 800_000n` (skipping estimation)
259
+ - Setting `maxFeePerGas` and `maxPriorityFeePerGas` to the calculated value
260
+ 7. Transaction lands in `nextAuctionBlock` → snipe complete
261
+
217
262
  ## BidInAuctionParams Reference
218
263
 
219
264
  ```typescript
220
265
  interface BidInAuctionParams {
221
- poolKey: PoolKey; // The Uniswap V4 pool key (get from getTokenRewards)
222
- zeroForOne: boolean; // true = ETH→token, false = token→ETH
223
- amountIn: bigint; // Amount of input token to swap (in wei)
266
+ poolKey: PoolKey; // Uniswap V4 pool key (get from getTokenRewards)
267
+ zeroForOne: boolean; // true if WETH is currency0 (buying token)
268
+ amountIn: bigint; // WETH to swap pulled via transferFrom (auto-wrapped by SDK)
224
269
  amountOutMinimum: bigint;// Minimum output (slippage protection)
225
270
  round: bigint; // Must match current on-chain auction round
226
271
  bidAmount: bigint; // ETH bid amount (sent as msg.value)
@@ -231,15 +276,18 @@ interface BidInAuctionResult {
231
276
  }
232
277
  ```
233
278
 
234
- ## Auction Parameters (Defaults)
279
+ ## Auction Constants (Current Deployment)
235
280
 
236
281
  | Parameter | Value | Description |
237
282
  |-----------|-------|-------------|
283
+ | Max rounds | 5 | Total auction rounds per token |
284
+ | Blocks between auctions | 2 | Rounds occur every 2 blocks |
285
+ | Blocks before first auction | 2 | First auction = deploy block + 2 |
286
+ | Payment per gas unit | 0.0001 ETH (1e14 wei) | Converts gas delta to bid ETH |
238
287
  | Starting fee | 800,000 (80%) | Fee at auction start |
239
- | Ending fee | 400,000 (40%) | Fee after decay completes |
288
+ | Ending fee | 400,000 (40%) | Fee floor after decay |
240
289
  | Decay period | 32 seconds | Time for fee to decay from start to end |
241
- | Gas peg | Dynamic | Base gas price, updated per round |
242
- | Max rounds | Contract-defined | Total auction rounds per token |
290
+ | Gas peg | ~6.3M wei (dynamic) | Set at pool creation, equals Base baseFee |
243
291
 
244
292
  ## Timing Strategy
245
293
 
@@ -247,32 +295,23 @@ The auction fee **decays over time**, so there's a tradeoff:
247
295
 
248
296
  - **Bid early** (high fee): You pay up to 80% of the swap as a fee, but you get the tokens before others. Useful if you expect rapid price appreciation.
249
297
  - **Bid late** (lower fee): The fee decays to 40% over 32 seconds. You pay less in fees but risk being outbid or missing the auction window.
250
- - **Wait for auction to end**: After all rounds complete, trading is at normal pool fees (typically 1%). No auction mechanics apply.
298
+ - **Wait for auction to end**: After all 5 rounds complete, trading is at normal pool fees (typically 1%). No auction mechanics apply.
251
299
 
252
300
  ```typescript
253
301
  // Check current fee percentage
254
302
  const feePercent = auction.currentFee / 10000; // e.g., 80.0, 60.5, 40.0
255
303
  console.log(`Current fee: ${feePercent}%`);
256
-
257
- // Check fee decay progress
258
- const feeConfig = await sdk.getAuctionFeeConfig(poolId);
259
- const decayStart = await sdk.getAuctionDecayStartTime(poolId);
260
- const now = BigInt(Math.floor(Date.now() / 1000));
261
- const decayProgress = Number(now - decayStart) / Number(feeConfig.secondsToDecay);
262
- console.log(`Decay progress: ${Math.min(decayProgress * 100, 100).toFixed(1)}%`);
263
304
  ```
264
305
 
265
- ## Important Notes
266
-
267
- 1. **`round` must be current**: If you submit a bid with a stale round number, the transaction will revert. Always fetch `getAuctionState()` right before bidding.
268
-
269
- 2. **Gas price is the bid**: The auction uses `tx.gasprice` as the bidding mechanism. The SDK's `bidInAuction()` sets `maxFeePerGas` to the calculated value. On Base (L2), gas prices are low, so even small bids produce manageable gas costs.
270
-
271
- 3. **Slippage protection**: Set `amountOutMinimum` to a non-zero value in production. Calculate it based on the current pool price and your acceptable slippage tolerance.
272
-
273
- 4. **The bid amount is sent as `msg.value`**: This ETH goes to the auction contract, not to the swap. The `amountIn` is the separate ETH amount for the actual token swap.
306
+ ## Common Errors
274
307
 
275
- 5. **`zeroForOne` direction**: Almost always `true` for sniping (buying tokens with ETH). Only set to `false` if selling tokens back through the auction.
308
+ | Error | Selector | Cause | Fix |
309
+ |-------|----------|-------|-----|
310
+ | `GasPriceTooLow()` | `0x8c19df83` | `tx.gasprice ≤ gasPeg` | Use `getAuctionGasPriceForBid()` and set both `maxFeePerGas` + `maxPriorityFeePerGas` |
311
+ | `Unauthorized()` | `0x82b42900` | Fee Locker hasn't authorized the Sniper Auction as depositor | Protocol admin must call `FeeLocker.addDepositor(SniperAuctionAddress)` |
312
+ | `NotAuctionBlock()` | — | Tx didn't land in `nextAuctionBlock` | Submit 1 block early, tx must mine in the exact auction block |
313
+ | WETH `transferFrom` revert | — | Insufficient WETH balance or allowance | SDK handles this automatically; ensure enough ETH for wrap |
314
+ | Gas estimation failure | — | `eth_estimateGas` runs at baseFee < gasPeg | SDK sets `gas: 800_000n` manually |
276
315
 
277
316
  ## Read-Only Auction Queries (No Wallet Needed)
278
317
 
@@ -283,7 +322,7 @@ const auction = await sdk.getAuctionState(poolId);
283
322
  const feeConfig = await sdk.getAuctionFeeConfig(poolId);
284
323
  const decayStart = await sdk.getAuctionDecayStartTime(poolId);
285
324
  const maxRounds = await sdk.getAuctionMaxRounds();
286
- const gasPrice = await sdk.getAuctionGasPriceForBid(auction.gasPeg, parseEther("0.01"));
325
+ const gasPrice = await sdk.getAuctionGasPriceForBid(auction.gasPeg, parseEther("0.001"));
287
326
  ```
288
327
 
289
328
  ## Contract Addresses
@@ -291,6 +330,6 @@ const gasPrice = await sdk.getAuctionGasPriceForBid(auction.gasPeg, parseEther("
291
330
  ```typescript
292
331
  import { ADDRESSES } from "liquid-sdk";
293
332
 
294
- ADDRESSES.SNIPER_AUCTION_V2 // Auction state contract
295
- ADDRESSES.SNIPER_UTIL_V2 // Bid execution contract (called by bidInAuction)
333
+ ADDRESSES.SNIPER_AUCTION_V2 // 0x187e8627... — Auction state contract
334
+ ADDRESSES.SNIPER_UTIL_V2 // 0x2B6cd5Be... — Bid execution contract
296
335
  ```