798 lines
24 KiB
TypeScript
798 lines
24 KiB
TypeScript
import * as anchor from '@project-serum/anchor';
|
|
import * as errors from './errors';
|
|
import {
|
|
AccountInfo,
|
|
Cluster,
|
|
ConfirmOptions,
|
|
Connection,
|
|
Keypair,
|
|
PublicKey,
|
|
SendOptions,
|
|
Transaction,
|
|
TransactionSignature,
|
|
} from '@solana/web3.js';
|
|
import { NativeMint } from './mint';
|
|
import { SwitchboardEvents } from './SwitchboardEvents';
|
|
import { TransactionObject, TransactionOptions } from './TransactionObject';
|
|
import { fromCode as fromSwitchboardCode } from './generated/errors/custom';
|
|
import { fromCode as fromAnchorCode } from './generated/errors/anchor';
|
|
import { ACCOUNT_DISCRIMINATOR_SIZE } from '@project-serum/anchor';
|
|
import {
|
|
AggregatorAccountData,
|
|
BufferRelayerAccountData,
|
|
CrankAccountData,
|
|
JobAccountData,
|
|
LeaseAccountData,
|
|
OracleAccountData,
|
|
OracleQueueAccountData,
|
|
PermissionAccountData,
|
|
SbState,
|
|
SlidingResultAccountData,
|
|
VrfAccountData,
|
|
} from './generated';
|
|
import {
|
|
BUFFER_DISCRIMINATOR,
|
|
CrankAccount,
|
|
DISCRIMINATOR_MAP,
|
|
JobAccount,
|
|
ProgramStateAccount,
|
|
QueueAccount,
|
|
SwitchboardAccountData,
|
|
SwitchboardAccountType,
|
|
} from './accounts';
|
|
import {
|
|
SWITCHBOARD_LABS_DEVNET_PERMISSIONED_CRANK,
|
|
SWITCHBOARD_LABS_DEVNET_PERMISSIONED_QUEUE,
|
|
SWITCHBOARD_LABS_MAINNET_PERMISSIONED_CRANK,
|
|
SWITCHBOARD_LABS_MAINNET_PERMISSIONED_QUEUE,
|
|
SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_CRANK,
|
|
SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_QUEUE,
|
|
SWITCHBOARD_LABS_MAINNET_PERMISSIONLESS_CRANK,
|
|
SWITCHBOARD_LABS_MAINNET_PERMISSIONLESS_QUEUE,
|
|
} from './const';
|
|
import { OracleJob, sleep } from '@switchboard-xyz/common';
|
|
import { LoadedJobDefinition } from './types';
|
|
|
|
export type SendTransactionOptions = (ConfirmOptions | SendOptions) & {
|
|
skipConfrimation?: boolean;
|
|
};
|
|
export const DEFAULT_SEND_TRANSACTION_OPTIONS: SendTransactionOptions = {
|
|
skipPreflight: false,
|
|
maxRetries: 10,
|
|
skipConfrimation: false,
|
|
};
|
|
|
|
/**
|
|
* Switchboard Devnet Program ID
|
|
*/
|
|
export const SBV2_DEVNET_PID = new PublicKey(
|
|
'2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG'
|
|
);
|
|
|
|
/**
|
|
* Switchboard Mainnet Program ID
|
|
*/
|
|
export const SBV2_MAINNET_PID = new PublicKey(
|
|
'SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f'
|
|
);
|
|
|
|
/**
|
|
* A generated keypair that is assigned as the _payerKeypair_ when in read-only mode.
|
|
*/
|
|
export const READ_ONLY_KEYPAIR = Keypair.generate();
|
|
/**
|
|
* Returns the Switchboard Program ID for the specified Cluster.
|
|
*/
|
|
export const getSwitchboardProgramId = (
|
|
cluster: Cluster | 'localnet'
|
|
): PublicKey => {
|
|
switch (cluster) {
|
|
case 'devnet':
|
|
return SBV2_DEVNET_PID;
|
|
case 'mainnet-beta':
|
|
return SBV2_MAINNET_PID;
|
|
case 'testnet':
|
|
default:
|
|
throw new Error(`Switchboard PID not found for cluster (${cluster})`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns true if being run inside a web browser, false if in a Node process or electron app.
|
|
*
|
|
* Taken from @project-serum/anchor implementation.
|
|
*/
|
|
const isBrowser =
|
|
process.env.ANCHOR_BROWSER ||
|
|
(typeof window !== 'undefined' && !window.process?.hasOwnProperty('type')); // eslint-disable-line no-prototype-builtins
|
|
|
|
/**
|
|
* Wrapper class for the Switchboard anchor Program.
|
|
*
|
|
* Basic usage example:
|
|
*
|
|
* ```ts
|
|
* import { Connection } from "@solana/web3.js";
|
|
* import { SwitchboardProgram, TransactionObject } from '@switchboard-xyz/solana.js';
|
|
*
|
|
* const program = await SwitchboardProgram.load(
|
|
* "mainnet-beta",
|
|
* new Connection("https://api.mainnet-beta.solana.com"),
|
|
* payerKeypair
|
|
* );
|
|
*
|
|
* const txn = new TransactionObject(program.walletPubkey, [], []);
|
|
* const txnSignature = await program.signAndSend(txn);
|
|
* ```
|
|
*/
|
|
export class SwitchboardProgram {
|
|
private static readonly _readOnlyKeypair = READ_ONLY_KEYPAIR;
|
|
|
|
private readonly _program: anchor.Program;
|
|
|
|
/** The solana cluster to load the Switchboard program for. */
|
|
readonly cluster: Cluster | 'localnet';
|
|
|
|
readonly programState: {
|
|
publicKey: PublicKey;
|
|
bump: number;
|
|
};
|
|
|
|
readonly mint: NativeMint;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
constructor(
|
|
program: anchor.Program,
|
|
cluster: Cluster | 'localnet',
|
|
mint: NativeMint
|
|
) {
|
|
this._program = program;
|
|
this.cluster = cluster;
|
|
|
|
const stateAccount = ProgramStateAccount.fromSeed(this);
|
|
this.programState = {
|
|
publicKey: stateAccount[0].publicKey,
|
|
bump: stateAccount[1],
|
|
};
|
|
this.mint = mint;
|
|
}
|
|
|
|
static async loadAnchorProgram(
|
|
cluster: Cluster | 'localnet',
|
|
connection: Connection,
|
|
payerKeypair: Keypair = READ_ONLY_KEYPAIR,
|
|
programId: PublicKey = getSwitchboardProgramId(cluster)
|
|
): Promise<anchor.Program> {
|
|
const provider = new anchor.AnchorProvider(
|
|
connection,
|
|
// If no keypair is provided, default to dummy keypair
|
|
new AnchorWallet(payerKeypair ?? SwitchboardProgram._readOnlyKeypair),
|
|
{ commitment: 'confirmed' }
|
|
);
|
|
const anchorIdl = await anchor.Program.fetchIdl(programId, provider);
|
|
if (!anchorIdl) {
|
|
throw new Error(`Failed to find IDL for ${programId.toBase58()}`);
|
|
}
|
|
const program = new anchor.Program(anchorIdl, programId, provider);
|
|
|
|
return program;
|
|
}
|
|
|
|
/**
|
|
* Create and initialize a {@linkcode SwitchboardProgram} connection object.
|
|
*
|
|
* @param cluster - the solana cluster to load the Switchboard program for.
|
|
*
|
|
* @param connection - the Solana connection object used to connect to an RPC node.
|
|
*
|
|
* @param payerKeypair - optional, payer keypair used to pay for on-chain transactions.
|
|
*
|
|
* @param programId - optional, override the cluster's default programId.
|
|
*
|
|
* @return the {@linkcode SwitchboardProgram} used to create and interact with Switchboard accounts.
|
|
*
|
|
* Basic usage example:
|
|
*
|
|
* ```ts
|
|
* import { Connection } from "@solana/web3.js";
|
|
* import { SwitchboardProgram, TransactionObject } from '@switchboard-xyz/solana.js';
|
|
*
|
|
* const program = await SwitchboardProgram.load(
|
|
* "mainnet-beta",
|
|
* new Connection("https://api.mainnet-beta.solana.com"),
|
|
* payerKeypair
|
|
* );
|
|
*
|
|
* const txn = new TransactionObject(program.walletPubkey, [], []);
|
|
* const txnSignature = await program.signAndSend(txn);
|
|
* ```
|
|
*/
|
|
static load = async (
|
|
cluster: Cluster | 'localnet',
|
|
connection: Connection,
|
|
payerKeypair: Keypair = READ_ONLY_KEYPAIR,
|
|
programId: PublicKey = getSwitchboardProgramId(cluster)
|
|
): Promise<SwitchboardProgram> => {
|
|
const program = await SwitchboardProgram.loadAnchorProgram(
|
|
cluster,
|
|
connection,
|
|
payerKeypair,
|
|
programId
|
|
);
|
|
const mint = await NativeMint.load(
|
|
program.provider as anchor.AnchorProvider
|
|
);
|
|
return new SwitchboardProgram(program, cluster, mint);
|
|
};
|
|
|
|
/**
|
|
* Create and initialize a {@linkcode SwitchboardProgram} connection object.
|
|
*
|
|
* @param provider - the anchor provider containing the rpc and wallet connection.
|
|
*
|
|
* @return the {@linkcode SwitchboardProgram} used to create and interact with Switchboard accounts.
|
|
*
|
|
* Basic usage example:
|
|
*
|
|
* ```ts
|
|
* import * as anchor from "@project-serum/anchor";
|
|
* import { Connection } from "@solana/web3.js";
|
|
* import { AnchorWallet, SwitchboardProgram, TransactionObject } from '@switchboard-xyz/solana.js';
|
|
*
|
|
* const connection = new Connection("https://api.mainnet-beta.solana.com");
|
|
* const provider = new anchor.AnchorProvider(
|
|
connection,
|
|
new AnchorWallet(payerKeypair ?? SwitchboardProgram._readOnlyKeypair),
|
|
{ commitment: 'confirmed' }
|
|
);
|
|
* const program = await SwitchboardProgram.fromProvider(provider);
|
|
*
|
|
* const txn = new TransactionObject(program.walletPubkey, [], []);
|
|
* const txnSignature = await program.signAndSend(txn);
|
|
* ```
|
|
*/
|
|
static fromProvider = async (
|
|
provider: anchor.AnchorProvider
|
|
): Promise<SwitchboardProgram> => {
|
|
const payer = (provider.wallet as AnchorWallet).payer;
|
|
|
|
// try mainnet program ID
|
|
const mainnetAccountInfo = await provider.connection.getAccountInfo(
|
|
SBV2_MAINNET_PID
|
|
);
|
|
if (mainnetAccountInfo && mainnetAccountInfo.executable) {
|
|
return await SwitchboardProgram.load(
|
|
'mainnet-beta',
|
|
provider.connection,
|
|
payer,
|
|
SBV2_MAINNET_PID
|
|
);
|
|
}
|
|
|
|
// try devnet program ID
|
|
const devnetAccountInfo = await provider.connection.getAccountInfo(
|
|
SBV2_DEVNET_PID
|
|
);
|
|
if (devnetAccountInfo && devnetAccountInfo.executable) {
|
|
return await SwitchboardProgram.load(
|
|
'devnet',
|
|
provider.connection,
|
|
payer,
|
|
SBV2_DEVNET_PID
|
|
);
|
|
}
|
|
|
|
throw new Error(
|
|
`Failed to find the Switchboard program using the mainnet or devnet program ID`
|
|
);
|
|
};
|
|
|
|
/**
|
|
* The Switchboard Program ID for the currently connected cluster.
|
|
*/
|
|
public get programId(): PublicKey {
|
|
return this._program.programId;
|
|
}
|
|
/**
|
|
* The Switchboard Program ID for the currently connected cluster.
|
|
*/
|
|
public get idl(): anchor.Idl {
|
|
return this._program.idl;
|
|
}
|
|
/**
|
|
* The Switchboard Program ID for the currently connected cluster.
|
|
*/
|
|
public get coder(): anchor.BorshAccountsCoder {
|
|
return new anchor.BorshAccountsCoder(this._program.idl);
|
|
}
|
|
/**
|
|
* The anchor Provider used by this program to connect with Solana cluster.
|
|
*/
|
|
public get provider(): anchor.AnchorProvider {
|
|
return this._program.provider as anchor.AnchorProvider;
|
|
}
|
|
/**
|
|
* The Connection used by this program to connect with Solana cluster.
|
|
*/
|
|
public get connection(): Connection {
|
|
return this._program.provider.connection;
|
|
}
|
|
/**
|
|
* The Connection used by this program to connect with Solana cluster.
|
|
*/
|
|
public get wallet(): AnchorWallet {
|
|
return this.provider.wallet as AnchorWallet;
|
|
}
|
|
|
|
public get walletPubkey(): PublicKey {
|
|
return this.wallet.payer.publicKey;
|
|
}
|
|
/**
|
|
* Some actions exposed by this SDK require that a payer Keypair has been
|
|
* provided to {@linkcode SwitchboardProgram} in order to send transactions.
|
|
*/
|
|
public get isReadOnly(): boolean {
|
|
return (
|
|
this.provider.publicKey.toBase58() ===
|
|
SwitchboardProgram._readOnlyKeypair.publicKey.toBase58()
|
|
);
|
|
}
|
|
|
|
/** Verify a payer keypair was supplied. */
|
|
public verifyPayer(): void {
|
|
if (this.isReadOnly) {
|
|
throw new errors.SwitchboardProgramReadOnlyError();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify a fresh keypair was provided.
|
|
*
|
|
* **NOTE:** Creating new accounts without this check will prevent the ability to remove any existing funds. */
|
|
public async verifyNewKeypair(keypair: Keypair): Promise<void> {
|
|
const accountInfo = await this.connection.getAccountInfo(keypair.publicKey);
|
|
if (accountInfo) {
|
|
throw new errors.ExistingKeypair();
|
|
}
|
|
}
|
|
|
|
public get account(): anchor.AccountNamespace {
|
|
return this._program.account;
|
|
}
|
|
|
|
/**
|
|
* Load the Switchboard Labs permissionless Queue for either devnet or mainnet. The permissionless queue has the following permissions:
|
|
* - unpermissionedFeedsEnabled: True
|
|
* - unpermissionedVrfEnabled: True
|
|
* - enableBufferRelayers: False
|
|
*
|
|
* **Note:** {@linkcode AggregatorAccount}s and {@linkcode VrfAccount}s do not require permissions to join this queue. {@linkcode BufferRelayerAccount}s are disabled.
|
|
*/
|
|
async loadPermissionless(): Promise<{
|
|
queueAccount: QueueAccount;
|
|
queue: OracleQueueAccountData;
|
|
crankAccount: CrankAccount;
|
|
crank: CrankAccountData;
|
|
}> {
|
|
const queueKey =
|
|
this.cluster === 'mainnet-beta'
|
|
? SWITCHBOARD_LABS_MAINNET_PERMISSIONLESS_QUEUE
|
|
: this.cluster === 'devnet'
|
|
? SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_QUEUE
|
|
: null;
|
|
if (!queueKey) {
|
|
throw new Error(
|
|
`Failed to load the permissionless queue for cluster ${this.cluster}`
|
|
);
|
|
}
|
|
const [queueAccount, queue] = await QueueAccount.load(this, queueKey);
|
|
|
|
const crankKey =
|
|
this.cluster === 'mainnet-beta'
|
|
? SWITCHBOARD_LABS_MAINNET_PERMISSIONLESS_CRANK
|
|
: this.cluster === 'devnet'
|
|
? SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_CRANK
|
|
: null;
|
|
if (!crankKey) {
|
|
throw new Error(
|
|
`Failed to load the permissionless queue for cluster ${this.cluster}`
|
|
);
|
|
}
|
|
const [crankAccount, crank] = await CrankAccount.load(this, crankKey);
|
|
|
|
return { queueAccount, queue, crankAccount, crank };
|
|
}
|
|
|
|
/**
|
|
* Load the Switchboard Labs permissionled Queue for either devnet or mainnet. The permissioned queue has the following permissions:
|
|
* - unpermissionedFeedsEnabled: False
|
|
* - unpermissionedVrfEnabled: False
|
|
* - enableBufferRelayers: False
|
|
*
|
|
* **Note:** The queue authority must grant {@linkcode AggregatorAccount}s PERMIT_ORACLE_QUEUE_USAGE and {@linkcode VrfAccount}s PERMIT_VRF_REQUESTS permissions before joining the queue and requesting oracle updates. {@linkcode BufferRelayerAccount}s are disabled.
|
|
*/
|
|
async loadPermissioned(): Promise<{
|
|
queueAccount: QueueAccount;
|
|
queue: OracleQueueAccountData;
|
|
crankAccount: CrankAccount;
|
|
crank: CrankAccountData;
|
|
}> {
|
|
const queueKey =
|
|
this.cluster === 'mainnet-beta'
|
|
? SWITCHBOARD_LABS_MAINNET_PERMISSIONED_QUEUE
|
|
: this.cluster === 'devnet'
|
|
? SWITCHBOARD_LABS_DEVNET_PERMISSIONED_QUEUE
|
|
: null;
|
|
if (!queueKey) {
|
|
throw new Error(
|
|
`Failed to load the permissioned queue for cluster ${this.cluster}`
|
|
);
|
|
}
|
|
const [queueAccount, queue] = await QueueAccount.load(
|
|
this,
|
|
this.cluster === 'mainnet-beta'
|
|
? SWITCHBOARD_LABS_MAINNET_PERMISSIONED_QUEUE
|
|
: SWITCHBOARD_LABS_DEVNET_PERMISSIONED_QUEUE
|
|
);
|
|
|
|
const crankKey =
|
|
this.cluster === 'mainnet-beta'
|
|
? SWITCHBOARD_LABS_MAINNET_PERMISSIONED_CRANK
|
|
: this.cluster === 'devnet'
|
|
? SWITCHBOARD_LABS_DEVNET_PERMISSIONED_CRANK
|
|
: null;
|
|
if (!crankKey) {
|
|
throw new Error(
|
|
`Failed to load the permissionless queue for cluster ${this.cluster}`
|
|
);
|
|
}
|
|
const [crankAccount, crank] = await CrankAccount.load(this, crankKey);
|
|
|
|
return { queueAccount, queue, crankAccount, crank };
|
|
}
|
|
|
|
public addEventListener<EventName extends keyof SwitchboardEvents>(
|
|
eventName: EventName,
|
|
callback: (
|
|
data: SwitchboardEvents[EventName],
|
|
slot: number,
|
|
signature: string
|
|
) => void | Promise<void>
|
|
): number {
|
|
return this._program.addEventListener(eventName as string, callback);
|
|
}
|
|
|
|
public async removeEventListener(listenerId: number) {
|
|
return await this._program.removeEventListener(listenerId);
|
|
}
|
|
|
|
public async signAndSendAll(
|
|
txns: Array<TransactionObject>,
|
|
opts: SendTransactionOptions = DEFAULT_SEND_TRANSACTION_OPTIONS,
|
|
txnOptions?: TransactionOptions,
|
|
delay = 0
|
|
): Promise<Array<TransactionSignature>> {
|
|
if (isBrowser) throw new errors.SwitchboardProgramIsBrowserError();
|
|
if (this.isReadOnly) throw new errors.SwitchboardProgramReadOnlyError();
|
|
|
|
const packed = TransactionObject.pack(txns);
|
|
|
|
const txnSignatures: Array<TransactionSignature> = [];
|
|
for await (const [i, txn] of packed.entries()) {
|
|
txnSignatures.push(await this.signAndSend(txn, opts, txnOptions));
|
|
if (
|
|
i !== packed.length - 1 &&
|
|
delay &&
|
|
typeof delay === 'number' &&
|
|
delay > 0
|
|
) {
|
|
await sleep(delay);
|
|
}
|
|
}
|
|
|
|
return txnSignatures;
|
|
}
|
|
|
|
public async signAndSend(
|
|
txn: TransactionObject,
|
|
opts: SendTransactionOptions = DEFAULT_SEND_TRANSACTION_OPTIONS,
|
|
txnOptions?: TransactionOptions
|
|
): Promise<TransactionSignature> {
|
|
if (isBrowser) throw new errors.SwitchboardProgramIsBrowserError();
|
|
if (this.isReadOnly) throw new errors.SwitchboardProgramReadOnlyError();
|
|
|
|
// filter extra signers
|
|
const signers = [this.wallet.payer, ...txn.signers];
|
|
const reqSigners = txn.ixns.reduce((signers, ixn) => {
|
|
ixn.keys.map(a => {
|
|
if (a.isSigner) {
|
|
signers.add(a.pubkey.toBase58());
|
|
}
|
|
});
|
|
return signers;
|
|
}, new Set<string>());
|
|
const filteredSigners = signers.filter(
|
|
s =>
|
|
s.publicKey.equals(txn.payer) || reqSigners.has(s.publicKey.toBase58())
|
|
);
|
|
|
|
const transaction = txn.toTxn(
|
|
txnOptions ?? (await this.connection.getLatestBlockhash())
|
|
);
|
|
|
|
try {
|
|
// skip confirmation
|
|
if (
|
|
opts &&
|
|
typeof opts.skipConfrimation === 'boolean' &&
|
|
opts.skipConfrimation
|
|
) {
|
|
const txnSignature = await this.connection.sendTransaction(
|
|
transaction,
|
|
filteredSigners,
|
|
opts
|
|
);
|
|
return txnSignature;
|
|
}
|
|
|
|
const txnSignature = await this.provider.sendAndConfirm(
|
|
transaction,
|
|
filteredSigners,
|
|
{
|
|
...DEFAULT_SEND_TRANSACTION_OPTIONS,
|
|
...opts,
|
|
}
|
|
);
|
|
|
|
return txnSignature;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
if ('code' in error && typeof error.code === 'number') {
|
|
// Check for other switchboard error.
|
|
const switchboardError = fromSwitchboardCode(error.code);
|
|
if (switchboardError) throw switchboardError;
|
|
// Check for other anchor error.
|
|
const anchorError = fromAnchorCode(error.code);
|
|
if (anchorError) throw anchorError;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getProgramJobAccounts(): Promise<Map<Uint8Array, LoadedJobDefinition>> {
|
|
const accountInfos = await this.connection
|
|
.getProgramAccounts(this.programId, {
|
|
filters: [
|
|
{
|
|
memcmp: {
|
|
offset: 0,
|
|
bytes: anchor.utils.bytes.bs58.encode(
|
|
JobAccountData.discriminator
|
|
),
|
|
},
|
|
},
|
|
],
|
|
})
|
|
.then((values: Array<AccountInfoResponse | undefined>) => {
|
|
return values.filter(Boolean) as Array<AccountInfoResponse>;
|
|
});
|
|
|
|
const jobs: Array<LoadedJobDefinition> = accountInfos
|
|
.map((job): LoadedJobDefinition | undefined => {
|
|
const jobAccount = new JobAccount(this, job.pubkey);
|
|
const state = JobAccountData.decode(job.account.data);
|
|
let oracleJob: OracleJob;
|
|
try {
|
|
oracleJob = OracleJob.decodeDelimited(state.data);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
account: jobAccount,
|
|
state: state,
|
|
job: oracleJob,
|
|
};
|
|
})
|
|
.filter(Boolean) as Array<LoadedJobDefinition>;
|
|
|
|
return new Map(jobs.map(job => [job.state.data, job]));
|
|
}
|
|
|
|
async getProgramAccounts(): Promise<{
|
|
aggregators: Map<string, AggregatorAccountData>;
|
|
buffers: Map<string, Buffer>;
|
|
bufferRelayers: Map<string, BufferRelayerAccountData>;
|
|
cranks: Map<string, CrankAccountData>;
|
|
jobs: Map<string, JobAccountData>;
|
|
leases: Map<string, LeaseAccountData>;
|
|
oracles: Map<string, OracleAccountData>;
|
|
permissions: Map<string, PermissionAccountData>;
|
|
programState: Map<string, SbState>;
|
|
queues: Map<string, OracleQueueAccountData>;
|
|
slidingResult: Map<string, SlidingResultAccountData>;
|
|
vrfs: Map<string, VrfAccountData>;
|
|
}> {
|
|
const accountInfos: Array<AccountInfoResponse> =
|
|
await this.connection.getProgramAccounts(this.programId);
|
|
|
|
// buffer - [42, 55, 46, 46, 45, 52, 78, 78]
|
|
// bufferRelayer - [50, 35, 51, 115, 169, 219, 158, 52]
|
|
// lease - [55, 254, 208, 251, 164, 44, 150, 50]
|
|
// permissions - [77, 37, 177, 164, 38, 39, 34, 109]
|
|
// slidingResult - [91, 4, 83, 187, 102, 216, 153, 254]
|
|
// vrf - [101, 35, 62, 239, 103, 151, 6, 18]
|
|
// crank - [111, 81, 146, 73, 172, 180, 134, 209]
|
|
// job - [124, 69, 101, 195, 229, 218, 144, 63]
|
|
// oracles - [128, 30, 16, 241, 170, 73, 55, 54]
|
|
// sbState - [159, 42, 192, 191, 139, 62, 168, 28]
|
|
// queue - [164, 207, 200, 51, 199, 113, 35, 109]
|
|
// aggregator - [217, 230, 65, 101, 201, 162, 27, 125]
|
|
|
|
const discriminatorMap: Map<
|
|
string,
|
|
Array<AccountInfoResponse>
|
|
> = accountInfos.reduce((map, account) => {
|
|
const discriminator = account.account.data
|
|
.slice(0, ACCOUNT_DISCRIMINATOR_SIZE)
|
|
.toString('utf-8');
|
|
|
|
const accounts = map.get(discriminator) ?? [];
|
|
accounts.push(account);
|
|
map.set(discriminator, accounts);
|
|
|
|
return map;
|
|
}, new Map<string, Array<AccountInfoResponse>>());
|
|
|
|
function decodeAccounts<T extends SwitchboardAccountData>(
|
|
accounts: Array<AccountInfoResponse>,
|
|
decode: (data: Buffer) => T
|
|
): Map<string, T> {
|
|
return accounts.reduce((map, account) => {
|
|
try {
|
|
const decoded = decode(account.account.data);
|
|
map.set(account.pubkey.toBase58(), decoded);
|
|
// eslint-disable-next-line no-empty
|
|
} catch {}
|
|
|
|
return map;
|
|
}, new Map<string, T>());
|
|
}
|
|
|
|
const aggregators: Map<string, AggregatorAccountData> = decodeAccounts(
|
|
discriminatorMap.get(
|
|
AggregatorAccountData.discriminator.toString('utf-8')
|
|
) ?? [],
|
|
AggregatorAccountData.decode
|
|
);
|
|
|
|
// TODO: Use aggregator.historyBuffer, crank.dataBuffer, queue.dataBuffer to filter these down and decode
|
|
const buffers: Map<string, Buffer> = (
|
|
discriminatorMap.get(BUFFER_DISCRIMINATOR.toString('utf-8')) ?? []
|
|
).reduce((map, buffer) => {
|
|
map.set(buffer.pubkey.toBase58(), buffer.account.data);
|
|
return map;
|
|
}, new Map<string, Buffer>());
|
|
|
|
const bufferRelayers: Map<string, BufferRelayerAccountData> =
|
|
decodeAccounts(
|
|
discriminatorMap.get(
|
|
BufferRelayerAccountData.discriminator.toString('utf-8')
|
|
) ?? [],
|
|
BufferRelayerAccountData.decode
|
|
);
|
|
|
|
const cranks: Map<string, CrankAccountData> = decodeAccounts(
|
|
discriminatorMap.get(CrankAccountData.discriminator.toString('utf-8')) ??
|
|
[],
|
|
CrankAccountData.decode
|
|
);
|
|
|
|
const jobs: Map<string, JobAccountData> = decodeAccounts(
|
|
discriminatorMap.get(JobAccountData.discriminator.toString('utf-8')) ??
|
|
[],
|
|
JobAccountData.decode
|
|
);
|
|
|
|
const leases: Map<string, LeaseAccountData> = decodeAccounts(
|
|
discriminatorMap.get(LeaseAccountData.discriminator.toString('utf-8')) ??
|
|
[],
|
|
LeaseAccountData.decode
|
|
);
|
|
|
|
const oracles: Map<string, OracleAccountData> = decodeAccounts(
|
|
discriminatorMap.get(OracleAccountData.discriminator.toString('utf-8')) ??
|
|
[],
|
|
OracleAccountData.decode
|
|
);
|
|
|
|
const permissions: Map<string, PermissionAccountData> = decodeAccounts(
|
|
discriminatorMap.get(
|
|
PermissionAccountData.discriminator.toString('utf-8')
|
|
) ?? [],
|
|
PermissionAccountData.decode
|
|
);
|
|
|
|
const programState: Map<string, SbState> = decodeAccounts(
|
|
discriminatorMap.get(SbState.discriminator.toString('utf-8')) ?? [],
|
|
SbState.decode
|
|
);
|
|
|
|
const queues: Map<string, OracleQueueAccountData> = decodeAccounts(
|
|
discriminatorMap.get(
|
|
OracleQueueAccountData.discriminator.toString('utf-8')
|
|
) ?? [],
|
|
OracleQueueAccountData.decode
|
|
);
|
|
|
|
const slidingResult: Map<string, SlidingResultAccountData> = decodeAccounts(
|
|
discriminatorMap.get(
|
|
SlidingResultAccountData.discriminator.toString('utf-8')
|
|
) ?? [],
|
|
SlidingResultAccountData.decode
|
|
);
|
|
|
|
const vrfs: Map<string, VrfAccountData> = decodeAccounts(
|
|
discriminatorMap.get(VrfAccountData.discriminator.toString('utf-8')) ??
|
|
[],
|
|
VrfAccountData.decode
|
|
);
|
|
|
|
return {
|
|
aggregators,
|
|
buffers,
|
|
bufferRelayers,
|
|
cranks,
|
|
jobs,
|
|
leases,
|
|
oracles,
|
|
permissions,
|
|
programState,
|
|
slidingResult,
|
|
queues,
|
|
vrfs,
|
|
};
|
|
}
|
|
|
|
static getAccountType(
|
|
accountInfo: AccountInfo<Buffer>
|
|
): SwitchboardAccountType | null {
|
|
const discriminator = accountInfo.data
|
|
.slice(0, ACCOUNT_DISCRIMINATOR_SIZE)
|
|
.toString('utf-8');
|
|
const accountType = DISCRIMINATOR_MAP.get(discriminator);
|
|
if (accountType) {
|
|
return accountType;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export class AnchorWallet implements anchor.Wallet {
|
|
constructor(readonly payer: Keypair) {
|
|
this.payer = payer;
|
|
}
|
|
get publicKey(): PublicKey {
|
|
return this.payer.publicKey;
|
|
}
|
|
private sign = (txn: Transaction): Transaction => {
|
|
txn.partialSign(this.payer);
|
|
return txn;
|
|
};
|
|
async signTransaction(txn: Transaction) {
|
|
return this.sign(txn);
|
|
}
|
|
async signAllTransactions(txns: Transaction[]) {
|
|
return txns.map(this.sign);
|
|
}
|
|
}
|
|
|
|
interface AccountInfoResponse {
|
|
pubkey: anchor.web3.PublicKey;
|
|
account: anchor.web3.AccountInfo<Buffer>;
|
|
}
|