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"