Skip to content

Commit

Permalink
feat: improved error handling (#2)
Browse files Browse the repository at this point in the history
* chore: add option to dump txs while triaging

* feat: not crashing on invalid IDLs, but returning failures instead
  • Loading branch information
thlorenz authored Nov 20, 2023
1 parent 76bac14 commit 553b19e
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 38 deletions.
5 changes: 4 additions & 1 deletion examples/check-idl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ const RPC = process.env.RPC ?? SOLANA_MAINNET

async function main() {
console.error('Checking IDLs on RPC %s', RPC)
const idlWrites = await findIdls(PROGRAM_ID, RPC)
const { idls: idlWrites, failures } = await findIdls(PROGRAM_ID, RPC)
console.log(JSON.stringify(parseWrites(idlWrites), null, 2))
console.log('Total of %d idls', idlWrites.length)
if (failures.length > 0) {
console.log('Failures: %O', failures)
}
}

main()
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"main": "dist/src/mudlands.js",
"types": "dist/src/mudlands.d.ts",
"scripts": {
"amman:start": "(cd test/anchor && amman start)",
"amman:stop": "amman stop",
"build": "tsc",
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"lint": "prettier --check src/ examples/ test/ README.md",
"lint:fix": "prettier --write src/ examples/ test/ README.md",
"depcheck": "depcheck",
"depcheck:fix": "for m in `depcheck --json | jq '.missing | keys[]' --raw-output`; do yarn add $m; done",
"test": "node --test --test-reporter=spec -r esbuild-runner/register ./test/*.ts",
Expand Down
85 changes: 64 additions & 21 deletions src/find-idls/idl-finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '../types'
import { unzipIdlAccData } from '../unzip'
import { fetchAccount, fetchSigs, fetchTx, logDebug, logTrace } from '../utils'
import { dumpTxs } from '../utils/dump-txs'
import {
extractCreateAccount,
ExtractCreateAccountResult,
Expand All @@ -20,6 +21,11 @@ import {
ExtractSetBufferResult,
} from './extract-setbuffer-tx'

export type IdlFailure = {
addr: string
err: Error
}

type IdlWrite = {
source: IdlSource
startSlot: number
Expand Down Expand Up @@ -53,23 +59,46 @@ export class IdlFinder {
readonly host: string
) {}

async findIdls(): Promise<DeserializedIdlInfo[]> {
const txIdls = await this.findIdlAccountTxs()
if (txIdls.length > 0) return txIdls
async findIdls(): Promise<{
idls: DeserializedIdlInfo[]
failures: IdlFailure[]
}> {
const { idls: txIdls, failures } = await this.findIdlAccountTxs()
if (txIdls.length > 0) return { idls: txIdls, failures }

logDebug(
const txNotFoundErr = new Error(
`Unable to find transactions for IDL address '${this.idlAddr}', trying to load account data`
)
const loadIdlFailures = [{ addr: '<None found>', err: txNotFoundErr }]
logDebug(txNotFoundErr.message)

// If we didn't find any IDL via transactions it could be due to the
// history of the RPC not reaching back far enough.
// Therefore we try to load the IDL that currently stored in the IDL account
const account = await fetchAccount(this.idlAddr, this.host)
if (account == null) {
logDebug(`Unable to find IDL at address '${this.idlAddr}'`)
return []
const accNotFoundErr = new Error(
`Unable to find IDL at address '${this.idlAddr}'`
)
logDebug(accNotFoundErr.message)
return {
idls: [],
failures: [
...loadIdlFailures,
{ addr: this.idlAddr, err: accNotFoundErr },
],
}
}
const data = Buffer.from(account.data[0], 'base64')
const unzipped = await unzipIdlAccData(data)
let unzipped
try {
unzipped = await unzipIdlAccData(data)
} catch (err: any) {
return {
idls: [],
failures: [...loadIdlFailures, { addr: this.idlAddr, err }],
}
}

// We set slots to 0 since we must assume that all accounts were written
// while this IDL was in use
Expand All @@ -80,10 +109,16 @@ export class IdlFinder {
idl: unzipped,
addr: this.idlAddr,
}
return [idlWrite]
return {
idls: [idlWrite],
failures: loadIdlFailures,
}
}

async findIdlAccountTxs(): Promise<DeserializedIdlInfo[]> {
async findIdlAccountTxs(): Promise<{
idls: DeserializedIdlInfo[]
failures: IdlFailure[]
}> {
const txs = (
await resolveTxsForAddress(this.idlAddr, 'IDL address', this.host)
).filter((tx) => tx.meta?.err == null)
Expand All @@ -99,6 +134,7 @@ export class IdlFinder {

logTrace('Transactions %O', infos)
}
await dumpTxs(txs)

// TODO(thlorenz): we look at the same transaction multiple times as we
// don't remove the ones that matched, we should improve on that
Expand Down Expand Up @@ -191,23 +227,30 @@ SetBuffer: %d`,

private async _deserializeIdlWrites(
grouped: Map<string, IdlWrite>
): Promise<DeserializedIdlInfo[]> {
): Promise<{ idls: DeserializedIdlInfo[]; failures: IdlFailure[] }> {
const idls = []
const failures: IdlFailure[] = []
for (const [addr, write] of grouped.entries()) {
write.writes.sort(sortBySlot)
const idl = await deserializeWriteTxData(write.writes.map((x) => x.data))
if (idl != null) {
idls.push({
source: write.source,
startSlot: write.startSlot,
slot: write.endSlot,
idl,
addr,
})
try {
const idl = await deserializeWriteTxData(
write.writes.map((x) => x.data)
)
if (idl != null) {
idls.push({
source: write.source,
startSlot: write.startSlot,
slot: write.endSlot,
idl,
addr,
})
}
} catch (err: any) {
failures.push({ addr, err })
}
}
idls.sort(sortBySlot)
return idls
return { idls, failures }
}

private _groupIdlWrites(): Map<string, IdlWrite> {
Expand Down Expand Up @@ -315,7 +358,7 @@ async function resolveTxsForAddress(addr: string, label: string, host: string) {
const sigs = await fetchSigs(addr, host)
logTrace(
`sigs for ${label} (${addr}):`,
sigs.map((sig) => sig.signature)
sigs.map((sig) => sig.signature).join('\n')
)
return Promise.all(sigs.map((sig) => fetchTx(sig.signature, host)))
}
2 changes: 1 addition & 1 deletion src/find-idls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
export * from './extract-createaccount-tx'
export * from './extract-idlwrite-tx'
export * from './extract-setbuffer-tx'
export { DeserializedIdlInfo } from './idl-finder'
export { DeserializedIdlInfo, IdlFailure } from './idl-finder'

import { idlAddrForProgram } from '../utils'
import { IdlFinder } from './idl-finder'
Expand Down
21 changes: 21 additions & 0 deletions src/utils/dump-txs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import path from 'path'
import fs from 'fs/promises'
import { TransactionInfo } from '../types'

async function dumpTx(dirname: string, tx: TransactionInfo) {
const sig = tx.transaction.signatures[0]
const json = JSON.stringify(tx, null, 2)
const filename = path.join(dirname, `${tx.slot}.${sig}.json`)

return fs.writeFile(filename, json, 'utf8')
}

export async function dumpTxs(txs: TransactionInfo[]) {
const dumpTxsSubdir = process.env.DUMP_TXS
if (dumpTxsSubdir == null) return

// NOTE: __dirname is broken when running with esr
const dirname = path.join(process.cwd(), 'txs', dumpTxsSubdir)
await fs.mkdir(dirname, { recursive: true })
return Promise.all(txs.map((tx) => dumpTx(dirname, tx)))
}
25 changes: 20 additions & 5 deletions test/anchor/test/large-idl.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import test from 'node:test'
import assert from 'assert/strict'
import {
FOO_IDL,
FOO_PROGRAM,
airdropFooAuth,
checkFailures,
configPaths,
initIdl,
parseWrites,
Expand All @@ -27,8 +29,18 @@ test('setup anchor', () => setupAnchor(paths))

test('airdrop', airdropFooAuth)

test('initially no idls', async () => {
const idlWrites = await findIdls(FOO_PROGRAM, LOCALHOST)
test('initially no idls', async (t) => {
const { idls: idlWrites, failures } = await findIdls(FOO_PROGRAM, LOCALHOST)
checkFailures(
t,
{
idlAddress: FOO_IDL,
txsFound: false,
idlAccountFound: false,
len: 2,
},
failures
)
assert.equal(idlWrites.length, 0)
})

Expand All @@ -37,7 +49,8 @@ test('init idl', async () => {
})

test('after init one idl', async (t) => {
const idlWrites = await findIdls(FOO_PROGRAM, LOCALHOST)
const { idls: idlWrites, failures } = await findIdls(FOO_PROGRAM, LOCALHOST)
assert.equal(failures.length, 0)
assert.equal(idlWrites.length, 1)

spok(t, parseWrites(idlWrites), [{ version: '0.0.0', ...collateralIdl }])
Expand All @@ -48,7 +61,8 @@ test('upgrade idl', () => {
})

test('after one upgrade two idls', async (t) => {
const idlWrites = await findIdls(FOO_PROGRAM, LOCALHOST)
const { idls: idlWrites, failures } = await findIdls(FOO_PROGRAM, LOCALHOST)
assert.equal(failures.length, 0)
assert.equal(idlWrites.length, 2)

spok(t, parseWrites(idlWrites), [
Expand All @@ -62,7 +76,8 @@ test('upgrade idl again', () => {
})

test('after another upgrade three idls', async (t) => {
const idlWrites = await findIdls(FOO_PROGRAM, LOCALHOST)
const { idls: idlWrites, failures } = await findIdls(FOO_PROGRAM, LOCALHOST)
assert.equal(failures.length, 0)
assert.equal(idlWrites.length, 3)

spok(t, parseWrites(idlWrites), [
Expand Down
25 changes: 20 additions & 5 deletions test/anchor/test/minimal-idl.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import test from 'node:test'
import assert from 'assert/strict'
import {
FOO_IDL,
FOO_PROGRAM,
airdropFooAuth,
checkFailures,
configPaths,
initIdl,
parseWrites,
Expand All @@ -21,8 +23,18 @@ test('setup anchor', () => setupAnchor(paths))

test('airdrop', airdropFooAuth)

test('initially no idls', async () => {
const idlWrites = await findIdls(FOO_PROGRAM, LOCALHOST)
test('initially no idls', async (t) => {
const { idls: idlWrites, failures } = await findIdls(FOO_PROGRAM, LOCALHOST)
checkFailures(
t,
{
idlAddress: FOO_IDL,
txsFound: false,
idlAccountFound: false,
len: 2,
},
failures
)
assert.equal(idlWrites.length, 0)
})

Expand All @@ -31,7 +43,8 @@ test('init idl', async () => {
})

test('after init one idl', async (t) => {
const idlWrites = await findIdls(FOO_PROGRAM, LOCALHOST)
const { idls: idlWrites, failures } = await findIdls(FOO_PROGRAM, LOCALHOST)
assert.equal(failures.length, 0)
assert.equal(idlWrites.length, 1)

spok(t, parseWrites(idlWrites), [
Expand All @@ -44,7 +57,8 @@ test('upgrade idl', () => {
})

test('after one upgrade two idls', async (t) => {
const idlWrites = await findIdls(FOO_PROGRAM, LOCALHOST)
const { idls: idlWrites, failures } = await findIdls(FOO_PROGRAM, LOCALHOST)
assert.equal(failures.length, 0)
assert.equal(idlWrites.length, 2)

spok(t, parseWrites(idlWrites), [
Expand All @@ -58,7 +72,8 @@ test('upgrade idl again', () => {
})

test('after another upgrade three idls', async (t) => {
const idlWrites = await findIdls(FOO_PROGRAM, LOCALHOST)
const { idls: idlWrites, failures } = await findIdls(FOO_PROGRAM, LOCALHOST)
assert.equal(failures.length, 0)
assert.equal(idlWrites.length, 3)

spok(t, parseWrites(idlWrites), [
Expand Down
1 change: 1 addition & 0 deletions test/anchor/test/utils/amman.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Amman, LOCALHOST } from '@metaplex-foundation/amman-client'
import { Connection, PublicKey } from '@solana/web3.js'

export const FOO_PROGRAM = '7w4ooixh9TFgfmcCUsDJzHd9QqDKyxz4Mq1Bke6PVXaY'
export const FOO_IDL = 'CyCbCVxJUzFbNnZGb4qXXVFMqGDK78ESX8zgeYZ4NVnt'
export const FOO_AUTH = 'FpZSvaqguQ2iqcJ8Xmc9AxTs4sUP7jJgJoFp8Hnj8a9P'

export const amman = Amman.instance({
Expand Down
32 changes: 32 additions & 0 deletions test/anchor/test/utils/check-failures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import spok, { Specifications, TestContext } from 'spok'
import assert from 'assert/strict'
import { IdlFailure } from '../../../../src/mudlands'

export function checkFailures(
t: TestContext,
expect: {
idlAddress: string
txsFound: boolean
idlAccountFound: boolean
len: number
},
failures: IdlFailure[]
) {
assert.equal(failures.length, expect.len)
if (!expect.txsFound) {
const failure = failures.shift()
spok(t, failure, <Specifications<IdlFailure>>{
$topic: 'txsNotFound failure',
err: (err: any) => spok.test(/unable to find transactions/i)(err.message),
})
}
if (!expect.idlAccountFound) {
const failure = failures.shift()
spok(t, failure, <Specifications<IdlFailure>>{
$topic: 'idlAccountNotFound failure',
addr: expect.idlAddress,
err: (err: any) =>
spok.test(/unable to find idl at address/i)(err.message),
})
}
}
1 change: 1 addition & 0 deletions test/anchor/test/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './amman'
export * from './anchor-tasks'
export * from './setup-anchor'
export * from './check-failures'

export function parseWrites(writes: { idl: Buffer }[]) {
return writes
Expand Down
Loading

0 comments on commit 553b19e

Please sign in to comment.