-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from blowfishxyz/fabio/explorer-kit-server
feat(server): Add server to request decoding of accounts & transactions
- Loading branch information
Showing
12 changed files
with
1,070 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@solanafm/explorer-kit-server": major | ||
--- | ||
|
||
Create Explore Kit Server |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
root: true, | ||
// This tells ESLint to load the config from the package `eslint-config-explorerkit` | ||
extends: ["explorerkit"], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Explorer Kit Server | ||
|
||
Build & watch | ||
|
||
``` | ||
pnpm dev | ||
``` | ||
|
||
Start server | ||
|
||
``` | ||
pnpm serve | ||
``` | ||
|
||
Example requests | ||
|
||
``` | ||
curl --location 'http://localhost:3000/decode/accounts' --header 'Content-Type: application/json' --data '{ | ||
"accounts": [ | ||
{ | ||
"ownerProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", | ||
"data": "/NFB6YMsrxCtkXSVyg8nG1spPNRwJ+pzcAftQOs5oL0mA+FEPRpnATHIUtp5LuY9RJEScraeiSf6ghxvpIcl2eGPjQUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" | ||
} | ||
] | ||
}' | ||
``` | ||
|
||
``` | ||
curl --location 'http://localhost:3000/decode/transactions' --header 'Content-Type: application/json' --data '{ | ||
"transactions": ["B8WHcXetQ5nZKHNhZFK6NYeyL9whFEczxqSXn8m8Gy7LvjwrNYgKT6Wm2ZuXu76cbZc1Nj2DX8N83h7AsaJ4fHQUFx2nEXqQM22iKT1oBkWSimnRXGT1k2JQBr45kgpC5JFgxYYHkKd2s6f6hfxby4uh2JPTzv3j3vt8BwZEbF6x9jqZUo3385RYCPFz44nbTtDZ8mN34pv2ZvpH7RoAf5QvjofAWzUG97sDa4rtaaemMR6tQsuZRDd3oJ7btm1kLtHRxmZDiL2aHNY5rkRTRbWEVm1tDyjWB5c7KxGBBKNH5u2ztQcAZSp7Dstiyn4cqjZEBVNd3vAQY6n61sutfYPGN5xxgrgxV6EkjpASFKt7PzHRSvpEonLUHHKB955ZHbnbNXSvUp9vv4vD5Xji3FY86TT9SYRRSrs2NJ6dD66NB1MSEoPnhmKRtmM1coh"] | ||
}' | ||
``` | ||
|
||
For example responses, see the [tests](./tests/server.test.ts). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
{ | ||
"name": "@solanafm/explorer-kit-server", | ||
"version": "1.0.0", | ||
"description": "Server to decode Solana entities over HTTP API", | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.mjs", | ||
"types": "./dist/index.d.ts", | ||
"files": [ | ||
"/dist" | ||
], | ||
"exports": { | ||
".": { | ||
"import": "./dist/index.mjs", | ||
"require": "./dist/index.js", | ||
"types": "./dist/index.d.ts" | ||
} | ||
}, | ||
"scripts": { | ||
"build": "tsup src/index.ts --format esm,cjs --dts", | ||
"dev": "tsup src/index.ts --format esm,cjs --dts --watch", | ||
"serve": "nodemon dist/index.js", | ||
"test": "vitest run", | ||
"clean": "rimraf .turbo && rimraf node_modules && rimraf dist", | ||
"lint": "TIMING=1 eslint \"src/**/*.ts*\"", | ||
"lint:fix": "TIMING=1 eslint \"src/**/*.ts*\" --fix", | ||
"publish-package": "pnpm build && npm publish --access=public" | ||
}, | ||
"keywords": [], | ||
"author": "fabioberger", | ||
"license": "GPL-3.0-or-later", | ||
"devDependencies": { | ||
"@types/body-parser": "^1.19.5", | ||
"@types/express": "^4.17.21", | ||
"@types/node": "^20.10.4", | ||
"@types/supertest": "^2.0.16", | ||
"@vitest/coverage-v8": "^0.34.2", | ||
"eslint": "^8.47.0", | ||
"eslint-config-explorerkit": "workspace:*", | ||
"nodemon": "^3.0.2", | ||
"supertest": "^6.3.3", | ||
"vitest": "^0.34.2" | ||
}, | ||
"dependencies": { | ||
"@solana/web3.js": "^1.87.2", | ||
"@solanafm/explorer-kit": "workspace:*", | ||
"@solanafm/explorer-kit-idls": "workspace:*", | ||
"axios": "^1.3.3", | ||
"body-parser": "^1.20.2", | ||
"bs58": "^5.0.0", | ||
"express": "^4.18.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { app } from "./server"; | ||
|
||
// Start the server | ||
const PORT = 3000; | ||
app.listen(PORT); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import { Message, MessageV0, VersionedTransaction } from "@solana/web3.js"; | ||
import { ParserType, InstructionParserInterface, AccountParserInterface, SolanaFMParser } from "@solanafm/explorer-kit"; | ||
import { getProgramIdl } from "@solanafm/explorer-kit-idls"; | ||
import bodyParser from "body-parser"; | ||
import bs58 from "bs58"; | ||
import express, { Express, Request, Response } from "express"; | ||
|
||
interface Account { | ||
ownerProgram: string; | ||
data: string; | ||
} | ||
|
||
interface DecodeAccountsRequestBody { | ||
accounts: Account[]; | ||
} | ||
|
||
interface DecodedAccount { | ||
error: string | null; | ||
decodedData: DecodedAccountData | null; | ||
} | ||
|
||
interface DecodedAccountData { | ||
owner: string; | ||
name: string; | ||
data: any; | ||
} | ||
|
||
interface DecodeTransactionsRequestBody { | ||
transactions: string[]; | ||
} | ||
|
||
interface DecodedTransactions { | ||
error: string | null; | ||
decodedInstructions: DecodedInstruction[] | null; | ||
} | ||
|
||
interface DecodedInstruction { | ||
programId: string; | ||
name: string; | ||
data: any; | ||
} | ||
|
||
interface GenericInstruction { | ||
programId: string; | ||
data: Uint8Array; | ||
} | ||
|
||
const app: Express = express(); | ||
app.use(bodyParser.json()); | ||
|
||
// Endpoint to decode accounts data | ||
app.post("/decode/accounts", async (req: Request, res: Response) => { | ||
const { accounts } = req.body as DecodeAccountsRequestBody; | ||
|
||
let accountParsers: { [key: string]: AccountParserInterface } = {}; | ||
let decodedAccounts: DecodedAccount[] = []; | ||
for (var account of accounts) { | ||
if (!isValidBase58(account.ownerProgram)) { | ||
decodedAccounts.push({ error: "'account.ownerProgram' is not a valid base58 string.", decodedData: null }); | ||
continue; | ||
} | ||
if (!isValidBase64(account.data)) { | ||
decodedAccounts.push({ error: "'account.data' is not a valid base64 string.", decodedData: null }); | ||
continue; | ||
} | ||
|
||
let accountParser = accountParsers[account.ownerProgram]; | ||
if (accountParser == undefined) { | ||
const SFMIdlItem = await getProgramIdl(account.ownerProgram); | ||
if (SFMIdlItem === null) { | ||
decodedAccounts.push({ error: "Failed to find program IDL", decodedData: null }); | ||
continue; | ||
} | ||
|
||
const parser = new SolanaFMParser(SFMIdlItem, account.ownerProgram); | ||
accountParser = parser.createParser(ParserType.ACCOUNT) as AccountParserInterface; | ||
accountParsers[account.ownerProgram] = accountParser; | ||
} | ||
|
||
// Parse the transaction | ||
const decodedData = accountParser.parseAccount(account.data); | ||
decodedAccounts.push({ | ||
error: null, | ||
decodedData: decodedData | ||
? { owner: account.ownerProgram, name: decodedData?.name, data: decodedData?.data } | ||
: null, | ||
}); | ||
continue; | ||
} | ||
|
||
return res.status(200).json({ decodedAccounts }); | ||
}); | ||
|
||
// Endpoint to decode transactions | ||
app.post("/decode/transactions", async (req: Request, res: Response) => { | ||
const { transactions } = req.body as DecodeTransactionsRequestBody; | ||
|
||
let instructionParsers: { [key: string]: InstructionParserInterface } = {}; | ||
let decodedTransactions: DecodedTransactions[] = []; | ||
for (var encodedTx of transactions) { | ||
let txBuffer = null; | ||
if (isValidBase58(encodedTx)) { | ||
txBuffer = Buffer.from(bs58.decode(encodedTx)); | ||
} else if (isValidBase64(encodedTx)) { | ||
txBuffer = Buffer.from(encodedTx, "base64"); | ||
} else { | ||
decodedTransactions.push({ error: "'transaction' is not a valid base64 string.", decodedInstructions: null }); | ||
continue; | ||
} | ||
|
||
try { | ||
const tx = VersionedTransaction.deserialize(txBuffer); | ||
let instructions: GenericInstruction[] = []; | ||
if (tx.message instanceof Message) { | ||
for (var ix of tx.message.instructions) { | ||
let programId = tx.message.accountKeys[ix.programIdIndex]; | ||
if (programId === undefined) { | ||
decodedTransactions.push({ error: "programId not found in accounts", decodedInstructions: null }); | ||
continue; | ||
} | ||
instructions.push({ | ||
programId: programId.toString(), // We know programId will exist | ||
data: bs58.decode(ix.data), | ||
}); | ||
} | ||
} else if (tx.message instanceof MessageV0) { | ||
for (var inst of tx.message.compiledInstructions) { | ||
let programId = tx.message.staticAccountKeys[inst.programIdIndex]; | ||
if (programId === undefined) { | ||
decodedTransactions.push({ error: "programId not found in staticAccountKeys", decodedInstructions: null }); | ||
continue; | ||
} | ||
instructions.push({ | ||
programId: programId.toString(), | ||
data: inst.data, | ||
}); | ||
} | ||
} else { | ||
throw new Error("Unsupported message version"); | ||
} | ||
|
||
let decodedInstructions: any[] = []; | ||
for (var instruction of instructions) { | ||
const programId = instruction.programId.toString(); | ||
let instructionParser = instructionParsers[programId]; | ||
|
||
if (instructionParser == undefined) { | ||
const SFMIdlItem = await getProgramIdl(programId); | ||
if (SFMIdlItem) { | ||
const parser = new SolanaFMParser(SFMIdlItem, programId); | ||
instructionParser = parser.createParser(ParserType.INSTRUCTION) as InstructionParserInterface; | ||
instructionParsers[programId] = instructionParser; | ||
} else { | ||
decodedTransactions.push({ error: "Failed to find program IDL", decodedInstructions: null }); | ||
continue; | ||
} | ||
} | ||
|
||
// Parse the transaction | ||
const decodedInstruction = instructionParser.parseInstructions(bs58.encode(instruction.data)); | ||
decodedInstructions.push({ name: decodedInstruction?.name, data: decodedInstruction?.data, programId }); | ||
} | ||
|
||
decodedTransactions.push({ error: null, decodedInstructions }); | ||
} catch (e: any) { | ||
decodedTransactions.push({ error: e.message, decodedInstructions: null }); | ||
} | ||
} | ||
|
||
return res.status(200).json({ decodedTransactions }); | ||
}); | ||
|
||
function isValidBase58(str: string): boolean { | ||
const base58Regex = /^[A-HJ-NP-Za-km-z1-9]+$/; | ||
return base58Regex.test(str); | ||
} | ||
|
||
function isValidBase64(str: string): boolean { | ||
try { | ||
const base64Encoded = Buffer.from(str, "base64").toString("base64"); | ||
return str === base64Encoded; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
|
||
export { app }; |
Oops, something went wrong.