Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions packages/btcindexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"test": "vitest run"
},
"dependencies": {
"@mysten/sui": "^1.36.0",
"bitcoinjs-lib": "^6.1.7",
"crypto-js": "^4.2.0",
"itty-router": "^5.0.18",
Expand Down
20 changes: 17 additions & 3 deletions packages/btcindexer/src/btcindexer-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,35 @@ import type { IRequest } from "itty-router";
import { parseBlocksFromStream } from "./btcblock";
import { Indexer } from "./btcindexer";
import { networks } from "bitcoinjs-lib";
import { SuiClient } from "./sui-client";

export class HIndexer {
public nbtcAddr: string;
public suiFallbackAddr: string;
public network: networks.Network;
public btcNetwork: networks.Network;

constructor() {
// TODO: need to provide through env variable
this.nbtcAddr = "TODO";
this.suiFallbackAddr = "TODO";
this.network = networks.regtest;
this.btcNetwork = networks.regtest;
}

newIndexer(env: Env): Indexer {
return new Indexer(env, this.nbtcAddr, this.suiFallbackAddr, this.network);
const suiClient = new SuiClient({
suiNetwork: env.SUI_NETWORK,
suiPackageId: env.SUI_PACKAGE_ID,
suiNbtcObjectId: env.NBTC_OBJECT_ID,
suiLightClientObjectId: env.LIGHT_CLIENT_OBJECT_ID,
suiSignerMnemonic: env.SUI_SIGNER_MNEMONIC,
});
return new Indexer(
env,
this.nbtcAddr,
this.suiFallbackAddr,
this.btcNetwork,
suiClient,
);
}

// NOTE: we may need to put this to a separate worker
Expand Down
11 changes: 11 additions & 0 deletions packages/btcindexer/src/btcindexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Deposit, Indexer, ProofResult } from "../src/btcindexer";
import { Block, networks, Transaction } from "bitcoinjs-lib";
import { MerkleTree } from "merkletreejs";
import SHA256 from "crypto-js/sha256";
import { SuiClient, SuiClientConfig } from "./sui-client";

interface TxInfo {
id: string;
Expand Down Expand Up @@ -68,6 +69,15 @@ function mkMockD1() {
};
}

const SUI_CLIENT_CONFIG: SuiClientConfig = {
suiNetwork: "testnet",
suiPackageId: "0xPACKAGE",
suiNbtcObjectId: "0xNBTC",
suiLightClientObjectId: "0xLIGHTCLIENT",
suiSignerMnemonic:
"test mnemonic test mnemonic test mnemonic test mnemonic test mnemonic test mnemonic",
};

const mkMockEnv = () =>
({
DB: mkMockD1(),
Expand All @@ -83,6 +93,7 @@ function prepareIndexer() {
REGTEST_DATA[303].depositAddr,
SUI_FALLBACK_ADDRESS,
networks.regtest,
new SuiClient(SUI_CLIENT_CONFIG),
);
return { mockEnv, indexer };
}
Expand Down
47 changes: 38 additions & 9 deletions packages/btcindexer/src/btcindexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { address, networks } from "bitcoinjs-lib";
import { OP_RETURN } from "./opcodes";
import { MerkleTree } from "merkletreejs";
import SHA256 from "crypto-js/sha256";
import { SuiClient } from "./sui-client";

const CONFIRMATION_DEPTH = 8;

Expand All @@ -23,19 +24,33 @@ export interface PendingTx {
block_height: number;
}

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

export class Indexer {
d1: D1Database; // SQL DB
blocksDB: KVNamespace;
nbtcTxDB: KVNamespace;
nbtcScriptHex: string;
suiFallbackAddr: string;

constructor(env: Env, nbtcAddr: string, fallbackAddr: string, network: networks.Network) {
suiClient: SuiClient;

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

// returns number of processed and add blocks
Expand Down Expand Up @@ -153,14 +168,16 @@ export class Indexer {

async processFinalizedTransactions(): Promise<void> {
const finalizedTxs = await this.d1
.prepare("SELECT tx_id, block_hash FROM nbtc_txs WHERE status = 'finalized'")
.all<{ tx_id: string; block_hash: string }>();
.prepare(
"SELECT tx_id, block_hash, height FROM nbtc_txs WHERE status = 'finalized'",
)
.all<BlockRecord>();

if (!finalizedTxs.results || finalizedTxs.results.length === 0) {
return;
}

const txsByBlock = new Map<string, { tx_id: string; block_hash: string }[]>();
const txsByBlock = new Map<string, BlockRecord[]>();

for (const tx of finalizedTxs.results) {
if (!txsByBlock.has(tx.block_hash)) {
Expand All @@ -172,8 +189,11 @@ export class Indexer {

const updates: D1PreparedStatement[] = [];
// TODO: do we imidietly process it and check if it was succesful and just change the status to minted? This should be a matter of seconds at most.
const setMintingStmt = this.d1.prepare(
"UPDATE nbtc_txs SET status = 'minting', updated_at = CURRENT_TIMESTAMP WHERE tx_id = ?",
const setMintedStmt = this.d1.prepare(
"UPDATE nbtc_txs SET status = 'minted', updated_at = CURRENT_TIMESTAMP WHERE tx_id = ?",
);
const setFailedStmt = this.d1.prepare(
"UPDATE nbtc_txs SET status = 'failed', updated_at = CURRENT_TIMESTAMP WHERE tx_id = ?",
);

for (const [block_hash, txsInBlock] of txsByBlock.entries()) {
Expand Down Expand Up @@ -211,8 +231,17 @@ export class Indexer {
}

// TODO: Call the minting smart contract.
// await suiClient.mintNBTC();
updates.push(setMintingStmt.bind(txInfo.tx_id));
const isSuccess = await this.suiClient.mintNbtc(
targetTx,
txInfo.block_height,
txIndex,
proof,
);
if (isSuccess) {
updates.push(setMintedStmt.bind(txInfo.tx_id));
} else {
updates.push(setFailedStmt.bind(txInfo.tx_id));
}
}
} catch (e) {
console.error(`Failed to process finalized txs in block ${block_hash}:`, e);
Expand Down
50 changes: 50 additions & 0 deletions packages/btcindexer/src/btctx-serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Transaction, TxInput, TxOutput } from "bitcoinjs-lib";

export interface SerializedBtcTx {
version: number[];
inputCount: number;
inputs: number[];
outputCount: number;
outputs: number[];
lockTime: number[];
}

function serializeU32(n: number): number[] {
const buffer = Buffer.alloc(4);
buffer.writeUInt32LE(n, 0);
return Array.from(buffer);
}

function serializeTxInputs(inputs: TxInput[]): number[] {
const buffers = inputs.map((vin) => {
const hash = Buffer.from(vin.hash);
const index = Buffer.alloc(4);
index.writeUInt32LE(vin.index, 0);
const scriptLen = Buffer.from([vin.script.length]);
const sequence = Buffer.alloc(4);
sequence.writeUInt32LE(vin.sequence, 0);
return Buffer.concat([hash, index, scriptLen, vin.script, sequence]);
});
return Array.from(Buffer.concat(buffers));
}

function serializeTxOutputs(outputs: TxOutput[]): number[] {
const buffers = outputs.map((vout) => {
const value = Buffer.alloc(8);
value.writeBigUInt64LE(BigInt(vout.value), 0);
const scriptLen = Buffer.from([vout.script.length]);
return Buffer.concat([value, scriptLen, vout.script]);
});
return Array.from(Buffer.concat(buffers));
}

export function serializeBtcTx(transaction: Transaction): SerializedBtcTx {
return {
version: serializeU32(transaction.version),
inputCount: transaction.ins.length,
inputs: serializeTxInputs(transaction.ins),
outputCount: transaction.outs.length,
outputs: serializeTxOutputs(transaction.outs),
lockTime: serializeU32(transaction.locktime),
};
}
86 changes: 86 additions & 0 deletions packages/btcindexer/src/sui-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { SuiClient as Client, getFullnodeUrl } from "@mysten/sui/client";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { Transaction as SuiTransaction } from "@mysten/sui/transactions";
import { Transaction } from "bitcoinjs-lib";
import { serializeBtcTx } from "./btctx-serializer";
import { ProofResult } from "./btcindexer";

export interface SuiClientConfig {
suiNetwork: "testnet" | "mainnet" | "devnet";
suiPackageId: string;
suiNbtcObjectId: string;
suiLightClientObjectId: string;
suiSignerMnemonic: string;
}

export class SuiClient {
private client: Client;
private signer: Ed25519Keypair;
private packageId: string;
private nbtcObjectId: string;
private lightClientObjectId: string;

constructor(config: SuiClientConfig) {
this.client = new Client({ url: getFullnodeUrl(config.suiNetwork) });
this.signer = Ed25519Keypair.deriveKeypair(config.suiSignerMnemonic);
this.packageId = config.suiPackageId;
this.nbtcObjectId = config.suiNbtcObjectId;
this.lightClientObjectId = config.suiLightClientObjectId;
}

async mintNbtc(
transaction: Transaction,
blockHeight: number,
txIndex: number,
proof: ProofResult,
): Promise<boolean> {
try {
const tx = new SuiTransaction();
const target = `${this.packageId}::nbtc::mint` as const;

const serializedTx = serializeBtcTx(transaction);

tx.moveCall({
target: target,
arguments: [
tx.object(this.nbtcObjectId),
tx.object(this.lightClientObjectId),
tx.pure.vector("u8", serializedTx.version),
tx.pure.u32(serializedTx.inputCount),
tx.pure.vector("u8", serializedTx.inputs),
tx.pure.u32(serializedTx.outputCount),
tx.pure.vector("u8", serializedTx.outputs),
tx.pure.vector("u8", serializedTx.lockTime),
tx.pure.vector(
"vector<u8>",
proof.proofPath.map((p) => Array.from(p)),
),
tx.pure.u64(blockHeight),
tx.pure.u64(txIndex),
],
});

const result = await this.client.signAndExecuteTransaction({
signer: this.signer,
transaction: tx,
options: {
showEffects: true,
},
});

if (result.effects?.status.status === "success") {
console.log(`Mint successful. Digest: ${result.digest}`);
return true;
} else {
console.error(
`Mint failed. Digest: ${result.digest}:`,
result.effects?.status.error,
);
return false;
}
} catch (error) {
console.error(`Error during mint call`, error);
return false;
}
}
}
Loading