import { AggregatorAccount, CreateQueueFeedParams, QueueAccount, SB_V2_PID, TransactionObject, } from "../src/index.js"; import * as sbv2 from "../src/index.js"; import { clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, } from "@solana/web3.js"; import { OracleJob } from "@switchboard-xyz/common"; import assert from "assert"; import dotenv from "dotenv"; import fs from "fs"; import os from "os"; import path from "path"; dotenv.config(); type SolanaCluster = "localnet" | "devnet" | "mainnet-beta"; export const sleep = (ms: number): Promise => new Promise((s) => setTimeout(s, ms)); export const DEFAULT_KEYPAIR_PATH = path.join( os.homedir(), ".config/solana/id.json" ); export interface TestContext { cluster: SolanaCluster; program: sbv2.SwitchboardProgram; payer: Keypair; toUrl: (signature: string) => string; round: (num: number, decimalPlaces: number) => number; } export function isLocalnet(): boolean { if (process.env.SOLANA_LOCALNET) { switch (process.env.SOLANA_LOCALNET) { case "1": case "true": case "localnet": { return true; } } } return false; } export function getCluster(): SolanaCluster { if (process.env.SOLANA_CLUSTER) { const cluster = String(process.env.SOLANA_CLUSTER); if ( cluster === "localnet" || cluster === "devnet" || cluster === "mainnet-beta" ) { return cluster; } else { throw new Error( `SOLANA_CLUSTER must be localnet, devnet, or mainnet-beta` ); } } if (isLocalnet()) { return "localnet"; } return "devnet"; } export function getProgramId(cluster: SolanaCluster): PublicKey { if (process.env.SWITCHBOARD_PROGRAM_ID) { return new PublicKey(process.env.SWITCHBOARD_PROGRAM_ID); } return SB_V2_PID; } export function getRpcUrl(cluster: SolanaCluster): string { if (isLocalnet()) { return "http://0.0.0.0:8899"; } if (process.env.SOLANA_RPC_URL) { return String(process.env.SOLANA_RPC_URL); } if (cluster === "localnet") { return "http://0.0.0.0:8899"; } return clusterApiUrl(cluster); } export async function setupTest(): Promise { const cluster = getCluster(); const payer: Keypair = fs.existsSync(DEFAULT_KEYPAIR_PATH) ? Keypair.fromSecretKey( new Uint8Array( JSON.parse(fs.readFileSync(DEFAULT_KEYPAIR_PATH, "utf8")) ) ) : Keypair.generate(); const programId = getProgramId(cluster); const program = await sbv2.SwitchboardProgram.load( cluster, new Connection(getRpcUrl(cluster), { commitment: "confirmed" }), payer, programId ); // request airdrop if low on funds const payerBalance = await program.connection.getBalance(payer.publicKey); if (payerBalance === 0) { const airdropTxn = await program.connection.requestAirdrop( payer.publicKey, 1 * LAMPORTS_PER_SOL ); console.log(`Airdrop requested: ${airdropTxn}`); await program.connection.confirmTransaction(airdropTxn); } // Check if programStateAccount exists try { const programState = await program.connection.getAccountInfo( program.programState.publicKey ); if (!programState || programState.data === null) { await sbv2.ProgramStateAccount.getOrCreate(program); } } catch (e) { console.error(e); } // Check if attestationProgramStateAccount exists try { const attestationProgramState = await program.connection.getAccountInfo( program.attestationProgramState.publicKey ); if (!attestationProgramState || attestationProgramState.data === null) { await sbv2.AttestationProgramStateAccount.getOrCreate(program); } } catch (e) { console.error(e); } await program.mint.getOrCreateAssociatedUser(program.walletPubkey); return { cluster, program, payer, toUrl: (signature) => cluster === "localnet" ? `https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899` : `https://explorer.solana.com/tx/${signature}${ cluster === "devnet" ? "?cluster=devnet" : "" }`, round: (num: number, decimalPlaces = 2): number => { assert(decimalPlaces > 0 && decimalPlaces < 16); const base = Math.pow(10, decimalPlaces); return Math.round((num + Number.EPSILON) * base) / base; }, }; } export async function createFeed( queueAccount: QueueAccount, feedConfig?: Partial ): Promise { const [aggregatorAccount] = await queueAccount.createFeed({ name: feedConfig?.name ?? `Aggregator`, queueAuthority: feedConfig?.queueAuthority, batchSize: feedConfig?.batchSize ?? 1, minRequiredOracleResults: feedConfig?.minRequiredOracleResults ?? 1, minRequiredJobResults: feedConfig?.minRequiredOracleResults ?? 1, minUpdateDelaySeconds: feedConfig?.minUpdateDelaySeconds ?? 10, fundAmount: feedConfig?.fundAmount ?? 0, enable: feedConfig?.enable ?? true, jobs: feedConfig?.jobs && feedConfig?.jobs.length > 0 ? feedConfig?.jobs : [ { weight: 2, data: OracleJob.encodeDelimited( OracleJob.fromObject({ tasks: [ { valueTask: { value: 1, }, }, ], }) ).finish(), }, ], }); return aggregatorAccount; } export async function createFeeds( queueAccount: QueueAccount, numFeeds: number, feedConfig?: Partial ): Promise> { const aggregators: Array = []; const txns: Array> = []; for (const i of Array.from(Array(numFeeds).keys())) { const [aggregatorAccount, txn] = await queueAccount.createFeedInstructions( queueAccount.program.walletPubkey, { name: feedConfig?.name ?? `Aggregator-${i + 1}`, queueAuthority: feedConfig?.queueAuthority, batchSize: feedConfig?.batchSize ?? 1, minRequiredOracleResults: feedConfig?.minRequiredOracleResults ?? 1, minRequiredJobResults: feedConfig?.minRequiredOracleResults ?? 1, minUpdateDelaySeconds: feedConfig?.minUpdateDelaySeconds ?? 5 + Math.floor(Math.random() * 25), // 5 - 30 sec, fundAmount: feedConfig?.fundAmount ?? 0, disableWrap: true, enable: feedConfig?.enable ?? true, slidingWindow: feedConfig?.slidingWindow ?? false, jobs: feedConfig?.jobs && feedConfig?.jobs.length > 0 ? feedConfig?.jobs : [ { weight: 2, data: OracleJob.encodeDelimited( OracleJob.fromObject({ tasks: [ { valueTask: { value: 1, }, }, ], }) ).finish(), }, ], } ); aggregators.push(aggregatorAccount); txns.push(txn); } await queueAccount.program.signAndSendAll( TransactionObject.pack(txns.flat()) ); return aggregators; }