986 lines
30 KiB
TypeScript
986 lines
30 KiB
TypeScript
import {
|
|
AttestationProgramStateAccount,
|
|
BUFFER_DISCRIMINATOR,
|
|
CrankAccount,
|
|
DISCRIMINATOR_MAP,
|
|
JobAccount,
|
|
ProgramStateAccount,
|
|
QueueAccount,
|
|
SwitchboardAccountData,
|
|
SwitchboardAccountType,
|
|
} from "./accounts/index.js";
|
|
import {
|
|
AggregatorAccountData,
|
|
BufferRelayerAccountData,
|
|
CrankAccountData,
|
|
JobAccountData,
|
|
LeaseAccountData,
|
|
OracleAccountData,
|
|
OracleQueueAccountData,
|
|
PermissionAccountData,
|
|
SbState,
|
|
SlidingResultAccountData,
|
|
VrfAccountData,
|
|
} from "./generated/index.js";
|
|
import {
|
|
DEVNET_GENESIS_HASH,
|
|
MAINNET_GENESIS_HASH,
|
|
SWITCHBOARD_LABS_DEVNET_PERMISSIONED_CRANK,
|
|
SWITCHBOARD_LABS_DEVNET_PERMISSIONED_QUEUE,
|
|
SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_CRANK,
|
|
SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_QUEUE,
|
|
SWITCHBOARD_LABS_MAINNET_PERMISSIONED_CRANK,
|
|
SWITCHBOARD_LABS_MAINNET_PERMISSIONED_QUEUE,
|
|
SWITCHBOARD_LABS_MAINNET_PERMISSIONLESS_CRANK,
|
|
SWITCHBOARD_LABS_MAINNET_PERMISSIONLESS_QUEUE,
|
|
} from "./const.js";
|
|
import * as errors from "./errors.js";
|
|
import { NativeMint } from "./mint.js";
|
|
import { SwitchboardEvents } from "./SwitchboardEvents.js";
|
|
import { TransactionObject, TransactionOptions } from "./TransactionObject.js";
|
|
import { LoadedJobDefinition } from "./types.js";
|
|
|
|
import {
|
|
ACCOUNT_DISCRIMINATOR_SIZE,
|
|
AccountNamespace,
|
|
AnchorProvider,
|
|
BorshAccountsCoder,
|
|
Idl,
|
|
Program,
|
|
utils as AnchorUtils,
|
|
Wallet,
|
|
} from "@coral-xyz/anchor";
|
|
import {
|
|
AccountInfo,
|
|
Cluster,
|
|
ConfirmOptions,
|
|
Connection,
|
|
Keypair,
|
|
PublicKey,
|
|
SendOptions,
|
|
Transaction,
|
|
TransactionSignature,
|
|
VersionedTransaction,
|
|
} from "@solana/web3.js";
|
|
import { OracleJob } from "@switchboard-xyz/common";
|
|
|
|
export type SendTransactionOptions = (ConfirmOptions | SendOptions) & {
|
|
skipConfrimation?: boolean;
|
|
};
|
|
export const DEFAULT_SEND_TRANSACTION_OPTIONS: SendTransactionOptions = {
|
|
skipPreflight: false,
|
|
maxRetries: 10,
|
|
skipConfrimation: false,
|
|
};
|
|
|
|
/**
|
|
* Switchboard's V2 Program ID
|
|
*/
|
|
export const SB_V2_PID = new PublicKey(
|
|
"SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
|
|
);
|
|
|
|
/**
|
|
* Switchboard's Attestation Program ID
|
|
*/
|
|
export const SB_ATTESTATION_PID = new PublicKey(
|
|
"2No5FVKPAAYqytpkEoq93tVh33fo4p6DgAnm4S6oZHo7"
|
|
);
|
|
|
|
/**
|
|
* 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 "localnet":
|
|
case "devnet":
|
|
case "mainnet-beta":
|
|
return SB_V2_PID;
|
|
case "testnet":
|
|
default:
|
|
throw new Error(`Switchboard PID not found for cluster (${cluster})`);
|
|
}
|
|
};
|
|
/**
|
|
* Returns the Program ID for the Switchboard Attestation Program for the specified Cluster.
|
|
*/
|
|
export const getSwitchboardAttestationProgramId = (
|
|
cluster: Cluster | "localnet"
|
|
): PublicKey => {
|
|
switch (cluster) {
|
|
case "localnet":
|
|
case "devnet":
|
|
case "mainnet-beta":
|
|
return SB_ATTESTATION_PID;
|
|
case "testnet":
|
|
default:
|
|
throw new Error(
|
|
`Switchboard Attestation PID not found for cluster (${cluster})`
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Wrapper class for the Switchboard anchor Program.
|
|
*
|
|
* This class provides an interface to interact with the Switchboard program on the Solana network.
|
|
* It allows you to load the program, create and initialize connection objects, 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);
|
|
* ```
|
|
*/
|
|
export class SwitchboardProgram {
|
|
// The read-only keypair for the Switchboard program.
|
|
private static readonly _readOnlyKeypair = READ_ONLY_KEYPAIR;
|
|
|
|
// The anchor program instance.
|
|
private readonly _program: Program;
|
|
|
|
// The anchor program instance for Switchboard's attestation program.
|
|
private readonly _attestationProgram: Program | undefined;
|
|
|
|
/** The Solana cluster to load the Switchboard program for. */
|
|
readonly cluster: Cluster | "localnet";
|
|
|
|
// The pubkey and bump of the Switchboard program state account.
|
|
readonly programState: {
|
|
publicKey: PublicKey;
|
|
bump: number;
|
|
};
|
|
|
|
// The pubkey and bump of the Switchboard quote verifier program state account.
|
|
readonly attestationProgramState: {
|
|
publicKey: PublicKey;
|
|
bump: number;
|
|
};
|
|
|
|
// The native mint for the Switchboard program.
|
|
readonly mint: NativeMint;
|
|
|
|
/**
|
|
* Constructor for the SwitchboardProgram class.
|
|
*
|
|
* @param program - The anchor program instance.
|
|
* @param cluster - The Solana cluster to load the Switchboard program for.
|
|
* @param mint - The native mint for the Switchboard program.
|
|
*/
|
|
constructor(
|
|
program: Program,
|
|
attestationProgram: Program | undefined,
|
|
cluster: Cluster | "localnet",
|
|
mint: NativeMint
|
|
) {
|
|
this._program = program;
|
|
this._attestationProgram = attestationProgram;
|
|
this.cluster = cluster;
|
|
|
|
// Derive the state account from the seed.
|
|
const stateAccount = ProgramStateAccount.fromSeed(this);
|
|
this.programState = {
|
|
publicKey: stateAccount[0].publicKey,
|
|
bump: stateAccount[1],
|
|
};
|
|
|
|
this.programState = {
|
|
publicKey: stateAccount[0].publicKey,
|
|
bump: stateAccount[1],
|
|
};
|
|
|
|
// TODO: produce the attestation state account from the seed.
|
|
const attestationStateAccount =
|
|
AttestationProgramStateAccount.fromSeed(this);
|
|
this.attestationProgramState = {
|
|
publicKey: attestationStateAccount[0].publicKey,
|
|
bump: attestationStateAccount[1],
|
|
};
|
|
|
|
this.mint = mint;
|
|
}
|
|
|
|
/**
|
|
* Load the anchor program for the Switchboard.
|
|
*
|
|
* This method fetches the IDL for the Switchboard program, and initializes an anchor program
|
|
* instance using the fetched IDL, provided program ID, and provider.
|
|
*
|
|
* @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 program ID to override the cluster's default programId.
|
|
*
|
|
* @returns The initialized anchor program instance for the Switchboard.
|
|
*/
|
|
static async loadAnchorProgram(
|
|
cluster: Cluster | "localnet",
|
|
connection: Connection,
|
|
payerKeypair: Keypair = READ_ONLY_KEYPAIR,
|
|
programId?: PublicKey
|
|
): Promise<Program> {
|
|
const pid = programId ?? getSwitchboardProgramId(cluster);
|
|
const provider = new AnchorProvider(
|
|
connection,
|
|
// If no keypair is provided, default to dummy keypair
|
|
new AnchorWallet(payerKeypair ?? SwitchboardProgram._readOnlyKeypair),
|
|
{ commitment: "confirmed" }
|
|
);
|
|
const anchorIdl = await Program.fetchIdl(pid, provider);
|
|
if (!anchorIdl) {
|
|
throw new Error(`Failed to find IDL for ${pid.toBase58()}`);
|
|
}
|
|
const program = new Program(anchorIdl, pid, 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 = READ_ONLY_KEYPAIR,
|
|
programId = getSwitchboardProgramId(cluster),
|
|
attestationProgramId = getSwitchboardAttestationProgramId(cluster)
|
|
): Promise<SwitchboardProgram> => {
|
|
const [program, attestationProgram] = await Promise.all([
|
|
SwitchboardProgram.loadAnchorProgram(
|
|
cluster,
|
|
connection,
|
|
payerKeypair,
|
|
programId
|
|
),
|
|
SwitchboardProgram.loadAnchorProgram(
|
|
cluster,
|
|
connection,
|
|
payerKeypair,
|
|
attestationProgramId
|
|
).catch((err) => {
|
|
console.error(`Failed to load AttestationProgram`);
|
|
console.error(err);
|
|
return undefined;
|
|
}),
|
|
]);
|
|
const mint = await NativeMint.load(program.provider as AnchorProvider);
|
|
return new SwitchboardProgram(program, attestationProgram, cluster, mint);
|
|
};
|
|
|
|
public verifyAttestation(): void {
|
|
if (this._attestationProgram === undefined) {
|
|
throw new Error(`Attestation Program is missing`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 "@coral-xyz/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 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: AnchorProvider,
|
|
programId?: PublicKey,
|
|
attestationProgramId?: PublicKey
|
|
): Promise<SwitchboardProgram> => {
|
|
const payer = (provider.wallet as AnchorWallet).payer;
|
|
const program = await SwitchboardProgram.fromConnection(
|
|
provider.connection,
|
|
payer,
|
|
programId,
|
|
attestationProgramId
|
|
);
|
|
return program;
|
|
};
|
|
|
|
/**
|
|
* Create and initialize a {@linkcode SwitchboardProgram} connection object.
|
|
*
|
|
* @param connection - The Solana connection object used to connect to an RPC node.
|
|
* @param payer - Optional, payer keypair used to pay for on-chain transactions (defaults to READ_ONLY_KEYPAIR).
|
|
* @param programId - Optional, override the cluster's default programId.
|
|
*
|
|
* @return The {@linkcode SwitchboardProgram} instance used to create and interact with Switchboard accounts.
|
|
*
|
|
* Basic usage example:
|
|
*
|
|
* ```ts
|
|
* import * as anchor from "@coral-xyz/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 program = await SwitchboardProgram.fromConnection(connection);
|
|
* ```
|
|
*/
|
|
static fromConnection = async (
|
|
connection: Connection,
|
|
payer = READ_ONLY_KEYPAIR,
|
|
programId?: PublicKey,
|
|
attestationProgramId?: PublicKey
|
|
): Promise<SwitchboardProgram> => {
|
|
const genesisHash = await connection.getGenesisHash();
|
|
const cluster =
|
|
genesisHash === MAINNET_GENESIS_HASH
|
|
? "mainnet-beta"
|
|
: genesisHash === DEVNET_GENESIS_HASH
|
|
? "devnet"
|
|
: "localnet";
|
|
|
|
const pid = programId ?? SB_V2_PID;
|
|
const programAccountInfo = await connection.getAccountInfo(pid);
|
|
if (programAccountInfo === null) {
|
|
throw new Error(
|
|
`Failed to load Switchboard V2 program at ${pid}, try manually providing a programId`
|
|
);
|
|
}
|
|
|
|
const attestationPid = attestationProgramId ?? SB_ATTESTATION_PID;
|
|
const attestationProgramAccountInfo = await connection.getAccountInfo(
|
|
attestationPid
|
|
);
|
|
if (attestationProgramAccountInfo === null) {
|
|
throw new Error(
|
|
`Failed to load Switchboard Attestation program at ${attestationPid}, try manually providing a programId`
|
|
);
|
|
}
|
|
|
|
const program = await SwitchboardProgram.load(
|
|
cluster,
|
|
connection,
|
|
payer,
|
|
pid,
|
|
attestationPid
|
|
);
|
|
return program;
|
|
};
|
|
|
|
/**
|
|
* Retrieves the Switchboard V2 Program ID for the currently connected cluster.
|
|
* @return The PublicKey of the Switchboard V2 Program ID.
|
|
*/
|
|
public get programId(): PublicKey {
|
|
return this._program.programId;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Switchboard Attestation Program ID for the currently connected cluster.
|
|
* @return The PublicKey of the Switchboard Attestation Program ID.
|
|
*/
|
|
public get attestationProgramId(): PublicKey {
|
|
return this._attestationProgram.programId;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Switchboard V2 Program IDL.
|
|
* @return The IDL of the Switchboard V2 Program.
|
|
*/
|
|
public get idl(): Idl {
|
|
return this._program.idl;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Switchboard Attestation Program IDL.
|
|
* @return The IDL of the Switchboard Attestation Program.
|
|
*/
|
|
public get attestationIdl(): Idl {
|
|
return this._program.idl;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Switchboard V2 Borsh Accounts Coder.
|
|
* @return The BorshAccountsCoder for the Switchboard V2 Program.
|
|
*/
|
|
public get coder(): BorshAccountsCoder {
|
|
return new BorshAccountsCoder(this._program.idl);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Switchboard Attestatio Borsh Accounts Coder.
|
|
* @return The BorshAccountsCoder for the Switchboard Attestation Program.
|
|
*/
|
|
public get attestationCoder(): BorshAccountsCoder {
|
|
return new BorshAccountsCoder(this._attestationProgram.idl);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the anchor Provider used by this program to connect with the Solana cluster.
|
|
* @return The AnchorProvider instance for the Switchboard Program.
|
|
*/
|
|
public get provider(): AnchorProvider {
|
|
return this._program.provider as AnchorProvider;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Connection used by this program to connect with the Solana cluster.
|
|
* @return The Connection instance for the Switchboard Program.
|
|
*/
|
|
public get connection(): Connection {
|
|
return this.provider.connection;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Wallet used by this program.
|
|
* @return The AnchorWallet instance for the Switchboard Program.
|
|
*/
|
|
public get wallet(): AnchorWallet {
|
|
return this.provider.wallet as AnchorWallet;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the wallet's PublicKey.
|
|
* @return The PublicKey of the wallet.
|
|
*/
|
|
public get walletPubkey(): PublicKey {
|
|
return this.wallet.payer.publicKey;
|
|
}
|
|
|
|
/**
|
|
* Checks if the program is read-only.
|
|
* @return A boolean indicating if the SwitchboardProgram instance is read-only.
|
|
*/
|
|
public get isReadOnly(): boolean {
|
|
return (
|
|
this.provider.publicKey.toBase58() ===
|
|
SwitchboardProgram._readOnlyKeypair.publicKey.toBase58()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verifies that a payer keypair has been supplied to the {@linkcode SwitchboardProgram}.
|
|
* Throws an error if the program is read-only.
|
|
*/
|
|
public verifyPayer(): void {
|
|
if (this.isReadOnly) {
|
|
throw new errors.SwitchboardProgramReadOnlyError();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifies that a new keypair has been provided and the corresponding account does not already exist.
|
|
*
|
|
* **NOTE:** Creating new accounts without this check may prevent the ability to withdraw any existing funds.
|
|
*
|
|
* @param keypair - The Keypair to be verified.
|
|
* @throws Will throw an error if the account for the keypair already exists.
|
|
*/
|
|
public async verifyNewKeypair(keypair: Keypair): Promise<void> {
|
|
const accountInfo = await this.connection.getAccountInfo(keypair.publicKey);
|
|
if (accountInfo) {
|
|
throw new errors.ExistingKeypair();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the account namespace for the Switchboard V2 Program.
|
|
* @return The AccountNamespace instance for the Switchboard V2 Program.
|
|
*/
|
|
public get account(): AccountNamespace {
|
|
return this._program.account;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the account namespace for the Switchboard Attestation Program.
|
|
* @return The AccountNamespace instance for the Switchboard Attestation Program.
|
|
*/
|
|
public get attestationAccount(): AccountNamespace {
|
|
return this._attestationProgram.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 };
|
|
}
|
|
|
|
/**
|
|
* Adds an event listener for the specified AnchorEvent, allowing consumers to monitor the chain for events
|
|
* such as AggregatorOpenRound, VrfRequestRandomness, and AggregatorSaveResult.
|
|
*
|
|
* @param eventName - The name of the event to listen for.
|
|
* @param callback - A callback function to handle the event data, slot, and signature.
|
|
* @return A unique listener ID that can be used to remove the event listener.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Removes the event listener with the specified listener ID.
|
|
*
|
|
* @param listenerId - The unique ID of the event listener to be removed.
|
|
*/
|
|
public async removeEventListener(listenerId: number) {
|
|
return await this._program.removeEventListener(listenerId);
|
|
}
|
|
|
|
/**
|
|
* Adds an event listener for the specified AnchorEvent, allowing consumers to monitor the chain for events
|
|
* emitted from Switchboard's attestation program.
|
|
*
|
|
* @param eventName - The name of the event to listen for.
|
|
* @param callback - A callback function to handle the event data, slot, and signature.
|
|
* @return A unique listener ID that can be used to remove the event listener.
|
|
*/
|
|
public addAttestationEventListener<EventName extends keyof SwitchboardEvents>(
|
|
eventName: EventName,
|
|
callback: (
|
|
data: SwitchboardEvents[EventName],
|
|
slot: number,
|
|
signature: string
|
|
) => void | Promise<void>
|
|
): number {
|
|
return this._attestationProgram.addEventListener(
|
|
eventName as string,
|
|
callback
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Removes the event listener with the specified listener ID.
|
|
*
|
|
* @param listenerId - The unique ID of the event listener to be removed.
|
|
*/
|
|
public async removeAttestationEventListener(listenerId: number) {
|
|
return await this._attestationProgram.removeEventListener(listenerId);
|
|
}
|
|
|
|
public async signAndSendAll(
|
|
txns: Array<TransactionObject>,
|
|
opts: SendTransactionOptions = DEFAULT_SEND_TRANSACTION_OPTIONS,
|
|
txnOptions?: TransactionOptions,
|
|
delay = 0
|
|
): Promise<Array<TransactionSignature>> {
|
|
const txnSignatures = await TransactionObject.signAndSendAll(
|
|
this.provider,
|
|
txns,
|
|
opts,
|
|
txnOptions,
|
|
delay
|
|
);
|
|
return txnSignatures;
|
|
}
|
|
|
|
public async signAndSend(
|
|
txn: TransactionObject,
|
|
opts: SendTransactionOptions = DEFAULT_SEND_TRANSACTION_OPTIONS,
|
|
txnOptions?: TransactionOptions
|
|
): Promise<TransactionSignature> {
|
|
const txnSignature = await txn.signAndSend(this.provider, opts, txnOptions);
|
|
return txnSignature;
|
|
}
|
|
|
|
async getProgramJobAccounts(): Promise<Map<Uint8Array, LoadedJobDefinition>> {
|
|
const accountInfos = await this.connection
|
|
.getProgramAccounts(this.programId, {
|
|
filters: [
|
|
{
|
|
memcmp: {
|
|
offset: 0,
|
|
bytes: AnchorUtils.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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a transaction object is a VersionedTransaction or not
|
|
*
|
|
* @param tx
|
|
* @returns bool
|
|
*/
|
|
export const isVersionedTransaction = (tx): tx is VersionedTransaction => {
|
|
return "version" in tx;
|
|
};
|
|
|
|
export class AnchorWallet implements Wallet {
|
|
constructor(readonly payer: Keypair) {}
|
|
|
|
get publicKey(): PublicKey {
|
|
return this.payer.publicKey;
|
|
}
|
|
|
|
async signTransaction<T extends Transaction | VersionedTransaction>(
|
|
tx: T
|
|
): Promise<T> {
|
|
if (isVersionedTransaction(tx)) {
|
|
tx.sign([this.payer]);
|
|
} else {
|
|
tx.partialSign(this.payer);
|
|
}
|
|
|
|
return tx;
|
|
}
|
|
|
|
async signAllTransactions<T extends Transaction | VersionedTransaction>(
|
|
txs: T[]
|
|
): Promise<T[]> {
|
|
return txs.map((t) => {
|
|
if (isVersionedTransaction(t)) {
|
|
t.sign([this.payer]);
|
|
} else {
|
|
t.partialSign(this.payer);
|
|
}
|
|
return t;
|
|
});
|
|
}
|
|
}
|
|
|
|
interface AccountInfoResponse {
|
|
pubkey: PublicKey;
|
|
account: AccountInfo<Buffer>;
|
|
}
|