Skip to content

Commit

Permalink
feat: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
thlorenz committed Oct 31, 2023
0 parents commit 1182645
Show file tree
Hide file tree
Showing 53 changed files with 6,085 additions and 0 deletions.
1 change: 1 addition & 0 deletions .depcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ignores: ['esbuild', 'esbuild-runner', '@metaplex-foundation/amman', 'supports-color']
1 change: 1 addition & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../prettierrc-base.js')
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Mudlands

Mu _ltiple_ _I_ dl and S _olana tooling_.

## Example

To try this example have a look at [`./examples/cndy-idls.ts`](./examples/cndy-idls.ts).

For another example have a look at [`./examples/usd-idls.ts`](./examples/usd-idls.ts) (this one
only uploaded one IDL so it finishes quickly).

Run with `yarn ex:usd`, but replace `SOLANA_MAINNET` with an RPC node like helius first.

```ts
import { findIdls } from '@ironforge/mudlands'

// Helper to log IDLs
function parseWrites(writes: { idl: Buffer }[]) {
return writes.map((w) => JSON.parse(w.idl.toString()))
}

// Find all IDLs created for CandyMachine
const CANDY_PROGRAM_ID = 'cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ'

// May use different RPC provider to avoid getting rate limited
const SOLANA_MAINNET = 'https://api.mainnet-beta.solana.com'

const idlWrites = await findIdls(CANDY_PROGRAM_ID, SOLANA_MAINNET)
console.log(JSON.stringify(parseWrites(idlWrites), null, 2))
```

## LICENSE

MIT
30 changes: 30 additions & 0 deletions examples/check-idl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { findIdls } from '../src/mudlands'

// Helper to log IDLs
function parseWrites(writes: { idl: Buffer }[]) {
return writes.map((w) => JSON.parse(w.idl.toString()))
}

const PROGRAM_ID = process.argv[2]
if (PROGRAM_ID == null) {
console.error('Usage: esr check-idl.ts <programId>')
process.exit(1)
}

// May use different RPC provider to avoid getting rate limited
const SOLANA_MAINNET = 'https://api.mainnet-beta.solana.com'
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)
console.log(JSON.stringify(parseWrites(idlWrites), null, 2))
console.log('Total of %d idls', idlWrites.length)
}

main()
.then(() => process.exit(0))
.catch((err: any) => {
console.error(err)
process.exit(1)
})
26 changes: 26 additions & 0 deletions examples/cndy-idls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { findIdls } from '../src/mudlands'

// Helper to log IDLs
function parseWrites(writes: { idl: Buffer }[]) {
return writes.map((w) => JSON.parse(w.idl.toString()))
}

// Find all IDLs created for CandyMachine
const CANDY_PROGRAM_ID = 'cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ'

// May use different RPC provider to avoid getting rate limited
const SOLANA_MAINNET = 'https://api.mainnet-beta.solana.com'
const RPC = process.env.RPC ?? SOLANA_MAINNET

async function main() {
const idlWrites = await findIdls(CANDY_PROGRAM_ID, RPC)
console.log(JSON.stringify(parseWrites(idlWrites), null, 2))
console.log('Total of %d idls', idlWrites.length)
}

main()
.then(() => process.exit(0))
.catch((err: any) => {
console.error(err)
process.exit(1)
})
24 changes: 24 additions & 0 deletions examples/usd-idls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { findIdls } from '../src/mudlands'

// Helper to log IDLs
function parseWrites(writes: { idl: Buffer }[]) {
return writes.map((w) => JSON.parse(w.idl.toString()))
}

// Find all IDLs created for CandyMachine
const USD_PROGRAM_ID = '1USDCmv8QmvZ9JaL7bmevGsNHn7ez8TNahJzCN551sb'

// May use different RPC provider to avoid getting rate limited
const SOLANA_MAINNET = 'https://api.mainnet-beta.solana.com'

async function main() {
const idlWrites = await findIdls(USD_PROGRAM_ID, SOLANA_MAINNET)
console.log(JSON.stringify(parseWrites(idlWrites), null, 2))
}

main()
.then(() => process.exit(0))
.catch((err: any) => {
console.error(err)
process.exit(1)
})
55 changes: 55 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@ironforge/mudlands",
"version": "0.0.0",
"description": "Discovers all IDLs of a program.",
"main": "src/mudlands.ts",
"types": "src/mudlands.ts",
"private": true,
"scripts": {
"build": "tsc",
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"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",
"test:ix": "yarn test:ix:anchor && yarn test:ix:misc",
"test:ix:anchor": "./test/anchor/run-ix.sh",
"test:ix:misc": "node --test --test-reporter=spec -r esbuild-runner/register ./test/ix/*.ts",
"ex:cndy": "node -r esbuild-runner/register ./examples/cndy-idls.ts",
"ex:usd": "node -r esbuild-runner/register ./examples/usd-idls.ts",
"check:idl": "node -r esbuild-runner/register ./examples/check-idl.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ironforge-cloud/mudlands.git"
},
"license": "ISC",
"bugs": {
"url": "https://github.com/ironforge-cloud/mudlands/issues"
},
"homepage": "https://github.com/ironforge-cloud/mudlands#readme",
"dependencies": {
"@noble/ed25519": "^1.7.3",
"@noble/hashes": "^1.3.2",
"@solana/web3.js": "^1.78.5",
"bn.js": "^5.2.1",
"bs58": "^5.0.0"
},
"devDependencies": {
"@metaplex-foundation/amman": "^0.12.1",
"@metaplex-foundation/amman-client": "^0.2.4",
"@metaplex-foundation/rustbin": "^0.3.5",
"@types/bn.js": "^5.1.2",
"@types/debug": "^4.1.8",
"@types/node": "^20.6.2",
"debug": "^4.3.4",
"depcheck": "^1.4.6",
"esbuild": "^0.19.3",
"esbuild-runner": "^2.2.2",
"execa": "^7.2.0",
"prettier": "^3.0.3",
"spok": "^1.5.5",
"supports-color": "^9.4.0",
"typescript": "^5.2.2"
}
}
36 changes: 36 additions & 0 deletions src/find-idls/accounts-match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Instruction } from '../types'

