Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
16 changes: 7 additions & 9 deletions packages/btcindexer/db/migrations/0001_initial_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ CREATE INDEX IF NOT EXISTS btc_blocks_is_scanned_height ON btc_blocks (is_scanne
-- This table tracks the nBTC deposit txs (minting)
CREATE TABLE IF NOT EXISTS nbtc_minting (
tx_id TEXT NOT NULL PRIMARY KEY,
address_id INTEGER NOT NULL, -- nbtc pkg is linked through address_id
address_id TEXT NOT NULL, -- nbtc pkg is linked through address_id
sender TEXT NOT NULL,
vout INTEGER NOT NULL,
block_hash TEXT,
Expand All @@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS nbtc_minting (
updated_at INTEGER NOT NULL, -- timestamp_ms
sui_tx_id TEXT,
retry_count INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (address_id) REFERENCES nbtc_deposit_addresses(id)
FOREIGN KEY (address_id) REFERENCES nbtc_deposit_addresses(deposit_address)
) STRICT;

CREATE INDEX IF NOT EXISTS nbtc_minting_status ON nbtc_minting (address_id, status);
Expand All @@ -50,27 +50,25 @@ CREATE TABLE IF NOT EXISTS setups (
) STRICT;

CREATE TABLE IF NOT EXISTS nbtc_deposit_addresses (
id INTEGER PRIMARY KEY,
-- make sure we don't share bitcoin deposit address between setups
deposit_address TEXT PRIMARY KEY,
setup_id INTEGER NOT NULL,
deposit_address TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (setup_id) REFERENCES setups(id) ON DELETE CASCADE,
-- make sure we don't share bitcoin deposit address between setups
UNIQUE(deposit_address)
FOREIGN KEY (setup_id) REFERENCES setups(id) ON DELETE CASCADE
) STRICT;

CREATE TABLE IF NOT EXISTS nbtc_utxos (
nbtc_utxo_id INTEGER NOT NULL PRIMARY KEY, -- Sui ID asigned to this UTXO
-- TODO: This is an ID assigned by the smart contract. The primary key should be a combination of (setup_id, nbtc_utxo_id)
address_id INTEGER NOT NULL,
address_id TEXT NOT NULL,
dwallet_id TEXT NOT NULL,
txid TEXT NOT NULL, -- Bitcoin transaction ID
vout INTEGER NOT NULL,
amount INTEGER NOT NULL,
script_pubkey BLOB NOT NULL,
status TEXT NOT NULL DEFAULT 'available', -- 'available', 'locked', 'spent' TODO: lets remove the 'spent' utxos after some time?
locked_until INTEGER,
FOREIGN KEY (address_id) REFERENCES nbtc_deposit_addresses(id)
FOREIGN KEY (address_id) REFERENCES nbtc_deposit_addresses(deposit_address)
) STRICT;

CREATE INDEX IF NOT EXISTS nbtc_utxos_selection ON nbtc_utxos(address_id, status, amount);
Expand Down
15 changes: 1 addition & 14 deletions packages/btcindexer/src/btcindexer.helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,27 +278,14 @@ export async function setupTestIndexerSuite(

const depositAddr = options.depositAddress || defaultBlock.depositAddr;

// Validate that the deposit address exists in the database
const addressResult = await db
.prepare(`SELECT id FROM nbtc_deposit_addresses WHERE deposit_address = ?`)
.bind(depositAddr)
.first<{ id: number }>();

if (!addressResult) {
throw new Error(
`Deposit address '${depositAddr}' not found in database. ` +
`Make sure to include it in the depositAddresses array during setupTestIndexer().`,
);
}

await db
.prepare(
`INSERT INTO nbtc_minting (tx_id, address_id, sender, vout, block_hash, block_height, sui_recipient, amount, status, created_at, updated_at, retry_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.bind(
options.txId,
addressResult.id,
depositAddr,
options.sender || "sender_address",
options.vout ?? 0,
options.blockHash || defaultBlock.hash,
Expand Down
26 changes: 10 additions & 16 deletions packages/btcindexer/src/cf-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export class CFStorage implements Storage {
const now = Date.now();
const insertOrUpdateNbtcTxStmt = this.d1.prepare(
`INSERT INTO nbtc_minting (tx_id, address_id, sender, vout, block_hash, block_height, sui_recipient, amount, status, created_at, updated_at, sui_tx_id, retry_count)
VALUES (?, (SELECT a.id FROM nbtc_deposit_addresses a JOIN setups p ON a.setup_id = p.id WHERE p.btc_network = ? AND p.sui_network = ? AND p.nbtc_pkg = ? AND a.deposit_address = ?), ?, ?, ?, ?, ?, ?, '${MintTxStatus.Confirming}', ?, ?, NULL, 0)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '${MintTxStatus.Confirming}', ?, ?, NULL, 0)
ON CONFLICT(tx_id) DO UPDATE SET
block_hash = excluded.block_hash,
block_height = excluded.block_height,
Expand All @@ -197,9 +197,6 @@ export class CFStorage implements Storage {
const statements = txs.map((tx) =>
insertOrUpdateNbtcTxStmt.bind(
tx.txId,
tx.btcNetwork,
tx.suiNetwork,
tx.nbtcPkg,
tx.depositAddress,
tx.sender,
tx.vout,
Expand Down Expand Up @@ -230,7 +227,7 @@ export class CFStorage implements Storage {
.prepare(
`SELECT m.tx_id, m.vout, m.block_hash, m.block_height, p.nbtc_pkg, p.sui_network, p.btc_network, p.id as setup_id
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN nbtc_deposit_addresses a ON m.address_id = a.deposit_address
JOIN setups p ON a.setup_id = p.id
WHERE m.status = '${MintTxStatus.Finalized}' OR (m.status = '${MintTxStatus.MintFailed}' AND m.retry_count <= ?)`,
)
Expand All @@ -246,7 +243,7 @@ export class CFStorage implements Storage {
.prepare(
`SELECT m.tx_id, m.vout, m.block_hash, m.block_height, p.nbtc_pkg, p.sui_network, p.btc_network
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN nbtc_deposit_addresses a ON m.address_id = a.deposit_address
JOIN setups p ON a.setup_id = p.id
WHERE m.status = '${MintTxStatus.Minted}' AND m.block_height >= ?`,
)
Expand All @@ -266,7 +263,7 @@ export class CFStorage implements Storage {
m.block_height
FROM nbtc_minting m
INNER JOIN btc_blocks b ON m.block_height = b.height
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN nbtc_deposit_addresses a ON m.address_id = a.deposit_address
JOIN setups p ON a.setup_id = p.id AND p.btc_network = b.network
WHERE m.status = '${MintTxStatus.Minted}'
AND m.block_height >= ?
Expand Down Expand Up @@ -340,7 +337,7 @@ export class CFStorage implements Storage {
.prepare(
`SELECT DISTINCT m.block_hash, p.btc_network as network
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN nbtc_deposit_addresses a ON m.address_id = a.deposit_address
JOIN setups p ON a.setup_id = p.id
WHERE m.status = '${MintTxStatus.Confirming}' AND m.block_hash IS NOT NULL`,
)
Expand Down Expand Up @@ -368,7 +365,7 @@ export class CFStorage implements Storage {
.prepare(
`SELECT m.tx_id, m.block_hash, m.block_height, p.btc_network, a.deposit_address
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN nbtc_deposit_addresses a ON m.address_id = a.deposit_address
JOIN setups p ON a.setup_id = p.id
WHERE m.status = '${MintTxStatus.Confirming}'`,
)
Expand All @@ -393,7 +390,7 @@ export class CFStorage implements Storage {
.prepare(
`SELECT m.*, p.nbtc_pkg, p.sui_network, p.btc_network
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN nbtc_deposit_addresses a ON m.address_id = a.deposit_address
JOIN setups p ON a.setup_id = p.id
WHERE m.tx_id = ?`,
)
Expand All @@ -406,7 +403,7 @@ export class CFStorage implements Storage {
.prepare(
`SELECT m.*, p.nbtc_pkg, p.sui_network, p.btc_network
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN nbtc_deposit_addresses a ON m.address_id = a.deposit_address
JOIN setups p ON a.setup_id = p.id
WHERE m.sui_recipient = ? ORDER BY m.created_at DESC`,
)
Expand All @@ -419,15 +416,12 @@ export class CFStorage implements Storage {
const now = Date.now();
const insertStmt = this.d1.prepare(
`INSERT OR IGNORE INTO nbtc_minting (tx_id, address_id, sender, vout, sui_recipient, amount, status, created_at, updated_at, sui_tx_id, retry_count)
VALUES (?, (SELECT a.id FROM nbtc_deposit_addresses a JOIN setups p ON a.setup_id = p.id WHERE p.btc_network = ? AND p.sui_network = ? AND p.nbtc_pkg = ? AND a.deposit_address = ?), ?, ?, ?, ?, '${MintTxStatus.Broadcasting}', ?, ?, NULL, 0)`,
VALUES (?, ?, ?, ?, ?, ?, '${MintTxStatus.Broadcasting}', ?, ?, NULL, 0)`,
);

const statements = deposits.map((deposit) =>
insertStmt.bind(
deposit.txId,
deposit.btcNetwork,
deposit.suiNetwork,
deposit.nbtcPkg,
deposit.depositAddress,
deposit.sender,
deposit.vout,
Expand Down Expand Up @@ -455,7 +449,7 @@ export class CFStorage implements Storage {
const query = this.d1.prepare(`
SELECT m.*, p.nbtc_pkg, p.sui_network, p.btc_network
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN nbtc_deposit_addresses a ON m.address_id = a.deposit_address
JOIN setups p ON a.setup_id = p.id
WHERE m.sender = ? AND p.btc_network = ?
ORDER BY m.created_at DESC
Expand Down
4 changes: 2 additions & 2 deletions packages/btcindexer/src/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ describe("CFStorage", () => {
await db
.prepare(
`
INSERT INTO nbtc_deposit_addresses (id, setup_id, deposit_address, is_active)
VALUES (10, 1, 'bcrt1qAddress1', 1)
INSERT INTO nbtc_deposit_addresses (setup_id, deposit_address, is_active)
VALUES (1, 'bcrt1qAddress1', 1)
`,
)
.run();
Expand Down
2 changes: 1 addition & 1 deletion packages/sui-indexer/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface Utxo {
vout: number;
amount: number;
script_pubkey: Uint8Array;
address_id: number;
address_id: string; // deposit Bitcoin address
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it should be just address

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I will rename to deposit_address.

status: UtxoStatus;
locked_until: number | null;
}
Expand Down
11 changes: 5 additions & 6 deletions packages/sui-indexer/src/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,16 @@ async function insertSetup(

async function insertDepositAddress(
database: D1Database,
id: number,
setupId: number,
depositAddress: string,
isActive = 1,
) {
await database
.prepare(
`INSERT INTO nbtc_deposit_addresses (id, setup_id, deposit_address, is_active)
VALUES (?, ?, ?, ?)`,
`INSERT INTO nbtc_deposit_addresses (setup_id, deposit_address, is_active)
VALUES (?, ?, ?)`,
)
.bind(id, setupId, depositAddress, isActive)
.bind(setupId, depositAddress, isActive)
.run();
}

Expand All @@ -166,7 +165,7 @@ describe("IndexerStorage", () => {
"0xLCC1",
"0xFallback1",
);
await insertDepositAddress(db, 1, 1, depositAddress1);
await insertDepositAddress(db, 1, depositAddress1);
});

afterEach(async () => dropTables(await mf.getD1Database("DB")));
Expand Down Expand Up @@ -276,7 +275,7 @@ describe("IndexerStorage", () => {
"0xLCC2",
"0xFallback2",
);
await insertDepositAddress(db, 2, 2, depositAddress2);
await insertDepositAddress(db, 2, depositAddress2);

await insertUtxo(
storage,
Expand Down
19 changes: 3 additions & 16 deletions packages/sui-indexer/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ interface UtxoRow {
vout: number;
amount: number;
script_pubkey: ArrayBuffer;
address_id: number;
address_id: string;
status: UtxoStatus;
locked_until: number | null;
}
Expand Down Expand Up @@ -155,19 +155,6 @@ export class D1Storage {
throw new Error(`Failed to derive address from script_pubkey: ${e}`);
}

const addrRow = await this.db
.prepare(
"SELECT id FROM nbtc_deposit_addresses WHERE setup_id = ? AND deposit_address = ?",
)
.bind(u.setup_id, depositAddress)
.first<{ id: number }>();

if (!addrRow) {
throw new Error(
`Deposit address not found for setup_id=${u.setup_id}, address=${depositAddress}`,
);
}

const stmt = this.db.prepare(
`INSERT OR REPLACE INTO nbtc_utxos
(nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount, script_pubkey, status, locked_until)
Expand All @@ -177,7 +164,7 @@ export class D1Storage {
await stmt
.bind(
u.nbtc_utxo_id,
addrRow.id,
depositAddress,
u.dwallet_id,
Comment on lines 164 to 168
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insertUtxo now writes depositAddress directly into nbtc_utxos.address_id without verifying that the derived address is registered for u.setup_id. With deposit_address being globally unique, a caller bug (wrong setup_id vs. script_pubkey) would still satisfy the FK and silently associate the UTXO with the other setup’s address. Consider restoring the invariant check by selecting setup_id from nbtc_deposit_addresses for depositAddress and throwing if it doesn’t match u.setup_id (or enforcing the relationship at the schema level).

Copilot uses AI. Check for mistakes.
u.txid,
u.vout,
Expand Down Expand Up @@ -560,7 +547,7 @@ export class D1Storage {
const query = `
SELECT u.nbtc_utxo_id, u.dwallet_id, u.txid, u.vout, u.amount, u.script_pubkey, u.address_id, u.status, u.locked_until
FROM nbtc_utxos u
JOIN nbtc_deposit_addresses a ON u.address_id = a.id
JOIN nbtc_deposit_addresses a ON u.address_id = a.deposit_address
WHERE a.setup_id = ?
AND u.status = ?
ORDER BY u.amount DESC;
Expand Down