solana.js: added SwitchboardTestContext

This commit is contained in:
Conner Gallagher 2022-12-08 11:36:40 -07:00
parent 7bcf2e52f8
commit 73e43c653d
8 changed files with 661 additions and 594 deletions

View File

@ -17,12 +17,16 @@
},
"./package.json": "./package.json",
"./idl/mainnet": {
"import": "./dist/esm/idl/mainnet.json",
"require": "./dist/cjs/idl/mainnet.json"
"import": "./lib/esm/idl/mainnet.json",
"require": "./lib/cjs/idl/mainnet.json"
},
"./idl/devnet": {
"import": "./dist/esm/idl/devnet.json",
"require": "./dist/cjs/idl/devnet.json"
"import": "./lib/esm/idl/devnet.json",
"require": "./lib/cjs/idl/devnet.json"
},
"./test": {
"import": "./lib/esm/test/index.js",
"require": "./lib/cjs/test/index.js"
}
},
"main": "lib/cjs/index.js",

View File

@ -96,7 +96,7 @@ export class CrankAccount extends Account<types.CrankAccountData> {
const keypair = params.keypair ?? Keypair.generate();
program.verifyNewKeypair(keypair);
const buffer = anchor.web3.Keypair.generate();
const buffer = params.dataBufferKeypair ?? anchor.web3.Keypair.generate();
program.verifyNewKeypair(buffer);
const maxRows = params.maxRows ?? 500;
@ -464,6 +464,10 @@ export interface CrankInitParams {
* Optional
*/
keypair?: Keypair;
/**
* Optional
*/
dataBufferKeypair?: Keypair;
}
/**

View File

@ -145,12 +145,10 @@ export class OracleAccount extends Account<types.OracleAccountData> {
} & OracleInitParams &
OracleStakeParams
): Promise<[OracleAccount, TransactionObject]> {
const tokenWallet = Keypair.generate();
const tokenWallet = params.stakingWalletKeypair ?? Keypair.generate();
const authority = params.authority?.publicKey ?? payer;
const txns: TransactionObject[] = [];
const [oracleAccount, oracleBump] = OracleAccount.fromSeed(
program,
params.queueAccount.publicKey,
@ -208,8 +206,6 @@ export class OracleAccount extends Account<types.OracleAccountData> {
params.authority ? [params.authority, tokenWallet] : [tokenWallet]
);
txns.push(oracleInit);
if (params.stakeAmount && params.stakeAmount > 0) {
const depositTxn = await oracleAccount.stakeInstructions(payer, {
stakeAmount: params.stakeAmount,
@ -217,15 +213,10 @@ export class OracleAccount extends Account<types.OracleAccountData> {
funderTokenAccount: params.funderTokenAccount,
tokenAccount: tokenWallet.publicKey,
});
txns.push(depositTxn);
oracleInit.combine(depositTxn);
}
const packed = TransactionObject.pack(txns);
if (packed.length > 1) {
throw new Error(`Expected a single TransactionObject`);
}
return [oracleAccount, packed[0]];
return [oracleAccount, oracleInit];
}
public static async create(
@ -542,6 +533,10 @@ export interface OracleInitParams {
metadata?: string;
/** Alternative keypair that will be the authority for the oracle. If not set the payer will be used. */
authority?: Keypair;
/**
* Optional,
*/
stakingWalletKeypair?: Keypair;
}
export interface OracleStakeParams {

View File

@ -6,6 +6,7 @@ import * as spl from '@solana/spl-token';
import * as errors from '../errors';
import { Mint } from '../mint';
import {
Keypair,
PublicKey,
SystemProgram,
TransactionInstruction,
@ -45,105 +46,102 @@ export class ProgramStateAccount extends Account<types.SbState> {
*/
static async getOrCreate(
program: SwitchboardProgram,
params: {
mint?: PublicKey;
daoMint?: PublicKey;
mint: PublicKey = Mint.native
): Promise<[ProgramStateAccount, number, TransactionSignature | undefined]> {
const [account, bump, txn] =
await ProgramStateAccount.getOrCreateInstructions(
program,
program.walletPubkey,
mint
);
if (txn) {
const txnSignature = await program.signAndSend(txn);
return [account, bump, txnSignature];
}
): Promise<[ProgramStateAccount, number]> {
const payer = program.wallet.payer;
return [account, bump, undefined];
}
static async getOrCreateInstructions(
program: SwitchboardProgram,
payer: PublicKey,
mint: PublicKey = Mint.native
): Promise<[ProgramStateAccount, number, TransactionObject | undefined]> {
const [account, bump] = ProgramStateAccount.fromSeed(program);
try {
await account.loadData();
} catch (e) {
const vaultKeypair = Keypair.generate();
const ixns: TransactionInstruction[] = [];
// load the mint
let splMint: spl.Mint;
try {
const [mint, vault]: [PublicKey, PublicKey] = await (async () => {
if (params.mint === undefined) {
const mint = await spl.createMint(
program.connection,
payer,
payer.publicKey,
null,
9
);
const vault = await spl.createAccount(
program.connection,
payer,
mint,
anchor.web3.Keypair.generate().publicKey
);
await spl.mintTo(
program.connection,
payer,
mint,
vault,
payer,
100_000_000
);
return [mint, vault];
} else {
return [
params.mint,
await spl.createAccount(
program.connection,
payer,
params.mint,
payer.publicKey
),
];
}
})();
const programInit = new TransactionObject(
program.walletPubkey,
[
types.programInit(
program,
{ params: { stateBump: bump } },
{
state: account.publicKey,
authority: program.wallet.publicKey,
payer: program.wallet.publicKey,
tokenMint: mint,
vault: vault,
systemProgram: SystemProgram.programId,
tokenProgram: spl.TOKEN_PROGRAM_ID,
daoMint: params.daoMint ?? mint,
}
),
],
[]
// try to load mint if it exists
splMint = await spl.getMint(program.connection, mint);
} catch {
// create new mint
const mintIxn = spl.createInitializeMintInstruction(
mint,
9,
payer,
payer
);
await program.signAndSend(programInit);
} catch {} // eslint-disable-line no-empty
}
return [account, bump];
}
static createAccountInstruction(
program: SwitchboardProgram,
mint = Mint.native,
daoMint = Mint.native
): [ProgramStateAccount, TransactionInstruction] {
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(program);
const vault = anchor.web3.Keypair.generate();
const ixn = types.programInit(
program,
{ params: { stateBump: stateBump } },
{
state: programStateAccount.publicKey,
authority: program.wallet.publicKey,
payer: program.wallet.publicKey,
tokenMint: mint,
vault: vault.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: spl.TOKEN_PROGRAM_ID,
daoMint: daoMint ?? mint,
ixns.push(mintIxn);
splMint = {
address: mint,
mintAuthority: payer,
supply: BigInt('100000000000000000'),
decimals: 9,
isInitialized: true,
freezeAuthority: payer,
tlvData: Buffer.from(''),
};
}
);
return [programStateAccount, ixn];
// create the vault
const vaultInitIxn = spl.createInitializeAccountInstruction(
vaultKeypair.publicKey,
splMint.address,
payer
);
ixns.push(vaultInitIxn);
if (splMint.mintAuthority?.equals(payer)) {
ixns.push(
spl.createMintToInstruction(
splMint.address,
vaultKeypair.publicKey,
payer,
BigInt('100000000000000000')
)
);
}
ixns.push(
types.programInit(
program,
{ params: { stateBump: bump } },
{
state: account.publicKey,
authority: program.wallet.publicKey,
payer: program.wallet.publicKey,
tokenMint: splMint.address,
vault: vaultKeypair.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: spl.TOKEN_PROGRAM_ID,
daoMint: splMint.address,
}
)
);
const programInit = new TransactionObject(payer, ixns, []);
return [account, bump, programInit];
}
return [account, bump, undefined];
}
/**

View File

@ -29,7 +29,19 @@ import {
SlidingResultAccountData,
VrfAccountData,
} from './generated';
import { CrankAccount, DISCRIMINATOR_MAP, QueueAccount } from './accounts';
import {
CrankAccount,
CrankInitParams,
DISCRIMINATOR_MAP,
OracleAccount,
OracleInitParams,
OracleStakeParams,
PermissionAccount,
PermissionSetParams,
ProgramStateAccount,
QueueAccount,
QueueInitParams,
} from './accounts';
import {
SWITCHBOARD_LABS_DEVNET_PERMISSIONED_CRANK,
SWITCHBOARD_LABS_DEVNET_PERMISSIONED_QUEUE,
@ -41,7 +53,6 @@ import {
SWITCHBOARD_LABS_MAINNET_PERMISSIONLESS_QUEUE,
} from './const';
import { types } from '.';
/**
* Switchboard Devnet Program ID
*/
@ -691,6 +702,115 @@ export class SwitchboardProgram {
return null;
}
async createNetworkInstructions(
payer: PublicKey,
params: QueueInitParams & {
cranks?: Array<Omit<CrankInitParams, 'queueAccount'>>;
oracles?: Array<
OracleInitParams &
OracleStakeParams &
Partial<PermissionSetParams> & {
queueAuthorityPubkey?: PublicKey;
}
>;
}
): Promise<[Array<TransactionObject>, NetworkInitResponse]> {
const txns: TransactionObject[] = [];
// get or create the program state
const [programState, stateBump, programInit] =
await ProgramStateAccount.getOrCreateInstructions(
this,
this.walletPubkey
);
if (programInit) {
txns.push(programInit);
}
// create a new queue
const [queueAccount, queueInit] = await QueueAccount.createInstructions(
this,
this.walletPubkey,
params
);
txns.push(queueInit);
const cranks: Array<[CrankAccount, TransactionObject]> = await Promise.all(
(params.cranks ?? []).map(async crankInitParams => {
return await queueAccount.createCrankInstructions(
payer,
crankInitParams
);
})
);
txns.push(...cranks.map(crank => crank[1]));
const oracles: Array<
[TransactionObject[], OracleAccount, PermissionAccount, number]
> = await Promise.all(
(params.oracles ?? []).map(async oracleInitParams => {
const [oracleAccount, oracleInit] =
await queueAccount.createOracleInstructions(payer, oracleInitParams);
const [oraclePermissionAccount, oraclePermissionBump] =
PermissionAccount.fromSeed(
this,
this.walletPubkey,
queueAccount.publicKey,
oracleAccount.publicKey
);
return [
oracleInit,
oracleAccount,
oraclePermissionAccount,
oraclePermissionBump,
];
})
);
txns.push(...oracles.map(oracle => oracle[0]).flat());
const accounts: NetworkInitResponse = {
programState: {
account: programState,
bump: stateBump,
},
queueAccount,
cranks: cranks.map(c => c[0]),
oracles: oracles.map(o => {
return {
account: o[1],
permissions: {
account: o[2],
bump: o[3],
},
};
}),
};
return [TransactionObject.pack(txns), accounts];
}
async createNetwork(
params: QueueInitParams & {
cranks?: Array<Omit<CrankInitParams, 'queueAccount'>>;
oracles?: Array<
OracleInitParams &
OracleStakeParams &
Partial<PermissionSetParams> & {
queueAuthorityPubkey?: PublicKey;
}
>;
}
): Promise<[NetworkInitResponse, Array<TransactionSignature>]> {
const [networkInit, accounts] = await this.createNetworkInstructions(
this.walletPubkey,
params
);
const txnSignatures = await this.signAndSendAll(networkInit);
return [accounts, txnSignatures];
}
}
export class AnchorWallet implements anchor.Wallet {
@ -716,3 +836,19 @@ interface AccountInfoResponse {
pubkey: anchor.web3.PublicKey;
account: anchor.web3.AccountInfo<Buffer>;
}
export interface NetworkInitResponse {
programState: {
account: ProgramStateAccount;
bump: number;
};
queueAccount: QueueAccount;
cranks: Array<CrankAccount>;
oracles: Array<{
account: OracleAccount;
permissions: {
account: PermissionAccount;
bump: number;
};
}>;
}

View File

@ -1,484 +0,0 @@
// import * as anchor from '@project-serum/anchor';
// import { clusterApiUrl, Connection, Keypair, PublicKey } from '@solana/web3.js';
// import {
// AggregatorAccount,
// CrankAccount,
// OracleAccount,
// PermissionAccount,
// ProgramStateAccount,
// QueueAccount,
// } from './accounts';
// import {
// AnchorWallet,
// getSwitchboardProgramId,
// SwitchboardProgram,
// } from './program';
// import fs from 'fs';
// import path from 'path';
// export const LATEST_DOCKER_VERSION = '';
// export class SwitchboardTestContext {
// constructor(
// readonly program: SwitchboardProgram,
// readonly queue: QueueAccount
// ) {}
// public async isReady(): Promise<boolean> {
// return await this.queue.isReady();
// }
// // public async awaitOracleHeartbeat(timeout = 30): Promise<void> {
// // throw new Error(`Not implemented yet`);
// // }
// // static async create(
// // program: SwitchboardProgram
// // ): Promise<SwitchboardTestContext> {
// // // Create queue
// // // Create oracle
// // // Create token accounts
// // }
// static async load(): Promise<SwitchboardTestContext> {
// throw new Error(`Not implemented yet`);
// }
// public static findSwitchboardEnv(envFileName = 'switchboard.env'): string {
// throw new Error(`Not implemented yet`);
// }
// public async createStaticFeed(
// value: number,
// timeout = 30
// ): Promise<AggregatorAccount> {
// throw new Error(`Not implemented yet`);
// }
// public async updateStaticFeed(
// aggregatorAccount: AggregatorAccount,
// value: number,
// timeout = 30
// ): Promise<AggregatorAccount> {
// throw new Error(`Not implemented yet`);
// }
// }
// export interface ISwitchboardTestEnvironment {
// programId: PublicKey;
// programDataAddress: PublicKey;
// idlAddress: PublicKey;
// programState: PublicKey;
// switchboardVault: PublicKey;
// switchboardMint: PublicKey;
// tokenWallet: PublicKey;
// queue: PublicKey;
// queueAuthority: PublicKey;
// queueBuffer: PublicKey;
// crank: PublicKey;
// crankBuffer: PublicKey;
// oracle: PublicKey;
// oracleAuthority: PublicKey;
// oracleEscrow: PublicKey;
// oraclePermissions: PublicKey;
// payerKeypairPath: string;
// }
// export class SwitchboardTestEnvironment implements ISwitchboardTestEnvironment {
// programId: PublicKey;
// programDataAddress: PublicKey;
// idlAddress: PublicKey;
// programState: PublicKey;
// switchboardVault: PublicKey;
// switchboardMint: PublicKey;
// tokenWallet: PublicKey;
// queue: PublicKey;
// queueAuthority: PublicKey;
// queueBuffer: PublicKey;
// crank: PublicKey;
// crankBuffer: PublicKey;
// oracle: PublicKey;
// oracleAuthority: PublicKey;
// oracleEscrow: PublicKey;
// oraclePermissions: PublicKey;
// payerKeypairPath: string;
// constructor(ctx: ISwitchboardTestEnvironment) {
// this.programId = ctx.programId;
// this.programDataAddress = ctx.programDataAddress;
// this.idlAddress = ctx.idlAddress;
// this.programState = ctx.programState;
// this.switchboardVault = ctx.switchboardVault;
// this.switchboardMint = ctx.switchboardMint;
// this.tokenWallet = ctx.tokenWallet;
// this.queue = ctx.queue;
// this.queueAuthority = ctx.queueAuthority;
// this.queueBuffer = ctx.queueBuffer;
// this.crank = ctx.crank;
// this.crankBuffer = ctx.crankBuffer;
// this.oracle = ctx.oracle;
// this.oracleAuthority = ctx.oracleAuthority;
// this.oracleEscrow = ctx.oracleEscrow;
// this.oraclePermissions = ctx.oraclePermissions;
// this.payerKeypairPath = ctx.payerKeypairPath;
// }
// private getAccountCloneString(): string {
// const accounts = Object.keys(this).map(key => {
// // iterate over additionalClonedAccounts and collect pubkeys
// if (typeof this[key] === 'string') {
// return;
// }
// return `--clone ${(this[key] as PublicKey).toBase58()} \`# ${key}\` `;
// });
// return accounts.filter(Boolean).join(`\\\n`);
// }
// public toJSON(): ISwitchboardTestEnvironment {
// return {
// programId: this.programId,
// programDataAddress: this.programDataAddress,
// idlAddress: this.idlAddress,
// programState: this.programState,
// switchboardVault: this.switchboardVault,
// switchboardMint: this.switchboardMint,
// tokenWallet: this.tokenWallet,
// queue: this.queue,
// queueAuthority: this.queueAuthority,
// queueBuffer: this.queueBuffer,
// crank: this.crank,
// crankBuffer: this.crankBuffer,
// oracle: this.oracle,
// oracleAuthority: this.oracleAuthority,
// oracleEscrow: this.oracleEscrow,
// oraclePermissions: this.oraclePermissions,
// payerKeypairPath: this.payerKeypairPath,
// };
// }
// /** Write switchboard test environment to filesystem */
// public writeAll(outputDir: string): void {
// fs.mkdirSync(outputDir, { recursive: true });
// this.writeEnv(outputDir);
// this.writeJSON(outputDir);
// this.writeScripts(outputDir);
// this.writeDockerCompose(outputDir);
// this.writeAnchorToml(outputDir);
// }
// /** Write the env file to filesystem */
// public writeEnv(filePath: string): void {
// const ENV_FILE_PATH = path.join(filePath, 'switchboard.env');
// let fileStr = '';
// fileStr += `SWITCHBOARD_PROGRAM_ID="${this.programId.toBase58()}"\n`;
// fileStr += `SWITCHBOARD_PROGRAM_DATA_ADDRESS="${this.programDataAddress.toBase58()}"\n`;
// fileStr += `SWITCHBOARD_IDL_ADDRESS="${this.idlAddress.toBase58()}"\n`;
// fileStr += `SWITCHBOARD_PROGRAM_STATE="${this.programState.toBase58()}"\n`;
// fileStr += `SWITCHBOARD_VAULT="${this.switchboardVault.toBase58()}"\n`;
// fileStr += `SWITCHBOARD_MINT="${this.switchboardMint.toBase58()}"\n`;
// fileStr += `TOKEN_WALLET="${this.tokenWallet.toBase58()}"\n`;
// fileStr += `ORACLE_QUEUE="${this.queue.toBase58()}"\n`;
// fileStr += `ORACLE_QUEUE_AUTHORITY="${this.queueAuthority.toBase58()}"\n`;
// fileStr += `ORACLE_QUEUE_BUFFER="${this.queueBuffer.toBase58()}"\n`;
// fileStr += `CRANK="${this.crank.toBase58()}"\n`;
// fileStr += `CRANK_BUFFER="${this.crankBuffer.toBase58()}"\n`;
// fileStr += `ORACLE="${this.oracle.toBase58()}"\n`;
// fileStr += `ORACLE_AUTHORITY="${this.oracleAuthority.toBase58()}"\n`;
// fileStr += `ORACLE_ESCROW="${this.oracleEscrow.toBase58()}"\n`;
// fileStr += `ORACLE_PERMISSIONS="${this.oraclePermissions.toBase58()}"\n`;
// // fileStr += `SWITCHBOARD_ACCOUNTS="${this.getAccountCloneString()}"\n`;
// fs.writeFileSync(ENV_FILE_PATH, fileStr);
// // console.log(
// // `${chalk.green('Env File saved to:')} ${ENV_FILE_PATH.replace(
// // process.cwd(),
// // '.'
// // )}`
// // );
// }
// public writeJSON(outputDir: string): void {
// const JSON_FILE_PATH = path.join(outputDir, 'switchboard.json');
// fs.writeFileSync(
// JSON_FILE_PATH,
// JSON.stringify(
// this.toJSON(),
// (key, value) => {
// if (value instanceof PublicKey) {
// return value.toBase58();
// }
// return value;
// },
// 2
// )
// );
// }
// public writeScripts(outputDir: string): void {
// const LOCAL_VALIDATOR_SCRIPT = path.join(
// outputDir,
// 'start-local-validator.sh'
// );
// // create bash script to startup local validator with appropriate accounts cloned
// const baseValidatorCommand = `solana-test-validator -r --ledger .anchor/test-ledger --mint ${this.oracleAuthority.toBase58()} --bind-address 0.0.0.0 --url ${clusterApiUrl(
// 'devnet'
// )} --rpc-port 8899 `;
// const cloneAccountsString = this.getAccountCloneString();
// const startValidatorCommand = `${baseValidatorCommand} ${cloneAccountsString}`;
// fs.writeFileSync(
// LOCAL_VALIDATOR_SCRIPT,
// `#!/bin/bash\n\nmkdir -p .anchor/test-ledger\n\n${startValidatorCommand}`
// );
// fs.chmodSync(LOCAL_VALIDATOR_SCRIPT, '755');
// // console.log(
// // `${chalk.green('Bash script saved to:')} ${LOCAL_VALIDATOR_SCRIPT.replace(
// // process.cwd(),
// // '.'
// // )}`
// // );
// // create bash script to start local oracle
// const ORACLE_SCRIPT = path.join(outputDir, 'start-oracle.sh');
// // const startOracleCommand = `docker-compose -f docker-compose.switchboard.yml up`;
// fs.writeFileSync(
// ORACLE_SCRIPT,
// `#!/usr/bin/env bash
// script_dir=$( cd -- "$( dirname -- "\${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
// docker-compose -f "$script_dir"/docker-compose.switchboard.yml up
// `
// // `#!/bin/bash\n\n${startOracleCommand}`
// );
// fs.chmodSync(ORACLE_SCRIPT, '755');
// // console.log(
// // `${chalk.green('Bash script saved to:')} ${ORACLE_SCRIPT.replace(
// // process.cwd(),
// // '.'
// // )}`
// // );
// }
// public writeDockerCompose(outputDir: string): void {
// const DOCKER_COMPOSE_FILEPATH = path.join(
// outputDir,
// 'docker-compose.switchboard.yml'
// );
// const dockerComposeString = `version: "3.3"
// services:
// oracle:
// image: "switchboardlabs/node:\${SBV2_ORACLE_VERSION:-${LATEST_DOCKER_VERSION}}" # https://hub.docker.com/r/switchboardlabs/node/tags
// network_mode: host
// restart: always
// secrets:
// - PAYER_SECRETS
// environment:
// - VERBOSE=1
// - LIVE=1
// - CLUSTER=\${CLUSTER:-localnet}
// - HEARTBEAT_INTERVAL=30 # Seconds
// - ORACLE_KEY=${this.oracle.toBase58()}
// # - RPC_URL=\${RPC_URL}
// secrets:
// PAYER_SECRETS:
// file: ${this.payerKeypairPath}
// `;
// fs.writeFileSync(DOCKER_COMPOSE_FILEPATH, dockerComposeString);
// // console.log(
// // `${chalk.green(
// // 'Docker-Compose saved to:'
// // )} ${DOCKER_COMPOSE_FILEPATH.replace(process.cwd(), '.')}`
// // );
// }
// public writeAnchorToml(outputDir: string): void {
// const ANCHOR_TOML_FILEPATH = path.join(
// outputDir,
// 'Anchor.switchboard.toml'
// );
// const anchorTomlString = `[provider]
// cluster = "localnet"
// wallet = "${this.payerKeypairPath}"
// [test]
// startup_wait = 10000
// [test.validator]
// url = "https://api.devnet.solana.com"
// [[test.validator.clone]] # programID
// address = "${this.programId}"
// [[test.validator.clone]] # idlAddress
// address = "${this.idlAddress}"
// [[test.validator.clone]] # programState
// address = "${this.programState}"
// [[test.validator.clone]] # switchboardVault
// address = "${this.switchboardVault}"
// [[test.validator.clone]] # tokenWallet
// address = "${this.tokenWallet}"
// [[test.validator.clone]] # queue
// address = "${this.queue}"
// [[test.validator.clone]] # queueAuthority
// address = "${this.queueAuthority}"
// [[test.validator.clone]] # queueBuffer
// address = "${this.queueBuffer}"
// [[test.validator.clone]] # crank
// address = "${this.crank}"
// [[test.validator.clone]] # crankBuffer
// address = "${this.crankBuffer}"
// [[test.validator.clone]] # oracle
// address = "${this.oracle}"
// [[test.validator.clone]] # oracleAuthority
// address = "${this.oracleAuthority}"
// [[test.validator.clone]] # oracleEscrow
// address = "${this.oracleEscrow}"
// [[test.validator.clone]] # oraclePermissions
// address = "${this.oraclePermissions}"
// `;
// fs.writeFileSync(ANCHOR_TOML_FILEPATH, anchorTomlString);
// // console.log(
// // `${chalk.green('Anchor.toml saved to:')} ${ANCHOR_TOML_FILEPATH.replace(
// // process.cwd(),
// // '.'
// // )}`
// // );
// }
// /** Build a devnet environment to later clone to localnet */
// static async create(
// payerKeypairPath: string,
// additionalClonedAccounts?: Record<string, PublicKey>,
// alternateProgramId?: PublicKey
// ): Promise<SwitchboardTestEnvironment> {
// const fullKeypairPath =
// payerKeypairPath.charAt(0) === '/'
// ? payerKeypairPath
// : path.join(process.cwd(), payerKeypairPath);
// if (!fs.existsSync(fullKeypairPath)) {
// throw new Error('Failed to find payer keypair path');
// }
// const payerKeypair = Keypair.fromSecretKey(
// new Uint8Array(
// JSON.parse(
// fs.readFileSync(fullKeypairPath, {
// encoding: 'utf-8',
// })
// )
// )
// );
// const connection = new Connection(clusterApiUrl('devnet'), {
// commitment: 'confirmed',
// });
// const programId = alternateProgramId ?? getSwitchboardProgramId('devnet');
// const wallet = new AnchorWallet(payerKeypair);
// const provider = new anchor.AnchorProvider(connection, wallet, {});
// const anchorIdl = await anchor.Program.fetchIdl(programId, provider);
// if (!anchorIdl) {
// throw new Error(`failed to read idl for ${programId}`);
// }
// const switchboardProgram = new anchor.Program(
// anchorIdl,
// programId,
// provider
// );
// const programDataAddress = getProgramDataAddress(
// switchboardProgram.programId
// );
// const idlAddress = await getIdlAddress(switchboardProgram.programId);
// const queueResponse = await createQueue(
// switchboardProgram,
// {
// authority: payerKeypair.publicKey,
// name: 'Test Queue',
// metadata: `created ${anchorBNtoDateString(
// new anchor.BN(Math.floor(Date.now() / 1000))
// )}`,
// minStake: new anchor.BN(0),
// reward: new anchor.BN(0),
// crankSize: 10,
// oracleTimeout: 180,
// numOracles: 1,
// unpermissionedFeeds: true,
// unpermissionedVrf: true,
// enableBufferRelayers: true,
// },
// 10
// );
// const queueAccount = queueResponse.queueAccount;
// const queue = await queueAccount.loadData();
// const [programStateAccount, stateBump] =
// ProgramStateAccount.fromSeed(switchboardProgram);
// const programState = await programStateAccount.loadData();
// const mint = await queueAccount.loadMint();
// const payerSwitchboardWallet = await getOrCreateSwitchboardTokenAccount(
// switchboardProgram,
// mint
// );
// const crankAccount = new CrankAccount(
// switchboardProgram,
// queueResponse.crankPubkey
// );
// const crank = await crankAccount.loadData();
// const oracleAccount = new OracleAccount(
// switchboardProgram,
// queueResponse.oracles[0]
// );
// const oracle = await oracleAccount.loadData();
// const [permissionAccount] = PermissionAccount.fromSeed(
// switchboardProgram,
// queue.authority,
// queueAccount.publicKey,
// oracleAccount.publicKey
// );
// const permission = await permissionAccount.loadData();
// const ctx: ISwitchboardTestEnvironment = {
// programId: switchboardProgram.programId,
// programDataAddress,
// idlAddress,
// programState: programStateAccount.publicKey,
// switchboardVault: programState.tokenVault,
// switchboardMint: mint.address,
// tokenWallet: payerSwitchboardWallet,
// queue: queueResponse.queueAccount.publicKey,
// queueAuthority: queue.authority,
// queueBuffer: queue.dataBuffer,
// crank: crankAccount.publicKey,
// crankBuffer: crank.dataBuffer,
// oracle: oracleAccount.publicKey,
// oracleAuthority: oracle.oracleAuthority,
// oracleEscrow: oracle.tokenAccount,
// oraclePermissions: permissionAccount.publicKey,
// payerKeypairPath: fullKeypairPath,
// };
// return new SwitchboardTestEnvironment(ctx);
// }
// }

View File

@ -0,0 +1,413 @@
import * as anchor from '@project-serum/anchor';
import { clusterApiUrl, Connection, Keypair, PublicKey } from '@solana/web3.js';
import { QueueAccount } from '../accounts';
import { SwitchboardProgram } from '../program';
import fs from 'fs';
import path from 'path';
import { BNtoDateTimeString } from '@switchboard-xyz/common';
import { Mint } from '../mint';
export const LATEST_DOCKER_VERSION = 'dev-v2-RC_12_05_22_22_48';
/** Get the program data address for a given programId
* @param programId the programId for a given on-chain program
* @return the publicKey of the address holding the upgradeable program buffer
*/
export const getProgramDataAddress = (programId: PublicKey): PublicKey => {
return anchor.utils.publicKey.findProgramAddressSync(
[programId.toBytes()],
new PublicKey('BPFLoaderUpgradeab1e11111111111111111111111')
)[0];
};
/** Get the IDL address for a given programId
* @param programId the programId for a given on-chain program
* @return the publicKey of the IDL address
*/
export const getIdlAddress = async (
programId: PublicKey
): Promise<PublicKey> => {
const base = (await PublicKey.findProgramAddress([], programId))[0];
return PublicKey.createWithSeed(base, 'anchor:idl', programId);
};
export class SwitchboardTestContext {
constructor(
readonly program: SwitchboardProgram,
readonly queue: QueueAccount
) {}
// static async load(): Promise<SwitchboardTestContext> {
// throw new Error(`Not implemented yet`);
// }
// public static findSwitchboardEnv(envFileName = 'switchboard.env'): string {
// throw new Error(`Not implemented yet`);
// }
// public async createStaticFeed(
// value: number,
// timeout = 30
// ): Promise<AggregatorAccount> {
// throw new Error(`Not implemented yet`);
// }
// public async updateStaticFeed(
// aggregatorAccount: AggregatorAccount,
// value: number,
// timeout = 30
// ): Promise<AggregatorAccount> {
// throw new Error(`Not implemented yet`);
// }
static async createEnvironment(
payerKeypairPath: string,
alternateProgramId?: PublicKey
): Promise<SwitchboardTestEnvironment> {
const fullKeypairPath =
payerKeypairPath.startsWith('/') || payerKeypairPath.startsWith('C:')
? payerKeypairPath
: path.join(process.cwd(), payerKeypairPath);
if (!fs.existsSync(fullKeypairPath)) {
throw new Error('Failed to find payer keypair path');
}
const payerKeypair = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(
fs.readFileSync(fullKeypairPath, {
encoding: 'utf-8',
})
)
)
);
const connection = new Connection(clusterApiUrl('devnet'), {
commitment: 'confirmed',
});
const program = await SwitchboardProgram.load(
'devnet',
connection,
payerKeypair,
alternateProgramId
);
const [userTokenWallet, userTokenInit] =
await program.mint.getOrCreateWrappedUserInstructions(
program.walletPubkey,
{ fundUpTo: 1 }
);
await program.signAndSend(userTokenInit);
const programDataAddress = getProgramDataAddress(program.programId);
const idlAddress = await getIdlAddress(program.programId);
// use pre-generated keypairs so we dont need to rely on account loading
const dataBufferKeypair = Keypair.generate();
const crankBufferKeypair = Keypair.generate();
const oracleStakingWalletKeypair = Keypair.generate();
const [accounts] = await program.createNetwork({
name: 'Test Queue',
metadata: `created ${BNtoDateTimeString(
new anchor.BN(Math.floor(Date.now() / 1000))
)}`,
reward: 0,
minStake: 0,
queueSize: 10,
dataBufferKeypair: dataBufferKeypair,
cranks: [
{
name: 'Test Crank',
maxRows: 100,
dataBufferKeypair: crankBufferKeypair,
},
],
oracles: [
{
name: 'Test Oracle',
enable: true,
stakingWalletKeypair: oracleStakingWalletKeypair,
},
],
});
const crank = accounts.cranks.shift();
if (!crank) {
throw new Error(`Failed to create the crank`);
}
const oracle = accounts.oracles.shift();
if (!oracle) {
throw new Error(`Failed to create the oracle`);
}
// async load the accounts
const programState = await accounts.programState.account.loadData();
return new SwitchboardTestEnvironment({
programId: program.programId,
programDataAddress: programDataAddress,
idlAddress: idlAddress,
programState: accounts.programState.account.publicKey,
switchboardVault: programState.tokenVault,
switchboardMint: programState.tokenMint.equals(PublicKey.default)
? Mint.native
: programState.tokenMint,
tokenWallet: userTokenWallet,
queue: accounts.queueAccount.publicKey,
queueAuthority: program.walletPubkey,
queueBuffer: dataBufferKeypair.publicKey,
crank: crank.publicKey,
crankBuffer: crankBufferKeypair.publicKey,
oracle: oracle.account.publicKey,
oracleAuthority: program.walletPubkey,
oracleEscrow: oracleStakingWalletKeypair.publicKey,
oraclePermissions: oracle.permissions.account.publicKey,
payerKeypairPath: fullKeypairPath,
});
}
}
export interface ISwitchboardTestEnvironment {
programId: PublicKey;
programDataAddress: PublicKey;
idlAddress: PublicKey;
programState: PublicKey;
switchboardVault: PublicKey;
switchboardMint: PublicKey;
tokenWallet: PublicKey;
queue: PublicKey;
queueAuthority: PublicKey;
queueBuffer: PublicKey;
crank: PublicKey;
crankBuffer: PublicKey;
oracle: PublicKey;
oracleAuthority: PublicKey;
oracleEscrow: PublicKey;
oraclePermissions: PublicKey;
payerKeypairPath: string;
}
export class SwitchboardTestEnvironment implements ISwitchboardTestEnvironment {
programId: PublicKey;
programDataAddress: PublicKey;
idlAddress: PublicKey;
programState: PublicKey;
switchboardVault: PublicKey;
switchboardMint: PublicKey;
tokenWallet: PublicKey;
queue: PublicKey;
queueAuthority: PublicKey;
queueBuffer: PublicKey;
crank: PublicKey;
crankBuffer: PublicKey;
oracle: PublicKey;
oracleAuthority: PublicKey;
oracleEscrow: PublicKey;
oraclePermissions: PublicKey;
payerKeypairPath: string;
constructor(ctx: ISwitchboardTestEnvironment) {
this.programId = ctx.programId;
this.programDataAddress = ctx.programDataAddress;
this.idlAddress = ctx.idlAddress;
this.programState = ctx.programState;
this.switchboardVault = ctx.switchboardVault;
this.switchboardMint = ctx.switchboardMint;
this.tokenWallet = ctx.tokenWallet;
this.queue = ctx.queue;
this.queueAuthority = ctx.queueAuthority;
this.queueBuffer = ctx.queueBuffer;
this.crank = ctx.crank;
this.crankBuffer = ctx.crankBuffer;
this.oracle = ctx.oracle;
this.oracleAuthority = ctx.oracleAuthority;
this.oracleEscrow = ctx.oracleEscrow;
this.oraclePermissions = ctx.oraclePermissions;
this.payerKeypairPath = ctx.payerKeypairPath;
}
public get envFileString(): string {
return Object.keys(this)
.map(key => {
if (this[key] instanceof PublicKey) {
return `${camelCaseToUpperCaseWithUnderscores(key)}=${this[
key
].toBase58()}`;
}
return;
})
.filter(Boolean)
.join('\n');
}
public get anchorToml(): string {
return [
`[provider]
cluster = "localnet"
wallet = "${this.payerKeypairPath}"
[test]
startup_wait = 10000
[test.validator]
url = "https://api.devnet.solana.com"
`,
Object.keys(this).map(
key => `[[test.validator.clone]] # ${key}
address = "${this[key]}"`
),
].join('\n\n');
}
public get accountCloneString(): string {
const accounts = Object.keys(this).map(key => {
if (typeof this[key] === 'string') {
return;
}
return `--clone ${(this[key] as PublicKey).toBase58()} \`# ${key}\` `;
});
return accounts.filter(Boolean).join(`\\\n`);
}
public get dockerCompose(): string {
return `version: "3.3"
services:
oracle:
image: "switchboardlabs/node:\${SBV2_ORACLE_VERSION:-${LATEST_DOCKER_VERSION}}" # https://hub.docker.com/r/switchboardlabs/node/tags
network_mode: host
restart: always
secrets:
- PAYER_SECRETS
environment:
- VERBOSE=1
- CLUSTER=\${CLUSTER:-localnet}
- HEARTBEAT_INTERVAL=30 # Seconds
- ORACLE_KEY=${this.oracle.toBase58()}
- TASK_RUNNER_SOLANA_RPC=${clusterApiUrl('mainnet-beta')}
# - RPC_URL=\${RPC_URL}
secrets:
PAYER_SECRETS:
file: ${this.payerKeypairPath}`;
}
public get localValidatorScript(): string {
return `#!/bin/bash
mkdir -p .anchor/test-ledger
solana-test-validator -r --ledger .anchor/test-ledger --mint ${this.oracleAuthority.toBase58()} --bind-address 0.0.0.0 --url ${clusterApiUrl(
'devnet'
)} --rpc-port 8899 ${this.accountCloneString}`;
}
public get startOracleScript(): string {
return `#!/usr/bin/env bash
script_dir=$( cd -- "$( dirname -- "\${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
docker-compose -f "$script_dir"/docker-compose.switchboard.yml up`;
}
public toJSON(): ISwitchboardTestEnvironment {
return {
programId: this.programId,
programDataAddress: this.programDataAddress,
idlAddress: this.idlAddress,
programState: this.programState,
switchboardVault: this.switchboardVault,
switchboardMint: this.switchboardMint,
tokenWallet: this.tokenWallet,
queue: this.queue,
queueAuthority: this.queueAuthority,
queueBuffer: this.queueBuffer,
crank: this.crank,
crankBuffer: this.crankBuffer,
oracle: this.oracle,
oracleAuthority: this.oracleAuthority,
oracleEscrow: this.oracleEscrow,
oraclePermissions: this.oraclePermissions,
payerKeypairPath: this.payerKeypairPath,
};
}
/** Write switchboard test environment to filesystem */
public writeAll(outputDir: string): void {
fs.mkdirSync(outputDir, { recursive: true });
this.writeEnv(outputDir);
this.writeJSON(outputDir);
this.writeScripts(outputDir);
this.writeDockerCompose(outputDir);
this.writeAnchorToml(outputDir);
}
/** Write the env file to filesystem */
public writeEnv(filePath: string): void {
const ENV_FILE_PATH = path.join(filePath, 'switchboard.env');
fs.writeFileSync(ENV_FILE_PATH, this.envFileString);
}
public writeJSON(outputDir: string): void {
const JSON_FILE_PATH = path.join(outputDir, 'switchboard.json');
fs.writeFileSync(
JSON_FILE_PATH,
JSON.stringify(
this.toJSON(),
(key, value) => {
if (value instanceof PublicKey) {
return value.toBase58();
}
return value;
},
2
)
);
}
public writeScripts(outputDir: string): void {
// create script to start local validator with accounts cloned
const LOCAL_VALIDATOR_SCRIPT = path.join(
outputDir,
'start-local-validator.sh'
);
fs.writeFileSync(LOCAL_VALIDATOR_SCRIPT, this.localValidatorScript);
fs.chmodSync(LOCAL_VALIDATOR_SCRIPT, '755');
// create bash script to start local oracle
const ORACLE_SCRIPT = path.join(outputDir, 'start-oracle.sh');
fs.writeFileSync(ORACLE_SCRIPT, this.startOracleScript);
fs.chmodSync(ORACLE_SCRIPT, '755');
}
public writeDockerCompose(outputDir: string): void {
const DOCKER_COMPOSE_FILEPATH = path.join(
outputDir,
'docker-compose.switchboard.yml'
);
fs.writeFileSync(DOCKER_COMPOSE_FILEPATH, this.dockerCompose);
}
public writeAnchorToml(outputDir: string) {
const ANCHOR_TOML_FILEPATH = path.join(
outputDir,
'Anchor.switchboard.toml'
);
fs.writeFileSync(ANCHOR_TOML_FILEPATH, this.anchorToml);
}
}
function camelCaseToUpperCaseWithUnderscores(input: string) {
// Use a regular expression to match words that begin with a capital letter
const matches = input.match(/[A-Z][a-z]*/g);
// If there are no matches, return the original string
if (matches === null) {
return input;
}
// Convert the matches to upper case and join them with underscores
return matches.map(match => match.toUpperCase()).join('_');
}

View File

@ -0,0 +1 @@
export * from './SwitchboardTestContext';