diff --git a/cli/src/OutputFileBaseCommand.ts b/cli/src/OutputFileBaseCommand.ts new file mode 100644 index 0000000..d8ccab4 --- /dev/null +++ b/cli/src/OutputFileBaseCommand.ts @@ -0,0 +1,148 @@ +import { Flags } from "@oclif/core"; +import { Input } from "@oclif/parser"; +import * as anchor from "@project-serum/anchor"; +import { SwitchboardDecimal } from "@switchboard-xyz/switchboard-v2"; +import Big from "big.js"; +import fs from "fs"; +import path from "path"; +import BaseCommand from "./BaseCommand"; + +abstract class OutputFileBaseCommand extends BaseCommand { + outputBasePath: string; + + // outputTxtFile?: string; + + outputJsonFile?: string; + + outputCsvFile?: string; + + static flags = { + ...BaseCommand.flags, + force: Flags.boolean({ description: "overwrite output file if exists" }), + outputFile: Flags.string({ + char: "f", + description: "output file to save aggregator pubkeys to", + required: true, + }), + json: Flags.boolean({ + description: "output aggregator accounts in json format", + }), + csv: Flags.boolean({ + description: "output aggregator accounts in csv format", + }), + // txt: Flags.boolean({ + // description: "output aggregator pubkeys in txt format", + // }), + }; + + async init() { + await super.init(); + const { flags } = await this.parse((>this.constructor) as any); + BaseCommand.flags = flags as any; + + const parsedPath = path.parse( + flags.outputFile.startsWith("/") || flags.outputFile.startsWith("C:") + ? flags.outputFile + : path.join(process.cwd(), flags.outputFile) + ); + this.outputBasePath = path.join(parsedPath.dir, parsedPath.name); + + if (parsedPath.ext === ".json" || flags.json) { + this.outputJsonFile = `${this.outputBasePath}.json`; + if (fs.existsSync(this.outputJsonFile) && !flags.force) { + throw new Error( + `output json file already exists: ${this.outputJsonFile}` + ); + } + } + + if (parsedPath.ext === ".csv" || flags.csv) { + this.outputCsvFile = `${this.outputBasePath}.csv`; + if (fs.existsSync(this.outputCsvFile) && !flags.force) { + throw new Error( + `output csv file already exists: ${this.outputCsvFile}` + ); + } + } + + if (!(this.outputJsonFile || this.outputCsvFile)) { + throw new Error(`no output format specified, try --json, or --csv`); + } + } + + jsonReplacers(key: any, value: any) { + if (typeof value === "string") { + return value; + } else if (typeof value === "number") { + return value; + } else if (typeof value === "boolean") { + return value.toString(); + } else { + if (value instanceof Big) { + return value.toString(); + } else if (anchor.BN.isBN(value)) { + return value.toString(10); + } else if ( + ("scale" in value && "mantissa" in value) || + value instanceof SwitchboardDecimal + ) { + return new SwitchboardDecimal(value.mantissa, value.scale) + .toBig() + .toString(); + } + } + + return value; + } + + save(rows?: T | T[], headers?: K[]) { + if (rows) { + this.saveJson(rows); + } + if (rows !== undefined && headers !== undefined) { + this.saveCsv(rows, headers); + } + } + + saveJson(rows: T | T[]) { + if (this.outputJsonFile) { + fs.writeFileSync( + this.outputJsonFile, + JSON.stringify(rows, this.jsonReplacers, 2) + ); + } + } + + saveCsv(rows: T | T[], headers: K[]) { + if (this.outputCsvFile) { + const grid: string[][] = []; + grid.push(headers as string[]); + if (Array.isArray(rows)) { + rows.forEach((row) => { + const cols: string[] = []; + headers.forEach((col) => { + const val = row[col]; + cols.push( + typeof val === "string" ? val : this.jsonReplacers(undefined, val) + ); + }); + grid.push(cols); + }); + } else { + const cols: string[] = []; + headers.forEach((col) => { + const val = rows[col]; + cols.push( + typeof val === "string" ? val : this.jsonReplacers(undefined, val) + ); + }); + grid.push(cols); + } + + const lines = grid.map((col) => col.join(",")); + fs.writeFileSync(this.outputCsvFile, lines.join("\n")); + } + } +} + +export default OutputFileBaseCommand; diff --git a/cli/src/commands/aggregator/save/history.ts b/cli/src/commands/aggregator/save/history.ts new file mode 100644 index 0000000..249d96d --- /dev/null +++ b/cli/src/commands/aggregator/save/history.ts @@ -0,0 +1,63 @@ +import { PublicKey } from "@solana/web3.js"; +import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2"; +import OutputFileBaseCommand from "../../../OutputFileBaseCommand"; + +export default class AggregatorUpdate extends OutputFileBaseCommand { + static description = "request a new aggregator result from a set of oracles"; + + static flags = { + ...OutputFileBaseCommand.flags, + }; + + static args = [ + { + name: "aggregatorKey", + description: "public key of the aggregator account to deserialize", + }, + ]; + + static examples = [ + "$ sbv2 aggregator:save:history --outputFile ../aggregator-history.json --csv", + ]; + + async run() { + const { args, flags } = await this.parse(AggregatorUpdate); + + const aggregatorAccount = new AggregatorAccount({ + program: this.program, + publicKey: new PublicKey(args.aggregatorKey), + }); + const aggregator = await aggregatorAccount.loadData(); + + const history = await aggregatorAccount.loadHistory(); + + this.save( + history.map((r) => { + return { + timestamp: Number.parseInt(r.timestamp.toString(10)), + datetime: new Date(r.timestamp.toNumber() * 1000).toUTCString(), + value: r.value, + }; + }), + ["timestamp", "datetime", "value"] + ); + + if (this.silent) { + return; + } else { + this.logger.log(`Files saved`); + } + } + + async catch(error) { + // if ( + // error instanceof AggregatorIllegalRoundOpenCall || + // error.toString().includes("0x177d") + // ) { + // this.context.logger.info(error.toString()); + // this.exit(0); + // } + + super.catch(error, "failed to save aggregator history"); + } +} diff --git a/cli/src/commands/sandbox.ts b/cli/src/commands/sandbox.ts index fb3fac6..3420111 100644 --- a/cli/src/commands/sandbox.ts +++ b/cli/src/commands/sandbox.ts @@ -1,4 +1,5 @@ import { Flags } from "@oclif/core"; +import { PublicKey } from "@solana/web3.js"; import BaseCommand from "../BaseCommand"; export default class SandboxCommand extends BaseCommand { @@ -70,110 +71,21 @@ export default class SandboxCommand extends BaseCommand { // console.log(byteArray.length); // console.log(`[${Uint8Array.from(historyKey.toBuffer()).toString()}]`); - // const parseAddress = new PublicKey( - // "DpoK8Zz69APV9ntjuY9C4LZCxANYMV56M2cbXEdkjxME" - // ); - // console.log(`parseAddress: [${Uint8Array.from(parseAddress.toBuffer())}]`); + const parseAddress = new PublicKey( + "DpoK8Zz69APV9ntjuY9C4LZCxANYMV56M2cbXEdkjxME" + ); + console.log(`parseAddress: [${Uint8Array.from(parseAddress.toBuffer())}]`); - // const accountInfo = await this.program.provider.connection.getAccountInfo( - // parseAddress - // ); - // const byteArray = Uint8Array.from(accountInfo.data); - // console.log(`parseAddress Data [${byteArray.length}]: [${byteArray}]`); + const accountInfo = await this.program.provider.connection.getAccountInfo( + parseAddress + ); + const byteArray = Uint8Array.from(accountInfo.data); + console.log(`parseAddress Data [${byteArray.length}]: [${byteArray}]`); - // const owner = accountInfo.owner; - // console.log(`Owner: [${Uint8Array.from(owner.toBuffer())}]`); + const owner = accountInfo.owner; + console.log(`Owner: [${Uint8Array.from(owner.toBuffer())}]`); - // console.log(accountInfo.lamports); - - // const jobData = Buffer.from( - // OracleJob.encodeDelimited( - // OracleJob.create({ - // tasks: [ - // OracleJob.Task.create({ - // httpTask: OracleJob.HttpTask.create({ - // url: "https://jsonplaceholder.typicode.com/todos/1", - // }), - // }), - // ], - // }) - // ).finish() - // ); - - // const jobLen = jobData.length; - // console.log(`JobData Len: ${jobLen}`); - - // const bufferAccountSize = - // this.program.account.bufferRelayerAccountData.size; - // const bufferAccountSize = 2048; - // const bufferRentExemption = - // await this.program.provider.connection.getMinimumBalanceForRentExemption( - // bufferAccountSize - // ); - - // console.log( - // chalkString( - // "Buffer", - // `${bufferRentExemption} lamports, ${ - // bufferRentExemption / LAMPORTS_PER_SOL - // } SOL, ${bufferAccountSize} bytes` - // ) - // ); - - // const escrowSize = spl.ACCOUNT_SIZE; - // const escrowRentExemption = - // await this.program.provider.connection.getMinimumBalanceForRentExemption( - // escrowSize - // ); - // console.log( - // chalkString( - // "Escrow", - // `${escrowRentExemption} lamports, ${ - // escrowRentExemption / LAMPORTS_PER_SOL - // } SOL, ${escrowSize} bytes` - // ) - // ); - - // const jobAccountSize = - // this.program.account.jobAccountData.size + jobData.length; - // const jobRentExemption = - // await this.program.provider.connection.getMinimumBalanceForRentExemption( - // jobAccountSize - // ); - - // console.log( - // chalkString( - // "Job", - // `${jobRentExemption} lamports, ${ - // jobRentExemption / LAMPORTS_PER_SOL - // } SOL, ${jobAccountSize} bytes` - // ) - // ); - - // const permissionAccountSize = - // this.program.account.permissionAccountData.size; - // const permissionRentExemption = - // await this.program.provider.connection.getMinimumBalanceForRentExemption( - // permissionAccountSize - // ); - // console.log( - // chalkString( - // "Permission", - // `${permissionRentExemption} lamports, ${ - // permissionRentExemption / LAMPORTS_PER_SOL - // } SOL, ${permissionAccountSize} bytes` - // ) - // ); - - // const totalLamports = - // bufferRentExemption + - // escrowRentExemption + - // jobRentExemption + - // permissionRentExemption; - - // console.log(chalkString("Total Lamports", totalLamports)); - - // console.log(chalkString("Total SOL", totalLamports / LAMPORTS_PER_SOL)); + console.log(accountInfo.lamports); } async catch(error) { diff --git a/libraries/protos/job_schemas.proto b/libraries/protos/job_schemas.proto index 16e268e..ee3bf43 100644 --- a/libraries/protos/job_schemas.proto +++ b/libraries/protos/job_schemas.proto @@ -51,6 +51,10 @@ message OracleJob { MAX = 2; // Sum up all of the results. SUM = 3; + // Average all of the results. + MEAN = 4; + // Grab the median of the results. + MEDIAN = 5; } // The technique that will be used to aggregate the results if walking the specified path // returns multiple numerical results. diff --git a/libraries/ts/package.json b/libraries/ts/package.json index c1abb93..4179105 100644 --- a/libraries/ts/package.json +++ b/libraries/ts/package.json @@ -1,6 +1,6 @@ { "name": "@switchboard-xyz/switchboard-v2", - "version": "0.0.123-beta.4", + "version": "0.0.123", "license": "MIT", "author": "mitch@switchboard.xyz", "description": "API wrapper for intergating with the Switchboardv2 program", diff --git a/libraries/ts/src/index.ts b/libraries/ts/src/index.ts index c4aca00..9adb3de 100644 --- a/libraries/ts/src/index.ts +++ b/libraries/ts/src/index.ts @@ -2,3 +2,13 @@ /*eslint-disable import/extensions */ export * from "./protos/index.js"; export * from "./sbv2.js"; + +import protobuf from "protobufjs/minimal.js"; + +// prevent enums from being converted to strings +protobuf.util.toJSONOptions = { + longs: String, + enums: Number, + bytes: String, + json: true, +}; diff --git a/libraries/ts/src/protos/index.d.ts b/libraries/ts/src/protos/index.d.ts index cbdc503..eda5fd9 100644 --- a/libraries/ts/src/protos/index.d.ts +++ b/libraries/ts/src/protos/index.d.ts @@ -408,7 +408,9 @@ export namespace OracleJob { NONE = 0, MIN = 1, MAX = 2, - SUM = 3 + SUM = 3, + MEAN = 4, + MEDIAN = 5 } } diff --git a/libraries/ts/src/protos/index.js b/libraries/ts/src/protos/index.js index 24d15f9..6f1a1c1 100644 --- a/libraries/ts/src/protos/index.js +++ b/libraries/ts/src/protos/index.js @@ -896,6 +896,8 @@ case 1: case 2: case 3: + case 4: + case 5: break; } return null; @@ -932,6 +934,14 @@ case 3: message.aggregationMethod = 3; break; + case "MEAN": + case 4: + message.aggregationMethod = 4; + break; + case "MEDIAN": + case 5: + message.aggregationMethod = 5; + break; } return message; }; @@ -979,6 +989,8 @@ * @property {number} MIN=1 MIN value * @property {number} MAX=2 MAX value * @property {number} SUM=3 SUM value + * @property {number} MEAN=4 MEAN value + * @property {number} MEDIAN=5 MEDIAN value */ JsonParseTask.AggregationMethod = (function() { var valuesById = {}, values = Object.create(valuesById); @@ -986,6 +998,8 @@ values[valuesById[1] = "MIN"] = 1; values[valuesById[2] = "MAX"] = 2; values[valuesById[3] = "SUM"] = 3; + values[valuesById[4] = "MEAN"] = 4; + values[valuesById[5] = "MEDIAN"] = 5; return values; })(); diff --git a/libraries/ts/src/sbv2.ts b/libraries/ts/src/sbv2.ts index 6ee1055..fc19363 100644 --- a/libraries/ts/src/sbv2.ts +++ b/libraries/ts/src/sbv2.ts @@ -242,6 +242,10 @@ export class SwitchboardDecimal { result.e = e; return result; } + + toString() { + this.toBig().toString(); + } } /** diff --git a/libraries/ts/tsconfig.base.json b/libraries/ts/tsconfig.base.json index 36deb9d..764fcb7 100644 --- a/libraries/ts/tsconfig.base.json +++ b/libraries/ts/tsconfig.base.json @@ -14,7 +14,7 @@ "esModuleInterop": true, "resolveJsonModule": true, "skipLibCheck": true, - "types": ["mocha", "node", "long", "big.js"], + "types": ["mocha", "node", "long"], "allowJs": true, "checkJs": false, // strict diff --git a/yarn.lock b/yarn.lock index a157fe2..a781759 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4170,6 +4170,48 @@ node-fetch "^3.2.6" protobufjs "^6.11.3" +"@switchboard-xyz/switchboard-v2@^0.0.121": + version "0.0.121" + resolved "https://registry.npmjs.org/@switchboard-xyz/switchboard-v2/-/switchboard-v2-0.0.121.tgz#5df38491feba9262a13fb071c86179f9acfdc865" + integrity sha512-IPA3QD/nThH57ekJaD3aov/5fa/JLETNb3TeHKx8mfRSE/P2b94nosvXsYiDpXE05v/zS4oDk6FD1YOO+fZXzQ== + dependencies: + "@project-serum/anchor" "^0.24.2" + "@solana/spl-governance" "^0.0.34" + "@solana/spl-token" "^0.2.0" + "@solana/web3.js" "^1.44.3" + "@types/big.js" "^6.1.4" + assert "^2.0.0" + big.js "^6.2.0" + bs58 "^4.0.1" + chan "^0.6.1" + crypto-js "^4.0.0" + glob "^8.0.3" + long "^4.0.0" + mocha "^9.1.1" + node-fetch "^3.2.6" + protobufjs "^6.11.3" + +"@switchboard-xyz/switchboard-v2@^0.0.122": + version "0.0.122" + resolved "https://registry.npmjs.org/@switchboard-xyz/switchboard-v2/-/switchboard-v2-0.0.122.tgz#11713cd909e17c52cab424f1c13f037a7eac89ec" + integrity sha512-LAjAEHq3VSRyJxGJj/FF9Q48tCST0+F56H2/nZ66UhpALucuKs1ppOlAKoqrFeTC4KYoQhcK2cE+B9WmTxA1pg== + dependencies: + "@project-serum/anchor" "^0.24.2" + "@solana/spl-governance" "^0.0.34" + "@solana/spl-token" "^0.2.0" + "@solana/web3.js" "^1.44.3" + "@types/big.js" "^6.1.4" + assert "^2.0.0" + big.js "^6.2.0" + bs58 "^4.0.1" + chan "^0.6.1" + crypto-js "^4.0.0" + glob "^8.0.3" + long "^4.0.0" + mocha "^9.1.1" + node-fetch "^3.2.6" + protobufjs "^6.11.3" + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"