Skip to content

Commit 733b4ef

Browse files
committed
Merge branch 'master' into rayane/event-ika-polling
2 parents 26d926d + f3de548 commit 733b4ef

File tree

12 files changed

+105
-91
lines changed

12 files changed

+105
-91
lines changed

bun.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"typescript": "^5.9.3",
2929
"typescript-eslint": "^8.49.0",
3030
"wrangler": "^4.54.0",
31-
"@gonative-cc/nbtc": "^0.1.0"
31+
"@gonative-cc/nbtc": "^0.1.1"
3232
},
3333
"lint-staged": {
3434
"*.{js,cjs,mjs,jsx,ts,tsx}": [

packages/btcindexer/db/migrations/0001_initial_schema.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ CREATE INDEX IF NOT EXISTS btc_blocks_is_scanned_height ON btc_blocks (is_scanne
1616
-- This table tracks the nBTC deposit txs (minting)
1717
CREATE TABLE IF NOT EXISTS nbtc_minting (
1818
tx_id TEXT NOT NULL PRIMARY KEY,
19-
address_id INTEGER NOT NULL, -- nbtc pkg is linked through address_id
19+
address_id INTEGER NOT NULL, -- nbtc pkg is linked through address_id -> nbtc_deposit_addresses.setup_id
2020
sender TEXT NOT NULL,
2121
vout INTEGER NOT NULL,
2222
block_hash TEXT,

packages/btcindexer/scripts/seed-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ async function main() {
6767
continue;
6868
}
6969

70-
const checkAddrQuery = `SELECT id FROM nbtc_deposit_addresses WHERE setup_id = ${setupId} AND deposit_address = '${entry.btc_address}'`;
71-
const existingAddrId = await executeQuery<number>(checkAddrQuery, DB_NAME, local, "id");
70+
const checkAddrQuery = `SELECT 1 as "exists" FROM nbtc_deposit_addresses WHERE setup_id = ${setupId} AND deposit_address = '${entry.btc_address}'`;
71+
const existingAddrId = await executeQuery<number>(checkAddrQuery, DB_NAME, local, "exists");
7272

7373
if (existingAddrId) {
7474
continue;

packages/btcindexer/src/btcindexer.helpers.test.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ export async function setupTestIndexerSuite(
261261
mockSuiClient.tryMintNbtcBatch.mockResolvedValue(result);
262262
};
263263

264-
const insertTx = async (options: {
264+
const insertTx = async (args: {
265265
txId: string;
266266
status: MintTxStatus | string;
267267
retryCount?: number;
@@ -276,39 +276,28 @@ export async function setupTestIndexerSuite(
276276
const defaultBlock = testData[329] || testData[327] || Object.values(testData)[0];
277277
if (!defaultBlock) throw new Error("No test data available for default values");
278278

279-
const depositAddr = options.depositAddress || defaultBlock.depositAddr;
280-
281-
// Validate that the deposit address exists in the database
282-
const addressResult = await db
283-
.prepare(`SELECT id FROM nbtc_deposit_addresses WHERE deposit_address = ?`)
284-
.bind(depositAddr)
285-
.first<{ id: number }>();
286-
287-
if (!addressResult) {
288-
throw new Error(
289-
`Deposit address '${depositAddr}' not found in database. ` +
290-
`Make sure to include it in the depositAddresses array during setupTestIndexer().`,
291-
);
292-
}
293-
279+
const depositAddr = args.depositAddress || defaultBlock.depositAddr;
294280
await db
295281
.prepare(
296282
`INSERT INTO nbtc_minting (tx_id, address_id, sender, vout, block_hash, block_height, sui_recipient, amount, status, created_at, updated_at, retry_count)
297-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
283+
VALUES (
284+
?,
285+
(SELECT id FROM nbtc_deposit_addresses WHERE deposit_address = ?),
286+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
298287
)
299288
.bind(
300-
options.txId,
301-
addressResult.id,
302-
options.sender || "sender_address",
303-
options.vout ?? 0,
304-
options.blockHash || defaultBlock.hash,
305-
options.blockHeight || defaultBlock.height,
306-
options.suiRecipient || "0xtest_recipient",
307-
options.amount || 10000,
308-
options.status,
289+
args.txId,
290+
depositAddr,
291+
args.sender || "sender_address",
292+
args.vout ?? 0,
293+
args.blockHash || defaultBlock.hash,
294+
args.blockHeight || defaultBlock.height,
295+
args.suiRecipient || "0xtest_recipient",
296+
args.amount || 10000,
297+
args.status,
309298
Date.now(),
310299
Date.now(),
311-
options.retryCount || 0,
300+
args.retryCount || 0,
312301
)
313302
.run();
314303
};

packages/btcindexer/src/cf-storage.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -185,22 +185,21 @@ export class CFStorage implements Storage {
185185
const now = Date.now();
186186
const insertOrUpdateNbtcTxStmt = this.d1.prepare(
187187
`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)
188-
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)
189-
ON CONFLICT(tx_id) DO UPDATE SET
190-
block_hash = excluded.block_hash,
191-
block_height = excluded.block_height,
192-
status = '${MintTxStatus.Confirming}',
193-
updated_at = excluded.updated_at,
194-
address_id = excluded.address_id,
195-
sender = excluded.sender`,
188+
VALUES (?,
189+
(SELECT a.id FROM nbtc_deposit_addresses a WHERE a.deposit_address = ?),
190+
?, ?, ?, ?, ?, ?, '${MintTxStatus.Confirming}', ?, ?, NULL, 0)
191+
ON CONFLICT(tx_id) DO UPDATE SET
192+
block_hash = excluded.block_hash,
193+
block_height = excluded.block_height,
194+
status = '${MintTxStatus.Confirming}',
195+
updated_at = excluded.updated_at,
196+
address_id = excluded.address_id,
197+
sender = excluded.sender`,
196198
);
197199
const statements = txs.map((tx) =>
198200
insertOrUpdateNbtcTxStmt.bind(
199201
tx.txId,
200-
tx.btcNetwork,
201-
tx.suiNetwork,
202-
tx.nbtcPkg,
203-
tx.depositAddress,
202+
tx.depositAddress, // inner select param
204203
tx.sender,
205204
tx.vout,
206205
tx.blockHash,
@@ -419,15 +418,14 @@ export class CFStorage implements Storage {
419418
const now = Date.now();
420419
const insertStmt = this.d1.prepare(
421420
`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)
422-
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)`,
421+
VALUES (?,
422+
(SELECT a.id FROM nbtc_deposit_addresses a WHERE a.deposit_address = ?),
423+
?, ?, ?, ?, '${MintTxStatus.Broadcasting}', ?, ?, NULL, 0)`,
423424
);
424425

425426
const statements = deposits.map((deposit) =>
426427
insertStmt.bind(
427428
deposit.txId,
428-
deposit.btcNetwork,
429-
deposit.suiNetwork,
430-
deposit.nbtcPkg,
431429
deposit.depositAddress,
432430
deposit.sender,
433431
deposit.vout,

packages/sui-indexer/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ UTXOs are stored in the `nbtc_utxos` table in the D1 database.
116116

117117
**Key Concept:** The database acts as a cache of the on-chain state. The `SuiIndexer` ensures this cache stays synchronized with the canonical state on the Sui blockchain.
118118

119+
## IKA Coin Management
120+
121+
We use IKA coins to pay for presign/sign requests. The coin selection logic lives in `packages/lib/src/coin-ops.ts`.
122+
123+
### How it works
124+
125+
1. `fetchAllIkaCoins()` grabs all IKA coins for the signer
126+
2. `selectCoins()` picks coins to hit the target amount (takes first 80, then sorts by balance if needed)
127+
3. `prepareCoin()` merges them if we need multiple coins
128+
129+
### Concurrency
130+
131+
The process is not safe for parallel workload. See the doc comment in `fetchAllIkaCoins`.
132+
119133
## API
120134

121135
This package exposes [Cloudflare RPC](../../README.md#cloudflare-rpc).

packages/sui-indexer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dependencies": {
1616
"@gonative-cc/btcindexer": "workspace:*",
1717
"@gonative-cc/lib": "workspace:*",
18+
"@gonative-cc/nbtc": "0.1.1",
1819
"@ika.xyz/sdk": "^0.2.7",
1920
"@mysten/bcs": "^1.9.2",
2021
"@mysten/sui": "^1.45.2",

packages/sui-indexer/src/ika_client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export class IkaClientImp implements IkaClient {
8585
return this.ikaConfig.objects.ikaDWalletCoordinator.objectID;
8686
}
8787

88+
// Fetches all IKA coins for an address. Used with prepareCoin() to select & merge coins.
89+
// NOTE: this can't be used in a parallel workload. Currently, the workers only run one cron at a time with one processSolvedRedeems process.
90+
// So no parallel coin selection happens.
8891
// TODO: we should have maxCoins?: number limit
8992
async fetchAllIkaCoins(owner: string): Promise<CoinStruct[]> {
9093
const allCoins: CoinStruct[] = [];

packages/sui-indexer/src/redeem-service.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,14 @@ export class RedeemService {
111111

112112
private async processSolvedRedeem(req: RedeemRequestWithInputs) {
113113
const client = this.getSuiClient(req.sui_network);
114+
const inputsToVerify: RedeemInput[] = [];
114115

115116
for (const input of req.inputs) {
116117
try {
117118
if (!input.sign_id) {
118119
await this.requestIkaSig(client, req, input);
119120
} else if (input.sign_id && !input.verified) {
120-
// TODO: this should be triggered when getting the event from ika
121-
await this.recordIkaSig(client, req, input);
121+
inputsToVerify.push(input);
122122
}
123123
} catch (e) {
124124
logError(
@@ -133,6 +133,22 @@ export class RedeemService {
133133
);
134134
}
135135
}
136+
137+
if (inputsToVerify.length > 0) {
138+
try {
139+
await this.recordIkaSignatures(client, req, inputsToVerify);
140+
} catch (e) {
141+
logError(
142+
{
143+
msg: "Failed to batch verify signatures",
144+
method: "processSolvedRedeem",
145+
redeemId: req.redeem_id,
146+
count: inputsToVerify.length,
147+
},
148+
e,
149+
);
150+
}
151+
}
136152
}
137153
// TODO: handle front runs
138154
private async requestIkaSig(
@@ -260,36 +276,40 @@ export class RedeemService {
260276
}
261277
}
262278

263-
private async recordIkaSig(
279+
private async recordIkaSignatures(
264280
client: SuiClient,
265281
req: RedeemRequestWithInputs,
266-
input: RedeemInput,
282+
inputs: RedeemInput[],
267283
) {
284+
const inputsWithSignId = inputs.filter(
285+
(input): input is RedeemInput & { sign_id: string } => input.sign_id !== null,
286+
);
287+
288+
if (inputsWithSignId.length === 0) {
289+
return;
290+
}
291+
268292
logger.info({
269-
msg: "Verifying signature for input",
293+
msg: "Batch verifying signatures",
270294
redeemId: req.redeem_id,
271-
utxoId: input.utxo_id,
272-
inputIdx: input.input_index,
273-
signId: input.sign_id,
295+
count: inputsWithSignId.length,
274296
});
275297

276-
if (!input.sign_id) {
277-
throw new Error("Input signature ID is missing");
278-
}
279-
280-
await client.validateSignature(
298+
await client.validateSignatures(
281299
req.redeem_id,
282-
input.input_index,
283-
input.sign_id,
300+
inputsWithSignId,
284301
req.nbtc_pkg,
285302
req.nbtc_contract,
286303
);
287304

288-
await this.storage.markRedeemInputVerified(req.redeem_id, input.utxo_id);
305+
for (const input of inputsWithSignId) {
306+
await this.storage.markRedeemInputVerified(req.redeem_id, input.utxo_id);
307+
}
308+
289309
logger.info({
290-
msg: "Signature verified",
310+
msg: "Signatures verified",
291311
redeemId: req.redeem_id,
292-
utxoId: input.utxo_id,
312+
count: inputsWithSignId.length,
293313
});
294314
}
295315

0 commit comments

Comments
 (0)