forked from solana-fm/explorer-kit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement /decode/errors endpoint
- Loading branch information
Showing
28 changed files
with
1,021 additions
and
612 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
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,36 @@ | ||
name: ExplorerKit Server CI | ||
|
||
on: | ||
pull_request: | ||
paths: | ||
- "packages/explorerkit-server/**" | ||
|
||
env: | ||
REDIS_URL: redis://localhost:6379 | ||
|
||
jobs: | ||
run-test-cases: | ||
runs-on: ubuntu-latest | ||
timeout-minutes: 10 | ||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v3 | ||
- name: Setup pnpm package manager | ||
uses: pnpm/action-setup@v2 | ||
with: | ||
version: 8.6.10 | ||
- name: Setup Node | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: 20 | ||
cache: "pnpm" | ||
- name: Install Node Modules | ||
run: pnpm install | ||
- name: Build workspace | ||
run: pnpm --filter "./packages/explorerkit-*" build | ||
- name: Run ExplorerKit Server Lint | ||
run: pnpm lint | ||
working-directory: ./packages/explorerkit-server | ||
- name: Run ExplorerKit Server Tests | ||
run: pnpm test | ||
working-directory: ./packages/explorerkit-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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
version: "3" | ||
services: | ||
redis: | ||
image: redis:alpine | ||
container_name: client | ||
restart: unless-stopped | ||
expose: | ||
- 6379 | ||
explorerkit: | ||
depends_on: | ||
- redis | ||
build: | ||
context: . | ||
dockerfile: Dockerfile | ||
container_name: server | ||
restart: on-failure | ||
environment: | ||
- REDIS_URL=redis://redis:6379 | ||
- PORT=3000 | ||
ports: | ||
- "3000:3000" | ||
volumes: | ||
- .:/app |
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,2 @@ | ||
REDIS_URL=redis://localhost:6379 | ||
PORT=3000 |
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 @@ | ||
v20 |
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
53 changes: 53 additions & 0 deletions
53
packages/explorerkit-server/src/components/decoders/errors.spec.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,53 @@ | ||
import { beforeAll, describe, expect, it, vi } from "vitest"; | ||
|
||
import { decodeProgramError } from "@/components/decoders/errors"; | ||
import { loadAllIdls } from "@/components/idls"; | ||
|
||
vi.mock("@/core/shared-dependencies", (loadActual) => { | ||
const deps = { | ||
cache: new Map(), | ||
}; | ||
|
||
return { | ||
...loadActual(), | ||
initSharedDependencies: () => {}, | ||
getSharedDep: (name: keyof typeof deps) => deps[name], | ||
getSharedDeps: () => deps, | ||
}; | ||
}); | ||
|
||
describe("errors", () => { | ||
let idls = new Map(); | ||
const jupError = { | ||
errorCode: 0x1771, | ||
programId: "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", | ||
}; | ||
|
||
beforeAll(async () => { | ||
idls = await loadAllIdls([jupError.programId]); | ||
}); | ||
|
||
describe("decodeProgramError", () => { | ||
it("should return the decoded error", async () => { | ||
const result = decodeProgramError(idls, jupError); | ||
expect(result).toEqual({ | ||
decodedMessage: "Slippage tolerance exceeded", | ||
errorCode: 6001, | ||
kind: "SlippageToleranceExceeded", | ||
programId: "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", | ||
}); | ||
}); | ||
|
||
it("should return error for an unknown programId", () => { | ||
const result = decodeProgramError(idls, { | ||
errorCode: 0x1771, | ||
programId: "UnknownProgramId", | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: 6001, | ||
programId: "UnknownProgramId", | ||
}); | ||
}); | ||
}); | ||
}); |
31 changes: 31 additions & 0 deletions
31
packages/explorerkit-server/src/components/decoders/errors.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,31 @@ | ||
import { ErrorParserInterface, ParserType } from "@solanafm/explorer-kit"; | ||
|
||
import { IdlsMap } from "@/components/idls"; | ||
import { ProgramError } from "@/types"; | ||
|
||
export function decodeProgramError(idls: IdlsMap, programError: ProgramError): ProgramError { | ||
if (!programError.errorCode) { | ||
return programError; | ||
} | ||
|
||
const programId = programError.programId; | ||
const parser = idls.get(programId); | ||
|
||
if (!parser) { | ||
return programError; | ||
} | ||
|
||
const hexErrorCode = `0x${programError.errorCode.toString(16)}`; | ||
const errorParser = parser.createParser(ParserType.ERROR) as ErrorParserInterface; | ||
const parsedError = errorParser.parseError(hexErrorCode); | ||
|
||
if (!parsedError) { | ||
return programError; | ||
} | ||
|
||
return { | ||
...programError, | ||
decodedMessage: parsedError.data, | ||
kind: parsedError.name, | ||
}; | ||
} |
65 changes: 65 additions & 0 deletions
65
packages/explorerkit-server/src/components/decoders/instructions.spec.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,65 @@ | ||
import { beforeAll, describe, expect, it, vi } from "vitest"; | ||
|
||
import { decodeInstruction } from "@/components/decoders/instructions"; | ||
import { loadAllIdls } from "@/components/idls"; | ||
|
||
vi.mock("@/core/shared-dependencies", (loadActual) => { | ||
const deps = { | ||
cache: new Map(), | ||
}; | ||
|
||
return { | ||
...loadActual(), | ||
initSharedDependencies: () => {}, | ||
getSharedDep: (name: keyof typeof deps) => deps[name], | ||
getSharedDeps: () => deps, | ||
}; | ||
}); | ||
|
||
describe("instructions", () => { | ||
let idls = new Map(); | ||
const instruction = { | ||
accountKeys: [], | ||
encodedData: "3ta97iYzVb3u", | ||
programId: "ComputeBudget111111111111111111111111111111", | ||
}; | ||
|
||
beforeAll(async () => { | ||
idls = await loadAllIdls([instruction.programId]); | ||
}); | ||
|
||
describe("decodeInstruction", () => { | ||
it("should return the decoded instruction", async () => { | ||
const result = await decodeInstruction(idls, { | ||
accountKeys: [], | ||
encodedData: "3ta97iYzVb3u", | ||
programId: "ComputeBudget111111111111111111111111111111", | ||
}); | ||
expect(result).toEqual({ | ||
accountKeys: [], | ||
decodedData: { | ||
computeUnitPrice: 317673, | ||
discriminator: 3, | ||
}, | ||
encodedData: "3ta97iYzVb3u", | ||
name: "setComputeUnitPrice", | ||
programId: "ComputeBudget111111111111111111111111111111", | ||
}); | ||
}); | ||
}); | ||
|
||
it("should return instruction for an unknown programId", async () => { | ||
const result = await decodeInstruction(idls, { | ||
accountKeys: [], | ||
encodedData: "3ta97iYzVb3u", | ||
programId: "UnknownProgramId", | ||
}); | ||
expect(result).toEqual({ | ||
accountKeys: [], | ||
decodedData: null, | ||
encodedData: "3ta97iYzVb3u", | ||
name: null, | ||
programId: "UnknownProgramId", | ||
}); | ||
}); | ||
}); |
88 changes: 88 additions & 0 deletions
88
packages/explorerkit-server/src/components/decoders/instructions.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,88 @@ | ||
import { InstructionParserInterface, ParserType } from "@solanafm/explorer-kit"; | ||
import { Buffer } from "buffer"; | ||
|
||
import { IdlsMap } from "@/components/idls"; | ||
import { Instruction, TopLevelInstruction } from "@/types"; | ||
|
||
export async function decodeInstruction(idls: IdlsMap, instruction: Instruction): Promise<Instruction> { | ||
const programId = instruction.programId.toString(); | ||
let parsedInstruction = { | ||
programId: programId.toString(), | ||
encodedData: instruction.encodedData, | ||
name: null, | ||
decodedData: null, | ||
accountKeys: instruction.accountKeys, | ||
}; | ||
|
||
let parser = idls.get(programId); | ||
if (parser == null) { | ||
return parsedInstruction; // Short-circuit without decodedData since IDL is missing | ||
} | ||
let instructionParser = parser.createParser(ParserType.INSTRUCTION) as InstructionParserInterface; | ||
|
||
// Parse the transaction | ||
const decodedInstruction = instructionParser.parseInstructions(instruction.encodedData, instruction.accountKeys); | ||
const decodedInstructionWithTypes = instructionParser.parseInstructions( | ||
instruction.encodedData, | ||
instruction.accountKeys, | ||
true | ||
); | ||
const finalDecodedInstructionData = postProcessDecodedInstruction( | ||
decodedInstruction?.data, | ||
decodedInstructionWithTypes?.data | ||
); | ||
return { | ||
programId: instruction.programId, | ||
encodedData: instruction.encodedData, | ||
name: decodedInstruction?.name || null, | ||
decodedData: finalDecodedInstructionData || null, | ||
accountKeys: instruction.accountKeys, | ||
}; | ||
} | ||
|
||
function postProcessDecodedInstruction(decodedInstructionData?: any, decodedInstructionDataWithTypes?: any): any { | ||
if (!decodedInstructionData || !decodedInstructionDataWithTypes) { | ||
return null; | ||
} | ||
|
||
Object.keys(decodedInstructionDataWithTypes).forEach((key) => { | ||
const property = decodedInstructionDataWithTypes[key]; | ||
// If [[u8, X]], base64 encode it | ||
if ( | ||
typeof property.type === "object" && | ||
typeof property.type.vec === "object" && | ||
Array.isArray(property.type.vec.array) && | ||
property.type.vec.array[0] === "u8" | ||
) { | ||
const rawValue = decodedInstructionData[key]; | ||
if (rawValue && Array.isArray(rawValue) && Array.isArray(rawValue[0])) { | ||
decodedInstructionData[key] = rawValue.map((arr: number[]) => Buffer.from(arr).toString("base64")); | ||
} | ||
} | ||
// If [u8, X], base64 encode it | ||
if (typeof property.type === "object" && Array.isArray(property.type.array) && property.type.array[0] === "u8") { | ||
// Base64 encode the byte array from decodedInstructionData | ||
const byteArray = decodedInstructionData[key]; | ||
if (byteArray && Array.isArray(byteArray)) { | ||
decodedInstructionData[key] = Buffer.from(byteArray).toString("base64"); | ||
} | ||
} | ||
// If bytes, base64 encode it | ||
if (property.type === "bytes") { | ||
const bytes = decodedInstructionData[key]; | ||
if (bytes && bytes.constructor === Uint8Array) { | ||
decodedInstructionData[key] = Buffer.from(bytes).toString("base64"); | ||
} | ||
} | ||
}); | ||
|
||
return decodedInstructionData; | ||
} | ||
|
||
export function getProgramIds(instructionsPerTransaction: (TopLevelInstruction[] | null)[]): string[] { | ||
const allProgramIds = instructionsPerTransaction | ||
.flatMap((transactionInstructions) => transactionInstructions || []) | ||
.flatMap((ix) => [ix.topLevelInstruction.programId, ...ix.flattenedInnerInstructions.map((i) => i.programId)]); | ||
|
||
return Array.from(new Set(allProgramIds)); | ||
} |
Oops, something went wrong.