Skip to content

Commit 9d8f090

Browse files
feat(btcindexer): add msgpack serialization and refactore router
Signed-off-by: Robert Zaremba <robert@zaremba.ch>
1 parent 54caf45 commit 9d8f090

File tree

11 files changed

+271
-135
lines changed

11 files changed

+271
-135
lines changed

packages/btcindexer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"bitcoinjs-lib": "^6.1.7",
1414
"crypto-js": "^4.2.0",
1515
"itty-router": "^5.0.18",
16-
"merkletreejs": "^0.5.2"
16+
"merkletreejs": "^0.5.2",
17+
"msgpackr": "^1.11.5"
1718
},
1819
"devDependencies": {
1920
"@types/crypto-js": "^4.2.2"

packages/btcindexer/src/btcblock.test.ts

Lines changed: 0 additions & 76 deletions
This file was deleted.

packages/btcindexer/src/examples.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

packages/btcindexer/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
* `Env` object can be regenerated with `pnpm run typegen`.
88
*/
99

10-
import router from "./router";
10+
import HttpServer from "./server";
11+
12+
const server = new HttpServer();
1113

1214
export default {
13-
fetch: router.fetch,
15+
fetch: server.router.fetch,
1416

1517
// The scheduled handler is invoked at the interval set in our wrangler.jsonc's
1618
// [[triggers]] configuration.

packages/btcindexer/src/router.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { PutBlocks, PutBlocksReq } from "./put-blocks";
2+
3+
export enum RestPath {
4+
blocks = "/bitcoin/blocks",
5+
nbtcTx = "/nbtc",
6+
}
7+
8+
export enum ContentType {
9+
msgpack = "application/vnd.msgpacvk",
10+
}
11+
12+
const msgPackHeaders = {
13+
"Content-Type": ContentType.msgpack,
14+
};
15+
16+
export default class Client {
17+
baseUrl: string;
18+
19+
constructor(baseUrl: string) {
20+
if (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
21+
22+
this.baseUrl = baseUrl;
23+
}
24+
25+
async putBlocks(putBlocks: PutBlocks[]) {
26+
return fetch(this.baseUrl + RestPath.blocks, {
27+
method: "PUT",
28+
headers: msgPackHeaders,
29+
body: PutBlocksReq.encode(putBlocks),
30+
});
31+
}
32+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, assert } from "vitest";
2+
import { newPutBlock, PutBlocksReq } from "./put-blocks";
3+
import { Block } from "bitcoinjs-lib";
4+
5+
function bufferToHex(buffer: Buffer) {
6+
// Convert to Uint8Array if it's an ArrayBuffer
7+
const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
8+
9+
return Array.from(bytes)
10+
.map((b) => b.toString(16).padStart(2, "0"))
11+
.join("");
12+
}
13+
14+
describe("encode PutBlocks", () => {
15+
// mainnet genesis block
16+
const blockHex =
17+
"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000";
18+
const block = Block.fromHex(blockHex);
19+
20+
it("should decode a single block", async () => {
21+
assert(block.getId() == "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f");
22+
23+
const expected = [newPutBlock(156, block)];
24+
const reqBytes = PutBlocksReq.encode(expected);
25+
const got = PutBlocksReq.decode(reqBytes);
26+
assert.lengthOf(got, 1);
27+
28+
const pb0 = got[0];
29+
assert.equal(pb0.block.getId(), block.getId());
30+
assert.equal(pb0.height, expected[0].height);
31+
assert.include(bufferToHex(reqBytes), blockHex);
32+
});
33+
34+
it("should decode multiple blocks", async () => {
35+
// for simplicity we encode 2 genesis blocks
36+
const expected = [newPutBlock(10, block), newPutBlock(11, block)];
37+
const reqBytes = PutBlocksReq.encode(expected);
38+
const got = PutBlocksReq.decode(reqBytes);
39+
assert.lengthOf(got, 2);
40+
assert.deepEqual(got, expected);
41+
});
42+
43+
it("should handle empty array", async () => {
44+
const reqBytes = PutBlocksReq.encode([]);
45+
const got = PutBlocksReq.decode(reqBytes);
46+
assert.deepEqual(got, []);
47+
});
48+
49+
it("should abort on an invalid block", async () => {
50+
const pbq = new PutBlocksReq(2, block);
51+
const buffer = Buffer.from("invaliddddd", "utf8");
52+
pbq.block = new Uint8Array(buffer);
53+
const reqBytes = PutBlocksReq.msgpack([pbq]);
54+
assert.throws(() => PutBlocksReq.decode(reqBytes), "Buffer too small");
55+
});
56+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Block } from "bitcoinjs-lib";
2+
import { pack, unpack } from "msgpackr";
3+
4+
export interface PutBlocks {
5+
height: number;
6+
block: Block; // bitcoin core encoding of Block
7+
}
8+
9+
export function newPutBlock(height: number, block: Block): PutBlocks {
10+
return { height, block };
11+
}
12+
13+
export class PutBlocksReq {
14+
height: number;
15+
block: Uint8Array; // bitcoin core encoding of Block
16+
17+
constructor(height: number, block: Block) {
18+
this.height = height;
19+
this.block = block.toBuffer();
20+
}
21+
22+
static decode(req: ArrayBuffer): PutBlocks[] {
23+
const putReq: PutBlocksReq[] = unpack(new Uint8Array(req));
24+
return putReq.map((r): PutBlocks => {
25+
return { height: r.height, block: Block.fromBuffer(Buffer.from(r.block)) };
26+
});
27+
}
28+
29+
static encode(putBlocks: PutBlocks[]): Buffer {
30+
const req = putBlocks.map((r) => new PutBlocksReq(r.height, r.block));
31+
return pack(req);
32+
}
33+
34+
// directly encode this struct to msgpack
35+
static msgpack(putBlocks: PutBlocksReq[]) {
36+
return pack(putBlocks);
37+
}
38+
}

packages/btcindexer/src/btcindexer_http.ts renamed to packages/btcindexer/src/server.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,64 @@
1-
import type { IRequest } from "itty-router";
1+
import { IRequest, Router, error, json } from "itty-router";
2+
import { networks } from "bitcoinjs-lib";
3+
24
import { parseBlocksFromStream } from "./btcblock";
35
import { Indexer } from "./btcindexer";
4-
import { networks } from "bitcoinjs-lib";
56
import SuiClient from "./sui_client";
7+
import { RestPath } from "./rpc/client";
8+
9+
import type { AppRouter, CFArgs } from "./routertype";
610

711
const NBTC_MODULE = "nbtc";
812

9-
export class HIndexer {
10-
public nbtcAddr: string;
11-
public suiFallbackAddr: string;
12-
public btcNetwork: networks.Network;
13+
export default class HttpServer {
14+
nbtcAddr: string;
15+
suiFallbackAddr: string;
16+
btcNetwork: networks.Network;
17+
18+
router: AppRouter;
1319

1420
constructor() {
1521
// TODO: need to provide through env variable
1622
this.nbtcAddr = "TODO";
1723
this.suiFallbackAddr = "TODO";
1824
this.btcNetwork = networks.regtest;
25+
26+
this.router = this.createRouter();
1927
}
2028

29+
createRouter() {
30+
const r = Router<IRequest, CFArgs>({
31+
catch: error,
32+
// convert non `Response` objects to JSON Responses. If a handler returns `Response`
33+
// object then it will be directly returned.
34+
finally: [json],
35+
});
36+
37+
r.put(RestPath.blocks, this.putBlocks);
38+
r.put(RestPath.nbtcTx, this.putNbtcTx);
39+
40+
//
41+
// TESTING
42+
// we can return Response object directly, to avoid JSON serialization
43+
r.get("/test/user/:id", (req) => new Response(`User ID: ${req.params.id}`));
44+
// curl http://localhost:8787/test/kv/ -X PUT -d '{"key": "k102", "val": "v1"}'
45+
r.put("/test/kv", this.putTestKV);
46+
// curl "http://localhost:8787/test/kv/1" -i
47+
r.get("/test/kv/:key", this.getTestKV);
48+
r.get("/test", (req: Request) => {
49+
const url = new URL(req.url);
50+
url.pathname = "/__scheduled";
51+
url.searchParams.append("cron", "* * * * *");
52+
return new Response(
53+
`To test the scheduled handler, ensure you have used the "--test-scheduled" then try running "curl ${url.href}".`,
54+
);
55+
});
56+
57+
r.all("/*", () => error(404, "Wrong Endpoint"));
58+
return r;
59+
}
60+
61+
// TODO: should be dependency or we should move it somewhere
2162
newIndexer(env: Env): Indexer {
2263
const suiClient = new SuiClient({
2364
network: env.SUI_NETWORK,
@@ -45,7 +86,9 @@ export class HIndexer {
4586
return { inserted: await i.putNbtcTx() };
4687
};
4788

89+
//
4890
// TODO: remove this
91+
//
4992
putTestKV = async (req: IRequest, env: Env) => {
5093
const kv = env.btc_blocks;
5194
const data = await req.json<{ key: string; val: string }>();
@@ -57,4 +100,9 @@ export class HIndexer {
57100
return allKeys;
58101
// return 1;
59102
};
103+
getTestKV = async (req: IRequest, env: Env) => {
104+
const kv = env.btc_blocks;
105+
const key = req.params.key;
106+
return kv.get(key);
107+
};
60108
}

0 commit comments

Comments
 (0)