@wireio/stake 0.2.4 → 0.2.5

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.
@@ -1,10 +1,12 @@
1
1
  import { BigNumber, ethers } from 'ethers';
2
2
  import { IStakingClient, Portfolio, PurchaseAsset, PurchaseQuote, StakerConfig, TrancheSnapshot } from '../../types';
3
- import { PublicKey as WirePubKey } from '@wireio/core';
3
+ import { ChainID, EvmChainID, PublicKey as WirePubKey } from '@wireio/core';
4
4
  import { EthereumContractService } from './contract';
5
5
  import { preLaunchReceipt, WithdrawRequestedEvent, WithdrawResult } from './types';
6
6
  import { DepositClient } from './clients/deposit.client';
7
7
  import { StakeClient } from './clients/stake.client';
8
+ import { PretokenClient } from './clients/pretoken.client';
9
+ import { buildEthereumTrancheSnapshot, sendOPPFinalize } from './utils';
8
10
 
9
11
 
10
12
 
@@ -16,10 +18,13 @@ export class EthereumStakingClient implements IStakingClient {
16
18
 
17
19
  private depositClient: DepositClient;
18
20
  private stakeClient: StakeClient;
21
+ private pretokenClient: PretokenClient;
22
+
19
23
 
20
24
  get contract() { return this.contractService.contract; }
21
25
  get network() { return this.config.network; }
22
26
 
27
+
23
28
  constructor(private config: StakerConfig) {
24
29
  try {
25
30
  this.provider = config.provider as ethers.providers.Web3Provider;
@@ -32,6 +37,7 @@ export class EthereumStakingClient implements IStakingClient {
32
37
  });
33
38
  this.depositClient = new DepositClient(this.contractService);
34
39
  this.stakeClient = new StakeClient(this.contractService);
40
+ this.pretokenClient = new PretokenClient(this.contractService);
35
41
  }
36
42
  catch (error) {
37
43
  // console.error('Error initializing EthereumStakingClient:', error);
@@ -59,18 +65,28 @@ export class EthereumStakingClient implements IStakingClient {
59
65
  }
60
66
 
61
67
 
62
- // TODO - In progress
63
- async withdraw(amount: number | string | bigint | BigNumber): Promise<string> {
64
- const amountWei = BigNumber.isBigNumber(amount)
65
- ? amount
66
- : BigNumber.from(amount);
67
-
68
- const result = await this.requestWithdraw(amountWei);
69
- return result.txHash;
68
+ /**
69
+ * Withdraw native ETH from the liqETH protocol via DepositManager.
70
+ * @param amount Amount in wei (or something convertible to BigNumber).
71
+ * Keep this as a bigint / string in the caller; avoid JS floats.
72
+ * @returns transaction hash
73
+ */
74
+ async withdraw(amount: bigint): Promise<string> {
75
+ // const amountWei = BigNumber.from(amount);
76
+ // const chainId = this.network?.chainId ?? (await this.provider.getNetwork()).chainId;
77
+ // const result = await this.depositClient.requestWithdraw(amountWei, this.signer, chainId);
78
+ // return result.txHash;
79
+ throw new Error("Method not yet implemented.");
70
80
  }
71
81
 
72
82
 
73
- // TODO - In progress
83
+
84
+ /**
85
+ * Stake liqETH via DepositManager.
86
+ * @param amount Amount in wei
87
+ * Keep this as a bigint / string in the caller; avoid JS floats.
88
+ * @returns transaction hash
89
+ */
74
90
  async stake(amount: bigint): Promise<string> {
75
91
  const walletAddress = await this.signer.getAddress();
76
92
  const amountWei = BigNumber.from(amount);
@@ -85,7 +101,13 @@ export class EthereumStakingClient implements IStakingClient {
85
101
  throw new Error("Method not yet implemented.");
86
102
  }
87
103
 
88
- // TODO
104
+
105
+ /**
106
+ * ETH Prelaunch function to unstake liqEth
107
+ * @param tokenId ReceiptNFT tokenId for the owned NFT that will be burned
108
+ * @param recipient Address to receive the liqEth funds linked to the burned NFT
109
+ * @returns the transaction hash
110
+ */
89
111
  async unstakePrelaunch(tokenId: bigint, recipient: string): Promise<string> {
90
112
  const tokenIdBigNum = BigNumber.from(tokenId)
91
113
  const result = await this.stakeClient.performWithdrawStake(tokenIdBigNum, recipient);
@@ -93,6 +115,69 @@ export class EthereumStakingClient implements IStakingClient {
93
115
  }
94
116
 
95
117
 
118
+
119
+ async buy(amount: bigint): Promise<string> {
120
+ const buyer = await this.signer.getAddress();
121
+ const amountBigNum = BigNumber.from(amount)
122
+
123
+ // ! Hoodi only - check if the mock aggregator price is stale, and if so, update it before submitting the buy request
124
+ await this.updateMockAggregatorPrice();
125
+
126
+
127
+ const bal = await this.contract.LiqEth.balanceOf(buyer);
128
+ const paused = await this.contract.Depositor.paused();
129
+ if(paused) {
130
+ throw new Error("Error - Depositor is in a paused state");
131
+ }
132
+
133
+ // if current liq balance is less than the requested buy amount, throw an error
134
+ if (bal.lt(amount)) {
135
+ throw new Error(`Balance insufficient for pre-token purchase`);
136
+ }
137
+
138
+ //check that the contract has allowance for the token
139
+ const depositorAddr = this.contract.Depositor.address;
140
+ const allowance = await this.contract.LiqEth.allowance(buyer, depositorAddr);
141
+
142
+ // if allowance is less than the requested stake amount, request permission to spend LiqEth
143
+ if (allowance.lt(amount)) {
144
+ const liqWrite = this.contractService.getWrite('LiqEth');
145
+
146
+ // currently requested unlimited amount - potentially change to only request up to the current amount?
147
+ const approveAmount = ethers.constants.MaxUint256;
148
+
149
+ console.warn(`allowance insufficient (${allowance.toString()} < ${amount.toString()}); sending approve(${depositorAddr}, ${approveAmount.toString()})`);
150
+
151
+ const approveTx = await liqWrite.approve(depositorAddr, approveAmount);
152
+ await approveTx.wait(1);
153
+
154
+ // re-read allowance to ensure approval succeeded
155
+ const newAllowance = await this.contract.LiqEth.allowance(buyer, depositorAddr);
156
+ if (newAllowance.lt(amount)) {
157
+ throw new Error('Approval failed or allowance still insufficient after approve');
158
+ }
159
+ }
160
+
161
+
162
+ let result = await this.pretokenClient.purchaseWarrantsWithLiqETH(amountBigNum, buyer);
163
+
164
+ return result && result.txHash ? result.txHash : "Error - no resulting txHash";
165
+ }
166
+
167
+
168
+
169
+
170
+ async getOPPStatus(): Promise<any> {
171
+ return await this.stakeClient.getOppStatus();
172
+ }
173
+
174
+
175
+
176
+ /**
177
+ * ETH Prelaunch function to list the ReceiptNFTs owned by a specific user
178
+ * @param address address to query the receipts for
179
+ * @returns array of receipts
180
+ */
96
181
  async fetchPrelaunchReceipts(address?: string): Promise<preLaunchReceipt[]> {
97
182
  if(address === undefined) address = await this.signer.getAddress();
98
183
 
@@ -114,11 +199,8 @@ export class EthereumStakingClient implements IStakingClient {
114
199
  }
115
200
  }
116
201
 
117
- // TODO
118
- buy(amount: bigint, purchaseAsset: PurchaseAsset): Promise<string> {
119
- throw new Error("Method not yet implemented.");
120
- }
121
-
202
+
203
+
122
204
  // TODO
123
205
  getBuyQuote(amount: bigint, purchaseAsset: PurchaseAsset): Promise<PurchaseQuote> {
124
206
  throw new Error("Method not yet implemented.");
@@ -178,94 +260,158 @@ export class EthereumStakingClient implements IStakingClient {
178
260
  },
179
261
  chainID: this.network.chainId
180
262
  }
181
- // console.log('ETH PORTFOLIO', portfolio);
182
263
  return portfolio;
183
264
  }
184
265
 
266
+
185
267
  /**
186
- * Program-level prelaunch WIRE / tranche snapshot for Solana.
187
- * Uses the same OutpostWireStateSnapshot primitive as getPortfolio().
188
- * TODO! for eth
268
+ * Program-level prelaunch WIRE / tranche snapshot for Ethereum
189
269
  */
190
- async getTrancheSnapshot(): Promise<TrancheSnapshot | null> {
191
- return null
270
+ async getTrancheSnapshot(options?: {
271
+ chainID?: ChainID;
272
+ windowBefore?: number;
273
+ windowAfter?: number;
274
+ }): Promise<TrancheSnapshot> {
275
+ const {
276
+ chainID = EvmChainID.Hoodi,
277
+ windowBefore,
278
+ windowAfter,
279
+ } = options ?? {};
280
+
281
+
282
+ const blockNumber = await this.provider.getBlockNumber();
283
+ const blockTag = { blockTag: blockNumber };
284
+
285
+ // Fetch all required contract data
286
+ const [totalSharesBn, indexBn, trancheNumberBn, trancheSupplyBn, tranchePriceWadBn, totalSupplyBn, supplyGrowthBps, priceGrowthBps, minPriceUsd, maxPriceUsd] = await Promise.all([
287
+ this.contract.Depositor.totalShares(blockTag),
288
+ this.contract.Depositor.index(blockTag),
289
+ this.contract.Warrant.trancheNumber(blockTag),
290
+ this.contract.Warrant.trancheSupply(blockTag),
291
+ this.contract.Warrant.tranchePriceUsd(blockTag),
292
+ this.contract.Warrant.totalSupply(blockTag),
293
+ this.contract.Warrant.supplyGrowthBps(blockTag),
294
+ this.contract.Warrant.priceGrowthBps(blockTag),
295
+ this.contract.EthUsdPriceConsumer.MIN_PRICE(),
296
+ this.contract.EthUsdPriceConsumer.MAX_PRICE(),
297
+ ]);
298
+
299
+
300
+ const totalTrancheSupply = BigInt(totalSupplyBn.toString()) / BigInt(1e10);
301
+ const currentTrancheSupply = BigInt(trancheSupplyBn.toString()) / BigInt(1e10);
302
+
303
+
304
+ // fetch price and timestamp from aggregator
305
+ const [ roundId, answer, startedAt, updatedAt, answeredInRound ] = await this.contract.Aggregator.latestRoundData();
306
+ console.log('mockaggregator answer', answer.toString())
307
+ let ethPriceUsd: bigint = BigInt(answer.toString());
308
+ let nativePriceTimestamp: number = Number(updatedAt);
309
+
310
+
311
+ // ! Placeholder from hoodi deployment - don't think this can be fetched dynamically
312
+ const initialTrancheSupply = BigInt(60000) * BigInt(1e8);
313
+
314
+ console.log('options for building eth tranche snapshot', {
315
+ chainID,
316
+ totalSharesBn,
317
+ indexBn,
318
+ trancheNumberBn,
319
+ currentTrancheSupply: currentTrancheSupply.toString(),
320
+ tranchePriceWadBn,
321
+ totalTrancheSupply: totalTrancheSupply.toString(),
322
+ initialTrancheSupply: initialTrancheSupply.toString(),
323
+ supplyGrowthBps,
324
+ priceGrowthBps,
325
+ minPriceUsd,
326
+ maxPriceUsd,
327
+
328
+ ethPriceUsd,
329
+ nativePriceTimestamp,
330
+ ladderWindowBefore: windowBefore,
331
+ ladderWindowAfter: windowAfter,
332
+ })
333
+
334
+ return buildEthereumTrancheSnapshot({
335
+ chainID,
336
+ totalSharesBn,
337
+ indexBn,
338
+ trancheNumberBn,
339
+ currentTrancheSupply,
340
+ tranchePriceWadBn,
341
+ totalTrancheSupply,
342
+ initialTrancheSupply,
343
+ supplyGrowthBps,
344
+ priceGrowthBps,
345
+ minPriceUsd,
346
+ maxPriceUsd,
347
+
348
+ ethPriceUsd,
349
+ nativePriceTimestamp,
350
+ ladderWindowBefore: windowBefore,
351
+ ladderWindowAfter: windowAfter,
352
+ });
192
353
  }
193
354
 
355
+
356
+
194
357
 
195
358
  // ---------------------------------------------------------------------
196
359
  // Internal ETH Staking client helper functions
197
360
  // ---------------------------------------------------------------------
198
361
 
199
- private async requestWithdraw(amountWei: BigNumber): Promise<WithdrawResult> {
200
- // deadline is a period of time in the future that the signature is valid for
201
- const deadline = Math.floor(Date.now() / 1000) + 3600;
202
- const liqEth = this.contract.LiqEth;
203
- const owner = await this.signer.getAddress();
204
- const liqEthAddress = this.contractService.getAddress('LiqEth');
205
-
206
- const nonce: BigNumber = await liqEth.nonces(owner);
207
- const chainId = this.network?.chainId ?? (await this.provider.getNetwork()).chainId;
208
- const domain = {
209
- name: await liqEth.name(),
210
- version: '1',
211
- chainId,
212
- verifyingContract: liqEthAddress,
213
- } as any;
214
-
215
- const types = {
216
- Permit: [
217
- { name: 'owner', type: 'address' },
218
- { name: 'spender', type: 'address' },
219
- { name: 'value', type: 'uint256' },
220
- { name: 'nonce', type: 'uint256' },
221
- { name: 'deadline', type: 'uint256' },
222
- ],
223
- } as any;
224
-
225
- const values = {
226
- owner,
227
- spender: this.contractService.getAddress('DepositManager'),
228
- value: amountWei,
229
- nonce: nonce,
230
- deadline,
231
- } as any;
232
-
233
- const signature = await (this.signer as any)._signTypedData(domain, types, values);
234
- const split = ethers.utils.splitSignature(signature);
235
-
236
- const tx = await this.contract.DepositManager.requestWithdrawal(
237
- amountWei,
238
- deadline,
239
- split.v,
240
- split.r,
241
- split.s
242
- );
243
-
244
- // wait for 1 confirmation
245
- const receipt = await tx.wait(1);
246
-
247
- // if WithdrawRequested event exists, parse it and get arguments
248
- let withdrawRequested: WithdrawRequestedEvent | undefined;
249
- const ev = receipt.events?.find((e) => e.event === 'WithdrawRequested');
250
-
251
- if (ev && ev.args) {
252
- const { user, ethAmount, nftId, readyAt } = ev.args;
253
- withdrawRequested = {
254
- user,
255
- ethAmount: BigNumber.from(ethAmount),
256
- nftId: BigNumber.from(nftId),
257
- readyAt: readyAt,
258
- };
259
- }
260
362
 
261
- return {
262
- txHash: tx.hash,
263
- receipt,
264
- withdrawRequested,
265
- } as WithdrawResult;
363
+
364
+
365
+ // ! This is a temporary measure for Hoodi testnet because there is no aggregator deployed
366
+ private async updateMockAggregatorPrice() {
367
+ const aggregator = this.contract.Aggregator;
368
+
369
+ // read latest round & compute age
370
+ const [roundId, answer, startedAt, updatedAt, answeredInRound] = await aggregator.latestRoundData();
371
+ const now = (await this.provider.getBlock("latest")).timestamp;
372
+ const ageSec = Number(now) - Number(updatedAt);
373
+
374
+ const ONE_HOUR = 1 * 3600;
375
+ // const ONE_HOUR = 10;
376
+ if (ageSec > ONE_HOUR) {
377
+ // safety check - only run in non-production contexts
378
+ const network = await this.provider.getNetwork();
379
+ const chainId = network.chainId;
380
+ const allowedTestChains = new Set([560048]);
381
+ if (!allowedTestChains.has(chainId)) {
382
+ console.warn(`MockAggregator is stale (${ageSec}s) but chainId ${chainId} is not a test/local network — skipping update.`);
383
+ return;
384
+ }
385
+
386
+
387
+ //fetch the current ETH / USD price
388
+ const res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
389
+ const data = await res.json();
390
+ const ethUsd = data.ethereum.usd;
391
+
392
+ const currentEthPrice = ethers.utils.parseUnits(ethUsd.toString(), 8);
393
+
394
+ try {
395
+ //update to be intentionally stale
396
+ // const alttx = await aggregator.updateStale(currentEthPrice, now - 7200);
397
+ // const altreceipt = await alttx.wait(1);
398
+ // console.log('stale update receipt', altreceipt)
399
+
400
+ //update answer with current timestamp
401
+ const tx = await aggregator.updateAnswer(currentEthPrice);
402
+ const txreceipt = await tx.wait(1);
403
+ console.log('MockAggregator answer updated - receipt:', txreceipt)
404
+ } catch (err: any) {
405
+ console.error('MockAggregator updateAnswer failed', err?.message || err);
406
+ }
407
+ } else {
408
+ console.log(`MockAggregator updated ${ageSec}s ago — no update needed`);
409
+ }
266
410
  }
267
411
 
268
412
 
269
413
 
414
+
415
+
270
416
  // TODO: implement claimRewards, etc.
271
417
  }
@@ -23,6 +23,8 @@ export const CONTRACT_NAMES = [
23
23
  'OPPCommon',
24
24
  'OPPInbound',
25
25
  'Warrant',
26
+ 'Aggregator',
27
+ 'EthUsdPriceConsumer',
26
28
 
27
29
  ] as const;
28
30