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
6 changes: 3 additions & 3 deletions packages/btcindexer/db/migrations/0001_initial_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
CREATE TABLE processed_blocks (
height INTEGER PRIMARY KEY,
hash TEXT NOT NULL UNIQUE,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
processed_at INTEGER DEFAULT unixepoch('subsec')
) STRICT;

-- This table tracks the nBTC deposit txs
Expand All @@ -14,8 +14,8 @@ CREATE TABLE nbtc_txs (
sui_recipient TEXT NOT NULL,
amount_sats INTEGER NOT NULL,
status TEXT NOT NULL, -- 'broadcasting' | 'confirming' | 'finalized' | 'minting' | 'minted' | 'reorg'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
created_at INTEGER DEFAULT unixepoch('subsec'),
updated_at INTEGER DEFAULT unixepoch('subsec')
) STRICT;

-- Indexes
Expand Down
3 changes: 2 additions & 1 deletion packages/btcindexer/src/btcindexer.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { describe, it, assert, vi } from "vitest";
import { Deposit, Indexer, ProofResult, storageFromEnv } from "../src/btcindexer";
import { Indexer, storageFromEnv } from "../src/btcindexer";
import { Block, networks, Transaction } from "bitcoinjs-lib";
import { MerkleTree } from "merkletreejs";
import SHA256 from "crypto-js/sha256";
import { SuiClient, SuiClientCfg } from "./sui_client";
import { Deposit, ProofResult } from "./models";

interface TxInfo {
id: string;
Expand Down
96 changes: 67 additions & 29 deletions packages/btcindexer/src/btcindexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,19 @@ import { OP_RETURN } from "./opcodes";
import { MerkleTree } from "merkletreejs";
import SHA256 from "crypto-js/sha256";
import SuiClient, { suiClientFromEnv } from "./sui_client";
import {
Deposit,
ProofResult,
PendingTx,
BlockRecord,
Storage,
NbtcTxStatus,
NbtcTxStatusResp,
NbtcTxD1Row,
} from "./models";

const CONFIRMATION_DEPTH = 8;

export interface Deposit {
vout: number;
amountSats: number;
suiRecipient: string;
}

export interface ProofResult {
proofPath: Buffer[];
merkleRoot: string;
}

export interface PendingTx {
tx_id: string;
block_hash: string | null;
block_height: number;
}

interface BlockRecord {
tx_id: string;
block_hash: string;
block_height: number;
}

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 };
}
Expand Down Expand Up @@ -183,6 +164,10 @@ export class Indexer implements Storage {
console.log(`Cron: No new nBTC deposits found in the scanned blocks`);
}

const latestHeightProcessed = Math.max(...blocksToProcess.results.map((b) => b.height));
await this.blocksDB.put("chain_tip", latestHeightProcessed.toString());
console.log(`Cron: Updated chain_tip to ${latestHeightProcessed}`);

const heightsToDelete = blocksToProcess.results.map((r) => r.height);
const heights = heightsToDelete.join(",");
const deleteStmt = `DELETE FROM processed_blocks WHERE height IN (${heights})`;
Expand Down Expand Up @@ -401,4 +386,57 @@ export class Indexer implements Storage {
}
return updates;
}

async getStatusByTxid(txid: string): Promise<NbtcTxStatusResp | null> {
const latestHeightStr = await this.blocksDB.get("chain_tip");
const latestHeight = latestHeightStr ? parseInt(latestHeightStr, 10) : 0;

const tx = await this.d1
.prepare("SELECT * FROM nbtc_txs WHERE tx_id = ?")
.bind(txid)
.first<NbtcTxD1Row>();

if (!tx) {
return null;
}

const blockHeight = tx.block_height as number;
const confirmations = blockHeight ? latestHeight - blockHeight + 1 : 0;

return {
btc_tx_id: tx.tx_id,
status: tx.status as NbtcTxStatus,
block_height: blockHeight,
confirmations: confirmations > 0 ? confirmations : 0,
sui_recipient: tx.sui_recipient,
amount_sats: tx.amount_sats,
};
}

