Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved error handling #2

Merged
merged 2 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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