Skip to content

Commit 58bb367

Browse files
authored
Merge branch 'master' into fix-redeem-btc-tx-tracking
2 parents 2f4cb37 + 7346234 commit 58bb367

29 files changed

+661
-118
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Monorepo Strategy
2+
- This is a TypeScript monorepo using [pnpm/npm/yarn] workspaces.
3+
- Shared logic lives in `packages/`. Individual Workers live in `apps/` or `services/`.
4+
- Always check `package.json` at the root and within sub-packages to understand dependency boundaries.
5+
- When suggesting new dependencies, prefer using existing workspace packages via `workspace:*` protocols.
6+
7+
# Cloudflare Workers Standards
8+
- Use **ES Modules (ESM)** format exclusively (e.g., `export default { fetch(...) }`).
9+
- **NO Service Worker syntax** (`addEventListener('fetch', ...)`) unless explicitly asked.
10+
- Target the Cloudflare Workers runtime; avoid Node.js built-ins (like `fs` or `path`) unless using `nodejs_compat`.
11+
- Always use **TypeScript** with strict type checking.
12+
- Prefer `wrangler.jsonc` or `wrangler.toml` for configuration.
13+
14+
# Focus on:
15+
- detect unnecessary wrapped elements.
16+
- suggest simplifications and reusability.
17+
- make sure the structure and code is maintainable.
18+
- suggest to move reusable pure functions to TS files, making sure that we can easily test the logic.
19+
- code reusability, maintenance, wisely breaking down functions into logical procedures (rather than having big functions).
20+
- Errors should be logged using functions from app/lib/log.ts, rather than `console.error`
21+
- workers is a multi package repo. Each package represents a service. The code and services should follow a service oriented architecture.
22+
- prefer to have helper functions to avoid code duplication.
23+
24+
# Do **not** comment on:
25+
- Single-vs-double quote style unless it breaks the linter.

.github/workflows/js.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- uses: actions/checkout@v6
2424

2525
# @v2
26-
- uses: oven-sh/setup-bun@563911925f97b52638b2d040c6390de93f9f241a
26+
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3
2727

2828
- run: bun ci
2929

README.md

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -50,53 +50,97 @@ Workers are based on the Cloudflare Workers framework.
5050
### Architecture Diagram
5151