async getStatusBySuiAddress(suiAddress: string): Promise<NbtcTxStatusResp[]> {
const latestHeightStr = await this.blocksDB.get("chain_tip");
const latestHeight = latestHeightStr ? parseInt(latestHeightStr, 10) : 0;

const dbResult = await this.d1
.prepare("SELECT * FROM nbtc_txs WHERE sui_recipient = ? ORDER BY created_at DESC")
.bind(suiAddress)
.all<NbtcTxD1Row>();

if (!dbResult.results) {
return [];
}

return dbResult.results.map((tx): NbtcTxStatusResp => {
const blockHeight = tx.block_height as number;
const confirmations = blockHeight ? latestHeight - blockHeight + 1 : 0;
return {
btc_tx_id: tx.tx_id,
status: tx.status as NbtcTxStatus,
block_height: blockHeight,
confirmations: confirmations > 0 ? confirmations : 0,
sui_recipient: tx.sui_recipient,
amount_sats: tx.amount_sats,
};
});
}
}
51 changes: 51 additions & 0 deletions packages/btcindexer/src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export interface Deposit {
vout: number;
amountSats: number;
suiRecipient: string;
}

export interface ProofResult {
proofPath: Buffer[];
merkleRoot: string;
}

export interface PendingTx {
tx_id: string;
block_hash: string | null;
block_height: number;
}

export interface BlockRecord {
tx_id: string;
block_hash: string;
block_height: number;
}

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

export type NbtcTxStatus = "confirming" | "finalized" | "minted" | "failed" | "reorg";

export interface NbtcTxStatusResp {
btc_tx_id: string;
status: NbtcTxStatus;
block_height: number | null;
confirmations: number;
sui_recipient: string;
amount_sats: number;
}

export interface NbtcTxD1Row {
tx_id: string;
block_hash: string;
block_height: number | null;
vout: number;
sui_recipient: string;
amount_sats: number;
status: NbtcTxStatus;
created_at: number;
updated_at: number;
}
26 changes: 26 additions & 0 deletions packages/btcindexer/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IRequest, Router, error, json } from "itty-router";
import { isValidSuiAddress } from "@mysten/sui/utils";

import { Indexer } from "./btcindexer";
import { RestPath } from "./api/client";
Expand Down Expand Up @@ -26,6 +27,10 @@
r.put(RestPath.blocks, this.putBlocks);
r.put(RestPath.nbtcTx, this.putNbtcTx);

// ?sui_recipient="0x..." - query by sui address
r.get(RestPath.nbtcTx, this.getStatusBySuiAddress);
r.get(RestPath.nbtcTx + "/:txid", this.getStatusByTxid); // query by bitcoin_tx_id

//
// TESTING
// we can return Response object directly, to avoid JSON serialization
Expand Down Expand Up @@ -78,7 +83,7 @@
}
};

putNbtcTx = async (req: IRequest) => {

Check warning on line 86 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 All @@ -99,4 +104,25 @@
const key = req.params.key;
return kv.get(key);
};

getStatusByTxid = async (req: IRequest) => {
const { txid } = req.params;
const result = await this.indexer().getStatusByTxid(txid);

if (result === null) {
return error(404, "Transaction not found.");
}
return result;
};

getStatusBySuiAddress = async (req: IRequest) => {
const suiRecipient = req.query.sui_recipient;
if (!suiRecipient || typeof suiRecipient !== "string") {
return error(400, "Missing or invalid sui_recipient query parameter.");
}
if (!isValidSuiAddress(suiRecipient)) {
return error(400, "Invalid SUI address format.");
}
return this.indexer().getStatusBySuiAddress(suiRecipient);
};
}
2 changes: 1 addition & 1 deletion packages/btcindexer/src/sui_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Signer } from "@mysten/sui/cryptography";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { Transaction as SuiTransaction } from "@mysten/sui/transactions";
import { Transaction } from "bitcoinjs-lib";
import { ProofResult } from "./btcindexer";
import { ProofResult } from "./models";

export interface SuiClientCfg {
network: "testnet" | "mainnet" | "devnet" | "localnet";
Expand Down