diff --git a/sdk/src/accounts/basicUserStatsAccountSubscriber.ts b/sdk/src/accounts/basicUserStatsAccountSubscriber.ts new file mode 100644 index 0000000000..dc5b48671d --- /dev/null +++ b/sdk/src/accounts/basicUserStatsAccountSubscriber.ts @@ -0,0 +1,65 @@ +import { + DataAndSlot, + UserStatsAccountEvents, + UserStatsAccountSubscriber, +} from './types'; +import { PublicKey } from '@solana/web3.js'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { UserStatsAccount } from '../types'; + +/** + * Basic implementation of UserStatsAccountSubscriber. It will only take in UserStatsAccount + * data during initialization and will not fetch or subscribe to updates. + */ +export class BasicUserStatsAccountSubscriber + implements UserStatsAccountSubscriber +{ + isSubscribed: boolean; + eventEmitter: StrictEventEmitter; + userStatsAccountPublicKey: PublicKey; + + callbackId?: string; + errorCallbackId?: string; + + userStats: DataAndSlot; + + public constructor( + userStatsAccountPublicKey: PublicKey, + data?: UserStatsAccount, + slot?: number + ) { + this.isSubscribed = true; + this.eventEmitter = new EventEmitter(); + this.userStatsAccountPublicKey = userStatsAccountPublicKey; + this.userStats = { data, slot }; + } + + async subscribe(_userStatsAccount?: UserStatsAccount): Promise { + return true; + } + + async addToAccountLoader(): Promise {} + + async fetch(): Promise {} + + doesAccountExist(): boolean { + return this.userStats !== undefined; + } + + async unsubscribe(): Promise {} + + assertIsSubscribed(): void {} + + public getUserStatsAccountAndSlot(): DataAndSlot { + return this.userStats; + } + + public updateData(userStatsAccount: UserStatsAccount, slot: number): void { + if (!this.userStats || slot >= (this.userStats.slot ?? 0)) { + this.userStats = { data: userStatsAccount, slot }; + this.eventEmitter.emit('userStatsAccountUpdate', userStatsAccount); + this.eventEmitter.emit('update'); + } + } +} diff --git a/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts b/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts new file mode 100644 index 0000000000..a464063f6c --- /dev/null +++ b/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts @@ -0,0 +1,69 @@ +import { Commitment, PublicKey } from '@solana/web3.js'; +import { UserStatsAccount } from '../types'; +import { BasicUserStatsAccountSubscriber } from './basicUserStatsAccountSubscriber'; +import { Program } from '@coral-xyz/anchor'; +import { UserStatsAccountSubscriber } from './types'; + +/** + * Simple implementation of UserStatsAccountSubscriber. It will fetch the UserStatsAccount + * data on subscribe (or call to fetch) if no account data is provided on init. + * Expect to use only 1 RPC call unless you call fetch repeatedly. + */ +export class OneShotUserStatsAccountSubscriber + extends BasicUserStatsAccountSubscriber + implements UserStatsAccountSubscriber +{ + program: Program; + commitment: Commitment; + + public constructor( + program: Program, + userStatsAccountPublicKey: PublicKey, + data?: UserStatsAccount, + slot?: number, + commitment?: Commitment + ) { + super(userStatsAccountPublicKey, data, slot); + this.program = program; + this.commitment = commitment ?? 'confirmed'; + } + + async subscribe(userStatsAccount?: UserStatsAccount): Promise { + if (userStatsAccount) { + this.userStats = { data: userStatsAccount, slot: this.userStats.slot }; + return true; + } + + await this.fetchIfUnloaded(); + if (this.doesAccountExist()) { + this.eventEmitter.emit('update'); + } + return true; + } + + async fetchIfUnloaded(): Promise { + if (this.userStats.data === undefined) { + await this.fetch(); + } + } + + async fetch(): Promise { + try { + const dataAndContext = + await this.program.account.userStats.fetchAndContext( + this.userStatsAccountPublicKey, + this.commitment + ); + if (dataAndContext.context.slot > (this.userStats?.slot ?? 0)) { + this.userStats = { + data: dataAndContext.data as UserStatsAccount, + slot: dataAndContext.context.slot, + }; + } + } catch (e) { + console.error( + `OneShotUserStatsAccountSubscriber.fetch() UserStatsAccount does not exist: ${e.message}` + ); + } + } +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 4d39ae6d0f..fb7f4c7957 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -924,7 +924,7 @@ export class DriftClient { this.userStats = new UserStats({ driftClient: this, userStatsAccountPublicKey: this.userStatsAccountPublicKey, - accountSubscription: this.userAccountSubscriptionConfig, + accountSubscription: this.userStatsAccountSubscriptionConfig, }); this.userStats.subscribe(); diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 7f30e2afa0..e8a91da782 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -29,6 +29,7 @@ export * from './accounts/pollingInsuranceFundStakeAccountSubscriber'; export * from './accounts/pollingHighLeverageModeConfigAccountSubscriber'; export * from './accounts/basicUserAccountSubscriber'; export * from './accounts/oneShotUserAccountSubscriber'; +export * from './accounts/oneShotUserStatsAccountSubscriber'; export * from './accounts/types'; export * from './addresses/pda'; export * from './adminClient'; diff --git a/sdk/src/userStats.ts b/sdk/src/userStats.ts index 8fdb30d2bf..a045f533c0 100644 --- a/sdk/src/userStats.ts +++ b/sdk/src/userStats.ts @@ -54,9 +54,14 @@ export class UserStats { }, config.accountSubscription.commitment ); + } else if (config.accountSubscription?.type === 'custom') { + this.accountSubscriber = + config.accountSubscription.userStatsAccountSubscriber; } else { + const exhaustiveCheck: never = config.accountSubscription; + throw new Error( - `Unknown user stats account subscription type: ${config.accountSubscription?.type}` + `Unknown user stats account subscription type: ${exhaustiveCheck}` ); } } diff --git a/sdk/src/userStatsConfig.ts b/sdk/src/userStatsConfig.ts index 693de80e85..2a8f1ce813 100644 --- a/sdk/src/userStatsConfig.ts +++ b/sdk/src/userStatsConfig.ts @@ -1,7 +1,7 @@ import { DriftClient } from './driftClient'; import { Commitment, PublicKey } from '@solana/web3.js'; import { BulkAccountLoader } from './accounts/bulkAccountLoader'; -import { GrpcConfigs } from './accounts/types'; +import { GrpcConfigs, UserStatsAccountSubscriber } from './accounts/types'; export type UserStatsConfig = { accountSubscription?: UserStatsSubscriptionConfig; @@ -22,6 +22,7 @@ export type UserStatsSubscriptionConfig = } | { type: 'custom'; + userStatsAccountSubscriber: UserStatsAccountSubscriber; } | { type: 'grpc'; diff --git a/sdk/src/wallet.ts b/sdk/src/wallet.ts index c7ad92f4a2..05c5759a17 100644 --- a/sdk/src/wallet.ts +++ b/sdk/src/wallet.ts @@ -5,6 +5,7 @@ import { VersionedTransaction, } from '@solana/web3.js'; import { IWallet, IVersionedWallet } from './types'; +import nacl from 'tweetnacl'; export class Wallet implements IWallet, IVersionedWallet { constructor(readonly payer: Keypair) {} @@ -41,3 +42,13 @@ export class Wallet implements IWallet, IVersionedWallet { return this.payer.publicKey; } } + +export class WalletV2 extends Wallet { + constructor(readonly payer: Keypair) { + super(payer); + } + + async signMessage(message: Uint8Array): Promise { + return Buffer.from(nacl.sign.detached(message, this.payer.secretKey)); + } +}