5252
```mermaid
53-
graph TB
54-
subgraph "External Systems"
55-
Bitcoin[Bitcoin Network]
56-
Sui[Sui Network]
57-
Relayer[Bitcoin Relayer]
58-
UI[bYield UI]
59-
end
60-
61-
subgraph "Native Workers Infrastructure"
62-
BI[(BTCIndexer)]
63-
BL[(Block-Ingestor)]
64-
SI[(Sui Indexer)]
65-
end
66-
67-
%% External to Workers flows
68-
Bitcoin -->|sends blocks| Relayer
69-
Relayer -->|submits blocks| BL
70-
UI -->|queries status| BI
71-
UI -->|requests redemption| SI
72-
73-
%% Block ingestion flow
74-
BL -->|forwards blocks| BI
75-
76-
%% Bitcoin indexing flow
77-
BI -->|detects nBTC deposits| BI
78-
BI -->|mints nBTC| Sui
79-
BI -->|verifies blocks| Sui
80-
81-
%% Redemption flow
82-
UI -->|initiates redemption| Sui
83-
Sui -->|emits redemption events| SI
84-
SI -->|monitors events & proposes UTXOs| Sui
85-
86-
%% Cross-component communication
87-
BI -.-> BL
88-
SI -.-> BI
89-
90-
style BI fill:#e1f5fe
91-
style BL fill:#f3e5f5
92-
style SI fill:#fff3e0
53+
flowchart-elk TB
54+
%% ================
55+
%% External systems
56+
%% ================
57+
subgraph EXT["External systems"]
58+
BTC["Bitcoin network"]
59+
BTCI["Bitcoin Electrs<br>(indexer)"]
60+
SGQL["Sui GraphQL"]
61+
USER["Sui wallet"]
62+
end
63+
64+
%% ==========
65+
%% BYield app
66+
%% ==========
67+
subgraph BYIELD["BYield (gonative-cc/byield)"]
68+
UI["BYield UI<br>(React Router app/)"]
69+
APPW["BYield Edge backend<br>(Cloudflare Workers)"]
70+
BYDB["BYield DB<br>(SQL + KV)"]
71+
end
72+
73+
%% =======================
74+
%% Native backend services
75+
%% =======================
76+
subgraph CF["Native Workers (gonative-cc/workers)"]
77+
REL["Relayer"]
78+
BL["Block-Ingestor<br>REST ingress"]
79+
IDX["BTCIndexer<br>state machine + cron"]
80+
SI["Sui-Indexer<br>poll/index Sui events"]
81+
RS["Redeem-Solver<br>UTXO proposal engine"]
82+
83+
Q[("Cloudflare Queue")]
84+
KV[("Cloudflare KV<br>(raw blocks)")]
85+
D1[("Cloudflare D1<br>(state, config)")]
86+
end
87+
88+
%% ==============
89+
%% Sui on-chain
90+
%% ==============
91+
subgraph SUI["Native on Sui (gonative-cc/sui-native)"]
92+
SPV["bitcoin_spv<br>(light client / verify headers)"]
93+
NBTC["nBTC"]
94+
SWAP["nbtc_swap<br>(nBTC<->SUI)"]
95+
EXEC["bitcoin_executor<br>(mirroring + script execution)"]
96+
NBTC --> SPV
97+
EXEC --> SPV
98+
end
99+
100+
%% =================
101+
%% User / app access
102+
%% =================
103+
USER --> UI
104+
UI <--> APPW
105+
APPW <--> BYDB
106+
107+
USER -->|sign/submit tx| NBTC
108+
USER -->|optional swap| SWAP
109+
UI -->|read chain state| SGQL
110+
111+
%% =================
112+
%% Minting (BTC->Sui)
113+
%% =================
114+
BTC --> REL
115+
REL -->|submits blocks| BL
116+
BL -->|store raw blocks| KV
117+
BL -->|enqueue heights/batches| Q
118+
Q --> IDX
119+
120+
IDX -->|read blocks| KV
121+
122+
IDX -->|verify canonical chain| SPV
123+
IDX -->|mint nBTC on Sui| NBTC
124+
125+
%% ====================
126+
%% Redemption (Sui->BTC)
127+
%% ====================
128+
SI -->|poll events| SGQL
129+
SI --> RS
130+
RS -->|track tx confirmations| IDX
131+
132+
%% =========================
133+
%% UI queries / observability
134+
%% =========================
135+
UI -->|status queries| IDX
136+
UI -->|redeem assistance| RS
93137
```
94138

95139
## Setup
96140

97141
### Dependencies
98142

99-
- bun >= 1.20.0
143+
- bun >= 1.3
100144
- proper editorconfig mode setup in your editor!
101145
- Go (for Go API Client for the workers)
102146

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ CREATE TABLE IF NOT EXISTS setups (
6666
nbtc_contract TEXT NOT NULL,
6767
lc_pkg TEXT NOT NULL,
6868
lc_contract TEXT NOT NULL,
69-
sui_fallback_address TEXT NOT NULL,
69+
nbtc_fallback_addr TEXT NOT NULL,
7070
is_active INTEGER NOT NULL DEFAULT TRUE,
7171
UNIQUE(sui_network, btc_network, nbtc_pkg)
7272
) STRICT;

packages/btcindexer/scripts/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export interface SetupCfg {
55
nbtc_contract: string;
66
lc_pkg: string;
77
lc_contract: string;
8-
sui_fallback_address: string;
8+
nbtc_fallback_addr: string;
99
btc_address: string;
1010
}
1111

@@ -28,7 +28,7 @@ export const SETUPS: Record<EnvName, Config> = {
2828
nbtc_contract: "0x9a0d5f810a8880fa69db46ce0b09bcb101f27fb3865adf365c33e2051d48f38a",
2929
lc_pkg: "0x106eb827fbdbfb30c7d35959acee8fdfee3a7bb80e8f85ca984d5db8c22f2114",
3030
lc_contract: "0xed0877a279110aab81c99f7956e3db5e7549f4c5b0f6cf163a51a5a2f9d5afa3",
31-
sui_fallback_address:
31+
nbtc_fallback_addr:
3232
"0x0c62bfbe82105cd8b783ae9d5b8b582b2b579fa86d3089acd7cbeb763e367867",
3333
btc_address: "bcrt1q2vawj829uru8zynh284s43cd5d4kn5j4n0098p",
3434
},

