Skip to content

Commit

Permalink
Merge pull request #9 from blowfishxyz/fabio/explorer-kit-server
Browse files Browse the repository at this point in the history
feat(server): Add server to request decoding of accounts & transactions
  • Loading branch information
doodoo-aihc authored Jan 17, 2024
2 parents 923ecd8 + 1768682 commit 5143185
Show file tree
Hide file tree
Showing 12 changed files with 1,070 additions and 82 deletions.
5 changes: 5 additions & 0 deletions .changeset/rich-parents-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solanafm/explorer-kit-server": major
---

Create Explore Kit Server
2 changes: 1 addition & 1 deletion CONTRIBUTING.MD
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
1. Run `pnpm install` in the root of the repository to install all dependencies.
2. Run `pnpm build` to create an initial build of the `explorerkit-idls` and `explorerkit-translator`
3. You should now be able to edit the source code to your liking and run `pnpm dev` to ensure that your changes are being watched
4. After you are dong with your changes, you can write test cases in the `tests` folder of the root of the package to ensure that your changes work as expected. You can run `pnpm test` in the root of the repository to run all the tests in the repository.
4. After you are done with your changes, you can write test cases in the `tests` folder of the root of the package to ensure that your changes work as expected. You can run `pnpm test` in the root of the repository to run all the tests in the repository.

## Running Tests

Expand Down
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const SFMIdlItem = await getProgramIdl(programId);
// You can also get an IDL at a specific slot context if you're trying to histroically parse a transaction / account
// but the IDL might not be backwards compatible.
const historicalSFMIdlItem = await getProgramIdl(programId, {
slotContext: 132322893,
slotContext: 132322893,
});
```

Expand All @@ -90,12 +90,12 @@ const historicalSFMIdlItem = await getProgramIdl(programId, {
const ixData = "1AMTAauCh9UPEJKKd6LnGGtWqFvRs2aUZkv9r6wNe3PTzB1KS9TbwYzM8Cp7vUSDYZXTxXJp5M"
// Checks if SFMIdlItem is defined, if not you will not be able to initialize the parser layout
if (SFMIdlItem) {
const parser = new SolanaFMParser(SFMIdlItem);
const instructionParser = parser.createParser(ParserType.INSTRUCTION);
const parser = new SolanaFMParser(SFMIdlItem, programId);
const instructionParser = instructionParser.createParser(ParserType.INSTRUCTION);

if (instructionParser && checkIfInstructionParser(instructionParser)) {
// Parse the transaction
const decodedData = parser.parseTransaction(ixData);
const decodedData = parser.parseInstructions(ixData);
}
}
```
Expand All @@ -105,7 +105,7 @@ Parsing an event data:
```ts
import { SolanaFMParser. checkIfEventParser, ParserType } from "@solanafm/explorer-kit"

// For event data they have to base-64 encoded and they can be extracted from logs or a inner instruction with CPI logs.
// For event data they have to base-64 encoded and they can be extracted from logs or a inner instruction with CPI logs.
// Phoenix Program Event Data
const eventData = "DwEABF2SDQAAAABDfDtlAAAAAKiVfA0AAAAAL9p3EN7QVm+wCbiCUn2jVyJyazsZQYgqVRhf6h2a/pX5SjR+9eBu2sQU7NYr1TEeH7vRFNOiXSyDLJ9g+fDJrwMAAgAABPzrK7CsLqR5NiVFXYwyp7QgatDNQXbn3JA8wOVXQfANFxMTAAAAAIB/AAAAAAAAg7MAAAAAAAAAAAAAAAAAAAIBAAT86yuwrC6keTYlRV2MMqe0IGrQzUF259yQPMDlV0HwDhcTEwAAAACCfwAAAAAAAByBAAAAAAAA6EwCAAAAAAAGAgAAAAAAAAAAAAAAAAAAAAAAnzQBAAAAAABzEb6ZAAAAALveBwAAAAAA"
const parser = new SolanaFMParser(SFMIdlItem);
Expand All @@ -120,18 +120,21 @@ if (eventParser && checkIfEventParser(eventParser)) {
Parsing an account data:

```ts
import { SolanaFMParser. checkIfAccountParser, ParserType } from "@solanafm/explorer-kit"
import { SolanaFMParser, checkIfAccountParser, ParserType } from "@solanafm/explorer-kit";

