-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from tchambard/feat/solidr-program/open-session…
…-instruction feat(solidr-program): open session
- Loading branch information
Showing
14 changed files
with
678 additions
and
231 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,5 +3,5 @@ | |
"printWidth": 180, | ||
"singleQuote": true, | ||
"trailingComma": "all", | ||
"tabWidth": 2 | ||
"tabWidth": 4 | ||
} |
225 changes: 124 additions & 101 deletions
225
packages/programs/solidr-program/client/AbstractSolanaClient.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,114 +1,137 @@ | ||
import { Address, BorshCoder, EventParser, Idl, IdlEvents, Program, Wallet } from '@coral-xyz/anchor'; | ||
import { Connection, PublicKey, SendOptions, Transaction } from '@solana/web3.js'; | ||
import { Connection, LAMPORTS_PER_SOL, PublicKey, SendOptions, Transaction, TransactionSignature } from '@solana/web3.js'; | ||
import * as _ from 'lodash'; | ||
|
||
export interface ITransactionResult { | ||
tx: string; | ||
accounts?: NodeJS.Dict<PublicKey>; | ||
events: any; | ||
} | ||
export type ITransactionResult = { | ||
tx: string; | ||
accounts?: NodeJS.Dict<PublicKey>; | ||
events: any; | ||
}; | ||
|
||
export type ProgramInstructionWrapper<T = any> = (fn: () => Promise<T>) => Promise<T>; | ||
|
||
export class AbstractSolanaClient<T extends Idl> { | ||
public readonly program: Program<T>; | ||
public readonly connection: Connection; | ||
protected readonly options?: SendOptions; | ||
|
||
constructor(program: Program<T>, options?: SendOptions) { | ||
this.program = program; | ||
this.connection = program.provider.connection; | ||
this.options = options; | ||
} | ||
|
||
public async signAndSendTransaction(payer: Wallet, tx: Transaction, accounts?: NodeJS.Dict<PublicKey>): Promise<ITransactionResult> { | ||
const recentBlockhash = await this.getRecentBlockhash(); | ||
tx.feePayer = payer.publicKey; | ||
tx.recentBlockhash = recentBlockhash; | ||
const signedTransaction = await payer.signTransaction(tx); | ||
const serializedTx = signedTransaction.serialize(); | ||
const sentTx = await this.connection.sendRawTransaction(serializedTx, this.options); | ||
return { | ||
tx: sentTx, | ||
events: await this.getTxEvents(sentTx), | ||
accounts, | ||
}; | ||
} | ||
|
||
public async getRecentBlockhash(): Promise<string> { | ||
return (await this.connection.getLatestBlockhash()).blockhash; | ||
} | ||
|
||
protected async getPage<R>(account: any, addresses: Address[], page: number = 1, perPage: number = 200): Promise<R[]> { | ||
const paginatedPublicKeys = addresses.slice((page - 1) * perPage, page * perPage); | ||
if (paginatedPublicKeys.length === 0) { | ||
return []; | ||
} | ||
return account.fetchMultiple(paginatedPublicKeys); | ||
} | ||
|
||
public addEventListener<E extends keyof IdlEvents<T>>(eventName: E & string, callback: (event: IdlEvents<any>[E], slot: number, signature: string) => void): number | undefined { | ||
try { | ||
return this.program.addEventListener(eventName, callback); | ||
} catch (e) { | ||
// silent error. problem encountered on vite dev server because of esm | ||
return; | ||
public readonly program: Program<T>; | ||
public readonly connection: Connection; | ||
protected readonly options?: SendOptions; | ||
protected readonly wrapFn: ProgramInstructionWrapper; | ||
|
||
constructor(program: Program<T>, options?: SendOptions, wrapFn?: ProgramInstructionWrapper<T>) { | ||
this.program = program; | ||
this.connection = program.provider.connection; | ||
this.options = options; | ||
this.wrapFn = wrapFn || this._wrapFn.bind(this); | ||
} | ||
} | ||
|
||
protected async _wrapFn(fn: () => Promise<T>): Promise<T> { | ||
try { | ||
return await fn(); | ||
} catch (e) { | ||
throw e; | ||
public async signAndSendTransaction(payer: Wallet, tx: Transaction, accounts?: NodeJS.Dict<PublicKey>): Promise<ITransactionResult> { | ||
const recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; | ||
tx.feePayer = payer.publicKey; | ||
tx.recentBlockhash = recentBlockhash; | ||
const signedTransaction = await payer.signTransaction(tx); | ||
const serializedTx = signedTransaction.serialize(); | ||
const sentTx = await this.connection.sendRawTransaction(serializedTx, this.options); | ||
await this.confirmTx(sentTx); | ||
|
||
return { | ||
tx: sentTx, | ||
events: await this.getTxEvents(sentTx), | ||
accounts, | ||
}; | ||
} | ||
} | ||
|
||
private async getTxEvents(tx: string): Promise<NodeJS.Dict<any> | undefined> { | ||
return this.callWithRetry(async () => { | ||
const txDetails = await this.connection.getTransaction(tx, { | ||
maxSupportedTransactionVersion: 0, | ||
commitment: 'confirmed', | ||
}); | ||
if (!txDetails) return; | ||
|
||
try { | ||
const eventParser = new EventParser(this.program.programId, new BorshCoder(this.program.idl)); | ||
// console.log('tx meta :>> ', txDetails?.meta); | ||
const events = eventParser.parseLogs(txDetails?.meta?.logMessages || []); | ||
// console.log('events :>> ', events.next()); | ||
const result: NodeJS.Dict<object> = {}; | ||
for (let event of events) { | ||
result[event.name] = event.data; | ||
|
||
protected async getPage<R>(account: any, addresses: Address[], page: number = 1, perPage: number = 200): Promise<R[]> { | ||
const paginatedPublicKeys = addresses.slice((page - 1) * perPage, page * perPage); | ||
if (paginatedPublicKeys.length === 0) { | ||
return []; | ||
} | ||
return result; | ||
} catch (e) { | ||
return; | ||
} | ||
}, 200); | ||
} | ||
|
||
private delay(ms: number): Promise<void> { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} | ||
|
||
private async callWithRetry(fn: () => Promise<any>, timeMs: number, retry: number = 10): Promise<any> { | ||
const call = async (attempt: number): Promise<any> => { | ||
try { | ||
const result = await fn(); | ||
if (result !== undefined) { | ||
return result; | ||
} else { | ||
throw new Error('No result'); | ||
return account.fetchMultiple(paginatedPublicKeys); | ||
} | ||
|
||
public addEventListener<E extends keyof IdlEvents<T>>( | ||
eventName: E & string, | ||
callback: (event: IdlEvents<any>[E], slot: number, signature: string) => void, | ||
): number | undefined { | ||
try { | ||
return this.program.addEventListener(eventName, callback); | ||
} catch (e) { | ||
// silent error. problem encountered on vite dev server because of esm | ||
return; | ||
} | ||
} catch (e) { | ||
if (attempt < retry) { | ||
await this.delay(timeMs); | ||
return call(attempt + 1); | ||
} else if (!e.message.match(/No result/)) { | ||
throw new Error(`Maximum retries reached without success. Last error: ${e.stack}`); | ||
} | ||
|
||
public async airdrop(to: PublicKey, sol: number): Promise<void> { | ||
const txHash = await this.program.provider.connection.requestAirdrop(to, sol * LAMPORTS_PER_SOL); | ||
return this.confirmTx(txHash); | ||
} | ||
|
||
protected async _wrapFn(fn: () => Promise<T>): Promise<T> { | ||
try { | ||
return await fn(); | ||
} catch (e) { | ||
throw e; | ||
} | ||
} | ||
}; | ||
} | ||
|
||
private async confirmTx(txHash: string) { | ||
const blockhashInfo = await this.program.provider.connection.getLatestBlockhash(); | ||
await this.program.provider.connection.confirmTransaction({ | ||
blockhash: blockhashInfo.blockhash, | ||
lastValidBlockHeight: blockhashInfo.lastValidBlockHeight, | ||
signature: txHash, | ||
}); | ||
} | ||
|
||
public async getLatestBlockhash(): Promise<string> { | ||
return (await this.connection.getLatestBlockhash()).blockhash; | ||
} | ||
|
||
private async getTxEvents(tx: string): Promise<NodeJS.Dict<any> | undefined> { | ||
return this.callWithRetry(async () => { | ||
const txDetails = await this.connection.getTransaction(tx, { | ||
maxSupportedTransactionVersion: 0, | ||
commitment: 'confirmed', | ||
}); | ||
if (!txDetails) return; | ||
|
||
return call(1); | ||
} | ||
try { | ||
const eventParser = new EventParser(this.program.programId, new BorshCoder(this.program.idl)); | ||
// console.log('tx meta :>> ', txDetails?.meta); | ||
const events = eventParser.parseLogs(txDetails?.meta?.logMessages || []); | ||
// console.log('events :>> ', events.next()); | ||
const result: NodeJS.Dict<object> = {}; | ||
for (let event of events) { | ||
result[event.name] = event.data; | ||
} | ||
return result; | ||
} catch (e) { | ||
return; | ||
} | ||
}, 200); | ||
} | ||
|
||
private delay(ms: number): Promise<void> { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} | ||
|
||
private async callWithRetry(fn: () => Promise<any>, timeMs: number, retry: number = 10): Promise<any> { | ||
const call = async (attempt: number): Promise<any> => { | ||
try { | ||
const result = await fn(); | ||
if (result !== undefined) { | ||
return result; | ||
} else { | ||
throw new Error('No result'); | ||
} | ||
} catch (e) { | ||
if (attempt < retry) { | ||
await this.delay(timeMs); | ||
return call(attempt + 1); | ||
} else if (!e.message.match(/No result/)) { | ||
throw new Error(`Maximum retries reached without success. Last error: ${e.stack}`); | ||
} | ||
} | ||
}; | ||
|
||
return call(1); | ||
} | ||
} |
Oops, something went wrong.