packages/btcindexer/scripts/seed-config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async function main() {
4444
!entry.nbtc_contract ||
4545
!entry.lc_pkg ||
4646
!entry.lc_contract ||
47-
!entry.sui_fallback_address ||
47+
!entry.nbtc_fallback_addr ||
4848
!entry.btc_address
4949
) {
5050
console.error("Invalid entry (missing fields)");
@@ -55,8 +55,8 @@ async function main() {
5555
let setupId = await executeQuery<number>(checkSetupRowQuery, DB_NAME, local, "id");
5656
if (!setupId) {
5757
const insertPkgQuery = `
58-
INSERT INTO setups (btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, sui_fallback_address)
59-
VALUES ('${entry.btc_network}', '${entry.sui_network}', '${entry.nbtc_pkg}', '${entry.nbtc_contract}', '${entry.lc_pkg}', '${entry.lc_contract}', '${entry.sui_fallback_address}')
58+
INSERT INTO setups (btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, nbtc_fallback_addr)
59+
VALUES ('${entry.btc_network}', '${entry.sui_network}', '${entry.nbtc_pkg}', '${entry.nbtc_contract}', '${entry.lc_pkg}', '${entry.lc_contract}', '${entry.nbtc_fallback_addr}')
6060
RETURNING id
6161
`;
6262
setupId = await executeQuery<number>(insertPkgQuery, DB_NAME, local, "id");

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const TEST_PACKAGE_CONFIG: NbtcPkgCfg = {
2828
nbtc_contract: "0xNBTC",
2929
lc_contract: "0xLIGHTCLIENT",
3030
lc_pkg: "0xLC_PKG",
31-
sui_fallback_address: SUI_FALLBACK_ADDRESS,
31+
nbtc_fallback_addr: SUI_FALLBACK_ADDRESS,
3232
is_active: true,
3333
};
3434

@@ -127,7 +127,7 @@ export async function setupTestIndexerSuite(
127127
`INSERT INTO setups (
128128
id, btc_network, sui_network, nbtc_pkg, nbtc_contract,
129129
lc_pkg, lc_contract,
130-
sui_fallback_address, is_active
130+
nbtc_fallback_addr, is_active
131131
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
132132
)
133133
.bind(
@@ -138,7 +138,7 @@ export async function setupTestIndexerSuite(
138138
packageConfig.nbtc_contract,
139139
packageConfig.lc_pkg,
140140
packageConfig.lc_contract,
141-
packageConfig.sui_fallback_address,
141+
packageConfig.nbtc_fallback_addr,
142142
packageConfig.is_active,
143143
)
144144
.run();

packages/btcindexer/src/btcindexer.ts

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { address, networks, Block, Transaction, type Network } from "bitcoinjs-lib";
22
import { BtcNet, type BlockQueueRecord, calculateConfirmations } from "@gonative-cc/lib/nbtc";
33
import type { SuiNet } from "@gonative-cc/lib/nsui";
4+
import { SUI_GRAPHQL_URLS } from "@gonative-cc/lib/nsui";
45
import type { Service } from "@cloudflare/workers-types";
56
import type { WorkerEntrypoint } from "cloudflare:workers";
67
import type { SuiIndexerRpc } from "@gonative-cc/sui-indexer/rpc-interface";
78
import { logError, logger } from "@gonative-cc/lib/logger";
8-
99
import { OP_RETURN } from "./opcodes";
1010
import { BitcoinMerkleTree } from "./bitcoin-merkle-tree";
1111
import { SuiClient, type SuiClientI } from "./sui_client";
12+
import { SuiGraphQLClient } from "./graphql-client";
1213
import type {
1314
Deposit,
1415
PendingTx,
@@ -317,7 +318,7 @@ export class Indexer {
317318
});
318319
let finalRecipient = suiRecipient;
319320
if (!finalRecipient) {
320-
finalRecipient = config.sui_fallback_address;
321+
finalRecipient = config.nbtc_fallback_addr;
321322
}
322323

323324
deposits.push({
@@ -350,16 +351,85 @@ export class Indexer {
350351
if (!finalizedTxs || finalizedTxs.length === 0) {
351352
return;
352353
}
354+
355+
const txsToProcess = await this.filterAlreadyMinted(finalizedTxs);
356+
357+
if (txsToProcess.length === 0) {
358+
logger.info({ msg: "No new deposits to process after front-run check" });
359+
return;
360+
}
361+
353362
logger.info({
354363
msg: "Minting: Found deposits to process",
355-
count: finalizedTxs.length,
364+
count: txsToProcess.length,
356365
});
357366

358-
const txsByBlock = this.groupTransactionsByBlock(finalizedTxs);
367+
const txsByBlock = this.groupTransactionsByBlock(txsToProcess);
359368
const { batches } = await this.prepareMintBatches(txsByBlock);
360369
await this.executeMintBatches(batches);
361370
}
362371

372+
// Filters out txs that have already been minted on-chain and updates the database (front-run detection).
373+
private async filterAlreadyMinted(finalizedTxs: FinalizedTxRow[]): Promise<FinalizedTxRow[]> {
374+
const txsBySetupId = new Map<number, FinalizedTxRow[]>();
375+
for (const tx of finalizedTxs) {
376+
const list = txsBySetupId.get(tx.setup_id) || [];
377+
list.push(tx);
378+
txsBySetupId.set(tx.setup_id, list);
379+
}
380+
381+
const txsToProcess: FinalizedTxRow[] = [];
382+
383+
for (const [setupId, txs] of txsBySetupId) {
384+
try {
385+
const config = this.getPackageConfig(setupId);
386+
const suiClient = this.getSuiClient(config.sui_network);
387+
const tableId = await suiClient.getMintedTxsTableId();
388+
389+
const graphqlUrl = SUI_GRAPHQL_URLS[config.sui_network];
390+
if (!graphqlUrl) {
391+
logger.warn({ msg: "No GraphQL URL for network", network: config.sui_network });
392+
txsToProcess.push(...txs);
393+
continue;
394+
}
395+
396+
const graphqlClient = new SuiGraphQLClient(graphqlUrl);
397+
const txIds = txs.map((t) => t.tx_id);
398+
const mintedTxIds = await graphqlClient.checkMintedStatus(tableId, txIds);
399+
400+
for (const tx of txs) {
401+
if (mintedTxIds.has(tx.tx_id)) {
402+
logger.info({
403+
msg: "Front-run detected: Transaction already minted",
404+
txId: tx.tx_id,
405+
});
406+
await this.storage.batchUpdateNbtcMintTxs([
407+
{
408+
txId: tx.tx_id,
409+
vout: tx.vout,
410+
status: MintTxStatus.Minted,
411+
},
412+
]);
413+
} else {
414+
txsToProcess.push(tx);
415+
}
416+
}
417+
} catch (e) {
418+
logError(
419+
{
420+
msg: "Error checking pre-mint status via GraphQL",
421+
method: "filterAlreadyMinted",
422+
setupId,
423+
},
424+
e,
425+
);
426+
txsToProcess.push(...txs);
427+
}
428+
}
429+
430+
return txsToProcess;
431+
}
432+
363433
/**
364434
* Groups a list of blockchain transactions (or any object containing a block_hash) by their block hash.
365435
* This optimization allows fetching and parsing the block data once for all related transactions.
@@ -742,12 +812,10 @@ export class Indexer {
742812
msg: "Finalization: Updating reorged transactions",
743813
count: reorgedTxIds.length,
744814
});
745-
// This requires a new method in the Storage interface like:
746-
// updateTxsStatus(txIds: string[], status: TxStatus): Promise<void>
747815
await this.storage.updateNbtcTxsStatus(reorgedTxIds, MintTxStatus.Reorg);
748816
}
749817

750-
// TODO: add a unit test for it so we make sure we do not finalize reorrged tx.
818+
// TODO: add a unit test for it so we make sure we do not finalize reorged tx.
751819
const validPendingTxs = pendingTxs.filter((tx) => !reorgedTxIds.includes(tx.tx_id));
752820
const { activeTxIds, inactiveTxIds } = this.selectFinalizedNbtcTxs(
753821
validPendingTxs,

0 commit comments

Comments
 (0)