@wireio/stake 0.1.0 → 0.1.2
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/README.md +57 -0
- package/lib/stake.browser.js +11836 -4103
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +374 -556
- package/lib/stake.js +12089 -4303
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +11836 -4103
- package/lib/stake.m.js.map +1 -1
- package/package.json +1 -1
- package/src/assets/ethereum/ABI/liqEth/DepositManager.sol/DepositManager.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/DepositManager.sol/DepositManager.json +1153 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IAccounting.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IAccounting.json +172 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositContract.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositContract.json +39 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositManager.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositManager.json +64 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthBurn.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthBurn.json +24 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthMint.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthMint.json +35 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IRewardsERC20.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IRewardsERC20.json +213 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IStakingModule.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IStakingModule.json +138 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IValidatorBalanceVerifier.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IValidatorBalanceVerifier.json +70 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IWithdrawalRecord.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IWithdrawalRecord.json +64 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/LiqEthCommon.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/LiqEthCommon.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/RewardsERC20.sol/RewardsERC20.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/RewardsERC20.sol/RewardsERC20.json +749 -0
- package/src/assets/ethereum/ABI/liqEth/RewardsERC20Pausable.sol/RewardsERC20Pausable.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/RewardsERC20Pausable.sol/RewardsERC20Pausable.json +812 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/BeaconRoots.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/BeaconRoots.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/SSZ.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/SSZ.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/ValidatorBalanceVerifier.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/ValidatorBalanceVerifier.json +225 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/BeaconRoots.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/BeaconRoots.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/SSZ.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/SSZ.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/YieldOracle.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/YieldOracle.json +813 -0
- package/src/assets/ethereum/ABI/liqEth/accounting.sol/Accounting.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/accounting.sol/Accounting.json +651 -0
- package/src/assets/ethereum/ABI/liqEth/liqEth.sol/LiqEthToken.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/liqEth.sol/LiqEthToken.json +1110 -0
- package/src/assets/ethereum/ABI/liqEth/liqEthBurn.sol/LiqEthBurn.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/liqEthBurn.sol/LiqEthBurn.json +391 -0
- package/src/assets/ethereum/ABI/liqEth/liqEthMint.sol/LiqEthMint.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/liqEthMint.sol/LiqEthMint.json +402 -0
- package/src/assets/ethereum/ABI/liqEth/stakingModule.sol/StakingModule.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/stakingModule.sol/StakingModule.json +1225 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalQueue.sol/WithdrawalQueue.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalQueue.sol/WithdrawalQueue.json +927 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/Uint64BE.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/Uint64BE.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/WithdrawalVault.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/WithdrawalVault.json +447 -0
- package/src/assets/solana/idl/liqsol_core.json +4239 -0
- package/src/assets/solana/idl/liqsol_token.json +183 -0
- package/src/assets/solana/idl/validator_leaderboard.json +270 -265
- package/src/assets/solana/types/liqsol_core.ts +4245 -0
- package/src/assets/solana/types/liqsol_token.ts +189 -0
- package/src/assets/solana/types/validator_leaderboard.ts +270 -265
- package/src/index.ts +1 -3
- package/src/networks/ethereum/contract.ts +101 -36
- package/src/networks/ethereum/ethereum.ts +141 -45
- package/src/networks/ethereum/types.ts +30 -2
- package/src/networks/solana/clients/deposit.client.ts +71 -109
- package/src/networks/solana/clients/distribution.client.ts +256 -383
- package/src/networks/solana/clients/leaderboard.client.ts +38 -133
- package/src/networks/solana/constants.ts +214 -130
- package/src/networks/solana/program.ts +25 -38
- package/src/networks/solana/solana.ts +120 -105
- package/src/networks/solana/types.ts +37 -47
- package/src/networks/solana/utils.ts +551 -0
- package/src/scripts/tsconfig.json +17 -0
- package/src/staker/staker.ts +10 -6
- package/src/staker/types.ts +14 -9
- package/src/assets/solana/idl/deposit.json +0 -296
- package/src/assets/solana/idl/distribution.json +0 -768
- package/src/assets/solana/idl/liq_sol_token.json +0 -298
- package/src/assets/solana/idl/mint_helper.json +0 -110
- package/src/assets/solana/idl/read_tracked_balance.json +0 -140
- package/src/assets/solana/idl/stake_controller.json +0 -2149
- package/src/assets/solana/idl/treasury.json +0 -110
- package/src/assets/solana/idl/validator_registry.json +0 -487
- package/src/assets/solana/idl/yield_oracle.json +0 -32
- package/src/assets/solana/types/deposit.ts +0 -302
- package/src/assets/solana/types/distribution.ts +0 -774
- package/src/assets/solana/types/liq_sol_token.ts +0 -304
- package/src/assets/solana/types/mint_helper.ts +0 -116
- package/src/assets/solana/types/read_tracked_balance.ts +0 -146
- package/src/assets/solana/types/stake_controller.ts +0 -2155
- package/src/assets/solana/types/stake_registry.ts +0 -441
- package/src/assets/solana/types/treasury.ts +0 -116
- package/src/assets/solana/types/validator_registry.ts +0 -493
- package/src/assets/solana/types/yield_oracle.ts +0 -38
- package/src/common/utils.ts +0 -9
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// src/networks/solana/utils.ts
|
|
2
|
+
|
|
3
|
+
import { Program, BN, AnchorProvider } from '@coral-xyz/anchor';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Connection,
|
|
7
|
+
Keypair,
|
|
8
|
+
PublicKey,
|
|
9
|
+
SystemProgram,
|
|
10
|
+
StakeProgram,
|
|
11
|
+
} from '@solana/web3.js';
|
|
12
|
+
import {
|
|
13
|
+
TOKEN_2022_PROGRAM_ID,
|
|
14
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
15
|
+
getAssociatedTokenAddress,
|
|
16
|
+
} from '@solana/spl-token';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
LIQSOL_CORE,
|
|
20
|
+
LIQSOL_TOKEN,
|
|
21
|
+
PAY_RATE_SCALE_FACTOR,
|
|
22
|
+
DEFAULT_AVERAGE_PAY_RATE,
|
|
23
|
+
EPHEMERAL_RENT_EXEMPTION,
|
|
24
|
+
LAMPORTS_PER_SOL,
|
|
25
|
+
lamportsToSol,
|
|
26
|
+
solToLamports,
|
|
27
|
+
deriveDepositAuthorityPda,
|
|
28
|
+
deriveLiqsolMintPda,
|
|
29
|
+
deriveLiqsolMintAuthorityPda,
|
|
30
|
+
deriveVaultPda,
|
|
31
|
+
deriveReservePoolPda,
|
|
32
|
+
deriveStakeControllerStatePda,
|
|
33
|
+
derivePayoutStatePda,
|
|
34
|
+
deriveBucketAuthorityPda,
|
|
35
|
+
deriveDistributionStatePda,
|
|
36
|
+
deriveUserRecordPda,
|
|
37
|
+
derivePayRateHistoryPda,
|
|
38
|
+
deriveStakeControllerVaultPda,
|
|
39
|
+
deriveEphemeralStakeAddress,
|
|
40
|
+
DEFAULT_PAY_RATE_LOOKBACK,
|
|
41
|
+
} from './constants';
|
|
42
|
+
|
|
43
|
+
import liqsolCoreIDL from '../../assets/solana/idl/liqsol_core.json';
|
|
44
|
+
|
|
45
|
+
// -----------------------------------------------------------------------------
|
|
46
|
+
// Read-only liqsol_core Program helper
|
|
47
|
+
// -----------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
let _liqsolCoreProgram: Program | null = null;
|
|
50
|
+
|
|
51
|
+
export function getLiqsolCoreProgram(
|
|
52
|
+
connection: Connection,
|
|
53
|
+
): Program {
|
|
54
|
+
if (_liqsolCoreProgram && _liqsolCoreProgram.provider.connection === connection) {
|
|
55
|
+
return _liqsolCoreProgram;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Dummy wallet – we're only doing read-only account fetches here.
|
|
59
|
+
const tmpKeypair = Keypair.generate();
|
|
60
|
+
const wallet : any = { publicKey: tmpKeypair.publicKey, signAllTransactions: async () => [], signTransaction: async () => tmpKeypair };
|
|
61
|
+
|
|
62
|
+
const provider = new AnchorProvider(connection, wallet, {
|
|
63
|
+
commitment: 'confirmed',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const program = new Program(
|
|
67
|
+
liqsolCoreIDL,
|
|
68
|
+
provider,
|
|
69
|
+
) as Program;
|
|
70
|
+
|
|
71
|
+
_liqsolCoreProgram = program;
|
|
72
|
+
return program;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -----------------------------------------------------------------------------
|
|
76
|
+
// Deposit accounts bundle
|
|
77
|
+
// -----------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
export interface DepositAccounts {
|
|
80
|
+
user: PublicKey;
|
|
81
|
+
depositAuthority: PublicKey;
|
|
82
|
+
liqsolMint: PublicKey;
|
|
83
|
+
liqsolMintAuthority: PublicKey;
|
|
84
|
+
userAta: PublicKey;
|
|
85
|
+
stakeControllerVault: PublicKey;
|
|
86
|
+
stakeControllerReservePool: PublicKey;
|
|
87
|
+
stakeControllerState: PublicKey;
|
|
88
|
+
payoutState: PublicKey;
|
|
89
|
+
bucketAuthority: PublicKey;
|
|
90
|
+
bucketTokenAccount: PublicKey;
|
|
91
|
+
distributionState: PublicKey;
|
|
92
|
+
userRecord: PublicKey;
|
|
93
|
+
payRateHistory: PublicKey;
|
|
94
|
+
ephemeralStake: PublicKey;
|
|
95
|
+
ephemeralSeed: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a complete DepositAccounts set for a given user, matching the
|
|
100
|
+
* on-chain PDAs used by the liqSOL core program.
|
|
101
|
+
*
|
|
102
|
+
* The optional `seed` lets you make deposit flows replayable/deterministic.
|
|
103
|
+
* If omitted, a random u32 seed is generated.
|
|
104
|
+
*/
|
|
105
|
+
export async function buildDepositAccounts(
|
|
106
|
+
connection: Connection,
|
|
107
|
+
user: PublicKey,
|
|
108
|
+
): Promise<DepositAccounts> {
|
|
109
|
+
const depositAuthority = deriveDepositAuthorityPda();
|
|
110
|
+
const liqsolMint = deriveLiqsolMintPda();
|
|
111
|
+
const liqsolMintAuthority = deriveLiqsolMintAuthorityPda();
|
|
112
|
+
const stakeControllerVault = deriveStakeControllerVaultPda();
|
|
113
|
+
const stakeControllerReservePool = deriveReservePoolPda();
|
|
114
|
+
const stakeControllerState = deriveStakeControllerStatePda();
|
|
115
|
+
const payoutState = derivePayoutStatePda();
|
|
116
|
+
const bucketAuthority = deriveBucketAuthorityPda();
|
|
117
|
+
const distributionState = deriveDistributionStatePda();
|
|
118
|
+
const userRecord = deriveUserRecordPda(user);
|
|
119
|
+
const payRateHistory = derivePayRateHistoryPda();
|
|
120
|
+
|
|
121
|
+
const userAta = await getAssociatedTokenAddress(
|
|
122
|
+
liqsolMint,
|
|
123
|
+
user,
|
|
124
|
+
false,
|
|
125
|
+
TOKEN_2022_PROGRAM_ID,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const bucketTokenAccount = await getAssociatedTokenAddress(
|
|
129
|
+
liqsolMint,
|
|
130
|
+
bucketAuthority,
|
|
131
|
+
true,
|
|
132
|
+
TOKEN_2022_PROGRAM_ID,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// -------------------------------------------------------------
|
|
136
|
+
// Ephemeral stake
|
|
137
|
+
// -------------------------------------------------------------
|
|
138
|
+
const seed = Math.floor(Math.random() * 2 ** 32);
|
|
139
|
+
const ephemeralSeed = `ephemeral_${seed}`;
|
|
140
|
+
const ephemeralStake = await deriveEphemeralStakeAddress(user, ephemeralSeed);
|
|
141
|
+
|
|
142
|
+
// `connection` is currently unused, but we keep it in the signature
|
|
143
|
+
// so this helper can evolve to preflight/validate accounts if needed.
|
|
144
|
+
void connection;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
user,
|
|
148
|
+
depositAuthority,
|
|
149
|
+
liqsolMint,
|
|
150
|
+
liqsolMintAuthority,
|
|
151
|
+
userAta,
|
|
152
|
+
stakeControllerVault,
|
|
153
|
+
stakeControllerReservePool,
|
|
154
|
+
stakeControllerState,
|
|
155
|
+
payoutState,
|
|
156
|
+
bucketAuthority,
|
|
157
|
+
bucketTokenAccount,
|
|
158
|
+
distributionState,
|
|
159
|
+
userRecord,
|
|
160
|
+
payRateHistory,
|
|
161
|
+
ephemeralStake,
|
|
162
|
+
ephemeralSeed,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// -----------------------------------------------------------------------------
|
|
167
|
+
// Balance + state getters (UI-friendly)
|
|
168
|
+
// -----------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export async function getUserLiqSolBalance(
|
|
171
|
+
connection: Connection,
|
|
172
|
+
user: PublicKey,
|
|
173
|
+
): Promise<number> {
|
|
174
|
+
const liqsolMint = deriveLiqsolMintPda();
|
|
175
|
+
const ata = await getAssociatedTokenAddress(
|
|
176
|
+
liqsolMint,
|
|
177
|
+
user,
|
|
178
|
+
false,
|
|
179
|
+
TOKEN_2022_PROGRAM_ID,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const bal = await connection.getTokenAccountBalance(ata);
|
|
184
|
+
return Number(bal.value.amount); // raw lamports, not decimals-adjusted
|
|
185
|
+
} catch {
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function getBucketLiqSolBalance(
|
|
191
|
+
connection: Connection,
|
|
192
|
+
): Promise<number> {
|
|
193
|
+
const liqsolMint = deriveLiqsolMintPda();
|
|
194
|
+
const bucketAuthority = deriveBucketAuthorityPda();
|
|
195
|
+
const ata = await getAssociatedTokenAddress(
|
|
196
|
+
liqsolMint,
|
|
197
|
+
bucketAuthority,
|
|
198
|
+
true,
|
|
199
|
+
TOKEN_2022_PROGRAM_ID,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const bal = await connection.getTokenAccountBalance(ata);
|
|
204
|
+
return Number(bal.value.amount);
|
|
205
|
+
} catch {
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function getReservePoolBalance(
|
|
211
|
+
connection: Connection,
|
|
212
|
+
): Promise<number> {
|
|
213
|
+
const reservePool = deriveReservePoolPda();
|
|
214
|
+
try {
|
|
215
|
+
const lamports = await connection.getBalance(reservePool);
|
|
216
|
+
return lamports;
|
|
217
|
+
} catch {
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Raw account info for the stake_controller state (Anchor decode is left
|
|
224
|
+
* to the caller so the SDK can stay IDL-agnostic at this layer).
|
|
225
|
+
*/
|
|
226
|
+
export async function getStakeControllerStateRaw(
|
|
227
|
+
connection: Connection,
|
|
228
|
+
): Promise<Uint8Array | null> {
|
|
229
|
+
const pda = deriveStakeControllerStatePda();
|
|
230
|
+
const info = await connection.getAccountInfo(pda);
|
|
231
|
+
return info?.data ?? null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Raw account info for the payout-state account.
|
|
236
|
+
*/
|
|
237
|
+
export async function getPayoutStateRaw(
|
|
238
|
+
connection: Connection,
|
|
239
|
+
): Promise<Uint8Array | null> {
|
|
240
|
+
const pda = derivePayoutStatePda();
|
|
241
|
+
const info = await connection.getAccountInfo(pda);
|
|
242
|
+
return info?.data ?? null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Raw account info for a user's distribution user_record.
|
|
247
|
+
*/
|
|
248
|
+
export async function getUserRecordRaw(
|
|
249
|
+
connection: Connection,
|
|
250
|
+
user: PublicKey,
|
|
251
|
+
): Promise<Uint8Array | null> {
|
|
252
|
+
const pda = deriveUserRecordPda(user);
|
|
253
|
+
const info = await connection.getAccountInfo(pda);
|
|
254
|
+
return info?.data ?? null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -----------------------------------------------------------------------------
|
|
258
|
+
// Pay-rate & fee utilities (on-chain compatible)
|
|
259
|
+
// -----------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
export async function getAveragePayRate(
|
|
262
|
+
connection: Connection,
|
|
263
|
+
lookback: number = DEFAULT_PAY_RATE_LOOKBACK,
|
|
264
|
+
): Promise<bigint> {
|
|
265
|
+
const program = getLiqsolCoreProgram(connection);
|
|
266
|
+
const payRateHistoryPda = derivePayRateHistoryPda();
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const anyProgram = program as any;
|
|
270
|
+
const payRateHistoryAccount = await anyProgram.account.payRateHistory.fetch(
|
|
271
|
+
payRateHistoryPda,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const entries: any[] = payRateHistoryAccount.entries ?? [];
|
|
275
|
+
const totalEntriesAdded = Number(
|
|
276
|
+
payRateHistoryAccount.totalEntriesAdded ?? 0,
|
|
277
|
+
);
|
|
278
|
+
const currentIndex: number = payRateHistoryAccount.currentIndex ?? 0;
|
|
279
|
+
const maxEntries: number =
|
|
280
|
+
payRateHistoryAccount.maxEntries ?? entries.length;
|
|
281
|
+
|
|
282
|
+
if (!entries.length) {
|
|
283
|
+
return DEFAULT_AVERAGE_PAY_RATE;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const entriesToFetch = Math.min(lookback, maxEntries, entries.length);
|
|
287
|
+
|
|
288
|
+
let idx: number;
|
|
289
|
+
if (totalEntriesAdded === 0) {
|
|
290
|
+
idx = 0;
|
|
291
|
+
} else if (currentIndex === 0) {
|
|
292
|
+
idx = maxEntries - 1;
|
|
293
|
+
} else {
|
|
294
|
+
idx = currentIndex - 1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let sum = BigInt(0);
|
|
298
|
+
let validCount = BigInt(0);
|
|
299
|
+
|
|
300
|
+
for (let i = 0; i < entriesToFetch; i++) {
|
|
301
|
+
const entry = entries[idx];
|
|
302
|
+
if (entry && typeof entry.scaledRate !== 'undefined') {
|
|
303
|
+
const rate = BigInt(entry.scaledRate.toString());
|
|
304
|
+
if (rate > BigInt(0)) {
|
|
305
|
+
sum += rate;
|
|
306
|
+
validCount += BigInt(1);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (totalEntriesAdded === 0) {
|
|
311
|
+
idx = (idx + 1) % maxEntries;
|
|
312
|
+
} else {
|
|
313
|
+
idx = idx === 0 ? maxEntries - 1 : idx - 1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (validCount === BigInt(0)) {
|
|
318
|
+
return DEFAULT_AVERAGE_PAY_RATE;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return sum / validCount;
|
|
322
|
+
} catch (err) {
|
|
323
|
+
// Fallback to default when history missing/unavailable
|
|
324
|
+
return DEFAULT_AVERAGE_PAY_RATE;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* On-chain fee formula:
|
|
329
|
+
* fee = (average_pay_rate * 4 * deposit_amount_lamports) / 10^12
|
|
330
|
+
*/
|
|
331
|
+
export function calculateExpectedFee(
|
|
332
|
+
depositAmountLamports: BN,
|
|
333
|
+
averagePayRate: BN,
|
|
334
|
+
): BN {
|
|
335
|
+
return averagePayRate
|
|
336
|
+
.mul(new BN(4))
|
|
337
|
+
.mul(depositAmountLamports)
|
|
338
|
+
.div(new BN(1_000_000_000_000)); // 10^12
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Convenience helper to preview how a deposit splits between user + bucket
|
|
343
|
+
* and how much goes into the reserve, assuming the simple model:
|
|
344
|
+
*
|
|
345
|
+
* - userLiqSol = amount - fee + EPHEMERAL_RENT_EXEMPTION
|
|
346
|
+
* - bucketLiqSol = fee
|
|
347
|
+
* - reserveLamports = amount + EPHEMERAL_RENT_EXEMPTION
|
|
348
|
+
*/
|
|
349
|
+
export function previewDepositEffects(params: {
|
|
350
|
+
depositAmountLamports: BN;
|
|
351
|
+
averagePayRate: BN;
|
|
352
|
+
rentExemptionLamports?: number;
|
|
353
|
+
}): {
|
|
354
|
+
feeLamports: BN;
|
|
355
|
+
userLiqSolLamports: BN;
|
|
356
|
+
bucketLiqSolLamports: BN;
|
|
357
|
+
reserveIncreaseLamports: BN;
|
|
358
|
+
} {
|
|
359
|
+
const { depositAmountLamports, averagePayRate } = params;
|
|
360
|
+
const rent = new BN(
|
|
361
|
+
params.rentExemptionLamports ?? EPHEMERAL_RENT_EXEMPTION,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const fee = calculateExpectedFee(depositAmountLamports, averagePayRate);
|
|
365
|
+
const userLiqSol = depositAmountLamports.sub(fee).add(rent);
|
|
366
|
+
const bucketLiqSol = fee;
|
|
367
|
+
const reserveIncrease = depositAmountLamports.add(rent);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
feeLamports: fee,
|
|
371
|
+
userLiqSolLamports: userLiqSol,
|
|
372
|
+
bucketLiqSolLamports: bucketLiqSol,
|
|
373
|
+
reserveIncreaseLamports: reserveIncrease,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// -----------------------------------------------------------------------------
|
|
378
|
+
// Epoch / scheduling helpers
|
|
379
|
+
// -----------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
export type EpochSnapshot = {
|
|
382
|
+
epoch: number;
|
|
383
|
+
slot: number;
|
|
384
|
+
firstSlot: number;
|
|
385
|
+
slotsInEpoch: number;
|
|
386
|
+
slotMs: number;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export async function getEpochSnapshot(
|
|
390
|
+
connection: Connection,
|
|
391
|
+
): Promise<EpochSnapshot> {
|
|
392
|
+
const info = await connection.getEpochInfo();
|
|
393
|
+
|
|
394
|
+
// Fallback slot time
|
|
395
|
+
let slotTimeMs = Number(process.env.SLOT_TIME_MS_FALLBACK ?? 400);
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const samples = await connection.getRecentPerformanceSamples(16);
|
|
399
|
+
if (samples.length) {
|
|
400
|
+
const avgMs =
|
|
401
|
+
samples.reduce(
|
|
402
|
+
(acc, s) => acc + (s.samplePeriodSecs * 1000) / s.numSlots,
|
|
403
|
+
0,
|
|
404
|
+
) / samples.length;
|
|
405
|
+
if (isFinite(avgMs) && avgMs > 0) {
|
|
406
|
+
slotTimeMs = avgMs;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
// ignore; keep fallback
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
epoch: info.epoch,
|
|
415
|
+
slot: info.slotIndex,
|
|
416
|
+
firstSlot: info.absoluteSlot - info.slotIndex,
|
|
417
|
+
slotsInEpoch: info.slotsInEpoch,
|
|
418
|
+
slotMs: slotTimeMs,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function msToEpochEnd(snapshot: EpochSnapshot): number {
|
|
423
|
+
const remainingSlots = snapshot.slotsInEpoch - snapshot.slot;
|
|
424
|
+
return remainingSlots * snapshot.slotMs;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Generic "execute around epoch boundaries" helper:
|
|
429
|
+
*
|
|
430
|
+
* - If current progress is within [early, late], run immediately.
|
|
431
|
+
* - If too early, sleep until `early` percentage into the epoch.
|
|
432
|
+
* - If too late, sleep until `early` percentage into the *next* epoch.
|
|
433
|
+
*
|
|
434
|
+
* This is generic; you can wrap any instruction builder in here (including
|
|
435
|
+
* deposit flows) without baking in program-specific logic.
|
|
436
|
+
*/
|
|
437
|
+
export async function scheduledInstruction<T>(
|
|
438
|
+
connection: Connection,
|
|
439
|
+
config: ScheduleConfig,
|
|
440
|
+
instruction: () => Promise<T>,
|
|
441
|
+
): Promise<T> {
|
|
442
|
+
const early = config.early ?? 0.10;
|
|
443
|
+
const late = config.late ?? 0.90;
|
|
444
|
+
|
|
445
|
+
const snapshot = await getEpochSnapshot(connection);
|
|
446
|
+
const progress = snapshot.slot / snapshot.slotsInEpoch;
|
|
447
|
+
|
|
448
|
+
// Case 1: Already in safe zone
|
|
449
|
+
if (progress >= early && progress <= late) {
|
|
450
|
+
return instruction();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Case 2: Early in epoch → wait until `early`
|
|
454
|
+
if (progress < early) {
|
|
455
|
+
const targetSlot = snapshot.slotsInEpoch * early;
|
|
456
|
+
const slotsRemaining = targetSlot - snapshot.slot;
|
|
457
|
+
const msToWait = slotsRemaining * snapshot.slotMs;
|
|
458
|
+
|
|
459
|
+
console.log(
|
|
460
|
+
`Epoch early (${(progress * 100).toFixed(
|
|
461
|
+
2,
|
|
462
|
+
)}%). Sleeping ${(msToWait / 1000).toFixed(1)}s until ${early * 100
|
|
463
|
+
}%...`,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
await sleep(Math.max(1000, msToWait));
|
|
467
|
+
return instruction();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Case 3: Late in epoch → wait for next epoch + early window
|
|
471
|
+
const msToNextEpoch = msToEpochEnd(snapshot);
|
|
472
|
+
const earlyWaitTime = snapshot.slotsInEpoch * early * snapshot.slotMs;
|
|
473
|
+
const totalWaitTime = msToNextEpoch + earlyWaitTime + 1000;
|
|
474
|
+
|
|
475
|
+
console.log(
|
|
476
|
+
`Epoch late (${(progress * 100).toFixed(
|
|
477
|
+
2,
|
|
478
|
+
)}%). Sleeping ${(totalWaitTime / 1000).toFixed(
|
|
479
|
+
1,
|
|
480
|
+
)}s until next epoch + ${early * 100}%...`,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
await sleep(totalWaitTime);
|
|
484
|
+
return instruction();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
// -----------------------------------------------------------------------------
|
|
489
|
+
// Generic helpers (used in tests + can be useful in apps)
|
|
490
|
+
// -----------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
export function getErrorMessage(error: any): string {
|
|
493
|
+
return (
|
|
494
|
+
error?.error?.errorCode?.code ||
|
|
495
|
+
error?.error?.errorMessage ||
|
|
496
|
+
error?.message ||
|
|
497
|
+
''
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function generateRandomDepositAmount(
|
|
502
|
+
minSol = 2,
|
|
503
|
+
maxSol = 100,
|
|
504
|
+
): BN {
|
|
505
|
+
const randomSol = Math.random() * (maxSol - minSol) + minSol;
|
|
506
|
+
return new BN(solToLamports(randomSol));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function generateTestKeypair(): Keypair {
|
|
510
|
+
return Keypair.generate();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export async function airdropSol(
|
|
514
|
+
connection: Connection,
|
|
515
|
+
publicKey: PublicKey,
|
|
516
|
+
amountSol: number,
|
|
517
|
+
): Promise<void> {
|
|
518
|
+
const lamports = solToLamports(amountSol);
|
|
519
|
+
const sig = await connection.requestAirdrop(publicKey, lamports);
|
|
520
|
+
await connection.confirmTransaction(sig, 'confirmed');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export async function waitForConfirmation(
|
|
524
|
+
connection: Connection,
|
|
525
|
+
signature: string,
|
|
526
|
+
): Promise<void> {
|
|
527
|
+
await connection.confirmTransaction(signature, 'confirmed');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function sleep(ms: number): Promise<void> {
|
|
531
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Simple helper used in tests: wait until safe zone, no-op.
|
|
536
|
+
*/
|
|
537
|
+
export async function waitUntilSafeToExecuteFunction(
|
|
538
|
+
connection: Connection,
|
|
539
|
+
config: ScheduleConfig = {},
|
|
540
|
+
): Promise<void> {
|
|
541
|
+
await scheduledInstruction(connection, config, async () => {
|
|
542
|
+
// no-op
|
|
543
|
+
return;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export interface ScheduleConfig {
|
|
548
|
+
early?: number; // fraction of epoch, default 0.10
|
|
549
|
+
late?: number; // fraction of epoch, default 0.90
|
|
550
|
+
}
|
|
551
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"target": "es2019",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"types": [
|
|
8
|
+
"node"
|
|
9
|
+
],
|
|
10
|
+
"isolatedModules": false
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
"./**/*",
|
|
14
|
+
"../networks/**/*",
|
|
15
|
+
"../assets/**/*"
|
|
16
|
+
]
|
|
17
|
+
}
|
package/src/staker/staker.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/staker/staker.ts
|
|
2
2
|
|
|
3
|
-
import { ChainID, SolChainID } from '@wireio/core';
|
|
3
|
+
import { ChainID, EvmChainID, SolChainID } from '@wireio/core';
|
|
4
4
|
import { IStakingClient, StakerConfig } from './types';
|
|
5
5
|
import { SolanaStakingClient } from '../networks/solana/solana';
|
|
6
|
+
import { EthereumStakingClient } from '../networks/ethereum/ethereum';
|
|
6
7
|
|
|
7
8
|
export class Staker {
|
|
8
9
|
public selectedChainID?: ChainID;
|
|
@@ -31,18 +32,21 @@ export class Staker {
|
|
|
31
32
|
if (!config) throw new Error('StakerConfig is required');
|
|
32
33
|
if (!Array.isArray(config)) config = [config];
|
|
33
34
|
|
|
35
|
+
// console.log('STAKER INIT', config);
|
|
36
|
+
|
|
34
37
|
config.forEach((cfg) => {
|
|
35
38
|
switch (cfg.network.chainId) {
|
|
36
39
|
case SolChainID.WireTestnet:
|
|
37
40
|
this.clients.set(cfg.network.chainId, new SolanaStakingClient(cfg));
|
|
38
41
|
break;
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
case EvmChainID.Hoodi:
|
|
44
|
+
this.clients.set(EvmChainID.Hoodi, new EthereumStakingClient(cfg));
|
|
45
|
+
break;
|
|
46
|
+
|
|
43
47
|
default:
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
console.log(`No staking client available for chain ${cfg.network.chainId}`);
|
|
49
|
+
throw new Error(`Unsupported network curve: ${cfg.network.name}`);
|
|
46
50
|
}
|
|
47
51
|
});
|
|
48
52
|
|
package/src/staker/types.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { BaseSignerWalletAdapter } from '@solana/wallet-adapter-base';
|
|
4
4
|
import { PublicKey as SolPubKey } from '@solana/web3.js';
|
|
5
|
-
import { ExternalNetwork, PublicKey } from '@wireio/core';
|
|
6
|
-
import { ethers } from 'ethers';
|
|
5
|
+
import { ChainID, ExternalNetwork, PublicKey } from '@wireio/core';
|
|
6
|
+
import { BigNumberish, ethers } from 'ethers';
|
|
7
7
|
|
|
8
8
|
export interface IStakingClient {
|
|
9
9
|
pubKey: PublicKey;
|
|
@@ -13,7 +13,7 @@ export interface IStakingClient {
|
|
|
13
13
|
deposit(amount: number): Promise<string>;
|
|
14
14
|
|
|
15
15
|
/** Register any untracked LIQ staked tokens */
|
|
16
|
-
register(): Promise<string>;
|
|
16
|
+
// register(): Promise<string>;
|
|
17
17
|
|
|
18
18
|
/** Fetch the portfolio for the LIQ stake user */
|
|
19
19
|
getPortfolio(): Promise<Portfolio>;
|
|
@@ -26,18 +26,23 @@ export type StakerConfig = {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface Portfolio {
|
|
29
|
-
/** Native
|
|
29
|
+
/** Native balance on chain: ETH, SOL */
|
|
30
30
|
native: BalanceView;
|
|
31
|
-
/** Actual
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
|
|
31
|
+
/** Actual Liquid balance of LiqETH, LiqSOL*/
|
|
32
|
+
liq: BalanceView;
|
|
33
|
+
/** Outpost Staked balance */
|
|
34
|
+
staked: BalanceView
|
|
35
|
+
/** SOL ONLY!
|
|
36
|
+
* Tracked liqSOL balance from distribution program */
|
|
37
|
+
tracked?: BalanceView;
|
|
35
38
|
/** Extra PDAs and account addresses */
|
|
36
39
|
extras?: Record<string, any>;
|
|
40
|
+
/** Chain ID of the network for which this portfolio is from */
|
|
41
|
+
chainID: ChainID;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export type BalanceView = {
|
|
40
|
-
amount:
|
|
45
|
+
amount: BigNumberish; // raw on-chain integer value
|
|
41
46
|
decimals: number; // number of decimal places
|
|
42
47
|
symbol?: string; // optional token symbol identifier
|
|
43
48
|
ata?: SolPubKey; // associated token account address
|