From 46722953c15f1f2ffa9a14958c4d9ab125fb9c35 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Sat, 29 Jan 2022 16:46:12 -0500 Subject: [PATCH] ts: verified build utility (#1371) --- CHANGELOG.md | 1 + ts/package.json | 3 +- ts/src/utils/index.ts | 1 + ts/src/utils/registry.ts | 110 +++++++++++++++++++++++++++++++++++++++ ts/yarn.lock | 40 ++++++++++++-- 5 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 ts/src/utils/registry.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 56ad1f91ef..ad8948dfc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ incremented for features. * lang: `Context` now has a new `bumps: BTree` argument, mapping account name to bump seed "found" by the accounts context. This allows one to access bump seeds without having to pass them in from the client or recalculate them in the handler ([#1367](calculated )). * ts: Remove error logging in the event parser when log websocket encounters a program error ([#1313](https://github.com/project-serum/anchor/pull/1313)). * ts: Add new `methods` namespace to the program client, introducing a more ergonomic builder API ([#1324](https://github.com/project-serum/anchor/pull/1324)). +* ts: Add registry utility for fetching the latest verified build ([#1371](https://github.com/project-serum/anchor/pull/1371)). ### Breaking diff --git a/ts/package.json b/ts/package.json index 27ce7c1077..eb5a60d76c 100644 --- a/ts/package.json +++ b/ts/package.json @@ -33,13 +33,14 @@ "test": "jest tests --detectOpenHandles" }, "dependencies": { - "@project-serum/borsh": "^0.2.2", + "@project-serum/borsh": "^0.2.4", "@solana/web3.js": "^1.17.0", "base64-js": "^1.5.1", "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.2", "camelcase": "^5.3.1", + "cross-fetch": "^3.1.5", "crypto-hash": "^1.3.0", "eventemitter3": "^4.0.7", "find": "^0.3.0", diff --git a/ts/src/utils/index.ts b/ts/src/utils/index.ts index 27983ecdcf..86b8895544 100644 --- a/ts/src/utils/index.ts +++ b/ts/src/utils/index.ts @@ -4,3 +4,4 @@ export * as publicKey from "./pubkey.js"; export * as bytes from "./bytes/index.js"; export * as token from "./token.js"; export * as features from "./features.js"; +export * as registry from "./registry.js"; diff --git a/ts/src/utils/registry.ts b/ts/src/utils/registry.ts new file mode 100644 index 0000000000..6002560e98 --- /dev/null +++ b/ts/src/utils/registry.ts @@ -0,0 +1,110 @@ +import BN from "bn.js"; +import fetch from "cross-fetch"; +import * as borsh from "@project-serum/borsh"; +import { Connection, PublicKey } from "@solana/web3.js"; + +/** + * Returns a verified build from the anchor registry. null if no such + * verified build exists, e.g., if the program has been upgraded since the + * last verified build. + */ +export async function verifiedBuild( + connection: Connection, + programId: PublicKey, + limit: number = 5 +): Promise { + const url = `https://anchor.projectserum.com/api/v0/program/${programId.toString()}/latest?limit=${limit}`; + const [programData, latestBuildsResp] = await Promise.all([ + fetchData(connection, programId), + fetch(url), + ]); + + // Filter out all non successful builds. + const latestBuilds = (await latestBuildsResp.json()).filter( + (b: Build) => !b.aborted && b.state === "Built" && b.verified === "Verified" + ); + if (latestBuilds.length === 0) { + return null; + } + + // Get the latest build. + const build = latestBuilds[0]; + + // Has the program been upgraded since the last build? + if (programData.slot.toNumber() !== build.verified_slot) { + return null; + } + + // Success. + return build; +} + +/** + * Returns the program data account for this program, containing the + * metadata for this program, e.g., the upgrade authority. + */ +export async function fetchData( + connection: Connection, + programId: PublicKey +): Promise { + const accountInfo = await connection.getAccountInfo(programId); + if (accountInfo === null) { + throw new Error("program account not found"); + } + const { program } = decodeUpgradeableLoaderState(accountInfo.data); + const programdataAccountInfo = await connection.getAccountInfo( + program.programdataAddress + ); + if (programdataAccountInfo === null) { + throw new Error("program data account not found"); + } + const { programData } = decodeUpgradeableLoaderState( + programdataAccountInfo.data + ); + return programData; +} + +const UPGRADEABLE_LOADER_STATE_LAYOUT = borsh.rustEnum( + [ + borsh.struct([], "uninitialized"), + borsh.struct( + [borsh.option(borsh.publicKey(), "authorityAddress")], + "buffer" + ), + borsh.struct([borsh.publicKey("programdataAddress")], "program"), + borsh.struct( + [ + borsh.u64("slot"), + borsh.option(borsh.publicKey(), "upgradeAuthorityAddress"), + ], + "programData" + ), + ], + undefined, + borsh.u32() +); + +export function decodeUpgradeableLoaderState(data: Buffer): any { + return UPGRADEABLE_LOADER_STATE_LAYOUT.decode(data); +} + +export type ProgramData = { + slot: BN; + upgradeAuthorityAddress: PublicKey | null; +}; + +export type Build = { + aborted: boolean; + address: string; + created_at: string; + updated_at: string; + descriptor: string[]; + docker: string; + id: number; + name: string; + sha256: string; + upgrade_authority: string; + verified: string; + verified_slot: number; + state: string; +}; diff --git a/ts/yarn.lock b/ts/yarn.lock index b4ed0e315a..36eeefd6ce 100644 --- a/ts/yarn.lock +++ b/ts/yarn.lock @@ -855,10 +855,10 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" -"@project-serum/borsh@^0.2.2": - version "0.2.2" - resolved "https://registry.npmjs.org/@project-serum/borsh/-/borsh-0.2.2.tgz" - integrity sha512-Ms+aWmGVW6bWd3b0+MWwoaYig2QD0F90h0uhr7AzY3dpCb5e2S6RsRW02vFTfa085pY2VLB7nTZNbFECQ1liTg== +"@project-serum/borsh@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.2.4.tgz#8884c3a759984a39d54bf5b7390bd1ee0b579f16" + integrity sha512-tQPc1ktAp1Jtn9D72DmObAfhAic9ivfYBOS5b+T4H7MvkQ84uML88LY1LfvGep30mCy+ua5rf+X9ocPfg6u9MA== dependencies: bn.js "^5.1.2" buffer-layout "^1.2.0" @@ -1836,6 +1836,13 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" @@ -3744,6 +3751,13 @@ node-addon-api@^2.0.0: resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz" @@ -4695,6 +4709,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + traverse-chain@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" @@ -4929,6 +4948,11 @@ walker@^1.0.7: dependencies: makeerror "1.0.x" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz" @@ -4951,6 +4975,14 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0: version "8.4.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz"