Skip to content

Commit 17663a8

Browse files
feat: nbtc minting (#38)
* PoC nbtc minting Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * rename Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * fix test Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * linter Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * apply code review suggestions: move sui client and btc serialization to a separate modules Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * linter Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * add new modules Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * Apply suggestions from code review Co-authored-by: Robert Zaremba <robert@zaremba.ch> Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com> * apply code review suggestions Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * use interface Signed-off-by: sczembor <stanislaw.czembor@gmail.com> * linter Signed-off-by: sczembor <stanislaw.czembor@gmail.com> --------- Signed-off-by: sczembor <stanislaw.czembor@gmail.com> Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com> Co-authored-by: Robert Zaremba <robert@zaremba.ch>
1 parent 5de3765 commit 17663a8

File tree

9 files changed

+6529
-6757
lines changed

9 files changed

+6529
-6757
lines changed

packages/btcindexer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"test": "vitest run"
1010
},
1111
"dependencies": {
12+
"@mysten/sui": "^1.36.0",
1213
"bitcoinjs-lib": "^6.1.7",
1314
"crypto-js": "^4.2.0",
1415
"itty-router": "^5.0.18",

packages/btcindexer/src/btcindexer-http.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,35 @@ import type { IRequest } from "itty-router";
22
import { parseBlocksFromStream } from "./btcblock";
33
import { Indexer } from "./btcindexer";
44
import { networks } from "bitcoinjs-lib";
5+
import { SuiClient } from "./sui-client";
56

67
export class HIndexer {
78
public nbtcAddr: string;
89
public suiFallbackAddr: string;
9-
public network: networks.Network;
10+
public btcNetwork: networks.Network;
1011

1112
constructor() {
1213
// TODO: need to provide through env variable
1314
this.nbtcAddr = "TODO";
1415
this.suiFallbackAddr = "TODO";
15-
this.network = networks.regtest;
16+
this.btcNetwork = networks.regtest;
1617
}
1718

1819
newIndexer(env: Env): Indexer {
19-
return new Indexer(env, this.nbtcAddr, this.suiFallbackAddr, this.network);
20+
const suiClient = new SuiClient({
21+
suiNetwork: env.SUI_NETWORK,
22+
suiPackageId: env.SUI_PACKAGE_ID,
23+
suiNbtcObjectId: env.NBTC_OBJECT_ID,
24+
suiLightClientObjectId: env.LIGHT_CLIENT_OBJECT_ID,
25+
suiSignerMnemonic: env.SUI_SIGNER_MNEMONIC,
26+
});
27+
return new Indexer(
28+
env,
29+
this.nbtcAddr,
30+
this.suiFallbackAddr,
31+
this.btcNetwork,
32+
suiClient,
33+
);
2034
}
2135

2236
// NOTE: we may need to put this to a separate worker

packages/btcindexer/src/btcindexer.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Deposit, Indexer, ProofResult } from "../src/btcindexer";
33
import { Block, networks, Transaction } from "bitcoinjs-lib";
44
import { MerkleTree } from "merkletreejs";
55
import SHA256 from "crypto-js/sha256";
6+
import { SuiClient, SuiClientConfig } from "./sui-client";
67

78
interface TxInfo {
89
id: string;
@@ -68,6 +69,15 @@ function mkMockD1() {
6869
};
6970
}
7071

72+
const SUI_CLIENT_CONFIG: SuiClientConfig = {
73+
suiNetwork: "testnet",
74+
suiPackageId: "0xPACKAGE",
75+
suiNbtcObjectId: "0xNBTC",
76+
suiLightClientObjectId: "0xLIGHTCLIENT",
77+
suiSignerMnemonic:
78+
"test mnemonic test mnemonic test mnemonic test mnemonic test mnemonic test mnemonic",
79+
};
80+
7181
const mkMockEnv = () =>
7282
({
7383
DB: mkMockD1(),
@@ -83,6 +93,7 @@ function prepareIndexer() {
8393
REGTEST_DATA[303].depositAddr,
8494
SUI_FALLBACK_ADDRESS,
8595
networks.regtest,
96+
new SuiClient(SUI_CLIENT_CONFIG),
8697
);
8798
return { mockEnv, indexer };
8899
}

packages/btcindexer/src/btcindexer.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { address, networks } from "bitcoinjs-lib";
33
import { OP_RETURN } from "./opcodes";
44
import { MerkleTree } from "merkletreejs";
55
import SHA256 from "crypto-js/sha256";
6+
import { SuiClient } from "./sui-client";
67

78
const CONFIRMATION_DEPTH = 8;
89

@@ -23,19 +24,33 @@ export interface PendingTx {
2324
block_height: number;
2425
}
2526

27+
interface BlockRecord {
28+
tx_id: string;
29+
block_hash: string;
30+
block_height: number;
31+
}
32+
2633
export class Indexer {
2734
d1: D1Database; // SQL DB
2835
blocksDB: KVNamespace;
2936
nbtcTxDB: KVNamespace;
3037
nbtcScriptHex: string;
3138
suiFallbackAddr: string;
32-
33-
constructor(env: Env, nbtcAddr: string, fallbackAddr: string, network: networks.Network) {
39+
suiClient: SuiClient;
40+
41+
constructor(
42+
env: Env,
43+
nbtcAddr: string,
44+
fallbackAddr: string,
45+
network: networks.Network,
46+
suiClient: SuiClient,
47+
) {
3448
this.d1 = env.DB;
3549
this.blocksDB = env.btc_blocks;
3650
this.nbtcTxDB = env.nbtc_txs;
3751
this.suiFallbackAddr = fallbackAddr;
3852
this.nbtcScriptHex = address.toOutputScript(nbtcAddr, network).toString("hex");
53+
this.suiClient = suiClient;
3954
}
4055

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

154169
async processFinalizedTransactions(): Promise<void> {
155170
const finalizedTxs = await this.d1
156-
.prepare("SELECT tx_id, block_hash FROM nbtc_txs WHERE status = 'finalized'")
157-
.all<{ tx_id: string; block_hash: string }>();
171+
.prepare(
172+
"SELECT tx_id, block_hash, height FROM nbtc_txs WHERE status = 'finalized'",
173+
)
174+
.all<BlockRecord>();
158175

159176
if (!finalizedTxs.results || finalizedTxs.results.length === 0) {
160177
return;
161178
}
162179

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

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

173190
const updates: D1PreparedStatement[] = [];
174191
// 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.
175-
const setMintingStmt = this.d1.prepare(
176-
"UPDATE nbtc_txs SET status = 'minting', updated_at = CURRENT_TIMESTAMP WHERE tx_id = ?",
192+
const setMintedStmt = this.d1.prepare(
193+
"UPDATE nbtc_txs SET status = 'minted', updated_at = CURRENT_TIMESTAMP WHERE tx_id = ?",
194+
);
195+
const setFailedStmt = this.d1.prepare(
196+
"UPDATE nbtc_txs SET status = 'failed', updated_at = CURRENT_TIMESTAMP WHERE tx_id = ?",
177197
);
178198

179199
for (const [block_hash, txsInBlock] of txsByBlock.entries()) {
@@ -210,9 +230,17 @@ export class Indexer {
210230
continue;
211231
}
212232

213-
// TODO: Call the minting smart contract.
214-
// await suiClient.mintNBTC();
215-
updates.push(setMintingStmt.bind(txInfo.tx_id));
233+
const isSuccess = await this.suiClient.tryMintNbtc(
234+
targetTx,
235+
txInfo.block_height,
236+
txIndex,
237+
proof,
238+
);
239+
if (isSuccess) {
240+
updates.push(setMintedStmt.bind(txInfo.tx_id));
241+
} else {
242+
updates.push(setFailedStmt.bind(txInfo.tx_id));
243+
}
216244
}
217245
} catch (e) {
218246
console.error(`Failed to process finalized txs in block ${block_hash}:`, e);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Transaction, TxInput, TxOutput } from "bitcoinjs-lib";
2+
3+
export interface SerializedBtcTx {
4+
version: number[];
5+
inputCount: number;
6+
inputs: number[];
7+
outputCount: number;
8+
outputs: number[];
9+
lockTime: number[];
10+
}
11+
12+
function serializeU32(n: number): number[] {
13+
const buffer = Buffer.alloc(4);
14+
buffer.writeUInt32LE(n, 0);
15+
return Array.from(buffer);
16+
}
17+
18+
function serializeTxInputs(inputs: TxInput[]): number[] {
19+
const buffers = inputs.map((vin) => {
20+
const hash = Buffer.from(vin.hash);
21+
const index = Buffer.alloc(4);
22+
index.writeUInt32LE(vin.index, 0);
23+
const scriptLen = Buffer.from([vin.script.length]);
24+
const sequence = Buffer.alloc(4);
25+
sequence.writeUInt32LE(vin.sequence, 0);
26+
return Buffer.concat([hash, index, scriptLen, vin.script, sequence]);
27+
});
28+
return Array.from(Buffer.concat(buffers));
29+
}
30+
31+
function serializeTxOutputs(outputs: TxOutput[]): number[] {
32+
const buffers = outputs.map((vout) => {
33+
const value = Buffer.alloc(8);
34+
value.writeBigUInt64LE(BigInt(vout.value), 0);
35+
const scriptLen = Buffer.from([vout.script.length]);
36+
return Buffer.concat([value, scriptLen, vout.script]);
37+
});
38+
return Array.from(Buffer.concat(buffers));
39+
}
40+
41+
export function serializeBtcTx(transaction: Transaction): SerializedBtcTx {
42+
return {
43+
version: serializeU32(transaction.version),
44+
inputCount: transaction.ins.length,
45+
inputs: serializeTxInputs(transaction.ins),
46+
outputCount: transaction.outs.length,
47+
outputs: serializeTxOutputs(transaction.outs),
48+
lockTime: serializeU32(transaction.locktime),
49+
};
50+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { SuiClient as Client, getFullnodeUrl } from "@mysten/sui/client";
2+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
3+
import { Transaction as SuiTransaction } from "@mysten/sui/transactions";
4+
import { Transaction } from "bitcoinjs-lib";
5+
import { serializeBtcTx } from "./btctx-serializer";
6+
import { ProofResult } from "./btcindexer";
7+
8+
export interface SuiClientConfig {
9+
suiNetwork: "testnet" | "mainnet" | "devnet";
10+
suiPackageId: string;
11+
suiNbtcObjectId: string;
12+
suiLightClientObjectId: string;
13+
suiSignerMnemonic: string;
14+
}
15+
16+
export class SuiClient {
17+
private client: Client;
18+
private signer: Ed25519Keypair;
19+
private packageId: string;
20+
private nbtcObjectId: string;
21+
private lightClientObjectId: string;
22+
23+
constructor(config: SuiClientConfig) {
24+
this.client = new Client({ url: getFullnodeUrl(config.suiNetwork) });
25+
this.signer = Ed25519Keypair.deriveKeypair(config.suiSignerMnemonic);
26+
this.packageId = config.suiPackageId;
27+
this.nbtcObjectId = config.suiNbtcObjectId;
28+
this.lightClientObjectId = config.suiLightClientObjectId;
29+
}
30+
31+
async mintNbtc(
32+
transaction: Transaction,
33+
blockHeight: number,
34+
txIndex: number,
35+
proof: ProofResult,
36+
): Promise<void> {
37+
const tx = new SuiTransaction();
38+
const target = `${this.packageId}::nbtc::mint` as const;
39+
const serializedTx = serializeBtcTx(transaction);
40+
41+
tx.moveCall({
42+
target: target,
43+
arguments: [
44+
tx.object(this.nbtcObjectId),
45+
tx.object(this.lightClientObjectId),
46+
tx.pure.vector("u8", serializedTx.version),
47+
tx.pure.u32(serializedTx.inputCount),
48+
tx.pure.vector("u8", serializedTx.inputs),
49+
tx.pure.u32(serializedTx.outputCount),
50+
tx.pure.vector("u8", serializedTx.outputs),
51+
tx.pure.vector("u8", serializedTx.lockTime),
52+
tx.pure.vector(
53+
"vector<u8>",
54+
proof.proofPath.map((p) => Array.from(p)),
55+
),
56+
tx.pure.u64(blockHeight),
57+
tx.pure.u64(txIndex),
58+
],
59+
});
60+
61+
const result = await this.client.signAndExecuteTransaction({
62+
signer: this.signer,
63+
transaction: tx,
64+
options: {
65+
showEffects: true,
66+
},
67+
});
68+
69+
if (result.effects?.status.status !== "success") {
70+
throw new Error(`Mint transaction failed: ${result.effects?.status.error}`);
71+
}
72+
}
73+
74+
async tryMintNbtc(
75+
transaction: Transaction,
76+
blockHeight: number,
77+
txIndex: number,
78+
proof: ProofResult,
79+
): Promise<boolean> {
80+
try {
81+
await this.mintNbtc(transaction, blockHeight, txIndex, proof);
82+
return true;
83+
} catch (error) {
84+
console.error(`Error during mint contract call`, error);
85+
return false;
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)