From e37a7b0cd98c84f1533a293007f8c86d6ad9baa1 Mon Sep 17 00:00:00 2001 From: Rayane Charif Date: Thu, 11 Dec 2025 14:50:45 +0100 Subject: [PATCH 1/7] test: Added test for sql queries made in storage.ts in redeem-solver package following the btcindexer approach. Setup the miniflare for redeem-solver with only the D1 db since it is the only one used across the package Signed-off-by: Rayane Charif --- packages/redeem_solver/package.json | 2 +- packages/redeem_solver/src/db.test.ts | 26 ++ packages/redeem_solver/src/storage.test.ts | 319 +++++++++++++++++++++ 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 packages/redeem_solver/src/db.test.ts create mode 100644 packages/redeem_solver/src/storage.test.ts diff --git a/packages/redeem_solver/package.json b/packages/redeem_solver/package.json index f4e3d01..578d2de 100644 --- a/packages/redeem_solver/package.json +++ b/packages/redeem_solver/package.json @@ -12,7 +12,7 @@ "deploy:prod": "wrangler deploy -c ./wrangler-prod.jsonc", "typecheck": "tsc", "cf-typegen": "wrangler types", - "test": "echo no tests" + "test": "bun test" }, "dependencies": { "@mysten/bcs": "^1.9.2", diff --git a/packages/redeem_solver/src/db.test.ts b/packages/redeem_solver/src/db.test.ts new file mode 100644 index 0000000..0ea6c11 --- /dev/null +++ b/packages/redeem_solver/src/db.test.ts @@ -0,0 +1,26 @@ +import * as path from "path"; +import { readdir } from "fs/promises"; +import { D1Database } from "@cloudflare/workers-types"; + +// Point to btcindexer's migrations since redeem_solver shares the same database schema +const MIGRATIONS_PATH = path.resolve(__dirname, "../../btcindexer/db/migrations"); + +export async function initDb(db: D1Database) { + const migrationFiles = await readdir(MIGRATIONS_PATH); + migrationFiles.sort(); + + for (const filename of migrationFiles) { + if (filename.endsWith(".sql")) { + const file = Bun.file(path.join(MIGRATIONS_PATH, filename)); + const migration = await file.text(); + const cleanedMigration = migration + .replace(/--.*/g, "") + .replace(/\n/g, " ") + .replace(/\s{2,}/g, " ") + .trim(); + if (cleanedMigration.length > 0) { + await db.exec(cleanedMigration); + } + } + } +} diff --git a/packages/redeem_solver/src/storage.test.ts b/packages/redeem_solver/src/storage.test.ts new file mode 100644 index 0000000..39b8e21 --- /dev/null +++ b/packages/redeem_solver/src/storage.test.ts @@ -0,0 +1,319 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "bun:test"; +import { Miniflare } from "miniflare"; +import { D1Storage } from "./storage"; +import { RedeemRequestStatus, UtxoStatus } from "@gonative-cc/sui-indexer/models"; +import { initDb } from "./db.test"; +import { toSuiNet } from "@gonative-cc/lib/nsui"; + +let mf: Miniflare; + +beforeAll(async () => { + mf = new Miniflare({ + script: "", + modules: true, + d1Databases: ["DB"], + d1Persist: false, + }); +}); + +afterAll(async () => { + await mf.dispose(); +}); + +beforeEach(async () => { + const db = await mf.getD1Database("DB"); + await initDb(db); +}); + +afterEach(async () => { + const db = await mf.getD1Database("DB"); + const tables = [ + "nbtc_utxos", + "nbtc_redeem_requests", + "nbtc_deposit_addresses", + "nbtc_packages", + ]; + const dropStms = tables.map((t) => `DROP TABLE IF EXISTS ${t};`).join(" "); + await db.exec(dropStms); +}); + +describe("D1Storage", () => { + let storage: D1Storage; + let db: D1Database; + + beforeEach(async () => { + db = await mf.getD1Database("DB"); + storage = new D1Storage(db); + await db + .prepare( + `INSERT INTO nbtc_packages (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, sui_fallback_address, is_active) + VALUES (1, 'regtest', 'devnet', '0xPkg1', '0xContract1', '0xLC1', '0xLCC1', '0xFallback1', 1)`, + ) + .run(); + await db + .prepare( + `INSERT INTO nbtc_deposit_addresses (id, package_id, deposit_address, is_active) + VALUES (1, 1, 'bcrt1qAddress1', 1)`, + ) + .run(); + }); + + it("getPendingRedeems should return pending redeems ordered by created_at", async () => { + const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); + + await db + .prepare( + `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + "redeem2", + 1, + "redeemer1", + recipientScript, + 5000, + 2000, + RedeemRequestStatus.Pending, + ) + .run(); + + await db + .prepare( + `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + "redeem1", + 1, + "redeemer1", + recipientScript, + 3000, + 1000, + RedeemRequestStatus.Pending, + ) + .run(); + + const redeems = await storage.getPendingRedeems(); + + expect(redeems.length).toBe(2); + expect(redeems[0]!.redeem_id).toBe("redeem1"); + expect(redeems[1]!.redeem_id).toBe("redeem2"); + expect(redeems[0]!.sui_network).toBe(toSuiNet("devnet")); + }); + + it("getRedeemsReadyForSolving should filter by status and created_at", async () => { + const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); + const now = Date.now(); + + await db + .prepare( + `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + "redeem1", + 1, + "redeemer1", + recipientScript, + 3000, + now - 5000, + RedeemRequestStatus.Proposed, + ) + .run(); + + await db + .prepare( + `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + "redeem2", + 1, + "redeemer1", + recipientScript, + 5000, + now + 5000, + RedeemRequestStatus.Proposed, + ) + .run(); + + const redeems = await storage.getRedeemsReadyForSolving(now); + + expect(redeems.length).toBe(1); + expect(redeems[0]!.redeem_id).toBe("redeem1"); + }); + + it("getAvailableUtxos should return utxos ordered by amount DESC", async () => { + const scriptPubkey = new Uint8Array([0x00, 0x14]); + + await db + .prepare( + `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind("utxo1", 1, "dwallet1", "tx1", 0, 1000, scriptPubkey, UtxoStatus.Available, null) + .run(); + + await db + .prepare( + `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind("utxo2", 1, "dwallet1", "tx2", 0, 5000, scriptPubkey, UtxoStatus.Available, null) + .run(); + + const utxos = await storage.getAvailableUtxos(1); + + expect(utxos.length).toBe(2); + expect(utxos[0]!.nbtc_utxo_id).toBe("utxo2"); + expect(utxos[1]!.nbtc_utxo_id).toBe("utxo1"); + }); + + it("getAvailableUtxos should filter by package_id and status", async () => { + const scriptPubkey = new Uint8Array([0x00, 0x14]); + + await db + .prepare( + `INSERT INTO nbtc_packages (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, sui_fallback_address, is_active) + VALUES (2, 'testnet', 'testnet', '0xPkg2', '0xContract2', '0xLC2', '0xLCC2', '0xFallback2', 1)`, + ) + .run(); + + await db + .prepare( + `INSERT INTO nbtc_deposit_addresses (id, package_id, deposit_address, is_active) + VALUES (2, 2, 'tb1qAddress2', 1)`, + ) + .run(); + + await db + .prepare( + `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind("utxo1", 1, "dwallet1", "tx1", 0, 1000, scriptPubkey, UtxoStatus.Available, null) + .run(); + + await db + .prepare( + `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + "utxo_locked", + 1, + "dwallet1", + "tx_locked", + 0, + 2000, + scriptPubkey, + UtxoStatus.Locked, + Date.now() + 10000, + ) + .run(); + + await db + .prepare( + `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind("utxo2", 2, "dwallet2", "tx2", 0, 3000, scriptPubkey, UtxoStatus.Available, null) + .run(); + + const utxos1 = await storage.getAvailableUtxos(1); + const utxos2 = await storage.getAvailableUtxos(2); + + expect(utxos1.length).toBe(1); + expect(utxos1[0]!.nbtc_utxo_id).toBe("utxo1"); + expect(utxos2.length).toBe(1); + expect(utxos2[0]!.nbtc_utxo_id).toBe("utxo2"); + }); + + it("markRedeemProposed should update redeem status and lock utxos", async () => { + const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); + const scriptPubkey = new Uint8Array([0x00, 0x14]); + const lockTimeMs = 60000; + + await db + .prepare( + `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + "redeem1", + 1, + "redeemer1", + recipientScript, + 3000, + 1000, + RedeemRequestStatus.Pending, + ) + .run(); + + await db + .prepare( + `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind("utxo1", 1, "dwallet1", "tx1", 0, 2000, scriptPubkey, UtxoStatus.Available, null) + .run(); + + await storage.markRedeemProposed("redeem1", ["utxo1"], lockTimeMs); + + const redeem = await db + .prepare("SELECT status FROM nbtc_redeem_requests WHERE redeem_id = ?") + .bind("redeem1") + .first<{ status: string }>(); + expect(redeem!.status).toBe(RedeemRequestStatus.Proposed); + + const utxo = await db + .prepare("SELECT status, locked_until FROM nbtc_utxos WHERE nbtc_utxo_id = ?") + .bind("utxo1") + .first<{ status: string; locked_until: number }>(); + expect(utxo!.status).toBe(UtxoStatus.Locked); + expect(utxo!.locked_until).toBeGreaterThan(Date.now()); + }); + + it("markRedeemSolved should update redeem status", async () => { + const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); + + await db + .prepare( + `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + "redeem1", + 1, + "redeemer1", + recipientScript, + 3000, + 1000, + RedeemRequestStatus.Proposed, + ) + .run(); + + await storage.markRedeemSolved("redeem1"); + + const redeem = await db + .prepare("SELECT status FROM nbtc_redeem_requests WHERE redeem_id = ?") + .bind("redeem1") + .first<{ status: string }>(); + expect(redeem!.status).toBe(RedeemRequestStatus.Solved); + }); + + it("getActiveNetworks should return distinct active networks", async () => { + await db + .prepare( + `INSERT INTO nbtc_packages (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, sui_fallback_address, is_active) + VALUES (2, 'mainnet', 'mainnet', '0xPkg2', '0xContract2', '0xLC2', '0xLCC2', '0xFallback2', 1)`, + ) + .run(); + + const networks = await storage.getActiveNetworks(); + + expect(networks.length).toBe(2); + expect(networks).toContain(toSuiNet("devnet")); + expect(networks).toContain(toSuiNet("mainnet")); + }); +}); From 656b4ae2900d90abe8a0d0b4f89a79aae8652b06 Mon Sep 17 00:00:00 2001 From: Rayane Charif Date: Thu, 11 Dec 2025 14:55:04 +0100 Subject: [PATCH 2/7] Added condition is_active=1 for test of function getActiveNetworks() Signed-off-by: Rayane Charif --- packages/redeem_solver/src/storage.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/redeem_solver/src/storage.test.ts b/packages/redeem_solver/src/storage.test.ts index 39b8e21..d96233b 100644 --- a/packages/redeem_solver/src/storage.test.ts +++ b/packages/redeem_solver/src/storage.test.ts @@ -303,6 +303,7 @@ describe("D1Storage", () => { }); it("getActiveNetworks should return distinct active networks", async () => { + // Add active package with different network await db .prepare( `INSERT INTO nbtc_packages (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, sui_fallback_address, is_active) @@ -310,10 +311,19 @@ describe("D1Storage", () => { ) .run(); + // Add inactive package (should NOT be included) + await db + .prepare( + `INSERT INTO nbtc_packages (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, sui_fallback_address, is_active) + VALUES (3, 'testnet', 'testnet', '0xPkg3', '0xContract3', '0xLC3', '0xLCC3', '0xFallback3', 0)`, + ) + .run(); + const networks = await storage.getActiveNetworks(); expect(networks.length).toBe(2); expect(networks).toContain(toSuiNet("devnet")); expect(networks).toContain(toSuiNet("mainnet")); + expect(networks).not.toContain(toSuiNet("testnet")); // Inactive package excluded }); }); From 7c378aa5b35563d83cdb825c9bfe02b74320fff7 Mon Sep 17 00:00:00 2001 From: Rayane Charif Date: Thu, 11 Dec 2025 14:56:36 +0100 Subject: [PATCH 3/7] removed comments Signed-off-by: Rayane Charif --- packages/redeem_solver/src/storage.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/redeem_solver/src/storage.test.ts b/packages/redeem_solver/src/storage.test.ts index d96233b..ce9b29c 100644 --- a/packages/redeem_solver/src/storage.test.ts +++ b/packages/redeem_solver/src/storage.test.ts @@ -303,15 +303,12 @@ describe("D1Storage", () => { }); it("getActiveNetworks should return distinct active networks", async () => { - // Add active package with different network await db .prepare( `INSERT INTO nbtc_packages (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, sui_fallback_address, is_active) VALUES (2, 'mainnet', 'mainnet', '0xPkg2', '0xContract2', '0xLC2', '0xLCC2', '0xFallback2', 1)`, ) .run(); - - // Add inactive package (should NOT be included) await db .prepare( `INSERT INTO nbtc_packages (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, sui_fallback_address, is_active) @@ -324,6 +321,6 @@ describe("D1Storage", () => { expect(networks.length).toBe(2); expect(networks).toContain(toSuiNet("devnet")); expect(networks).toContain(toSuiNet("mainnet")); - expect(networks).not.toContain(toSuiNet("testnet")); // Inactive package excluded + expect(networks).not.toContain(toSuiNet("testnet")); }); }); From e259917fc1f93ec6b9b37e4834434a898ed568a4 Mon Sep 17 00:00:00 2001 From: Rayane Charif Date: Thu, 11 Dec 2025 14:56:52 +0100 Subject: [PATCH 4/7] removed comments Signed-off-by: Rayane Charif --- packages/redeem_solver/src/db.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/redeem_solver/src/db.test.ts b/packages/redeem_solver/src/db.test.ts index 0ea6c11..3284eea 100644 --- a/packages/redeem_solver/src/db.test.ts +++ b/packages/redeem_solver/src/db.test.ts @@ -2,7 +2,6 @@ import * as path from "path"; import { readdir } from "fs/promises"; import { D1Database } from "@cloudflare/workers-types"; -// Point to btcindexer's migrations since redeem_solver shares the same database schema const MIGRATIONS_PATH = path.resolve(__dirname, "../../btcindexer/db/migrations"); export async function initDb(db: D1Database) { From 595074507535ad01b75db461d23f89df95dff716 Mon Sep 17 00:00:00 2001 From: Rayane Charif Date: Thu, 11 Dec 2025 16:57:27 +0100 Subject: [PATCH 5/7] Added helper function Signed-off-by: Rayane Charif --- packages/redeem_solver/src/storage.test.ts | 323 +++++++++++---------- packages/redeem_solver/src/storage.ts | 2 + 2 files changed, 173 insertions(+), 152 deletions(-) diff --git a/packages/redeem_solver/src/storage.test.ts b/packages/redeem_solver/src/storage.test.ts index ce9b29c..99a35a2 100644 --- a/packages/redeem_solver/src/storage.test.ts +++ b/packages/redeem_solver/src/storage.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "bun:test"; import { Miniflare } from "miniflare"; -import { D1Storage } from "./storage"; +import { D1Storage, UTXO_LOCK_TIME_MS } from "./storage"; import { RedeemRequestStatus, UtxoStatus } from "@gonative-cc/sui-indexer/models"; import { initDb } from "./db.test"; import { toSuiNet } from "@gonative-cc/lib/nsui"; @@ -41,6 +41,54 @@ describe("D1Storage", () => { let storage: D1Storage; let db: D1Database; + async function insertRedeemRequest( + redeemId: string, + packageId: number, + redeemer: string, + recipientScript: Uint8Array, + amountSats: number, + createdAt: number, + status: RedeemRequestStatus, + ) { + await db + .prepare( + `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind(redeemId, packageId, redeemer, recipientScript, amountSats, createdAt, status) + .run(); + } + + async function insertUtxo( + utxoId: string, + addressId: number, + dwalletId: string, + txid: string, + vout: number, + amountSats: number, + scriptPubkey: Uint8Array, + status: UtxoStatus, + lockedUntil: number | null, + ) { + await db + .prepare( + `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + utxoId, + addressId, + dwalletId, + txid, + vout, + amountSats, + scriptPubkey, + status, + lockedUntil, + ) + .run(); + } + beforeEach(async () => { db = await mf.getD1Database("DB"); storage = new D1Storage(db); @@ -61,37 +109,24 @@ describe("D1Storage", () => { it("getPendingRedeems should return pending redeems ordered by created_at", async () => { const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); - await db - .prepare( - `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - "redeem2", - 1, - "redeemer1", - recipientScript, - 5000, - 2000, - RedeemRequestStatus.Pending, - ) - .run(); - - await db - .prepare( - `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - "redeem1", - 1, - "redeemer1", - recipientScript, - 3000, - 1000, - RedeemRequestStatus.Pending, - ) - .run(); + await insertRedeemRequest( + "redeem2", + 1, + "redeemer1", + recipientScript, + 5000, + 2000, + RedeemRequestStatus.Pending, + ); + await insertRedeemRequest( + "redeem1", + 1, + "redeemer1", + recipientScript, + 3000, + 1000, + RedeemRequestStatus.Pending, + ); const redeems = await storage.getPendingRedeems(); @@ -105,37 +140,24 @@ describe("D1Storage", () => { const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); const now = Date.now(); - await db - .prepare( - `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - "redeem1", - 1, - "redeemer1", - recipientScript, - 3000, - now - 5000, - RedeemRequestStatus.Proposed, - ) - .run(); - - await db - .prepare( - `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - "redeem2", - 1, - "redeemer1", - recipientScript, - 5000, - now + 5000, - RedeemRequestStatus.Proposed, - ) - .run(); + await insertRedeemRequest( + "redeem1", + 1, + "redeemer1", + recipientScript, + 3000, + now - 5000, + RedeemRequestStatus.Proposed, + ); + await insertRedeemRequest( + "redeem2", + 1, + "redeemer1", + recipientScript, + 5000, + now + 5000, + RedeemRequestStatus.Proposed, + ); const redeems = await storage.getRedeemsReadyForSolving(now); @@ -146,21 +168,28 @@ describe("D1Storage", () => { it("getAvailableUtxos should return utxos ordered by amount DESC", async () => { const scriptPubkey = new Uint8Array([0x00, 0x14]); - await db - .prepare( - `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .bind("utxo1", 1, "dwallet1", "tx1", 0, 1000, scriptPubkey, UtxoStatus.Available, null) - .run(); - - await db - .prepare( - `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .bind("utxo2", 1, "dwallet1", "tx2", 0, 5000, scriptPubkey, UtxoStatus.Available, null) - .run(); + await insertUtxo( + "utxo1", + 1, + "dwallet1", + "tx1", + 0, + 1000, + scriptPubkey, + UtxoStatus.Available, + null, + ); + await insertUtxo( + "utxo2", + 1, + "dwallet1", + "tx2", + 0, + 5000, + scriptPubkey, + UtxoStatus.Available, + null, + ); const utxos = await storage.getAvailableUtxos(1); @@ -186,39 +215,39 @@ describe("D1Storage", () => { ) .run(); - await db - .prepare( - `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .bind("utxo1", 1, "dwallet1", "tx1", 0, 1000, scriptPubkey, UtxoStatus.Available, null) - .run(); - - await db - .prepare( - `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - "utxo_locked", - 1, - "dwallet1", - "tx_locked", - 0, - 2000, - scriptPubkey, - UtxoStatus.Locked, - Date.now() + 10000, - ) - .run(); - - await db - .prepare( - `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .bind("utxo2", 2, "dwallet2", "tx2", 0, 3000, scriptPubkey, UtxoStatus.Available, null) - .run(); + await insertUtxo( + "utxo1", + 1, + "dwallet1", + "tx1", + 0, + 1000, + scriptPubkey, + UtxoStatus.Available, + null, + ); + await insertUtxo( + "utxo_locked", + 1, + "dwallet1", + "tx_locked", + 0, + 2000, + scriptPubkey, + UtxoStatus.Locked, + Date.now() + 10000, + ); + await insertUtxo( + "utxo2", + 2, + "dwallet2", + "tx2", + 0, + 3000, + scriptPubkey, + UtxoStatus.Available, + null, + ); const utxos1 = await storage.getAvailableUtxos(1); const utxos2 = await storage.getAvailableUtxos(2); @@ -232,33 +261,29 @@ describe("D1Storage", () => { it("markRedeemProposed should update redeem status and lock utxos", async () => { const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); const scriptPubkey = new Uint8Array([0x00, 0x14]); - const lockTimeMs = 60000; - await db - .prepare( - `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - "redeem1", - 1, - "redeemer1", - recipientScript, - 3000, - 1000, - RedeemRequestStatus.Pending, - ) - .run(); - - await db - .prepare( - `INSERT INTO nbtc_utxos (nbtc_utxo_id, address_id, dwallet_id, txid, vout, amount_sats, script_pubkey, status, locked_until) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .bind("utxo1", 1, "dwallet1", "tx1", 0, 2000, scriptPubkey, UtxoStatus.Available, null) - .run(); - - await storage.markRedeemProposed("redeem1", ["utxo1"], lockTimeMs); + await insertRedeemRequest( + "redeem1", + 1, + "redeemer1", + recipientScript, + 3000, + 1000, + RedeemRequestStatus.Pending, + ); + await insertUtxo( + "utxo1", + 1, + "dwallet1", + "tx1", + 0, + 2000, + scriptPubkey, + UtxoStatus.Available, + null, + ); + + await storage.markRedeemProposed("redeem1", ["utxo1"], UTXO_LOCK_TIME_MS); const redeem = await db .prepare("SELECT status FROM nbtc_redeem_requests WHERE redeem_id = ?") @@ -277,21 +302,15 @@ describe("D1Storage", () => { it("markRedeemSolved should update redeem status", async () => { const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); - await db - .prepare( - `INSERT INTO nbtc_redeem_requests (redeem_id, package_id, redeemer, recipient_script, amount_sats, created_at, status) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - "redeem1", - 1, - "redeemer1", - recipientScript, - 3000, - 1000, - RedeemRequestStatus.Proposed, - ) - .run(); + await insertRedeemRequest( + "redeem1", + 1, + "redeemer1", + recipientScript, + 3000, + 1000, + RedeemRequestStatus.Proposed, + ); await storage.markRedeemSolved("redeem1"); diff --git a/packages/redeem_solver/src/storage.ts b/packages/redeem_solver/src/storage.ts index 684d252..da0a5f6 100644 --- a/packages/redeem_solver/src/storage.ts +++ b/packages/redeem_solver/src/storage.ts @@ -6,6 +6,8 @@ import { type Utxo, } from "@gonative-cc/sui-indexer/models"; +export const UTXO_LOCK_TIME_MS = 120000; // 2 minutes + interface RedeemRequestRow { redeem_id: string; package_id: number; From 8b40ca30e29cef9ef49863f026ac634b6aed9a7f Mon Sep 17 00:00:00 2001 From: Rayane Charif Date: Sat, 13 Dec 2025 10:32:26 +0100 Subject: [PATCH 6/7] Moved db.test.ts into common package for test setup in lib Signed-off-by: Rayane Charif --- packages/btcindexer/src/db.test.ts | 20 ++------------------ packages/lib/package.json | 3 ++- packages/lib/src/common-setup/db.test.ts | 23 +++++++++++++++++++++++ packages/redeem_solver/src/db.test.ts | 20 ++------------------ 4 files changed, 29 insertions(+), 37 deletions(-) create mode 100644 packages/lib/src/common-setup/db.test.ts diff --git a/packages/btcindexer/src/db.test.ts b/packages/btcindexer/src/db.test.ts index 519f41d..de24836 100644 --- a/packages/btcindexer/src/db.test.ts +++ b/packages/btcindexer/src/db.test.ts @@ -1,25 +1,9 @@ import * as path from "path"; -import { readdir } from "fs/promises"; import { D1Database } from "@cloudflare/workers-types"; +import { initDb as initDbShared } from "@gonative-cc/lib/common-setup/db.test"; const MIGRATIONS_PATH = path.resolve(__dirname, "../db/migrations"); export async function initDb(db: D1Database) { - const migrationFiles = await readdir(MIGRATIONS_PATH); - migrationFiles.sort(); - - for (const filename of migrationFiles) { - if (filename.endsWith(".sql")) { - const file = Bun.file(path.join(MIGRATIONS_PATH, filename)); - const migration = await file.text(); - const cleanedMigration = migration - .replace(/--.*/g, "") - .replace(/\n/g, " ") - .replace(/\s{2,}/g, " ") - .trim(); - if (cleanedMigration.length > 0) { - await db.exec(cleanedMigration); - } - } - } + await initDbShared(db, MIGRATIONS_PATH); } diff --git a/packages/lib/package.json b/packages/lib/package.json index 2b72153..51b4076 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -4,7 +4,8 @@ "license": "MPL-2.0", "version": "0.0.1", "exports": { - "./*": "./src/*.ts" + "./*": "./src/*.ts", + "./common-setup/*": "./src/common-setup/*.ts" }, "scripts": { "typecheck": "tsc", diff --git a/packages/lib/src/common-setup/db.test.ts b/packages/lib/src/common-setup/db.test.ts new file mode 100644 index 0000000..dcc024a --- /dev/null +++ b/packages/lib/src/common-setup/db.test.ts @@ -0,0 +1,23 @@ +import * as path from "path"; +import { readdir } from "fs/promises"; +import { D1Database } from "@cloudflare/workers-types"; + +export async function initDb(db: D1Database, migrationsPath: string) { + const migrationFiles = await readdir(migrationsPath); + migrationFiles.sort(); + + for (const filename of migrationFiles) { + if (filename.endsWith(".sql")) { + const file = Bun.file(path.join(migrationsPath, filename)); + const migration = await file.text(); + const cleanedMigration = migration + .replace(/--.*/g, "") + .replace(/\n/g, " ") + .replace(/\s{2,}/g, " ") + .trim(); + if (cleanedMigration.length > 0) { + await db.exec(cleanedMigration); + } + } + } +} diff --git a/packages/redeem_solver/src/db.test.ts b/packages/redeem_solver/src/db.test.ts index 3284eea..1e25985 100644 --- a/packages/redeem_solver/src/db.test.ts +++ b/packages/redeem_solver/src/db.test.ts @@ -1,25 +1,9 @@ import * as path from "path"; -import { readdir } from "fs/promises"; import { D1Database } from "@cloudflare/workers-types"; +import { initDb as initDbShared } from "@gonative-cc/lib/common-setup/db.test"; const MIGRATIONS_PATH = path.resolve(__dirname, "../../btcindexer/db/migrations"); export async function initDb(db: D1Database) { - const migrationFiles = await readdir(MIGRATIONS_PATH); - migrationFiles.sort(); - - for (const filename of migrationFiles) { - if (filename.endsWith(".sql")) { - const file = Bun.file(path.join(MIGRATIONS_PATH, filename)); - const migration = await file.text(); - const cleanedMigration = migration - .replace(/--.*/g, "") - .replace(/\n/g, " ") - .replace(/\s{2,}/g, " ") - .trim(); - if (cleanedMigration.length > 0) { - await db.exec(cleanedMigration); - } - } - } + await initDbShared(db, MIGRATIONS_PATH); } From 8a3c69c0c555661deeeecf112a3a0a2345c61ca3 Mon Sep 17 00:00:00 2001 From: Rayane Charif Date: Tue, 23 Dec 2025 14:02:40 +0100 Subject: [PATCH 7/7] Corrected test after merge Signed-off-by: Rayane Charif --- packages/redeem_solver/src/storage.test.ts | 70 +++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/redeem_solver/src/storage.test.ts b/packages/redeem_solver/src/storage.test.ts index 99a35a2..032ede0 100644 --- a/packages/redeem_solver/src/storage.test.ts +++ b/packages/redeem_solver/src/storage.test.ts @@ -42,10 +42,10 @@ describe("D1Storage", () => { let db: D1Database; async function insertRedeemRequest( - redeemId: string, + redeemId: number, packageId: number, redeemer: string, - recipientScript: Uint8Array, + recipientScript: ArrayBuffer, amountSats: number, createdAt: number, status: RedeemRequestStatus, @@ -60,13 +60,13 @@ describe("D1Storage", () => { } async function insertUtxo( - utxoId: string, + utxoId: number, addressId: number, dwalletId: string, txid: string, vout: number, amountSats: number, - scriptPubkey: Uint8Array, + scriptPubkey: ArrayBuffer, status: UtxoStatus, lockedUntil: number | null, ) { @@ -107,10 +107,10 @@ describe("D1Storage", () => { }); it("getPendingRedeems should return pending redeems ordered by created_at", async () => { - const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); + const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]).buffer; await insertRedeemRequest( - "redeem2", + 2, 1, "redeemer1", recipientScript, @@ -119,7 +119,7 @@ describe("D1Storage", () => { RedeemRequestStatus.Pending, ); await insertRedeemRequest( - "redeem1", + 1, 1, "redeemer1", recipientScript, @@ -131,17 +131,17 @@ describe("D1Storage", () => { const redeems = await storage.getPendingRedeems(); expect(redeems.length).toBe(2); - expect(redeems[0]!.redeem_id).toBe("redeem1"); - expect(redeems[1]!.redeem_id).toBe("redeem2"); + expect(redeems[0]!.redeem_id).toBe(1); + expect(redeems[1]!.redeem_id).toBe(2); expect(redeems[0]!.sui_network).toBe(toSuiNet("devnet")); }); it("getRedeemsReadyForSolving should filter by status and created_at", async () => { - const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); + const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]).buffer; const now = Date.now(); await insertRedeemRequest( - "redeem1", + 1, 1, "redeemer1", recipientScript, @@ -150,7 +150,7 @@ describe("D1Storage", () => { RedeemRequestStatus.Proposed, ); await insertRedeemRequest( - "redeem2", + 2, 1, "redeemer1", recipientScript, @@ -162,14 +162,14 @@ describe("D1Storage", () => { const redeems = await storage.getRedeemsReadyForSolving(now); expect(redeems.length).toBe(1); - expect(redeems[0]!.redeem_id).toBe("redeem1"); + expect(redeems[0]!.redeem_id).toBe(1); }); it("getAvailableUtxos should return utxos ordered by amount DESC", async () => { - const scriptPubkey = new Uint8Array([0x00, 0x14]); + const scriptPubkey = new Uint8Array([0x00, 0x14]).buffer; await insertUtxo( - "utxo1", + 1, 1, "dwallet1", "tx1", @@ -180,7 +180,7 @@ describe("D1Storage", () => { null, ); await insertUtxo( - "utxo2", + 2, 1, "dwallet1", "tx2", @@ -194,12 +194,12 @@ describe("D1Storage", () => { const utxos = await storage.getAvailableUtxos(1); expect(utxos.length).toBe(2); - expect(utxos[0]!.nbtc_utxo_id).toBe("utxo2"); - expect(utxos[1]!.nbtc_utxo_id).toBe("utxo1"); + expect(utxos[0]!.nbtc_utxo_id).toBe(2); + expect(utxos[1]!.nbtc_utxo_id).toBe(1); }); it("getAvailableUtxos should filter by package_id and status", async () => { - const scriptPubkey = new Uint8Array([0x00, 0x14]); + const scriptPubkey = new Uint8Array([0x00, 0x14]).buffer; await db .prepare( @@ -216,7 +216,7 @@ describe("D1Storage", () => { .run(); await insertUtxo( - "utxo1", + 1, 1, "dwallet1", "tx1", @@ -227,7 +227,7 @@ describe("D1Storage", () => { null, ); await insertUtxo( - "utxo_locked", + 3, 1, "dwallet1", "tx_locked", @@ -238,7 +238,7 @@ describe("D1Storage", () => { Date.now() + 10000, ); await insertUtxo( - "utxo2", + 2, 2, "dwallet2", "tx2", @@ -253,17 +253,17 @@ describe("D1Storage", () => { const utxos2 = await storage.getAvailableUtxos(2); expect(utxos1.length).toBe(1); - expect(utxos1[0]!.nbtc_utxo_id).toBe("utxo1"); + expect(utxos1[0]!.nbtc_utxo_id).toBe(1); expect(utxos2.length).toBe(1); - expect(utxos2[0]!.nbtc_utxo_id).toBe("utxo2"); + expect(utxos2[0]!.nbtc_utxo_id).toBe(2); }); it("markRedeemProposed should update redeem status and lock utxos", async () => { - const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); - const scriptPubkey = new Uint8Array([0x00, 0x14]); + const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]).buffer; + const scriptPubkey = new Uint8Array([0x00, 0x14]).buffer; await insertRedeemRequest( - "redeem1", + 1, 1, "redeemer1", recipientScript, @@ -272,7 +272,7 @@ describe("D1Storage", () => { RedeemRequestStatus.Pending, ); await insertUtxo( - "utxo1", + 1, 1, "dwallet1", "tx1", @@ -283,27 +283,27 @@ describe("D1Storage", () => { null, ); - await storage.markRedeemProposed("redeem1", ["utxo1"], UTXO_LOCK_TIME_MS); + await storage.markRedeemProposed(1, [1], UTXO_LOCK_TIME_MS); const redeem = await db .prepare("SELECT status FROM nbtc_redeem_requests WHERE redeem_id = ?") - .bind("redeem1") + .bind(1) .first<{ status: string }>(); expect(redeem!.status).toBe(RedeemRequestStatus.Proposed); const utxo = await db .prepare("SELECT status, locked_until FROM nbtc_utxos WHERE nbtc_utxo_id = ?") - .bind("utxo1") + .bind(1) .first<{ status: string; locked_until: number }>(); expect(utxo!.status).toBe(UtxoStatus.Locked); expect(utxo!.locked_until).toBeGreaterThan(Date.now()); }); it("markRedeemSolved should update redeem status", async () => { - const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]); + const recipientScript = new Uint8Array([0x76, 0xa9, 0x14]).buffer; await insertRedeemRequest( - "redeem1", + 1, 1, "redeemer1", recipientScript, @@ -312,11 +312,11 @@ describe("D1Storage", () => { RedeemRequestStatus.Proposed, ); - await storage.markRedeemSolved("redeem1"); + await storage.markRedeemSolved(1); const redeem = await db .prepare("SELECT status FROM nbtc_redeem_requests WHERE redeem_id = ?") - .bind("redeem1") + .bind(1) .first<{ status: string }>(); expect(redeem!.status).toBe(RedeemRequestStatus.Solved); });