sbv2-solana/javascript/solana.js/scripts/prepare.ts

350 lines
11 KiB
TypeScript

import * as sbv2 from './src';
import { jsonReplacers, setupOutputDir } from './utils';
import * as anchor from '@coral-xyz/anchor';
import * as spl from '@solana/spl-token';
import {
clusterApiUrl,
Connection,
LAMPORTS_PER_SOL,
PublicKey,
} from '@solana/web3.js';
import { OracleJob, toUtf8 } from '@switchboard-xyz/common';
// import { backOff } from 'exponential-backoff';
import fs from 'fs';
import _ from 'lodash';
import path from 'path';
const LEASE_THRESHOLD = (10 * 12500) / LAMPORTS_PER_SOL; // must have at least 10 queue rewards in the lease to migrate
const VERBOSE = process.env.VERBOSE || false;
interface JobDefinition {
account: sbv2.JobAccount;
data: sbv2.types.JobAccountData;
oracleJob: OracleJob;
}
interface AggregatorDefinition {
account: sbv2.AggregatorAccount;
data: sbv2.types.AggregatorAccountData;
historyBufferLength?: number;
// permissions
permissionAccount: sbv2.PermissionAccount;
permissions: sbv2.types.PermissionAccountData;
// lease
leaseAccount: sbv2.LeaseAccount;
lease: sbv2.types.LeaseAccountData;
// jobs
jobs: Array<JobDefinition>;
}
type AggregatorDefinitionWithLeaseBalance = AggregatorDefinition & {
balance: number;
};
async function main() {
const queuePubkey = new PublicKey(
process.argv.length > 2
? process.argv[2]
: 'F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy'
);
const [dirPath, feedDirPath, jobDirPath] = setupOutputDir(
'2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG'
);
const devnetConnection = new Connection(
process.env.SOLANA_DEVNET_RPC ?? clusterApiUrl('devnet')
);
console.log(`rpcUrl: ${devnetConnection.rpcEndpoint}`);
const payer = sbv2.SwitchboardTestContextV2.loadKeypair(
'~/.config/solana/id.json'
);
console.log(`payer: ${payer.publicKey.toBase58()}`);
const oldProgram = await sbv2.SwitchboardProgram.load(
'devnet',
devnetConnection,
payer,
new PublicKey('2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG')
);
const oldProgramAccounts = await oldProgram.getProgramAccounts();
const [queueAccount, queue] = await sbv2.QueueAccount.load(
oldProgram,
queuePubkey
);
// load all devnet permissionless aggregators
const allAggregators = Array.from(oldProgramAccounts.aggregators.entries());
console.log(`Found ${allAggregators.length} aggregators on old programId`);
const filteredAggregators = allAggregators.filter(
([aggregatorKey, aggregator]) =>
aggregator.queuePubkey.equals(queueAccount.publicKey)
);
console.log(`Found ${filteredAggregators.length} aggregators on queue`);
const aggregators = new Map<string, AggregatorDefinition>();
for (const [aggregatorKey, aggregator] of filteredAggregators) {
// check permission account exists
const [permissionAccount] = sbv2.PermissionAccount.fromSeed(
oldProgram,
queue.authority,
queueAccount.publicKey,
new PublicKey(aggregatorKey)
);
const permissions = oldProgramAccounts.permissions.get(
permissionAccount.publicKey.toBase58()
);
if (!permissions) {
if (VERBOSE) {
console.error(`No permissions found for aggregator ${aggregatorKey}`);
}
continue;
}
// check lease account exists and has an active balance
const [leaseAccount] = sbv2.LeaseAccount.fromSeed(
oldProgram,
sbv2.SWITCHBOARD_LABS_DEVNET_PERMISSIONLESS_QUEUE,
new PublicKey(aggregatorKey)
);
const lease = oldProgramAccounts.leases.get(
leaseAccount.publicKey.toBase58()
);
if (!lease) {
if (VERBOSE) {
console.error(`No lease found for aggregator ${aggregatorKey}`);
}
continue;
}
const jobPubkeys = aggregator.jobPubkeysData.slice(
0,
aggregator.jobPubkeysSize
);
const aggregatorJobs: Array<JobDefinition> = [];
for (const job of jobPubkeys) {
const jobData = oldProgramAccounts.jobs.get(job.toBase58());
if (!jobData) {
if (VERBOSE) {
console.error(`Failed to find jobData for ${job}`);
}
continue;
}
let oracleJob: OracleJob | undefined = undefined;
try {
oracleJob = OracleJob.decodeDelimited(jobData.data);
} catch (error) {
if (VERBOSE) {
console.error(`Failed to decode job ${job.toBase58()}, ${error}`);
}
}
if (!oracleJob) {
continue;
}
const jobDefinition = {
account: new sbv2.JobAccount(oldProgram, job),
data: jobData,
oracleJob: oracleJob,
};
aggregatorJobs.push(jobDefinition);
}
let historyBufferLength: number | undefined = undefined;
if (!aggregator.historyBuffer.equals(PublicKey.default)) {
const buffer = oldProgramAccounts.buffers.get(
aggregator.historyBuffer.toBase58()
);
if (buffer) {
historyBufferLength = Math.ceil((buffer.byteLength - 8) / 28);
} else {
console.error(
`Failed to fetch history buffer for aggregator ${aggregatorKey} (${aggregator.historyBuffer})`
);
}
}
aggregators.set(aggregatorKey, {
account: new sbv2.AggregatorAccount(
oldProgram,
new PublicKey(aggregatorKey)
),
data: aggregator,
historyBufferLength,
permissionAccount,
permissions,
leaseAccount,
lease,
jobs: aggregatorJobs,
});
}
// fetch all lease balances in batches of 100
const leaseBalanceMap = new Map<string, number>();
const allLeaseEscrows: Array<PublicKey> = Array.from(
aggregators.entries()
).map(([aggregatorKey, aggregator]) => aggregator.lease.escrow);
const leaseBatches: Array<Array<PublicKey>> = _.chunk(allLeaseEscrows, 100);
for await (const batch of leaseBatches) {
const leaseEscrowAccountInfos = await anchor.utils.rpc.getMultipleAccounts(
oldProgram.connection,
batch
);
for (const leaseEscrowAccountInfo of leaseEscrowAccountInfos) {
try {
const account = spl.AccountLayout.decode(
leaseEscrowAccountInfo!.account.data!
);
const leaseBalance =
Number.parseInt(account.amount.toString()) / LAMPORTS_PER_SOL;
leaseBalanceMap.set(
leaseEscrowAccountInfo!.publicKey.toBase58(),
leaseBalance
);
} catch (error) {
if (VERBOSE) {
console.error(`Failed to fetch lease escrow balance`);
}
}
}
}
const aggregatorToMigrate = new Map<
string,
AggregatorDefinitionWithLeaseBalance
>();
const jobsToMigrate = new Map<string, JobDefinition>();
for (const [aggregatorKey, aggregator] of aggregators.entries()) {
const leaseBalance = leaseBalanceMap.get(
aggregator.lease.escrow.toBase58()
);
if (!leaseBalance) {
continue;
}
if (leaseBalance < LEASE_THRESHOLD) {
console.error(
`Lease is below threshold for aggregator ${aggregatorKey}, lease balance = ${leaseBalance}`
);
continue;
} else {
aggregatorToMigrate.set(aggregatorKey, {
...aggregator,
balance: leaseBalance,
});
for (const job of aggregator.jobs) {
jobsToMigrate.set(job.account.publicKey.toBase58(), job);
}
}
}
console.log(`Found ${aggregatorToMigrate.size} aggregators to migrate`);
console.log(`Found ${jobsToMigrate.size} jobs to migrate`);
const jobPubkeys: Array<string> = [];
for (const [jobKey, job] of jobsToMigrate.entries()) {
try {
const jobInitDefinition = {
data: `[${new Uint8Array(job.data.data)}]`,
name: toUtf8(job.data.name),
authority: job.data.authority.toBase58(),
expiration: job.data.expiration.toNumber(),
variables: undefined,
};
const fileString = JSON.stringify(
{
publicKey: jobKey,
definition: jobInitDefinition,
data: job,
},
jsonReplacers,
2
);
if (fileString.includes('twapTask')) {
const twapTaskPath = path.join(dirPath, 'twapJobs');
fs.mkdirSync(twapTaskPath, { recursive: true });
fs.writeFileSync(path.join(twapTaskPath, `${jobKey}.json`), fileString);
} else {
fs.writeFileSync(path.join(jobDirPath, `${jobKey}.json`), fileString);
}
jobPubkeys.push(jobKey);
} catch (error) {}
}
fs.writeFileSync(path.join(dirPath, 'jobs.txt'), jobPubkeys.join('\n'));
const aggregatorPubkeys: Array<string> = [];
for (const [aggregatorKey, aggregator] of aggregatorToMigrate.entries()) {
try {
const jobs: Array<{ pubkey: string; weight: number }> = [];
for (const [i, job] of aggregator.data.jobPubkeysData
.slice(0, aggregator.data.jobPubkeysSize)
.entries()) {
jobs.push({
pubkey: job.toBase58(),
weight:
aggregator.data.jobWeights.length >= i
? aggregator.data.jobWeights[i] ?? 1
: 1,
});
}
const aggregatorInitDefinition = {
name: toUtf8(aggregator.data.name),
metadata: toUtf8(aggregator.data.metadata),
batchSize: aggregator.data.oracleRequestBatchSize,
minRequiredOracleResults: aggregator.data.minOracleResults,
minRequiredJobResults: aggregator.data.minJobResults,
minUpdateDelaySeconds: aggregator.data.minUpdateDelaySeconds,
startAfter: aggregator.data.startAfter.toNumber(),
varianceThreshold: aggregator.data.varianceThreshold.toBig().toNumber(),
forceReportPeriod: aggregator.data.forceReportPeriod.toNumber(),
expiration: aggregator.data.expiration.toNumber(),
disableCrank: aggregator.data.disableCrank,
authority: aggregator.data.authority.toBase58(),
historyLimit: aggregator.historyBufferLength,
slidingWindow:
aggregator.data.resolutionMode.kind === 'ModeSlidingResolution',
basePriorityFee: aggregator.data.basePriorityFee,
priorityFeeBump: aggregator.data.priorityFeeBump,
priorityFeeBumpPeriod: aggregator.data.priorityFeeBumpPeriod,
maxPriorityFeeMultiplier: aggregator.data.maxPriorityFeeMultiplier,
jobs: jobs,
pushCrank: !aggregator.data.crankPubkey.equals(PublicKey.default),
};
fs.writeFileSync(
path.join(feedDirPath, `${aggregatorKey}.json`),
JSON.stringify(
{
publicKey: aggregatorKey,
definition: aggregatorInitDefinition,
data: aggregator,
},
jsonReplacers,
2
)
);
aggregatorPubkeys.push(aggregatorKey);
} catch (error) {}
}
fs.writeFileSync(
path.join(dirPath, 'aggregators.txt'),
aggregatorPubkeys.join('\n')
);
}
main().catch(error => {
console.error(error);
});