diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a567d67
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Ryan Conceicao
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index 5f4f8c6..b44a264 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,98 @@
-# AnchorES
+
+ AnchorES
+
+
-Minimal Solana transaction parser.
+Minimal library for parsing Solana transaction, instructions and events.
-- Tree-shakeable: 10KB (2.5KB gzip) to parse Jupiter Swap Events
+- Tree-shakeable: 7.82kB (3.48kB gzip) to parse Jupiter Swap Events
- ESM and commonjs
- No IDL, structs defined in code
+- Typescript friendly
- Minimal dependencies
+
+> [!WARNING]
+> **This is still a work in progress**. Support for all Borsh types is not yet implemented, and the API is subject to change.
+
+## Install
+
+```bash
+npm i anchores
+```
+
+## Parsing Jupiter Swap Transactions
+
+We export a `parseTransaction` function that takes a program id, a list of instruction and event parsers, and the transaction to parse.
+
+```typescript
+import { parseTransaction } from "anchores";
+import { JUPITER_V6_PROGRAM_ID, SwapEvent } from "anchores/parsers/jupiter";
+
+const tx = await connection.getParsedTransaction("txhash");
+const events = parseTransaction(
+ JUPITER_V6_PROGRAM_ID,
+ {
+ events: [SwapEvent],
+ },
+ tx,
+);
+```
+
+See tests for other usage examples.
+
+## Declaring your own parser
+
+You can also declare your own parsers. Here is an example of a parser for the Jupiter Swap Event:
+
+```typescript
+import * as b from "anchores/binary";
+
+export function parseSwapEvent(data: Uint8Array) {
+ const reader = b.createReader(data);
+ return {
+ amm: b.publicKey(reader),
+ inputMint: b.publicKey(reader),
+ inputAmount: b.u64(reader),
+ outputMint: b.publicKey(reader),
+ outputAmount: b.u64(reader),
+ };
+}
+export type ParsedSwapEvent = ReturnType;
+```
+
+You can use this function directly or form a parser object to pass to `parseTransaction`, `decodeEvents`, `decodeStructs`.
+
+```typescript
+import { createSighash } from "anchores/anchor";
+
+export const SwapEvent = {
+ name: "SwapEvent" as const,
+ discriminator: createSighash("event", "SwapEvent"),
+ // declared above
+ parse: parseSwapEvent,
+};
+
+// Usage
+const events = parseTransaction(
+ PROGRAM_ID,
+ {
+ events: [SwapEvent],
+ },
+ tx,
+);
+// or
+const structs = tx.meta.innerInstructions.flatMap((inner) =>
+ inner.instructions.filter(isJupiterInstruction).map((ix) => {
+ const ixData = base58.decode(ix.data);
+ const instruct = decodeStructs([SwapInstruction], ixData);
+ if (instruct) {
+ return instruct;
+ }
+
+ const event = decodeEvents([SwapEvent], ixData);
+ return event;
+ }),
+);
+```
diff --git a/package.json b/package.json
index 27486b9..8787a32 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,22 @@
{
"name": "anchores",
"version": "0.0.1",
- "description": "",
- "keywords": [],
- "author": "",
- "license": "ISC",
+ "description": "Minimal library for parsing Solana transaction, instructions and events.",
+ "keywords": [
+ "solana",
+ "tx",
+ "transaction",
+ "parser",
+ "anchor",
+ "borsh",
+ "jupiter",
+ "instruction",
+ "event",
+ "web3"
+ ],
+ "author": "Ryan Conceicao",
+ "license": "MIT",
+ "repository": "ryoid/anchores",
"type": "module",
"sideEffects": false,
"files": [
diff --git a/src/README.md b/src/README.md
deleted file mode 100644
index fbc7115..0000000
--- a/src/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Tiny Solana Parser
-
-Minimal Solana transaction parser.
-
-- Tree-shakeable: 10KB (2.5KB gzip) for Jupiter Swaps parser
-- ESM and commonjs
-- No IDL, structs defined in code
-- Minimal dependencies
diff --git a/src/binary.test.ts b/src/binary.test.ts
new file mode 100644
index 0000000..f0f3a15
--- /dev/null
+++ b/src/binary.test.ts
@@ -0,0 +1,99 @@
+import { describe, expect, it } from "vitest";
+import * as b from "./binary.js";
+
+describe("binary functions", () => {
+ it("deserialize publicKey", () => {
+ const data = new Uint8Array([
+ 6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53,
+ 218, 196, 57, 220, 26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 1,
+ ]);
+ const reader = b.createReader(data);
+
+ expect(b.publicKey(reader)).toBe(
+ "So11111111111111111111111111111111111111112",
+ );
+ expect(reader.offset).toBe(32);
+ });
+
+ it("deserialize u32", () => {
+ const nsmall = [103, 0, 0, 0];
+ const nmax = [255, 255, 255, 255];
+ const reader = b.createReader(new Uint8Array([...nsmall, ...nmax]));
+
+ expect(b.u32(reader)).toBe(103);
+ expect(reader.offset).toBe(4);
+
+ expect(b.u32(reader)).toBe(4_294_967_295);
+ });
+
+ it("deserialize u64", () => {
+ const nsmall = [103, 0, 0, 0, 0, 0, 0, 0];
+ const nlarge = [0, 1, 2, 3, 4, 5, 6, 7];
+ const nmax = [255, 255, 255, 255, 255, 255, 255, 255];
+ const data = new Uint8Array([...nsmall, ...nlarge, ...nmax]);
+ const reader = b.createReader(data);
+
+ expect(b.u64(reader)).toBe(103n);
+ expect(reader.offset).toBe(8);
+
+ expect(b.u64(reader)).toBe(506_097_522_914_230_528n);
+
+ expect(b.u64(reader)).toBe(18_446_744_073_709_551_615n);
+ });
+
+ it("deserialize u128", () => {
+ const nsmall = [104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ const nlarge = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
+ const nmax = [
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255,
+ ];
+ const data = new Uint8Array([...nsmall, ...nlarge, ...nmax]);
+ const reader = b.createReader(data);
+
+ expect(b.u128(reader)).toBe(104n);
+ expect(reader.offset).toBe(16);
+
+ expect(b.u128(reader)).toBe(
+ 20_011_376_718_272_490_338_853_433_276_725_592_320n,
+ );
+ expect(b.u128(reader)).toBe(
+ 340_282_366_920_938_463_463_374_607_431_768_211_455n,
+ );
+ });
+
+ it("deserialize i32", () => {
+ const data = new Uint8Array([255, 255, 255, 127]);
+ const reader = b.createReader(data);
+
+ expect(b.i32(reader)).toBe(2_147_483_647);
+ expect(reader.offset).toBe(4);
+ });
+
+ it("deserialize bool", () => {
+ const data = new Uint8Array([1, 0]);
+ const reader = b.createReader(data);
+
+ expect(b.bool(reader)).toBe(true);
+ expect(reader.offset).toBe(1);
+
+ expect(b.bool(reader)).toBe(false);
+ expect(reader.offset).toBe(2);
+ });
+
+ it("deserialize string", () => {
+ const ssimple = [4, 0, 0, 0, 65, 66, 67, 68];
+ const sutf8 = [
+ 30, 0, 0, 0, 195, 179, 195, 177, 64, 226, 128, 161, 216, 143, 216, 171,
+ 32, 230, 188, 162, 224, 160, 182, 226, 173, 144, 240, 159, 148, 146, 244,
+ 128, 128, 128,
+ ];
+ const reader = b.createReader(new Uint8Array([...ssimple, ...sutf8]));
+
+ expect(b.string(reader)).toBe("ABCD");
+ expect(reader.offset).toBe(8);
+
+ expect(b.string(reader)).toBe("óñ@‡؏ث 漢࠶⭐🔒");
+ expect(reader.offset).toBe(42);
+ });
+});
diff --git a/src/binary.ts b/src/binary.ts
index 70d177d..bcb3c83 100644
--- a/src/binary.ts
+++ b/src/binary.ts
@@ -1,21 +1,77 @@
import { base58 } from "@scure/base";
-export function publicKey(data: Uint8Array, offset: number): string {
- return base58.encode(data.subarray(offset, offset + 32));
+export type Reader = {
+ data: Uint8Array;
+ view: DataView;
+ offset: number;
+};
+
+export function createReader(data: Uint8Array): Reader {
+ return {
+ data,
+ view: new DataView(data.buffer, data.byteOffset, data.byteLength),
+ offset: 0,
+ };
}
-export function u64(view: DataView, offset: number): bigint {
- return view.getBigUint64(offset, true);
+/**
+ * Deserializer function.
+ *
+ * Consume bytes by moving the reader's offset.
+ */
+export type Deserializer = (reader: Reader) => T;
+
+export function publicKey(reader: Reader): string {
+ const size = 32;
+ const value = base58.encode(
+ reader.data.subarray(reader.offset, reader.offset + size),
+ );
+ reader.offset += size;
+ return value;
+}
+
+export function u32(reader: Reader): number {
+ const value = reader.view.getUint32(reader.offset, true);
+ reader.offset += 4;
+ return value;
+}
+
+export function u64(reader: Reader): bigint {
+ const value = reader.view.getBigUint64(reader.offset, true);
+ reader.offset += 8;
+ return value;
+}
+
+export function u128(reader: Reader): bigint {
+ const size = 16;
+ const chunk = reader.data.subarray(reader.offset, reader.offset + size);
+ let value = 0n;
+ for (let i = size - 1; i >= 0; i--) {
+ value = value << 8n;
+ value = value + BigInt(chunk[i]);
+ }
+
+ reader.offset += size;
+ return value;
}
-export function u128(view: DataView, offset: number): bigint {
- return view.getBigUint64(offset, true) + view.getBigUint64(offset + 8, true);
+export function i32(reader: Reader): number {
+ const value = reader.view.getInt32(reader.offset, true);
+ reader.offset += 4;
+ return value;
}
-export function i32(view: DataView, offset: number): number {
- return view.getInt32(offset, true);
+export function bool(reader: Reader): boolean {
+ const value = reader.view.getUint8(reader.offset) > 0;
+ reader.offset += 1;
+ return value;
}
-export function bool(view: DataView, offset: number): boolean {
- return view.getUint8(offset) > 0;
+export function string(reader: Reader): string {
+ const len = u32(reader);
+ const value = new TextDecoder().decode(
+ reader.data.subarray(reader.offset, reader.offset + len),
+ );
+ reader.offset += len;
+ return value;
}
diff --git a/src/parsers/jupiter.ts b/src/parsers/jupiter.ts
index 4202445..cb35c7b 100644
--- a/src/parsers/jupiter.ts
+++ b/src/parsers/jupiter.ts
@@ -5,13 +5,13 @@ export const JUPITER_V6_PROGRAM_ID =
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";
export function parseSwapEvent(data: Uint8Array) {
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
+ const reader = b.createReader(data);
return {
- amm: b.publicKey(data, 0), // + 32
- inputMint: b.publicKey(data, 32), // + 32
- inputAmount: b.u64(view, 64), // + 8
- outputMint: b.publicKey(data, 72), // + 32
- outputAmount: b.u64(view, 104), // + 8
+ amm: b.publicKey(reader),
+ inputMint: b.publicKey(reader),
+ inputAmount: b.u64(reader),
+ outputMint: b.publicKey(reader),
+ outputAmount: b.u64(reader),
};
}
export type ParsedSwapEvent = ReturnType;
@@ -23,11 +23,11 @@ export const SwapEvent = {
};
export function parseFeeEvent(data: Uint8Array) {
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
+ const reader = b.createReader(data);
return {
- account: b.publicKey(data, 0), // + 32
- mint: b.publicKey(data, 32), // + 32
- amount: b.u64(view, 64), // + 8
+ account: b.publicKey(reader),
+ mint: b.publicKey(reader),
+ amount: b.u64(reader),
};
}
export type ParsedFeeEvent = ReturnType;
diff --git a/src/parsers/meteora-dlmm.ts b/src/parsers/meteora-dlmm.ts
index 1d56e3b..10f3575 100644
--- a/src/parsers/meteora-dlmm.ts
+++ b/src/parsers/meteora-dlmm.ts
@@ -5,19 +5,19 @@ export const METEORA_DLMM_PROGRAM_ID =
"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo";
export function parseSwapEvent(data: Uint8Array) {
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
+ const reader = b.createReader(data);
return {
- lbPair: b.publicKey(data, 0), // + 32
- from: b.publicKey(data, 32), // + 32
- startBinId: b.i32(view, 64), // + 4
- endBinId: b.i32(view, 68), // + 4
- amountIn: b.u64(view, 72), // + 8
- amountOut: b.u64(view, 80), // + 8
- swapForY: b.bool(view, 88), // + 1
- fee: b.u64(view, 89), // + 8
- protocolFee: b.u64(view, 97), // + 8
- feeBps: b.u128(view, 105), // + 16
- hostFee: b.u64(view, 121), // + 8
+ lbPair: b.publicKey(reader),
+ from: b.publicKey(reader),
+ startBinId: b.i32(reader),
+ endBinId: b.i32(reader),
+ amountIn: b.u64(reader),
+ amountOut: b.u64(reader),
+ swapForY: b.bool(reader),
+ fee: b.u64(reader),
+ protocolFee: b.u64(reader),
+ feeBps: b.u128(reader),
+ hostFee: b.u64(reader),
};
}
export type ParsedSwapEvent = ReturnType;
@@ -29,10 +29,10 @@ export const SwapEvent = {
};
export function parseSwapInstruction(data: Uint8Array) {
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
+ const reader = b.createReader(data);
return {
- amountIn: b.u64(view, 0), // + 8
- minAmountOut: b.u64(view, 8), // + 8
+ amountIn: b.u64(reader),
+ minAmountOut: b.u64(reader),
};
}
export type ParsedSwapInstruction = ReturnType;