sbv2-solana/cli/src/accounts/oracle/oracle.ts

375 lines
10 KiB
TypeScript

import * as anchor from "@project-serum/anchor";
import { Keypair, PublicKey } from "@solana/web3.js";
import {
OracleAccount,
OracleQueueAccount,
programWallet,
} from "@switchboard-xyz/switchboard-v2";
import chalk from "chalk";
import {
anchorBNtoDateTimeString,
buffer2string,
chalkString,
pubKeyConverter,
} from "../";
import { CommandContext, DEFAULT_CONTEXT } from "../../types/context";
import { LogProvider } from "../../types/context/logging";
import { getProgramPayer } from "../../utils";
import { PermissionClass } from "../permission";
import { ProgramStateClass } from "../state";
import {
fromOracleJSON,
IOracleClass,
OracleAccountData,
OracleDefinition,
OracleMetricsData,
} from "./types";
export class OracleClass implements IOracleClass {
account: OracleAccount;
logger: LogProvider;
publicKey: PublicKey;
name: string;
metadata: string;
authorityPublicKey: PublicKey;
tokenAccountPublicKey: PublicKey;
queuePublicKey: PublicKey;
balance: number;
lastHeartbeat: string;
numInUse: number;
metrics: OracleMetricsData;
permissionAccount?: PermissionClass;
private constructor() {}
private static async init(
context: CommandContext,
account: OracleAccount,
definition: OracleDefinition
): Promise<OracleClass> {
const oracle = new OracleClass();
oracle.logger = context.logger;
oracle.account = account;
oracle.publicKey = oracle.account.publicKey;
await oracle.loadData();
const queueAccount = new OracleQueueAccount({
program: account.program,
publicKey: oracle.queuePublicKey,
});
try {
oracle.permissionAccount = await PermissionClass.build(
context,
oracle.account,
queueAccount,
definition && "permissionAccount" in definition
? definition.permissionAccount
: undefined
);
} catch {}
await oracle.loadData();
return oracle;
}
async grantPermission(
context: CommandContext,
queueAuthority = programWallet(this.account.program)
): Promise<string> {
const queueAccount = new OracleQueueAccount({
program: this.account.program,
publicKey: this.queuePublicKey,
});
const { authority } = await queueAccount.loadData();
if (
this.permissionAccount.permission === "NONE" &&
queueAuthority.publicKey.equals(authority)
) {
const anchorWallet = programWallet(this.account.program);
this.permissionAccount = await PermissionClass.grantPermission(
context,
this.account,
anchorWallet
);
}
return this.permissionAccount.permission;
}
static async build(
context: CommandContext,
program: anchor.Program,
definition: OracleDefinition,
queueAccount?: OracleQueueAccount
): Promise<OracleClass> {
if ("account" in definition) {
if (definition.account instanceof OracleAccount) {
return OracleClass.fromAccount(context, definition.account);
}
throw new TypeError("account must be an instance of OracleAccount");
} else if ("publicKey" in definition) {
return OracleClass.fromPublicKey(context, program, definition.publicKey);
} else if (queueAccount) {
return OracleClass.fromJSON(context, queueAccount, definition);
}
throw new Error(
`need to provide oracle queue account to build new oracle account`
);
}
public static async fromAccount(
context: CommandContext,
account: OracleAccount
): Promise<OracleClass> {
return OracleClass.init(context, account, {});
}
public static fromPublicKey(
context: CommandContext,
program: anchor.Program,
publicKey: PublicKey
) {
return OracleClass.init(
context,
new OracleAccount({
program,
publicKey,
}),
{}
);
}
private static async fromJSON(
context: CommandContext,
queueAccount: OracleQueueAccount,
definition: fromOracleJSON
) {
const account = await OracleAccount.create(queueAccount.program, {
queueAccount,
name: definition.name ? Buffer.from(definition.name) : Buffer.from(""),
oracleAuthority: definition.authorityKeypair,
metadata: definition.metadata
? Buffer.from(definition.metadata)
: Buffer.from(""),
});
context.logger.info(
`created oracle account ${definition.name} ${account.publicKey}`
);
return OracleClass.init(context, account, definition);
}
static async fromDefault(
context: CommandContext,
queueAccount: OracleQueueAccount,
name = ""
): Promise<OracleClass> {
return OracleClass.build(
context,
queueAccount.program,
{ name },
queueAccount
);
}
static async getBalance(
oracleAccount: OracleAccount,
tokenAccount?: PublicKey,
context = DEFAULT_CONTEXT
): Promise<number> {
const oracleTokenAccount =
// eslint-disable-next-line unicorn/no-await-expression-member
tokenAccount ?? (await oracleAccount.loadData()).tokenAccount;
const tokenAmount =
await oracleAccount.program.provider.connection.getTokenAccountBalance(
oracleTokenAccount
);
return Number.parseInt(tokenAmount.value.amount, 10);
}
static async withdrawTokens(
context: CommandContext,
oracleAccount: OracleAccount,
amount: number,
withdrawAccount: PublicKey,
authority?: Keypair,
force = false
): Promise<string> {
const { queuePubkey, tokenAccount, oracleAuthority } =
await oracleAccount.loadData();
const authorityKeypair =
authority || getProgramPayer(oracleAccount.program);
if (!oracleAuthority.equals(authorityKeypair.publicKey)) {
throw new Error(
`invalid oracle authority provided (expected) ${oracleAuthority}, (received) ${authority.publicKey}`
);
}
const oracleQueueAccount = new OracleQueueAccount({
program: oracleAccount.program,
publicKey: queuePubkey,
});
const oracleQueueData = await oracleQueueAccount.loadData();
const minStake: number = oracleQueueData.minStake.toNumber();
// check final balance is greater than min stake
const initialOracleBalance = await OracleClass.getBalance(
oracleAccount,
tokenAccount
);
const finalOracleBalance = initialOracleBalance - amount;
if (amount > initialOracleBalance) {
throw new Error(
`requested withdraw amount ${amount} exceeds current balance ${initialOracleBalance}`
);
}
if (!force && minStake > finalOracleBalance)
throw new Error(
`withdrawing will result in your account falling below the minimum stake`
);
// withdraw
const withdrawTxn = await oracleAccount.withdraw({
amount: new anchor.BN(amount),
oracleAuthority: authorityKeypair,
withdrawAccount,
});
return withdrawTxn;
}
static async depositTokens(
context: CommandContext,
oracleAccount: OracleAccount,
amount: number,
funderTokenAccount?: PublicKey
): Promise<string> {
const oracleTokenAccount =
// eslint-disable-next-line unicorn/no-await-expression-member
(await oracleAccount.loadData()).tokenAccount;
const state = await ProgramStateClass.build(oracleAccount.program, context);
const payerTokenAccount =
funderTokenAccount ||
(await ProgramStateClass.getProgramTokenAddress(
oracleAccount.program,
context
));
// check payer has enough funds
const payerTokenBalance =
await oracleAccount.program.provider.connection.getBalance(
payerTokenAccount
);
if (amount > payerTokenBalance)
throw new Error(
`trying to deposit ${amount} tokens but current balance is ${payerTokenBalance}`
);
return state.token.transfer(
payerTokenAccount,
oracleTokenAccount,
programWallet(oracleAccount.program),
[],
amount
);
}
// loads anchor idl and parses response
async loadData() {
const data: OracleAccountData = await this.account.loadData();
this.publicKey = this.account.publicKey;
this.name = buffer2string(data.name as any);
this.metadata = buffer2string(data.metadata as any);
this.authorityPublicKey = data.oracleAuthority;
this.lastHeartbeat = anchorBNtoDateTimeString(data.lastHeartbeat);
this.numInUse = data.numInUse;
this.tokenAccountPublicKey = data.tokenAccount;
this.queuePublicKey = data.queuePubkey;
this.metrics = data.metrics;
this.balance = await OracleClass.getBalance(
this.account,
this.tokenAccountPublicKey
);
}
toJSON(): IOracleClass {
return {
name: this.name,
metadata: this.metadata,
publicKey: this.publicKey,
authorityPublicKey: this.authorityPublicKey,
queuePublicKey: this.queuePublicKey,
tokenAccountPublicKey: this.tokenAccountPublicKey,
permissionAccount: this.permissionAccount.toJSON(),
};
}
toString(): string {
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
}
prettyPrint(all = false, SPACING = 24): string {
let outputString = "";
outputString += chalk.underline(
chalkString("## Oracle", this.publicKey.toString(), SPACING) + "\r\n"
);
outputString += chalkString("name", this.name, SPACING) + "\r\n";
outputString += chalkString("metadata", this.metadata, SPACING) + "\r\n";
outputString += chalkString("balance", this.balance, SPACING) + "\r\n";
outputString +=
chalkString("oracleAuthority", this.authorityPublicKey, SPACING) + "\r\n";
outputString +=
chalkString("tokenAccount", this.tokenAccountPublicKey, SPACING) + "\r\n";
outputString +=
chalkString("queuePubkey", this.queuePublicKey, SPACING) + "\r\n";
if (this.permissionAccount) {
outputString +=
chalkString(
"permissionAccount",
this.permissionAccount.publicKey || "N/A",
SPACING
) + "\r\n";
outputString +=
chalkString(
"permissions",
this.permissionAccount.permission || "",
SPACING
) + "\r\n";
}
outputString +=
chalkString("lastHeartbeat", this.lastHeartbeat, SPACING) + "\r\n";
outputString += chalkString("numInUse", this.numInUse, SPACING) + "\r\n";
outputString +=
chalkString(
"metrics",
JSON.stringify(this.metrics, undefined, 2),
SPACING
) + "\r\n";
if (all && this.permissionAccount) {
outputString += this.permissionAccount.prettyPrint(all, SPACING);
}
return outputString;
}
}