import * as sbv2 from "../src/index.js"; import { CrankAccount, QueueAccount, SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_CRANK, SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_QUEUE, } from "../src/index.js"; import { Aggregator, CHECK_ICON, FAILED_ICON, jsonReplacers, setupOutputDir, } from "./utils.js"; import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js"; // import { backOff } from 'exponential-backoff'; import fs from "fs"; import os from "os"; import path from "path"; const VERBOSE = process.env.VERBOSE || false; const jobMapPath = path.join( os.homedir(), "devnet-migration", sbv2.SB_V2_PID.toBase58(), "job_map.csv" ); const aggregatorMapPath = path.join( os.homedir(), "devnet-migration", sbv2.SB_V2_PID.toBase58(), "aggregator_map.csv" ); async function main() { const [oldDirPath, oldFeedDirPath, oldJobDirPath] = setupOutputDir( "2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG" ); const [newDirPath, newFeedDirPath, newJobDirPath] = setupOutputDir( sbv2.SB_V2_PID.toBase58() ); const devnetConnection = new Connection( process.env.SOLANA_DEVNET_RPC ?? clusterApiUrl("devnet") ); console.log(`rpcUrl: ${devnetConnection.rpcEndpoint}`); const payer = sbv2.SwitchboardTestContextV2.loadKeypair( "~/switchboard_environments_v2/devnet/upgrade_authority/upgrade_authority.json" ); console.log(`payer: ${payer.publicKey.toBase58()}`); const newProgram = await sbv2.SwitchboardProgram.load( "devnet", devnetConnection, payer, sbv2.SB_V2_PID ); const [queueAccount, queue] = await QueueAccount.load( newProgram, SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_QUEUE ); const [crankAccount, crank] = await CrankAccount.load( newProgram, SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_CRANK ); const [payerTokenWallet] = await newProgram.mint.getOrCreateWrappedUser( payer.publicKey, { fundUpTo: 100 } ); const aggregators = new Map(); for (const file of fs.readdirSync(oldFeedDirPath)) { if (!file.endsWith(".json")) { continue; } const fileName = path.basename(file).replace(".json", ""); const aggregatorDef: Aggregator = JSON.parse( fs.readFileSync(path.join(oldFeedDirPath, file), "utf-8") ); aggregators.set(fileName, aggregatorDef); } console.log(`Found ${aggregators.size} aggregators to migrate`); const jobMap = loadJobMap(); const aggregatorMap = loadAggregatorMap(); console.log(`Found ${jobMap.size} jobs that have already been migrated`); console.log( `Found ${aggregatorMap.size} aggregators that have already been migrated` ); for (const [aggregatorKey, aggregator] of aggregators.entries()) { if (jobMap.has(aggregatorKey)) { continue; } const newFeedPath = path.join(newFeedDirPath, `${aggregatorKey}.json`); if (fs.existsSync(newFeedPath)) { continue; } try { // find job accounts const jobs: Array<{ pubkey: PublicKey; weight: number }> = aggregator.definition.jobs.map((j) => { const newJobKey = jobMap.get(j.pubkey); if (!newJobKey) { throw new Error(`Job ${j.pubkey} was not migrated`); } return { pubkey: new PublicKey(newJobKey), weight: j.weight ?? 1 }; }); if (jobs.length === 0) { console.log(`${FAILED_ICON} ${aggregatorKey} has no jobs`); continue; } // create a feed but keep ourself as the authority until we do all final checks const [aggregatorAccount] = await queueAccount.createFeed({ authority: payer.publicKey, name: aggregator.definition.name, metadata: aggregator.definition.metadata, batchSize: aggregator.definition.batchSize, minRequiredOracleResults: aggregator.definition.minRequiredOracleResults, minRequiredJobResults: aggregator.definition.minRequiredJobResults, minUpdateDelaySeconds: aggregator.definition.minUpdateDelaySeconds, startAfter: aggregator.definition.startAfter, varianceThreshold: aggregator.definition.varianceThreshold, forceReportPeriod: aggregator.definition.forceReportPeriod, expiration: aggregator.definition.expiration, disableCrank: aggregator.definition.disableCrank, slidingWindow: aggregator.definition.slidingWindow, basePriorityFee: aggregator.definition.basePriorityFee ?? 0, priorityFeeBump: aggregator.definition.priorityFeeBump ?? 0, priorityFeeBumpPeriod: aggregator.definition.priorityFeeBumpPeriod ?? 0, maxPriorityFeeMultiplier: aggregator.definition.maxPriorityFeeMultiplier ?? 0, jobs: jobs, crankPubkey: aggregator.definition.pushCrank ? crankAccount.publicKey : undefined, crankDataBuffer: aggregator.definition.pushCrank ? crank.dataBuffer : undefined, historyLimit: aggregator.definition.historyBufferLength ? aggregator.definition.historyBufferLength : 100, // enable some history, its free anyway queueAuthorityPubkey: queue.authority, enable: false, // permissionless queue fundAmount: Math.max(0.1, aggregator.data?.balance ?? 0.1), funderTokenWallet: payerTokenWallet, withdrawAuthority: new PublicKey( aggregator.data.lease.withdrawAuthority ), }); console.log( `${CHECK_ICON} ${aggregatorKey.padEnd( 44, " " )} -> ${aggregatorAccount.publicKey.toBase58()}` ); try { const accounts = await aggregatorAccount.fetchAccounts( undefined, queueAccount, queue, "processed" ); fs.writeFileSync( newFeedPath, JSON.stringify( { newPublicKey: aggregatorAccount.publicKey.toBase58(), oldPublicKey: aggregatorKey, newFeed: accounts, oldFeed: aggregator, }, jsonReplacers, 2 ) ); } catch { fs.writeFileSync( newFeedPath, JSON.stringify( { newPublicKey: aggregatorAccount.publicKey.toBase58(), oldPublicKey: aggregatorKey, oldFeed: aggregator, }, jsonReplacers, 2 ) ); } aggregatorMap.set(aggregatorKey, aggregatorAccount.publicKey.toBase58()); writeAggregatorMap(aggregatorMap); } catch (error) { console.error(`${FAILED_ICON} ${aggregatorKey} failed`); console.error(error); } // throw new Error(`Safety`); } } main().catch((error) => { console.error(error); }); function writeAggregatorMap(map: Map) { const fileString = `oldPubkey, newPubkey\n${Array.from(map.entries()) .map((r) => r.join(", ")) .join("\n")}`; fs.writeFileSync(aggregatorMapPath, fileString); } function loadJobMap(): Map { if (!fs.existsSync(jobMapPath)) { return new Map(); } const map = new Map(); const fileString = fs.readFileSync(jobMapPath, "utf-8"); const fileLines = fileString.split("\n").slice(1); fileLines.forEach((r) => { const [oldPubkey, newPubkey] = r.split(", "); map.set(oldPubkey, newPubkey); }); return map; } function loadAggregatorMap(): Map { if (!fs.existsSync(aggregatorMapPath)) { return new Map(); } const map = new Map(); const fileString = fs.readFileSync(aggregatorMapPath, "utf-8"); const fileLines = fileString.split("\n").slice(1); fileLines.forEach((r) => { const [oldPubkey, newPubkey] = r.split(", "); map.set(oldPubkey, newPubkey); }); return map; }