@jpool/bond-sdk 0.1.0-next.1 → 0.1.0-next.3

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/src/client.ts DELETED
@@ -1,568 +0,0 @@
1
- import type { Wallet } from '@coral-xyz/anchor'
2
- import type {
3
- Keypair,
4
- ParsedTransactionWithMeta,
5
- TransactionInstruction } from '@solana/web3.js'
6
- import type { Config, EpochHistoryItem, InitializeProps, RegisterValidatorProps, TopUpCollateralProps, TransactionHistoryItem, ValidatorBondAccount, WithdrawCollateralProps, WithdrawCompensationProps } from './types'
7
- import type { Jbond } from './types/jbond'
8
- import { AnchorProvider, BN, Program } from '@coral-xyz/anchor'
9
- import {
10
- Connection,
11
- LAMPORTS_PER_SOL,
12
- PublicKey,
13
- SystemProgram,
14
- Transaction,
15
- } from '@solana/web3.js'
16
- import bs58 from 'bs58'
17
- import { slotToEpoch } from './helpers'
18
- import IDL from './idl/jbond.json'
19
- import { BondTransactionType } from './types'
20
- import { NodeWallet } from './utils/wallet'
21
-
22
- export class JBondClient {
23
- connection: Connection
24
- program: Program<Jbond>
25
- provider: AnchorProvider
26
- config: Config
27
-
28
- constructor(config: Config, wallet?: Wallet) {
29
- this.config = config
30
- this.connection = new Connection(config.rpcUrl, 'confirmed')
31
-
32
- // If keypair is provided, use it. Otherwise, create a dummy keypair
33
- // since we're only building instructions
34
- this.provider = new AnchorProvider(
35
- this.connection,
36
- // @ts-expect-error support anonymous
37
- wallet ?? { publicKey: PublicKey.default },
38
- AnchorProvider.defaultOptions())
39
-
40
- this.program = new Program(IDL as any, this.provider)
41
- }
42
-
43
- /**
44
- * Creates an instance of `JBondClient` using a provided connection and wallet.
45
- */
46
- static fromWallet(config: Config, wallet?: Wallet): JBondClient {
47
- return new this(config, wallet)
48
- }
49
-
50
- /**
51
- * Creates an instance of `JBondClient` using the provided connection and keypair.
52
- */
53
- static fromKeypair(config: Config, keypair: Keypair): JBondClient {
54
- return JBondClient.fromWallet(config, new NodeWallet(keypair))
55
- }
56
-
57
- // Get PDA for global state
58
- getGlobalStatePDA(): [PublicKey, number] {
59
- return PublicKey.findProgramAddressSync(
60
- [Buffer.from('global_state')],
61
- this.program.programId,
62
- )
63
- }
64
-
65
- // Get PDA for validator bond account
66
- getValidatorBondPDA(
67
- voteAccount: PublicKey,
68
- ): [PublicKey, number] {
69
- return PublicKey.findProgramAddressSync(
70
- [
71
- Buffer.from('validator_bond'),
72
- voteAccount.toBuffer(),
73
- ],
74
- this.program.programId,
75
- )
76
- }
77
-
78
- // Build initialize instruction
79
- async buildInitializeInstruction(props: InitializeProps): Promise<TransactionInstruction> {
80
- const [globalState] = this.getGlobalStatePDA()
81
- return this.program.methods
82
- .initialize()
83
- .accountsStrict({
84
- globalState,
85
- authority: props.authority,
86
- reserve: this.config.reserveAddress,
87
- systemProgram: SystemProgram.programId,
88
- })
89
- .instruction()
90
- }
91
-
92
- // Build register validator instruction
93
- async buildRegisterValidatorInstruction(
94
- props: RegisterValidatorProps,
95
- ): Promise<TransactionInstruction> {
96
- const { creator, identity, voteAccount, initialCollateral, withdrawalAuthority } = props
97
- const [validatorBondAccountAddress] = this.getValidatorBondPDA(voteAccount)
98
-
99
- const accountInfo = await this.connection.getAccountInfo(validatorBondAccountAddress)
100
- if (accountInfo) {
101
- throw new Error('Validator bond account already exists')
102
- }
103
- const [globalState] = this.getGlobalStatePDA()
104
-
105
- const collateralLamports = new BN(initialCollateral * LAMPORTS_PER_SOL)
106
-
107
- return this.program.methods
108
- .bondInit(collateralLamports, withdrawalAuthority ?? null)
109
- .accountsStrict({
110
- creator,
111
- globalState,
112
- validatorBondAccount: validatorBondAccountAddress,
113
- identity,
114
- voteAccount,
115
- systemProgram: SystemProgram.programId,
116
- })
117
- .instruction()
118
- }
119
-
120
- // Build top up collateral instruction
121
- buildTopUpCollateralInstruction(
122
- props: TopUpCollateralProps,
123
- ): Promise<TransactionInstruction> {
124
- const { user, voteAccount, amount } = props
125
- const [validatorBondAccountAddress] = this.getValidatorBondPDA(voteAccount)
126
- const amountLamports = new BN(amount * LAMPORTS_PER_SOL)
127
-
128
- return this.program.methods
129
- .bondTopUp(amountLamports)
130
- .accountsStrict({
131
- validatorBondAccount: validatorBondAccountAddress,
132
- depositor: user,
133
- systemProgram: SystemProgram.programId,
134
- })
135
- .instruction()
136
- }
137
-
138
- async buildWithdrawCollateralInstruction(
139
- props: WithdrawCollateralProps,
140
- ): Promise<TransactionInstruction> {
141
- const { withdrawalAuthority: user, voteAccount, destination, amount } = props
142
- const [validatorBondAccountAddress] = this.getValidatorBondPDA(voteAccount)
143
- const amountLamports = new BN(amount * LAMPORTS_PER_SOL)
144
-
145
- return this.program.methods
146
- .bondWithdraw(amountLamports)
147
- .accountsStrict({
148
- validatorBondAccount: validatorBondAccountAddress,
149
- withdrawalAuthority: user,
150
- destination,
151
- systemProgram: SystemProgram.programId,
152
- })
153
- .instruction()
154
- }
155
-
156
- // Build withdraw compensation instruction
157
- buildWithdrawCompensationInstruction(
158
- props: WithdrawCompensationProps,
159
- ): Promise<TransactionInstruction> {
160
- const { authority, voteAccount, amount } = props
161
- const [globalState] = this.getGlobalStatePDA()
162
- const [validatorBondAccountAddress] = this.getValidatorBondPDA(voteAccount)
163
- const amountLamports = new BN(amount * LAMPORTS_PER_SOL)
164
-
165
- return this.program.methods
166
- .withdrawCompensation(amountLamports)
167
- .accountsStrict({
168
- globalState,
169
- validatorBondAccount: validatorBondAccountAddress,
170
- reserve: this.config.reserveAddress,
171
- authority,
172
- systemProgram: SystemProgram.programId,
173
- })
174
- .instruction()
175
- }
176
-
177
- // Build multiple withdraw compensation instructions
178
- buildWithdrawCompensationsInstructions(
179
- authority: PublicKey,
180
- withdrawals: Array<{ voteAccount: PublicKey, amount: number }>,
181
- ): Promise<TransactionInstruction[]> {
182
- return Promise.all(
183
- withdrawals.map(({ voteAccount, amount }) =>
184
- this.buildWithdrawCompensationInstruction({ authority, voteAccount, amount }),
185
- ),
186
- )
187
- }
188
-
189
- // Get validator bond account state
190
- async getValidatorBondAccount(
191
- voteAccount: PublicKey,
192
- ): Promise<ValidatorBondAccount | null> {
193
- const [validatorBondAccountAddress] = this.getValidatorBondPDA(voteAccount)
194
-
195
- try {
196
- const account = await (
197
- this.program.account as any
198
- ).validatorBondAccount.fetch(validatorBondAccountAddress)
199
-
200
- return {
201
- identity: account.identity.toString(),
202
- voteAccount: account.voteAccount.toString(),
203
- withdrawalAuthority: account.withdrawalAuthority ? account.withdrawalAuthority.toString() : null,
204
- totalWithdrawn: account.totalWithdrawn.toNumber() / LAMPORTS_PER_SOL,
205
- lastWithdrawalEpoch: account.lastWithdrawalEpoch.toNumber(),
206
- isActive: account.isActive,
207
- createdAt: account.createdAt.toNumber() * 1000, // TODO
208
- bump: account.bump,
209
- }
210
- } catch {
211
- return null
212
- }
213
- }
214
-
215
- /**
216
- * Get the collateral balance of a validator bond account
217
- * @param voteAccount - The vote account public key
218
- * @returns The available collateral balance in SOL (excluding rent-exempt amount)
219
- */
220
- async getValidatorCollateralBalance(voteAccount: PublicKey): Promise<number> {
221
- const [validatorBondAccountAddress] = this.getValidatorBondPDA(voteAccount)
222
-
223
- const accountInfo = await this.connection.getAccountInfo(validatorBondAccountAddress)
224
- if (!accountInfo) {
225
- return 0
226
- }
227
-
228
- const rentExempt = await this.connection.getMinimumBalanceForRentExemption(
229
- accountInfo.data.length,
230
- )
231
-
232
- const availableBalance = Math.max(0, accountInfo.lamports - rentExempt)
233
- return availableBalance / LAMPORTS_PER_SOL
234
- }
235
-
236
- // Get global state state
237
- async getGlobalState(): Promise<{
238
- authority: string
239
- totalValidators: number
240
- totalWithdrawn: number
241
- } | null> {
242
- const [globalState] = this.getGlobalStatePDA()
243
-
244
- try {
245
- const account = await (
246
- this.program.account as any
247
- ).globalState.fetch(globalState)
248
-
249
- return {
250
- authority: account.authority.toString(),
251
- totalValidators: account.totalValidators,
252
- totalWithdrawn: account.totalWithdrawn.toNumber() / LAMPORTS_PER_SOL,
253
- }
254
- } catch {
255
- return null
256
- }
257
- }
258
-
259
- // Get current epoch
260
- async getCurrentEpoch(): Promise<number> {
261
- const epochInfo = await this.connection.getEpochInfo()
262
- return epochInfo.epoch
263
- }
264
-
265
- // Helper methods for backward compatibility (can be removed if not needed)
266
- async initialize(authority?: PublicKey): Promise<string> {
267
- const authorityPubkey = authority || this.provider.wallet.publicKey
268
- const ix = await this.buildInitializeInstruction({ authority: authorityPubkey })
269
- const tx = await this.provider.sendAndConfirm(
270
- new Transaction().add(ix),
271
- [],
272
- )
273
- return tx
274
- }
275
-
276
- // Note: this method will be removed or changed in the future (CLI only)
277
- async registerValidator(
278
- voteAccount: PublicKey,
279
- initialCollateral: number,
280
- withdrawalAuthority?: PublicKey,
281
- identity?: PublicKey,
282
- ): Promise<string> {
283
- const identityPubkey = identity ?? this.provider.wallet.publicKey
284
- const ix = await this.buildRegisterValidatorInstruction(
285
- { creator: identityPubkey, identity: identityPubkey, voteAccount, initialCollateral, withdrawalAuthority },
286
- )
287
- const tx = await this.provider.sendAndConfirm(
288
- new Transaction().add(ix),
289
- [],
290
- )
291
- return tx
292
- }
293
-
294
- async topUpCollateral(
295
- voteAccount: PublicKey,
296
- amount: number,
297
- validator?: PublicKey,
298
- ): Promise<string> {
299
- const userPubkey = validator ?? this.provider.wallet.publicKey
300
- const ix = await this.buildTopUpCollateralInstruction(
301
- { user: userPubkey, voteAccount, amount },
302
- )
303
- const tx = await this.provider.sendAndConfirm(
304
- new Transaction().add(ix),
305
- [],
306
- )
307
- return tx
308
- }
309
-
310
- async withdrawCollateral(
311
- voteAccount: PublicKey,
312
- destination: PublicKey,
313
- amount: number,
314
- withdrawalAuthority?: PublicKey,
315
- ): Promise<string> {
316
- const authorityPubkey = withdrawalAuthority ?? this.provider.wallet.publicKey
317
- const ix = await this.buildWithdrawCollateralInstruction(
318
- { withdrawalAuthority: authorityPubkey, voteAccount, destination, amount },
319
- )
320
- const tx = await this.provider.sendAndConfirm(
321
- new Transaction().add(ix),
322
- [],
323
- )
324
- return tx
325
- }
326
-
327
- async claimCompensation(
328
- voteAccount: PublicKey,
329
- amount: number,
330
- authority?: PublicKey,
331
- ): Promise<string> {
332
- const authorityPubkey = authority ?? this.provider.wallet.publicKey
333
- const ix = await this.buildWithdrawCompensationInstruction(
334
- { authority: authorityPubkey, voteAccount, amount },
335
- )
336
- const tx = await this.provider.sendAndConfirm(
337
- new Transaction().add(ix),
338
- [],
339
- )
340
- return tx
341
- }
342
-
343
- /**
344
- * Get transaction history grouped by epochs
345
- * @param voteAccount - The vote account to get history for
346
- * @param epochsCount - Number of recent epochs to return (default: 10)
347
- * @returns Array of epoch history items sorted by epoch (descending)
348
- */
349
- async getHistoryGroupedByEpochs(
350
- voteAccount: PublicKey,
351
- epochsCount: number = 10,
352
- ): Promise<EpochHistoryItem[]> {
353
- const currentEpoch = await this.getCurrentEpoch()
354
- const fullHistory = await this.getFullHistory(voteAccount)
355
-
356
- const epochMap = new Map<number, EpochHistoryItem>()
357
-
358
- for (const item of fullHistory) {
359
- if (item.epoch < currentEpoch - epochsCount + 1) {
360
- continue
361
- }
362
- if (!epochMap.has(item.epoch)) {
363
- epochMap.set(item.epoch, {
364
- epoch: item.epoch,
365
- deposits: 0,
366
- withdrawals: 0,
367
- balanceChange: 0,
368
- signatures: [],
369
- })
370
- }
371
-
372
- const epochData = epochMap.get(item.epoch)!
373
-
374
- if (item.type === 'deposit') {
375
- epochData.deposits += item.amount
376
- } else if (item.type === 'withdrawal') {
377
- epochData.withdrawals += item.amount
378
- }
379
-
380
- if (!epochData.signatures.includes(item.signature)) {
381
- epochData.signatures.push(item.signature)
382
- }
383
- }
384
-
385
- for (const epochData of epochMap.values()) {
386
- epochData.balanceChange = epochData.deposits - epochData.withdrawals
387
- }
388
-
389
- const result = [...epochMap.values()].toSorted((a, b) => b.epoch - a.epoch)
390
-
391
- return result
392
- }
393
-
394
- async getHistory(
395
- voteAccount: PublicKey,
396
- options?: {
397
- cluster?: 'mainnet-beta' | 'testnet' | 'devnet'
398
- limit?: number
399
- before?: string
400
- until?: string
401
- },
402
- ): Promise<TransactionHistoryItem[]> {
403
- const [ValidatorBondAccount] = this.getValidatorBondPDA(voteAccount)
404
-
405
- const signatures = await this.connection.getSignaturesForAddress(
406
- ValidatorBondAccount,
407
- {
408
- limit: options?.limit || 1000,
409
- before: options?.before,
410
- until: options?.until,
411
- },
412
- )
413
-
414
- const signatureStrings = signatures.map(sig => sig.signature)
415
-
416
- const BATCH_SIZE = 100
417
- const allTransactions: (ParsedTransactionWithMeta | null)[] = []
418
-
419
- for (let i = 0; i < signatureStrings.length; i += BATCH_SIZE) {
420
- const batch = signatureStrings.slice(i, i + BATCH_SIZE)
421
- const transactions = await this.connection.getParsedTransactions(
422
- batch,
423
- {
424
- maxSupportedTransactionVersion: 0,
425
- },
426
- )
427
- allTransactions.push(...transactions)
428
- }
429
-
430
- const cluster = options?.cluster || 'mainnet-beta'
431
-
432
- const history: TransactionHistoryItem[] = []
433
-
434
- for (const [idx, tx] of allTransactions.entries()) {
435
- const sigInfo = signatures[idx]
436
-
437
- if (!tx || !tx.meta) {
438
- continue
439
- }
440
-
441
- try {
442
- const slot = (tx.slot || 0)
443
- const instructions = tx.transaction.message.instructions
444
-
445
- for (const instruction of instructions) {
446
- if ('programId' in instruction && instruction.programId.equals(this.program.programId)) {
447
- const ixData = instruction as any
448
-
449
- if ('parsed' in ixData && ixData.parsed) {
450
- continue
451
- }
452
-
453
- const data = ixData.data
454
- if (!data) {
455
- continue
456
- }
457
-
458
- let type: BondTransactionType | null = null
459
- let amount = 0
460
-
461
- try {
462
- const dataBuffer = bs58.decode(data)
463
-
464
- if (dataBuffer.length >= 16) {
465
- const discriminator = dataBuffer.slice(0, 8)
466
-
467
- const bondInitDiscriminator = this.getInstructionDiscriminator('bondInit')
468
- const bondTopUpDiscriminator = this.getInstructionDiscriminator('bondTopUp')
469
- const withdrawCompensationDiscriminator = this.getInstructionDiscriminator('withdrawCompensation')
470
- const bondWithdrawDiscriminator = this.getInstructionDiscriminator('bondWithdraw')
471
-
472
- const amountBytes = dataBuffer.slice(8, 16)
473
- const amountBN = new BN(amountBytes, 'le')
474
- amount = amountBN.toNumber() / LAMPORTS_PER_SOL
475
-
476
- if (Buffer.from(discriminator).equals(Buffer.from(bondInitDiscriminator))) {
477
- type = BondTransactionType.Deposit
478
- } else if (Buffer.from(discriminator).equals(Buffer.from(bondTopUpDiscriminator))) {
479
- type = BondTransactionType.Deposit
480
- } else if (Buffer.from(discriminator).equals(Buffer.from(withdrawCompensationDiscriminator))) {
481
- type = BondTransactionType.Compensation
482
- } else if (Buffer.from(discriminator).equals(Buffer.from(bondWithdrawDiscriminator))) {
483
- type = BondTransactionType.Withdrawal
484
- }
485
- }
486
- } catch {
487
- console.warn('Failed to decode instruction data')
488
- }
489
-
490
- if (type && amount > 0) {
491
- let beforeBalance: number | undefined
492
- let afterBalance: number | undefined
493
-
494
- // Find the validator bond account in the account keys
495
- const accountIndex = tx.transaction.message.accountKeys.findIndex(
496
- key => key.pubkey.equals(ValidatorBondAccount),
497
- )
498
-
499
- if (accountIndex !== -1 && tx.meta.preBalances && tx.meta.postBalances) {
500
- beforeBalance = Number(tx.meta.preBalances[accountIndex] || 0) / LAMPORTS_PER_SOL
501
- afterBalance = Number(tx.meta.postBalances[accountIndex] || 0) / LAMPORTS_PER_SOL
502
- }
503
-
504
- history.push({
505
- signature: sigInfo!.signature,
506
- slot,
507
- epoch: slotToEpoch(slot, cluster),
508
- type,
509
- amount,
510
- beforeBalance,
511
- afterBalance,
512
- })
513
- }
514
- }
515
- }
516
- } catch (error) {
517
- console.error(`Error processing transaction ${sigInfo!.signature}:`, error)
518
- }
519
- }
520
-
521
- return history.toSorted((a, b) => b.slot - a.slot)
522
- }
523
-
524
- // Helper method to get paginated history
525
- async getFullHistory(
526
- voteAccount: PublicKey,
527
- pageSize: number = 100,
528
- ): Promise<TransactionHistoryItem[]> {
529
- const allHistory: TransactionHistoryItem[] = []
530
- let before: string | undefined
531
-
532
- while (true) {
533
- const batch = await this.getHistory(voteAccount, {
534
- limit: pageSize,
535
- before,
536
- })
537
-
538
- if (batch.length === 0) {
539
- break
540
- }
541
-
542
- allHistory.push(...batch)
543
-
544
- // Get the signature of the last transaction for pagination
545
- before = batch.at(-1)?.signature
546
-
547
- // If we got less than the page size, we've reached the end
548
- if (batch.length < pageSize) {
549
- break
550
- }
551
- }
552
-
553
- return allHistory
554
- }
555
-
556
- private getInstructionDiscriminator(instructionName: string): Uint8Array {
557
- const instruction = this.program.idl.instructions.find(ix => ix.name === instructionName)
558
- if (!instruction) {
559
- throw new Error(`Instruction ${instructionName} not found in IDL`)
560
- }
561
-
562
- if (!instruction.discriminator || !Array.isArray(instruction.discriminator)) {
563
- throw new Error(`Discriminator not found for instruction ${instructionName}`)
564
- }
565
-
566
- return new Uint8Array(instruction.discriminator)
567
- }
568
- }
package/src/common.ts DELETED
@@ -1,2 +0,0 @@
1
- export const PROGRAM_ID = '4a3YovKEfm4jWhczCzJciHXL1xVkXWfGQjRCaMft7M4G'
2
- export const RESERVE_ADDRESS = '61mS9nEir6jx6cvte6NzQpyrFk3Fj4krMNLuHhi4tjJz'
package/src/config.ts DELETED
@@ -1,31 +0,0 @@
1
- import type { Config } from './types'
2
- import { readFileSync } from 'node:fs'
3
- import { Keypair, PublicKey } from '@solana/web3.js'
4
- import { PROGRAM_ID, RESERVE_ADDRESS } from './common'
5
-
6
- export function loadConfig(): Config {
7
- const rpcUrl = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com'
8
- const programId = new PublicKey(
9
- PROGRAM_ID || '4a3YovKEfm4jWhczCzJciHXL1xVkXWfGQjRCaMft7M4G',
10
- )
11
- const reserveAddress = new PublicKey(
12
- RESERVE_ADDRESS
13
- || '61mS9nEir6jx6cvte6NzQpyrFk3Fj4krMNLuHhi4tjJz',
14
- )
15
-
16
- return {
17
- rpcUrl,
18
- programId,
19
- reserveAddress,
20
- }
21
- }
22
-
23
- export function loadKeypair(keypairPath: string): Keypair {
24
- try {
25
- const keypairString = readFileSync(keypairPath, 'utf8')
26
- const keypairData = JSON.parse(keypairString)
27
- return Keypair.fromSecretKey(new Uint8Array(keypairData))
28
- } catch (error) {
29
- throw new Error(`Failed to load keypair from ${keypairPath}: ${error}`)
30
- }
31
- }
package/src/helpers.ts DELETED
@@ -1,7 +0,0 @@
1
- const SLOTS_PER_EPOCH_MAINNET = 432_000
2
- const SLOTS_PER_EPOCH_DEVNET = 400_000
3
-
4
- export function slotToEpoch(slot: number, cluster: 'mainnet-beta' | 'testnet' | 'devnet'): number {
5
- const slotsPerEpoch = cluster === 'mainnet-beta' ? SLOTS_PER_EPOCH_MAINNET : SLOTS_PER_EPOCH_DEVNET
6
- return Math.floor(slot / slotsPerEpoch)
7
- }