const SFMIdlItem = await getProgramIdl(programId);

// Account data have to be base-64 encoded
// Stake Pool Account Data
const accountData = "AWq1iyr99ATwNekhxZcljopQjeBixmWt+p/5CTXBmRbd3Noj1MlCDU6CVh08awajdvCUB/G3tPyo/emrHFdD8Wfh4Pippvxf8kLk81F78B7Wst0ZUaC6ttlDVyWShgT3cP/LqkIDCUdVLBkThURwDuYX1RR+JyWBHNvgnIkDCm914o2jckW1NrCzDbv9Jn/RWcT0cAMYKm8U4SfG/F878wV0XwxEYxirEMlfQJSVhXDNBXRlpU2rFNnd40gahv7V/Mvj/aPav/vdTOwRdFALTRZQlijB9G5myz+0QWe7U7EGIQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpE2P1ZIWKAQDUAp5GdmQBAMkBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQJwAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECcAAAAAAAAAAAAAAAAAAABWHWK1dGQBAAgnQqFYigEAv0rw1gHIAQAPfXpGLPQBABAnAAAAAAAAAAAAAAAAAAAAicd7jscBANVMdCNW7gEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
const accountData =
"AWq1iyr99ATwNekhxZcljopQjeBixmWt+p/5CTXBmRbd3Noj1MlCDU6CVh08awajdvCUB/G3tPyo/emrHFdD8Wfh4Pippvxf8kLk81F78B7Wst0ZUaC6ttlDVyWShgT3cP/LqkIDCUdVLBkThURwDuYX1RR+JyWBHNvgnIkDCm914o2jckW1NrCzDbv9Jn/RWcT0cAMYKm8U4SfG/F878wV0XwxEYxirEMlfQJSVhXDNBXRlpU2rFNnd40gahv7V/Mvj/aPav/vdTOwRdFALTRZQlijB9G5myz+0QWe7U7EGIQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpE2P1ZIWKAQDUAp5GdmQBAMkBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQJwAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECcAAAAAAAAAAAAAAAAAAABWHWK1dGQBAAgnQqFYigEAv0rw1gHIAQAPfXpGLPQBABAnAAAAAAAAAAAAAAAAAAAAicd7jscBANVMdCNW7gEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";

const parser = new SolanaFMParser(SFMIdlItem);
const eventParser = parser.createParser(ParserType.ACCOUNT);

if (eventParser && checkIfAccountParser(eventParser)) {
// Parse the transaction
const decodedData = parser.parseAccount(accountData);
// Parse the transaction
const decodedData = eventParser.parseAccount(accountData);
}
```

Expand All @@ -141,12 +144,11 @@ Once the data is parsed, the returned data type will look something like this
export type ParserOutput = {
// The name of the struct that's being used to parse the data
name: string;
// The parsed data according to the IDL schema
// The parsed data according to the IDL schema
data: any;
// ParserType depends on the type of parser you have initialized
// ParserType depends on the type of parser you have initialized
type: ParserType;
} | null;

```

More to be added soon...
Expand All @@ -161,8 +163,8 @@ You can also checkout the [examples](https://github.com/solana-fm/explorer-kit/e

## Supported Programs

| Program IDs | Program | Working Parsers |
|-----------------------------------------------------------------------------------------|--------------------------------|----------------------------------------------|
| Program IDs | Program | Working Parsers |
| --------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------- |
| 11111111111111111111111111111111 | System Program | Account, Instructions |
| Config1111111111111111111111111111111111111 | Config Program | Account, Instructions |
| Stake11111111111111111111111111111111111111 | Stake Program | Account, Instructions |
Expand Down
5 changes: 5 additions & 0 deletions packages/explorerkit-server/.eslintrc.js
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"],
};
34 changes: 34 additions & 0 deletions packages/explorerkit-server/README.md
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).
52 changes: 52 additions & 0 deletions packages/explorerkit-server/package.json
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"
}
}
5 changes: 5 additions & 0 deletions packages/explorerkit-server/src/index.ts
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);
187 changes: 187 additions & 0 deletions packages/explorerkit-server/src/server.ts
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 };
Loading

0 comments on commit 5143185

Please sign in to comment.