Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default tseslint.config(
// disable specific rules
rules: {
"@typescript-eslint/no-extraneous-class": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},

Expand Down
8 changes: 5 additions & 3 deletions packages/btcindexer/src/btcindexer.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, assert, vi } from "vitest";
import { Deposit, Indexer, ProofResult } from "../src/btcindexer";
import { Deposit, Indexer, ProofResult, storageFromEnv } from "../src/btcindexer";
import { Block, networks, Transaction } from "bitcoinjs-lib";
import { MerkleTree } from "merkletreejs";
import SHA256 from "crypto-js/sha256";
Expand Down Expand Up @@ -89,12 +89,14 @@ const mkMockEnv = () =>

function prepareIndexer() {
const mockEnv = mkMockEnv();
const storage = storageFromEnv(mockEnv);

const indexer = new Indexer(
mockEnv,
storage,
new SuiClient(SUI_CLIENT_CFG),
REGTEST_DATA[303].depositAddr,
SUI_FALLBACK_ADDRESS,
networks.regtest,
new SuiClient(SUI_CLIENT_CFG),
);
return { mockEnv, indexer };
}
Expand Down
46 changes: 38 additions & 8 deletions packages/btcindexer/src/btcindexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { address, networks, Block, Transaction } from "bitcoinjs-lib";
import { OP_RETURN } from "./opcodes";
import { MerkleTree } from "merkletreejs";
import SHA256 from "crypto-js/sha256";
import SuiClient from "./sui_client";
import SuiClient, { suiClientFromEnv } from "./sui_client";

const CONFIRMATION_DEPTH = 8;

Expand All @@ -30,27 +30,57 @@ interface BlockRecord {
block_height: number;
}