export function matchingAccounts(
ix: Instruction,
accountKeys: string[],
totalAccounts: number,
accountsByIndex: Map<number, string>,
dump = false
) {
if (accountKeys.length < totalAccounts) return null
if (ix.accounts.length !== totalAccounts) return null

const resolvedAccounts = new Array(ix.accounts.length)
for (let i = 0; i < ix.accounts.length; i++) {
const idx = ix.accounts[i]
resolvedAccounts[i] = accountKeys[idx]
}

// If the programID was already mentioned in the ix.accounts
// we don't need to add it again
const programIdIndexInAccounts = ix.accounts.indexOf(ix.programIdIndex)
if (programIdIndexInAccounts === -1) {
resolvedAccounts.push(accountKeys[ix.programIdIndex])
}
if (dump) {
console.log({ accountKeys, programIdIndexInAccounts })
console.log({ ixAccount: ix.accounts })
console.log({ resolvedAccounts })
}

for (const [idx, addr] of accountsByIndex.entries()) {
if (idx >= totalAccounts) continue
if (resolvedAccounts[idx] !== addr) return null
}
return resolvedAccounts
}
40 changes: 40 additions & 0 deletions src/find-idls/extract-createaccount-tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Meta, Transaction } from '../types'
import { ixDataMatchesDiscriminator } from '../utils'

const DISC = Buffer.from('40f4bc78a7e9690a', 'hex')

export type ExtractCreateAccountResult = {
idl: string
program: string
slot: number
}

export async function extractCreateAccount(
tx: Transaction,
meta: Meta | undefined,
programId: string,
idlId: string,
slot: number
): Promise<ExtractCreateAccountResult | null> {
if (meta?.err != null) {
throw new Error('Cannot handle transaction with error')
}

// 1. Headers
const header = tx.message.header
const headerMatches =
header.numReadonlySignedAccounts === 0 &&
header.numReadonlyUnsignedAccounts === 4 &&
header.numRequiredSignatures === 1

if (!headerMatches) return null

// 2. Main Instruction
const instructions = tx.message.instructions
if (instructions.length !== 1) return null

const ix = instructions[0]
if (!ixDataMatchesDiscriminator(ix.data, DISC)) return null

return { idl: idlId, program: programId, slot }
}
79 changes: 79 additions & 0 deletions src/find-idls/extract-idlwrite-tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Transaction } from '../types'
import { unzip } from '../unzip'
import { logError, logTrace } from '../utils'
import { decodeIxData, bufMatchesIxDiscriminator } from '../utils/ix-data'
import { matchingAccounts } from './accounts-match'

const DISC = Buffer.from('40f4bc78a7e9690a', 'hex')
const ZIP_MAGIC = Buffer.from('789c', 'hex')

/**
* Extracts IdlWrite transaction data if it is adn IdlWrite Transaction.
*
* @param {Transaction} tx
* @param {string} programId for which the IDL is written
* @param {string} tgtAddr the account that the IDL is written to
* - for `init` this is the IDL address
* - for `upgrade` this is another address that will then be used in
* `SetBuffer` to update the IDL address itself
* @returns {Buffer | null} the data of the IdlWrite instruction without the
* instruction prefix
*/
export async function extractIdlWriteTxData(
tx: Transaction,
programId: string,
tgtAddr: string
): Promise<Buffer | null> {
// 1. Headers
const header = tx.message.header
const headerMatches =
header.numReadonlySignedAccounts === 0 &&
header.numReadonlyUnsignedAccounts === 1 &&
header.numRequiredSignatures === 1

if (!headerMatches) return null

// 2. Main Instructions and Accounts
const instructions = tx.message.instructions
if (instructions.length !== 1) return null

/*
* pub struct IdlAccounts<'info> {
* pub idl: Account<'info, IdlAccount>,
* pub authority: Signer<'info>,
* }
*/
const ix = instructions[0]
const accountsByIdx = new Map([
[0, tgtAddr],
[2, programId],
])
const accs = matchingAccounts(ix, tx.message.accountKeys, 2, accountsByIdx)
if (accs == null) return null

const buf = decodeIxData(ix.data)

const discMatches = bufMatchesIxDiscriminator(buf, DISC)
if (!discMatches) return null

// Cut off instruction prefix
return buf.subarray(13)
}

export function deserializeWriteTxData(data: Buffer[]) {
const buf = Buffer.concat(data)
if (logTrace.enabled) {
logTrace(
'Deserializing IDL data from %d buffers, total bytes: %d',
data.length,
buf.byteLength
)
}
const hasZipMagicHeader = buf.subarray(0, 2).equals(ZIP_MAGIC)
if (!hasZipMagicHeader) {
logError('Failed to find magic ZIP header')
return null
}

return unzip(buf)
}
Loading

0 comments on commit 1182645

Please sign in to comment.