@wireio/stake 0.2.3 → 0.2.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/lib/stake.browser.js +270 -96
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +176 -84
- package/lib/stake.js +282 -102
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +270 -96
- package/lib/stake.m.js.map +1 -1
- package/package.json +1 -1
- package/src/networks/solana/clients/token.client.ts +72 -98
- package/src/networks/solana/solana.ts +284 -184
- package/src/networks/solana/utils.ts +209 -5
- package/src/types.ts +68 -30
|
@@ -2,32 +2,65 @@ import {
|
|
|
2
2
|
Commitment,
|
|
3
3
|
Connection,
|
|
4
4
|
ConnectionConfig,
|
|
5
|
+
LAMPORTS_PER_SOL,
|
|
5
6
|
PublicKey as SolPubKey,
|
|
6
7
|
Transaction,
|
|
7
8
|
TransactionInstruction,
|
|
8
9
|
TransactionSignature,
|
|
9
10
|
} from '@solana/web3.js';
|
|
10
|
-
import { AnchorProvider
|
|
11
|
+
import { AnchorProvider } from '@coral-xyz/anchor';
|
|
11
12
|
import { BaseSignerWalletAdapter } from '@solana/wallet-adapter-base';
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
15
|
+
getAssociatedTokenAddressSync,
|
|
16
|
+
TOKEN_2022_PROGRAM_ID,
|
|
17
|
+
} from '@solana/spl-token';
|
|
13
18
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
19
|
+
import {
|
|
20
|
+
ChainID,
|
|
21
|
+
ExternalNetwork,
|
|
22
|
+
KeyType,
|
|
23
|
+
PublicKey,
|
|
24
|
+
SolChainID,
|
|
25
|
+
} from '@wireio/core';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
IStakingClient,
|
|
29
|
+
Portfolio,
|
|
30
|
+
PurchaseAsset,
|
|
31
|
+
PurchaseQuote,
|
|
32
|
+
StakerConfig,
|
|
33
|
+
TrancheSnapshot,
|
|
34
|
+
} from '../../types';
|
|
16
35
|
|
|
17
36
|
import { DepositClient } from './clients/deposit.client';
|
|
18
37
|
import { DistributionClient } from './clients/distribution.client';
|
|
38
|
+
import { LeaderboardClient } from './clients/leaderboard.client';
|
|
39
|
+
import { OutpostClient } from './clients/outpost.client';
|
|
40
|
+
import { TokenClient } from './clients/token.client';
|
|
41
|
+
|
|
19
42
|
import {
|
|
20
43
|
deriveLiqsolMintPda,
|
|
21
44
|
deriveReservePoolPda,
|
|
22
45
|
deriveVaultPda,
|
|
23
46
|
} from './constants';
|
|
47
|
+
|
|
48
|
+
import { buildSolanaTrancheSnapshot } from './utils';
|
|
24
49
|
import { SolanaTransaction } from './types';
|
|
25
|
-
import { LeaderboardClient } from './clients/leaderboard.client';
|
|
26
|
-
import { OutpostClient } from './clients/outpost.client';
|
|
27
|
-
import { TokenClient } from './clients/token.client';
|
|
28
50
|
|
|
29
51
|
const commitment: Commitment = 'confirmed';
|
|
30
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Solana implementation of IStakingClient.
|
|
55
|
+
*
|
|
56
|
+
* Responsibilities:
|
|
57
|
+
* - Wire together liqSOL deposit/withdraw
|
|
58
|
+
* - Outpost stake/unstake
|
|
59
|
+
* - Prelaunch WIRE (pretokens) buy flows
|
|
60
|
+
* - Unified portfolio + tranche snapshot + buy quotes
|
|
61
|
+
*
|
|
62
|
+
* This class composes lower-level clients; it does not know about UI.
|
|
63
|
+
*/
|
|
31
64
|
export class SolanaStakingClient implements IStakingClient {
|
|
32
65
|
public pubKey: PublicKey;
|
|
33
66
|
public connection: Connection;
|
|
@@ -39,8 +72,13 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
39
72
|
private outpostClient: OutpostClient;
|
|
40
73
|
private tokenClient: TokenClient;
|
|
41
74
|
|
|
42
|
-
get solPubKey(): SolPubKey {
|
|
43
|
-
|
|
75
|
+
get solPubKey(): SolPubKey {
|
|
76
|
+
return new SolPubKey(this.pubKey.data.array);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get network(): ExternalNetwork {
|
|
80
|
+
return this.config.network;
|
|
81
|
+
}
|
|
44
82
|
|
|
45
83
|
constructor(private config: StakerConfig) {
|
|
46
84
|
const adapter = config.provider as BaseSignerWalletAdapter;
|
|
@@ -54,7 +92,10 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
54
92
|
}
|
|
55
93
|
|
|
56
94
|
const opts: ConnectionConfig = { commitment };
|
|
57
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
config.network.rpcUrls.length > 1 &&
|
|
97
|
+
config.network.rpcUrls[1].startsWith('ws')
|
|
98
|
+
) {
|
|
58
99
|
opts.wsEndpoint = config.network.rpcUrls[1];
|
|
59
100
|
}
|
|
60
101
|
|
|
@@ -63,7 +104,9 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
63
104
|
async signTransaction<T extends SolanaTransaction>(tx: T): Promise<T> {
|
|
64
105
|
return adapter.signTransaction(tx);
|
|
65
106
|
},
|
|
66
|
-
async signAllTransactions<T extends SolanaTransaction>(
|
|
107
|
+
async signAllTransactions<T extends SolanaTransaction>(
|
|
108
|
+
txs: T[],
|
|
109
|
+
): Promise<T[]> {
|
|
67
110
|
return Promise.all(txs.map((tx) => adapter.signTransaction(tx)));
|
|
68
111
|
},
|
|
69
112
|
};
|
|
@@ -77,77 +120,89 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
77
120
|
this.leaderboardClient = new LeaderboardClient(this.anchor);
|
|
78
121
|
this.outpostClient = new OutpostClient(this.anchor);
|
|
79
122
|
this.tokenClient = new TokenClient(this.anchor);
|
|
123
|
+
this.tokenClient = new TokenClient(this.anchor);
|
|
80
124
|
}
|
|
81
125
|
|
|
82
126
|
// ---------------------------------------------------------------------
|
|
83
|
-
//
|
|
127
|
+
// IStakingClient core methods
|
|
84
128
|
// ---------------------------------------------------------------------
|
|
85
129
|
|
|
86
130
|
/**
|
|
87
|
-
* Deposit SOL into liqSOL
|
|
88
|
-
*
|
|
89
|
-
* @return Transaction signature
|
|
131
|
+
* Deposit native SOL into liqSOL (liqsol_core::deposit).
|
|
132
|
+
* Handles tx build, sign, send, and confirmation.
|
|
90
133
|
*/
|
|
91
134
|
async deposit(amountLamports: bigint): Promise<string> {
|
|
92
|
-
if (amountLamports <= BigInt(0))
|
|
93
|
-
|
|
135
|
+
if (amountLamports <= BigInt(0)) {
|
|
136
|
+
throw new Error('Deposit amount must be greater than zero.');
|
|
137
|
+
}
|
|
138
|
+
|
|
94
139
|
const tx = await this.depositClient.buildDepositTx(amountLamports);
|
|
95
|
-
const { tx: prepared, blockhash, lastValidBlockHeight } = await this.prepareTx(
|
|
140
|
+
const { tx: prepared, blockhash, lastValidBlockHeight } = await this.prepareTx(
|
|
141
|
+
tx,
|
|
142
|
+
);
|
|
96
143
|
const signed = await this.signTransaction(prepared);
|
|
97
|
-
|
|
98
|
-
|
|
144
|
+
return await this.sendAndConfirmHttp(signed, {
|
|
145
|
+
blockhash,
|
|
146
|
+
lastValidBlockHeight,
|
|
147
|
+
});
|
|
99
148
|
}
|
|
100
149
|
|
|
101
150
|
/**
|
|
102
151
|
* Withdraw SOL from liqSOL protocol.
|
|
103
|
-
*
|
|
152
|
+
* NOTE: placeholder until a withdraw flow is implemented in DepositClient.
|
|
104
153
|
*/
|
|
105
|
-
async withdraw(
|
|
154
|
+
async withdraw(_amountLamports: bigint): Promise<string> {
|
|
106
155
|
throw new Error('Withdraw method not yet implemented.');
|
|
107
156
|
}
|
|
108
157
|
|
|
109
158
|
/**
|
|
110
159
|
* Stake liqSOL into Outpost (liqSOL -> pool).
|
|
111
|
-
*
|
|
112
|
-
* @param amountLamports Amount of liqSOL to stake (smallest unit)
|
|
160
|
+
* Ensures user ATA exists, then stakes via outpostClient.
|
|
113
161
|
*/
|
|
114
162
|
async stake(amountLamports: bigint): Promise<string> {
|
|
115
|
-
if (amountLamports <= BigInt(0))
|
|
116
|
-
|
|
117
|
-
|
|
163
|
+
if (amountLamports <= BigInt(0)) {
|
|
164
|
+
throw new Error('Stake amount must be greater than zero.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const preIxs = await this.outpostClient.maybeBuildCreateUserAtaIx(
|
|
168
|
+
this.solPubKey,
|
|
169
|
+
);
|
|
118
170
|
const stakeIx = await this.outpostClient.buildStakeLiqsolIx(amountLamports);
|
|
119
171
|
const tx = new Transaction().add(...preIxs, stakeIx);
|
|
172
|
+
|
|
120
173
|
const prepared = await this.prepareTx(tx);
|
|
121
174
|
const signed = await this.signTransaction(prepared.tx);
|
|
122
|
-
|
|
123
|
-
return result.signature;
|
|
175
|
+
return await this.sendAndConfirmHttp(signed, prepared);
|
|
124
176
|
}
|
|
125
177
|
|
|
126
178
|
/**
|
|
127
179
|
* Unstake liqSOL from Outpost (pool -> liqSOL).
|
|
128
|
-
*
|
|
129
|
-
* @param amountLamports Amount of liqSOL principal to unstake (smallest unit)
|
|
180
|
+
* Mirrors stake() but calls withdrawStake.
|
|
130
181
|
*/
|
|
131
182
|
async unstake(amountLamports: bigint): Promise<string> {
|
|
132
|
-
if (amountLamports <= BigInt(0))
|
|
183
|
+
if (amountLamports <= BigInt(0)) {
|
|
184
|
+
throw new Error('Unstake amount must be greater than zero.');
|
|
185
|
+
}
|
|
186
|
+
|
|
133
187
|
const user = this.solPubKey;
|
|
134
|
-
// const amount = new BN(amountLamports.toString());
|
|
135
188
|
const preIxs = await this.outpostClient.maybeBuildCreateUserAtaIx(user);
|
|
136
|
-
const withdrawIx =
|
|
189
|
+
const withdrawIx =
|
|
190
|
+
await this.outpostClient.buildWithdrawStakeIx(amountLamports);
|
|
137
191
|
const tx = new Transaction().add(...preIxs, withdrawIx);
|
|
192
|
+
|
|
138
193
|
const prepared = await this.prepareTx(tx);
|
|
139
194
|
const signed = await this.signTransaction(prepared.tx);
|
|
140
|
-
|
|
141
|
-
return result.signature;
|
|
195
|
+
return await this.sendAndConfirmHttp(signed, prepared);
|
|
142
196
|
}
|
|
143
197
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
198
|
+
/**
|
|
199
|
+
* Buy prelaunch WIRE “pretokens” using a supported asset.
|
|
200
|
+
*
|
|
201
|
+
* - SOL: uses purchase_with_sol
|
|
202
|
+
* - LIQSOL: uses purchase_with_liqsol
|
|
203
|
+
* - YIELD: uses purchase_warrants_from_yield
|
|
204
|
+
*
|
|
205
|
+
* ETH / LIQETH are not valid on Solana.
|
|
151
206
|
*/
|
|
152
207
|
async buy(amountLamports: bigint, purchaseAsset: PurchaseAsset): Promise<string> {
|
|
153
208
|
const user = this.solPubKey;
|
|
@@ -156,49 +211,63 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
156
211
|
|
|
157
212
|
switch (purchaseAsset) {
|
|
158
213
|
case PurchaseAsset.SOL: {
|
|
159
|
-
if (!amountLamports || amountLamports <= BigInt(0))
|
|
160
|
-
throw new Error(
|
|
161
|
-
|
|
162
|
-
ix = await this.tokenClient.buildPurchaseWithSolIx(
|
|
214
|
+
if (!amountLamports || amountLamports <= BigInt(0)) {
|
|
215
|
+
throw new Error('SOL pretoken purchase requires a positive amount.');
|
|
216
|
+
}
|
|
217
|
+
ix = await this.tokenClient.buildPurchaseWithSolIx(
|
|
218
|
+
amountLamports,
|
|
219
|
+
user,
|
|
220
|
+
);
|
|
163
221
|
break;
|
|
164
222
|
}
|
|
165
223
|
|
|
166
224
|
case PurchaseAsset.LIQSOL: {
|
|
167
|
-
if (!amountLamports || amountLamports <= BigInt(0))
|
|
168
|
-
throw new Error(
|
|
169
|
-
|
|
225
|
+
if (!amountLamports || amountLamports <= BigInt(0)) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'liqSOL pretoken purchase requires a positive amount.',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
170
230
|
preIxs = await this.outpostClient.maybeBuildCreateUserAtaIx(user);
|
|
171
|
-
ix = await this.tokenClient.buildPurchaseWithLiqsolIx(
|
|
231
|
+
ix = await this.tokenClient.buildPurchaseWithLiqsolIx(
|
|
232
|
+
amountLamports,
|
|
233
|
+
user,
|
|
234
|
+
);
|
|
172
235
|
break;
|
|
173
236
|
}
|
|
174
237
|
|
|
175
238
|
case PurchaseAsset.YIELD: {
|
|
239
|
+
// Amount is ignored by the on-chain program; it consumes tracked yield.
|
|
176
240
|
ix = await this.tokenClient.buildPurchaseFromYieldIx(user);
|
|
177
241
|
break;
|
|
178
242
|
}
|
|
179
243
|
|
|
180
244
|
case PurchaseAsset.ETH:
|
|
181
245
|
case PurchaseAsset.LIQETH: {
|
|
182
|
-
throw new Error(
|
|
246
|
+
throw new Error(
|
|
247
|
+
'ETH / LIQETH pretoken purchases are not supported on Solana.',
|
|
248
|
+
);
|
|
183
249
|
}
|
|
184
250
|
|
|
185
251
|
default:
|
|
186
|
-
throw new Error(`Unsupported pretoken purchase asset: ${String(
|
|
252
|
+
throw new Error(`Unsupported pretoken purchase asset: ${String(
|
|
253
|
+
purchaseAsset,
|
|
254
|
+
)}`);
|
|
187
255
|
}
|
|
188
256
|
|
|
189
257
|
const tx = new Transaction().add(...preIxs, ix);
|
|
190
258
|
const prepared = await this.prepareTx(tx);
|
|
191
259
|
const signed = await this.signTransaction(prepared.tx);
|
|
192
|
-
|
|
193
|
-
return res.signature;
|
|
260
|
+
return await this.sendAndConfirmHttp(signed, prepared);
|
|
194
261
|
}
|
|
195
262
|
|
|
196
263
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
264
|
+
* Aggregate view of the user’s balances on Solana:
|
|
265
|
+
* - native: SOL wallet balance
|
|
266
|
+
* - liq: liqSOL token balance (Token-2022 ATA)
|
|
267
|
+
* - staked: Outpost-staked liqSOL principal
|
|
268
|
+
* - tracked: distribution program trackedBalance (liqSOL)
|
|
269
|
+
* - wire: total prelaunch WIRE shares (warrants/pretokens, 1e8)
|
|
270
|
+
* - extras: useful internal addresses and raw state for debugging/UX
|
|
202
271
|
*/
|
|
203
272
|
async getPortfolio(): Promise<Portfolio> {
|
|
204
273
|
const user = this.solPubKey;
|
|
@@ -215,35 +284,36 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
215
284
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
216
285
|
);
|
|
217
286
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
287
|
+
const [
|
|
288
|
+
nativeLamports,
|
|
289
|
+
actualBalResp,
|
|
290
|
+
userRecord,
|
|
291
|
+
snapshot,
|
|
292
|
+
] = await Promise.all([
|
|
293
|
+
this.connection.getBalance(user, 'confirmed'),
|
|
294
|
+
this.connection
|
|
295
|
+
.getTokenAccountBalance(userLiqsolAta, 'confirmed')
|
|
296
|
+
.catch(() => null),
|
|
222
297
|
this.distributionClient.getUserRecord(user).catch(() => null),
|
|
223
298
|
this.outpostClient.getWireStateSnapshot(user).catch(() => null),
|
|
224
299
|
]);
|
|
225
300
|
|
|
226
301
|
const LIQSOL_DECIMALS = 9;
|
|
227
302
|
|
|
228
|
-
const actualAmountStr = actualBalResp?.value?.amount ??
|
|
229
|
-
|
|
303
|
+
const actualAmountStr = actualBalResp?.value?.amount ?? '0';
|
|
230
304
|
const trackedAmountStr =
|
|
231
|
-
userRecord?.trackedBalance
|
|
305
|
+
userRecord?.trackedBalance?.toString() ?? '0';
|
|
232
306
|
|
|
233
|
-
// Snapshot is canonical; receipt may be null if user never staked/purchased
|
|
234
307
|
const wireReceipt = snapshot?.wireReceipt ?? null;
|
|
235
308
|
const userWarrantRecord = snapshot?.userWarrantRecord ?? null;
|
|
236
309
|
const trancheState = snapshot?.trancheState ?? null;
|
|
237
310
|
const globalState = snapshot?.globalState ?? null;
|
|
238
311
|
|
|
239
312
|
const stakedAmountStr =
|
|
240
|
-
wireReceipt?.stakedLiqsol
|
|
313
|
+
wireReceipt?.stakedLiqsol?.toString() ?? '0';
|
|
241
314
|
|
|
242
|
-
// Prelaunch WIRE "shares" = total warrants purchased by this user (1e8 precision)
|
|
243
315
|
const wireSharesStr =
|
|
244
|
-
userWarrantRecord?.totalWarrantsPurchased
|
|
245
|
-
? userWarrantRecord.totalWarrantsPurchased.toString()
|
|
246
|
-
: "0";
|
|
316
|
+
userWarrantRecord?.totalWarrantsPurchased?.toString() ?? '0';
|
|
247
317
|
|
|
248
318
|
return {
|
|
249
319
|
native: {
|
|
@@ -279,7 +349,6 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
279
349
|
vaultPDA: vaultPDA.toBase58(),
|
|
280
350
|
wireReceipt,
|
|
281
351
|
userWarrantRecord,
|
|
282
|
-
// global pretoken context (handy for UI + quoting)
|
|
283
352
|
globalIndex: globalState?.currentIndex?.toString(),
|
|
284
353
|
totalShares: globalState?.totalShares?.toString(),
|
|
285
354
|
currentTrancheNumber: trancheState?.currentTrancheNumber?.toString(),
|
|
@@ -290,165 +359,190 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
290
359
|
}
|
|
291
360
|
|
|
292
361
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
362
|
+
* Unified, chain-agnostic tranche snapshot for Solana.
|
|
363
|
+
*
|
|
364
|
+
* Uses:
|
|
365
|
+
* - liqsol_core.globalState (currentIndex, totalShares, etc.)
|
|
366
|
+
* - liqsol_core.trancheState (price, supply, total sold, etc.)
|
|
367
|
+
* - Chainlink/PriceHistory for SOL/USD (via TokenClient.getSolPriceUsdSafe)
|
|
368
|
+
*
|
|
369
|
+
* windowBefore/windowAfter control how many ladder rows we precompute
|
|
370
|
+
* around the current tranche for UI, but you can pass nothing if you
|
|
371
|
+
* only need current tranche info.
|
|
295
372
|
*/
|
|
296
|
-
async getTrancheSnapshot(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
373
|
+
async getTrancheSnapshot(options?: {
|
|
374
|
+
chainID?: ChainID;
|
|
375
|
+
windowBefore?: number;
|
|
376
|
+
windowAfter?: number;
|
|
377
|
+
}): Promise<TrancheSnapshot> {
|
|
378
|
+
const {
|
|
379
|
+
chainID = SolChainID.WireTestnet,
|
|
380
|
+
windowBefore,
|
|
381
|
+
windowAfter,
|
|
382
|
+
} = options ?? {};
|
|
383
|
+
|
|
384
|
+
// Canonical program state
|
|
385
|
+
const [globalState, trancheState] = await Promise.all([
|
|
386
|
+
this.tokenClient.fetchGlobalState(),
|
|
387
|
+
this.tokenClient.fetchTrancheState(),
|
|
388
|
+
]);
|
|
300
389
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
390
|
+
// Latest SOL/USD price (1e8) + timestamp from PriceHistory
|
|
391
|
+
const { price: solPriceUsd, timestamp } =
|
|
392
|
+
await this.tokenClient.getSolPriceUsdSafe();
|
|
393
|
+
|
|
394
|
+
return buildSolanaTrancheSnapshot({
|
|
395
|
+
chainID,
|
|
396
|
+
globalState,
|
|
397
|
+
trancheState,
|
|
398
|
+
solPriceUsd,
|
|
399
|
+
nativePriceTimestamp: timestamp,
|
|
400
|
+
ladderWindowBefore: windowBefore,
|
|
401
|
+
ladderWindowAfter: windowAfter,
|
|
402
|
+
});
|
|
312
403
|
}
|
|
313
404
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Approximate prelaunch WIRE quote for a given amount & asset.
|
|
407
|
+
*
|
|
408
|
+
* Uses TrancheSnapshot + SOL/USD price for:
|
|
409
|
+
* - SOL: amount is lamports
|
|
410
|
+
* - LIQSOL: amount is liqSOL base units (decimals = 9)
|
|
411
|
+
* - YIELD: amount is treated as SOL lamports-equivalent of yield
|
|
412
|
+
*
|
|
413
|
+
* NOTE: On-chain rounding may differ slightly (this is UI-only).
|
|
414
|
+
*/
|
|
415
|
+
async getBuyQuote(
|
|
416
|
+
amount: bigint,
|
|
417
|
+
asset: PurchaseAsset,
|
|
418
|
+
opts?: { chainID?: ChainID },
|
|
419
|
+
): Promise<PurchaseQuote> {
|
|
420
|
+
// For non-YIELD purchases we require a positive amount.
|
|
421
|
+
if (asset !== PurchaseAsset.YIELD && amount <= BigInt(0)) {
|
|
422
|
+
throw new Error('amount must be > 0 for non-YIELD purchases');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const snapshot = await this.getTrancheSnapshot({
|
|
426
|
+
chainID: opts?.chainID,
|
|
427
|
+
});
|
|
320
428
|
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const tranche = snapshot.trancheState;
|
|
429
|
+
const wirePriceUsd = snapshot.currentPriceUsd; // 1e8
|
|
430
|
+
const solPriceUsd = snapshot.nativePriceUsd; // 1e8
|
|
324
431
|
|
|
325
|
-
if (!
|
|
326
|
-
throw new Error(
|
|
432
|
+
if (!wirePriceUsd || wirePriceUsd <= BigInt(0)) {
|
|
433
|
+
throw new Error('Invalid WIRE price in tranche snapshot');
|
|
434
|
+
}
|
|
435
|
+
if (!solPriceUsd || solPriceUsd <= BigInt(0)) {
|
|
436
|
+
throw new Error('No SOL/USD price available');
|
|
327
437
|
}
|
|
328
438
|
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
const wireDecimals = 8;
|
|
439
|
+
const ONE_E9 = BigInt(LAMPORTS_PER_SOL); // 1e9
|
|
440
|
+
const ONE_E8 = BigInt(100_000_000); // 1e8
|
|
332
441
|
|
|
333
|
-
let notionalUsd:
|
|
334
|
-
let wireSharesBn: BN; // 1e8 WIRE shares
|
|
442
|
+
let notionalUsd: bigint;
|
|
335
443
|
|
|
336
|
-
switch (
|
|
444
|
+
switch (asset) {
|
|
337
445
|
case PurchaseAsset.SOL: {
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// amount is lamports (1e9). Convert to USD notional:
|
|
342
|
-
// usdValue = lamports * solPriceUsd / 1e9
|
|
343
|
-
notionalUsd = new BN(amount)
|
|
344
|
-
.mul(solPriceUsd)
|
|
345
|
-
.div(new BN(1_000_000_000)); // 10^9
|
|
346
|
-
|
|
347
|
-
wireSharesBn = this.calculateExpectedWarrants(notionalUsd, wirePriceUsd);
|
|
446
|
+
// lamports * solPriceUsd / 1e9 → 1e8 USD
|
|
447
|
+
notionalUsd = (amount * solPriceUsd) / ONE_E9;
|
|
348
448
|
break;
|
|
349
449
|
}
|
|
350
450
|
|
|
351
451
|
case PurchaseAsset.LIQSOL: {
|
|
352
|
-
// liqSOL
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// liqSOL has 9 decimals as well; treat `amount` as raw token units.
|
|
356
|
-
notionalUsd = new BN(amount)
|
|
357
|
-
.mul(liqsolPriceUsd)
|
|
358
|
-
.div(new BN(1_000_000_000)); // 10^9
|
|
359
|
-
|
|
360
|
-
wireSharesBn = this.calculateExpectedWarrants(notionalUsd, wirePriceUsd);
|
|
452
|
+
// liqSOL also uses 9 decimals; use same conversion.
|
|
453
|
+
notionalUsd = (amount * solPriceUsd) / ONE_E9;
|
|
361
454
|
break;
|
|
362
455
|
}
|
|
363
456
|
|
|
364
457
|
case PurchaseAsset.YIELD: {
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
// You have two options for UI semantics:
|
|
368
|
-
// - treat `amount` as a virtual SOL amount and reuse the SOL path, or
|
|
369
|
-
// - ignore `amount` and quote based on the user's actual claimBalance.
|
|
370
|
-
//
|
|
371
|
-
// Here we keep it simple: `amount` is lamports-equivalent of SOL yield.
|
|
372
|
-
const solPriceUsd = await this.tokenClient.getSolPriceUsd();
|
|
373
|
-
|
|
374
|
-
notionalUsd = new BN(amount)
|
|
375
|
-
.mul(solPriceUsd)
|
|
376
|
-
.div(new BN(1_000_000_000));
|
|
377
|
-
|
|
378
|
-
wireSharesBn = this.calculateExpectedWarrants(notionalUsd, wirePriceUsd);
|
|
458
|
+
// Treat amount as lamports-equivalent of SOL yield (UI convention).
|
|
459
|
+
notionalUsd = (amount * solPriceUsd) / ONE_E9;
|
|
379
460
|
break;
|
|
380
461
|
}
|
|
381
462
|
|
|
382
463
|
case PurchaseAsset.ETH:
|
|
383
464
|
case PurchaseAsset.LIQETH:
|
|
384
|
-
throw new Error(
|
|
465
|
+
throw new Error('getBuyQuote for ETH/LIQETH is not supported on Solana');
|
|
385
466
|
|
|
386
467
|
default:
|
|
387
|
-
|
|
388
|
-
throw new Error(`Unsupported purchase asset: ${String(purchaseAsset)}`);
|
|
468
|
+
throw new Error(`Unsupported purchase asset: ${String(asset)}`);
|
|
389
469
|
}
|
|
390
470
|
|
|
471
|
+
// WIRE shares (1e8) = (notionalUsd * 1e8) / wirePriceUsd
|
|
472
|
+
// Add a small bias to avoid truncating to 0 on tiny buys.
|
|
473
|
+
const numerator = notionalUsd * ONE_E8;
|
|
474
|
+
const wireShares =
|
|
475
|
+
numerator === BigInt(0)
|
|
476
|
+
? BigInt(0)
|
|
477
|
+
: (numerator + wirePriceUsd - BigInt(1)) / wirePriceUsd;
|
|
478
|
+
|
|
391
479
|
return {
|
|
392
|
-
purchaseAsset,
|
|
480
|
+
purchaseAsset: asset,
|
|
393
481
|
amountIn: amount,
|
|
394
|
-
wireShares
|
|
395
|
-
wireDecimals,
|
|
396
|
-
wirePriceUsd
|
|
397
|
-
notionalUsd
|
|
482
|
+
wireShares,
|
|
483
|
+
wireDecimals: 8,
|
|
484
|
+
wirePriceUsd,
|
|
485
|
+
notionalUsd,
|
|
398
486
|
};
|
|
399
487
|
}
|
|
400
488
|
|
|
401
|
-
// ---------------------------------------------------------------------
|
|
402
|
-
// SOL-only extras
|
|
403
|
-
// ---------------------------------------------------------------------
|
|
404
|
-
|
|
405
489
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* expectedWarrants = (notionalUsd * 1e8) / wirePriceUsd
|
|
410
|
-
*
|
|
411
|
-
* where:
|
|
412
|
-
* - notionalUsd, wirePriceUsd are 1e8-scaled USD
|
|
413
|
-
* - result is 1e8-scaled WIRE "shares"
|
|
490
|
+
* Convenience helper to fetch the distribution userRecord for the current user.
|
|
491
|
+
* Used by balance-correction flows and debugging.
|
|
414
492
|
*/
|
|
415
|
-
private calculateExpectedWarrants(notionalUsd: BN, wirePriceUsd: BN): BN {
|
|
416
|
-
const SCALE = new BN("100000000"); // 1e8
|
|
417
|
-
return notionalUsd.mul(SCALE).div(wirePriceUsd);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
493
|
async getUserRecord() {
|
|
421
494
|
return this.distributionClient.getUserRecord(this.solPubKey);
|
|
422
495
|
}
|
|
423
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Run the "correct & register" flow on Solana:
|
|
499
|
+
* - builds the minimal transaction (maybe multi-user) to reconcile liqSOL
|
|
500
|
+
* - signs and sends the transaction if it can succeed
|
|
501
|
+
*/
|
|
424
502
|
async correctBalance(amount?: bigint): Promise<string> {
|
|
425
503
|
const build = await this.distributionClient.buildCorrectRegisterTx({ amount });
|
|
426
504
|
if (!build.canSucceed || !build.transaction) {
|
|
427
505
|
throw new Error(build.reason ?? 'Unable to build Correct&Register transaction');
|
|
428
506
|
}
|
|
429
507
|
|
|
430
|
-
const { tx, blockhash, lastValidBlockHeight } = await this.prepareTx(
|
|
508
|
+
const { tx, blockhash, lastValidBlockHeight } = await this.prepareTx(
|
|
509
|
+
build.transaction,
|
|
510
|
+
);
|
|
431
511
|
const signed = await this.signTransaction(tx);
|
|
432
|
-
const
|
|
433
|
-
|
|
512
|
+
const signature = await this.sendAndConfirmHttp(signed, {
|
|
513
|
+
blockhash,
|
|
514
|
+
lastValidBlockHeight,
|
|
515
|
+
});
|
|
516
|
+
return signature;
|
|
434
517
|
}
|
|
435
518
|
|
|
436
519
|
// ---------------------------------------------------------------------
|
|
437
520
|
// Tx helpers
|
|
438
521
|
// ---------------------------------------------------------------------
|
|
439
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Send a signed transaction over HTTP RPC and wait for confirmation.
|
|
525
|
+
* Throws if the transaction fails.
|
|
526
|
+
*/
|
|
440
527
|
private async sendAndConfirmHttp(
|
|
441
528
|
signed: SolanaTransaction,
|
|
442
529
|
ctx: { blockhash: string; lastValidBlockHeight: number },
|
|
443
|
-
): Promise<
|
|
444
|
-
const signature = await this.connection.sendRawTransaction(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
530
|
+
): Promise<string> {
|
|
531
|
+
const signature = await this.connection.sendRawTransaction(
|
|
532
|
+
signed.serialize(),
|
|
533
|
+
{
|
|
534
|
+
skipPreflight: false,
|
|
535
|
+
preflightCommitment: commitment,
|
|
536
|
+
maxRetries: 3,
|
|
537
|
+
},
|
|
538
|
+
);
|
|
449
539
|
|
|
450
540
|
const conf = await this.connection.confirmTransaction(
|
|
451
|
-
{
|
|
541
|
+
{
|
|
542
|
+
signature,
|
|
543
|
+
blockhash: ctx.blockhash,
|
|
544
|
+
lastValidBlockHeight: ctx.lastValidBlockHeight,
|
|
545
|
+
},
|
|
452
546
|
commitment,
|
|
453
547
|
);
|
|
454
548
|
|
|
@@ -456,29 +550,35 @@ export class SolanaStakingClient implements IStakingClient {
|
|
|
456
550
|
throw new Error(`Transaction failed: ${JSON.stringify(conf.value.err)}`);
|
|
457
551
|
}
|
|
458
552
|
|
|
459
|
-
return
|
|
553
|
+
return signature;
|
|
460
554
|
}
|
|
461
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Sign a single Solana transaction using the connected wallet adapter.
|
|
558
|
+
*/
|
|
462
559
|
async signTransaction(tx: SolanaTransaction): Promise<SolanaTransaction> {
|
|
463
560
|
return this.anchor.wallet.signTransaction(tx);
|
|
464
561
|
}
|
|
465
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Generic "fire and forget" send helper if the caller already
|
|
565
|
+
* prepared and signed the transaction.
|
|
566
|
+
*/
|
|
466
567
|
async sendTransaction(signed: SolanaTransaction): Promise<TransactionSignature> {
|
|
467
568
|
return this.anchor.sendAndConfirm(signed);
|
|
468
569
|
}
|
|
469
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Attach recent blockhash + fee payer to a transaction.
|
|
573
|
+
* Required before signing and sending.
|
|
574
|
+
*/
|
|
470
575
|
async prepareTx(
|
|
471
576
|
tx: Transaction,
|
|
472
577
|
): Promise<{ tx: Transaction; blockhash: string; lastValidBlockHeight: number }> {
|
|
473
|
-
const { blockhash, lastValidBlockHeight } =
|
|
578
|
+
const { blockhash, lastValidBlockHeight } =
|
|
579
|
+
await this.connection.getLatestBlockhash('confirmed');
|
|
474
580
|
tx.recentBlockhash = blockhash;
|
|
475
581
|
tx.feePayer = this.solPubKey;
|
|
476
582
|
return { tx, blockhash, lastValidBlockHeight };
|
|
477
583
|
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
export interface TxResult {
|
|
481
|
-
signature: string;
|
|
482
|
-
slot: number;
|
|
483
|
-
confirmed: boolean;
|
|
484
584
|
}
|