export class Indexer {
interface Storage {
d1: D1Database; // SQL DB
blocksDB: KVNamespace;
nbtcTxDB: KVNamespace;
}

export function storageFromEnv(env: Env): Storage {
return { d1: env.DB, blocksDB: env.btc_blocks, nbtcTxDB: env.nbtc_txs };
}

const btcNetworks = {
mainnet: networks.bitcoin,
testnet: networks.testnet,
regtest: networks.regtest,
};
const validBtcNet = Object.keys(btcNetworks).keys();

export function indexerFromEnv(env: Env): Indexer {
const storage = storageFromEnv(env);
const sc = suiClientFromEnv(env);

if (!env.BITCOIN_NETWORK) throw Error("BITCOIN_NETWORK env must be set");
if (!(env.BITCOIN_NETWORK in btcNetworks))
throw new Error("Invalid BITCOIN_NETWORK value. Must be in " + validBtcNet);
const btcNet = btcNetworks[env.BITCOIN_NETWORK];

return new Indexer(storage, sc, env.NBTC_DEPOSIT_ADDRESS, env.SUI_FALLBACK_ADDRESS, btcNet);
}

export class Indexer implements Storage {
d1: D1Database; // SQL DB
blocksDB: KVNamespace;
nbtcTxDB: KVNamespace;

nbtcScriptHex: string;
suiFallbackAddr: string;
nbtcClient: SuiClient;

constructor(
env: Env,
storage: Storage,
suiClient: SuiClient,
nbtcAddr: string,
fallbackAddr: string,
network: networks.Network,
suiClient: SuiClient,
) {
this.d1 = env.DB;
this.blocksDB = env.btc_blocks;
this.nbtcTxDB = env.nbtc_txs;
this.d1 = storage.d1;
this.blocksDB = storage.blocksDB;
this.nbtcTxDB = storage.nbtcTxDB;
this.nbtcClient = suiClient;
this.suiFallbackAddr = fallbackAddr;
this.nbtcScriptHex = address.toOutputScript(nbtcAddr, network).toString("hex");
this.nbtcClient = suiClient;
}

// returns number of processed and add blocks
Expand Down
17 changes: 11 additions & 6 deletions packages/btcindexer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,34 @@
* `Env` object can be regenerated with `pnpm run typegen`.
*/

import HttpServer from "./server";
import { indexerFromEnv } from "./btcindexer";
import HttpRouter from "./router";

const server = new HttpServer();
const router = new HttpRouter(undefined);

export default {
fetch: server.router.fetch,
async fetch(req: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
const indexer = indexerFromEnv(env);
return router.fetch(req, env, indexer);
},

// The scheduled handler is invoked at the interval set in our wrangler.jsonc's
// [[triggers]] configuration.
async scheduled(event: ScheduledController, env: Env /*,ctx*/): Promise<void> {
async scheduled(_event: ScheduledController, env: Env, _ctx): Promise<void> {
// A Cron Trigger can make requests to other endpoints on the Internet,
// publish to a Queue, query a D1 Database, and much more.
// You could store this result in KV, write to a D1 Database, or publish to a Queue.
// In this template, we'll just log the result:

// TODO: This should be refactored probably the best is to use chain tip stored in a KV namespace.
const server = new HttpServer();
const indexer = server.newIndexer(env);
// ideally use queue
const d1 = env.DB;
// TODO: move this to the indexer directly
const latestBlock = await d1
.prepare("SELECT MAX(height) as latest_height FROM processed_blocks")
.first<{ latest_height: number }>();

const indexer = indexerFromEnv(env);
if (latestBlock && latestBlock.latest_height) {
await indexer.updateConfirmationsAndFinalize(latestBlock.latest_height);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import { IRequest, Router, error, json } from "itty-router";
import { networks } from "bitcoinjs-lib";

import { Indexer } from "./btcindexer";
import SuiClient from "./sui_client";
import { RestPath } from "./api/client";

import type { AppRouter, CFArgs } from "./routertype";
import { PutBlocksReq } from "./api/put-blocks";

const NBTC_MODULE = "nbtc";
export default class HttpRouter {
#indexer?: Indexer;
#router: AppRouter;

export default class HttpServer {
btcNetwork: networks.Network;

router: AppRouter;

constructor() {
this.btcNetwork = networks.testnet;

this.router = this.createRouter();
constructor(indexer?: Indexer) {
this.#indexer = indexer;
this.#router = this.createRouter();
}

createRouter() {
Expand Down Expand Up @@ -53,35 +47,28 @@
return r;
}

// TODO: should be dependency or we should move it somewhere
newIndexer(env: Env): Indexer {
const suiClient = new SuiClient({
network: env.SUI_NETWORK,
nbtcPkg: env.SUI_PACKAGE_ID,
nbtcModule: NBTC_MODULE,
nbtcObjectId: env.NBTC_OBJECT_ID,
lightClientObjectId: env.LIGHT_CLIENT_OBJECT_ID,
signerMnemonic: env.SUI_SIGNER_MNEMONIC,
});
// we wrap the router.fetch method to provide the indexer to this object.
// Otherwise we would need to setup the server on each fetch request.
fetch = async (req: Request, env: Env, indexer: Indexer) => {
this.#indexer = indexer;
return this.#router.fetch(req, env);
};

return new Indexer(
env,
env.NBTC_DEPOSIT_ADDRESS,
env.SUI_FALLBACK_ADDRESS,
this.btcNetwork,
suiClient,
);
indexer(): Indexer {
if (this.#indexer === undefined) {
throw new Error("Indexer is not initialized");
}
return this.#indexer;
}

// NOTE: for handlers we user arrow function to avoid `bind` calls when using class methods
// in callbacks.

// NOTE: we may need to put this to a separate worker
putBlocks = async (req: IRequest, env: Env) => {
putBlocks = async (req: IRequest) => {
try {
const blocks = PutBlocksReq.decode(await req.arrayBuffer());
const i = this.newIndexer(env);
return { inserted: await i.putBlocks(blocks) };
return { inserted: await this.indexer().putBlocks(blocks) };
} catch (e) {
console.error("DEBUG: FAILED TO DECODE REQUEST BODY");
console.error(e);
Expand All @@ -91,9 +78,8 @@
}
};

putNbtcTx = async (req: IRequest, env: Env) => {
const i = this.newIndexer(env);
return { inserted: await i.putNbtcTx() };
putNbtcTx = async (req: IRequest) => {

Check warning on line 81 in packages/btcindexer/src/router.ts

View workflow job for this annotation

GitHub Actions / test

'req' is defined but never used. Allowed unused args must match /^_/u
return { inserted: await this.indexer().putNbtcTx() };
};

//
Expand Down
2 changes: 1 addition & 1 deletion packages/btcindexer/src/sui_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ describe.skip("Sui Contract Integration", () => {
const indexer = new Indexer(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{} as any,
suiClient,
"bcrt1qfnyeg7dd5vqs2mtc4rekwm8mgpxkj647p39zhw",
"fallback",
networks.regtest,
suiClient,
);
const block = Block.fromHex(REGTEST_DATA.BLOCK_HEX);
const txIndex = block.transactions?.findIndex((tx) => tx.getId() === REGTEST_DATA.TX_ID);
Expand Down
13 changes: 13 additions & 0 deletions packages/btcindexer/src/sui_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ export interface SuiClientCfg {
signerMnemonic: string;
}

const NBTC_MODULE = "nbtc";

export function suiClientFromEnv(env: Env): SuiClient {
return new SuiClient({
network: env.SUI_NETWORK,
nbtcPkg: env.SUI_PACKAGE_ID,
nbtcModule: NBTC_MODULE,
nbtcObjectId: env.NBTC_OBJECT_ID,
lightClientObjectId: env.LIGHT_CLIENT_OBJECT_ID,
signerMnemonic: env.SUI_SIGNER_MNEMONIC,
});
}

export class SuiClient {
private client: Client;
private signer: Signer;
Expand Down
18 changes: 9 additions & 9 deletions packages/btcindexer/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
"main": "src/index.ts",
"compatibility_date": "2025-06-20",
"observability": {
"enabled": true
"enabled": true,
},
"triggers": {
// every one minute
"crons": ["* * * * *"]
"crons": ["* * * * *"],
},
"compatibility_flags": ["nodejs_compat"],

Expand Down Expand Up @@ -44,7 +44,7 @@

"BITCOIN_NETWORK": "testnet",
"NBTC_DEPOSIT_ADDRESS": "tb1qjgqhst2hlgjkh36pg6r3hdnz7zvurafe9h5lkx",
"SUI_FALLBACK_ADDRESS": "0xf82fd2198d5af45bfc2a7f9a5df4fb30f3c3abf4a8a71b0e4bd415eabda99ff5"
"SUI_FALLBACK_ADDRESS": "0xf82fd2198d5af45bfc2a7f9a5df4fb30f3c3abf4a8a71b0e4bd415eabda99ff5",
},

/**
Expand All @@ -71,18 +71,18 @@
"database_name": "btcindexer-dev",
"database_id": "3a43879e-0799-442c-aed5-a3298df1c3bb",
"migrations_table": "migrations",
"migrations_dir": "./db/migrations/"
}
"migrations_dir": "./db/migrations/",
},
],

"kv_namespaces": [
{
"binding": "btc_blocks",
"id": "f8bc94f1f427436ab0797535fbc30cb7"
"id": "f8bc94f1f427436ab0797535fbc30cb7",
},
{
"binding": "nbtc_txs",
"id": "93968151e21e4cfa8f23041c0a5baf08"
}
]
"id": "93968151e21e4cfa8f23041c0a5baf08",
},
],
}