feat: add redeem broadcast and confirmation flow#280
Conversation
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Reviewer's GuideImplements the end-to-end Bitcoin redeem lifecycle across the indexer, Sui indexer, and redeem_solver, including broadcasting signed redeem transactions via Electrs/BTC indexer RPC, tracking confirmation in blocks, and exposing redeem data over storage, RPC, and HTTP APIs while wiring the redeem solver to the BTC indexer service. Sequence diagram for broadcasting ready Bitcoin redeem transactionssequenceDiagram
actor Scheduler
participant RedeemService
participant RedeemStorage as D1Storage
participant SuiClient as SuiClientImp
participant BtcIndexer as BtcIndexerRpc
participant Indexer as BtcIndexer.Indexer
participant Electrs as ElectrsService
participant BtcStorage as CFStorage
Scheduler->>RedeemService: scheduled()
RedeemService->>RedeemStorage: getSignedRedeems()
RedeemStorage-->>RedeemService: list of RedeemRequest (status = solved, all inputs verified)
loop for each redeem
RedeemService->>RedeemService: broadcastRedeem(redeem)
RedeemService->>SuiClient: getRedeemBtcTx(redeem_id, nbtc_pkg, nbtc_contract)
SuiClient->>SuiClient: devInspectTransactionBlock(build redeem_request/raw_signed_tx)
SuiClient-->>RedeemService: rawTxHex
RedeemService->>BtcIndexer: broadcastRedeemTx(rawTxHex, btcNetwork, redeem_id)
BtcIndexer->>Indexer: broadcastRedeemTx(rawTxHex, btcNetwork, redeem_id)
Indexer->>Electrs: broadcastTx(rawTxHex)
Electrs-->>Indexer: Response(ok, txId or error)
alt broadcast ok
Indexer->>BtcStorage: updateRedeemStatusToBroadcasted(redeem_id, txId)
BtcStorage-->>Indexer: ok
Indexer-->>BtcIndexer: { tx_id: txId }
BtcIndexer-->>RedeemService: { tx_id: txId }
RedeemService->>RedeemService: log success
else broadcast failed
Indexer-->>BtcIndexer: throw Error
BtcIndexer-->>RedeemService: Error
RedeemService->>RedeemService: logError(method broadcastRedeem)
end
end
Sequence diagram for confirming broadcasted redeems in new Bitcoin blockssequenceDiagram
participant BlockIngestor as BlockIngestor
participant Indexer as BtcIndexer.Indexer
participant Electrs as ElectrsService
participant BtcStorage as CFStorage
BlockIngestor->>Indexer: insertBlock(blockInfo)
Indexer->>Indexer: iterate block.transactions
loop for each tx in block.transactions
Indexer->>Indexer: process deposits (findNbtcDeposits)
Indexer->>Indexer: collect tx.getId() into txIds[]
end
alt there are txIds
Indexer->>BtcStorage: confirmRedeemsInBlock(txIds, blockInfo.height, blockInfo.hash)
BtcStorage->>BtcStorage: UPDATE nbtc_redeem_requests
BtcStorage-->>Indexer: ok
end
alt there are nbtcTxs
Indexer->>BtcStorage: insertOrUpdateNbtcTxs(nbtcTxs)
BtcStorage-->>Indexer: ok
end
Sequence diagram for Sui indexer marking redeem inputs verifiedsequenceDiagram
participant SuiChain as SuiChain
participant SuiIndexer as SuiIndexer.Handler
participant SuiStorage as IndexerStorage
SuiChain-->>SuiIndexer: stream of SuiEventNode
loop for each SuiEventNode
SuiIndexer->>SuiIndexer: handleEvents(events)
alt SolvedEvent
SuiIndexer->>SuiStorage: upsertRedeemInputs(redeem_id, utxo_ids, dwallet_ids)
SuiStorage-->>SuiIndexer: ok
else SignatureRecordedEvent
SuiIndexer->>SuiStorage: markRedeemInputVerified(redeem_id, utxo_id)
SuiStorage-->>SuiIndexer: ok
end
end
note over SuiStorage: Redeem is considered fully signed when
note over SuiStorage: all rows in nbtc_redeem_solutions for redeem_id
note over SuiStorage: have verified = 1
ER diagram for updated redeem-related BTC indexer tableserDiagram
setups {
int id PK
string btc_network
string nbtc_pkg
string nbtc_contract
}
nbtc_redeem_requests {
int redeem_id PK
int setup_id FK
string redeemer
int amount_sats
string sui_tx
string btc_tx
string status
int created_at
int btc_block_height
string btc_block_hash
int btc_broadcasted_at
}
nbtc_redeem_solutions {
int redeem_id FK
int utxo_id
int input_index
string dwallet_id
int created_at
int verified
}
setups ||--o{ nbtc_redeem_requests : has
nbtc_redeem_requests ||--o{ nbtc_redeem_solutions : has
Class diagram for redeem lifecycle services and storageclassDiagram
class RedeemService {
-Storage storage
-Map~SuiNet, SuiClient~ clients
-Service btcIndexer
-number utxoLockTimeMs
-number redeemDurationMs
+constructor(storage, clients, btcIndexer, utxoLockTimeMs, redeemDurationMs)
+processPendingRedeems()
+solveReadyRedeems()
+processSolvedRedeems()
+broadcastReadyRedeems()
-broadcastRedeem(req RedeemRequest)
-getSuiClient(net SuiNet) SuiClient
}
class SuiClient {
<<interface>>
+proposeRedeemUtxos(args ProposeRedeemCall) Promise~string~
+getRedeemBtcTx(redeemId number, nbtcPkg string, nbtcContract string) Promise~string~
}
class SuiClientImp {
+getRedeemBtcTx(redeemId number, nbtcPkg string, nbtcContract string) Promise~string~
}
SuiClient <|.. SuiClientImp
class BtcIndexerRpcI {
<<interface>>
+latestHeight(network BtcNet) Promise~object~
+putNbtcTx(txHex string, network BtcNet) Promise~PutNbtcTxResponse~
+broadcastRedeemTx(txHex string, network BtcNet, redeemId number) Promise~object~
+nbtcMintTx(txid string) Promise~NbtcTxResp~
+nbtcMintTxsBySuiAddr(suiAddress string) Promise~NbtcTxResp[]~
+depositsBySender(address string, network BtcNet) Promise~NbtcTxResp[]~
}
class BtcIndexerRpc {
+latestHeight(network BtcNet) Promise~object~
+putNbtcTx(txHex string, network BtcNet) Promise~PutNbtcTxResponse~
+broadcastRedeemTx(txHex string, network BtcNet, redeemId number) Promise~object~
+nbtcMintTx(txid string) Promise~NbtcTxResp~
+nbtcMintTxsBySuiAddr(suiAddress string) Promise~NbtcTxResp[]~
+depositsBySender(address string, network BtcNet) Promise~NbtcTxResp[]~
-getIndexer() Promise~Indexer~
}
BtcIndexerRpcI <|.. BtcIndexerRpc
class Indexer {
-Storage storage
-Electrs getElectrsClient(network BtcNet) Electrs
+insertBlock(blockInfo BlockInfo) Promise~InsertBlockResult~
+registerBroadcastedNbtcTx(txHex string, network BtcNet) Promise~PutNbtcTxResponse~
+broadcastRedeemTx(txHex string, network BtcNet, redeemId number) Promise~object~
+getLatestHeight(network BtcNet) Promise~object~
+getRedeemsBySuiAddr(suiAddress string, network BtcNet) Promise~NbtcRedeemRow[]~
}
class Electrs {
<<interface>>
+getTx(txId string) Promise~Response~
+broadcastTx(txHex string) Promise~Response~
}
class ElectrsService {
-string baseUrl
+getTx(txId string) Promise~Response~
+broadcastTx(txHex string) Promise~Response~
}
Electrs <|.. ElectrsService
class Storage {
<<interface>>
+updateRedeemStatusToBroadcasted(redeemId number, txId string) Promise~void~
+confirmRedeemsInBlock(txIds string[], blockHeight number, blockHash string) Promise~void~
+getNbtcRedeemsBySuiAddr(suiAddress string, network BtcNet) Promise~NbtcRedeemRow[]~
}
class CFStorage {
+updateRedeemStatusToBroadcasted(redeemId number, txId string) Promise~void~
+confirmRedeemsInBlock(txIds string[], blockHeight number, blockHash string) Promise~void~
+getNbtcRedeemsBySuiAddr(suiAddress string, network BtcNet) Promise~NbtcRedeemRow[]~
}
Storage <|.. CFStorage
class RedeemRequest {
int redeem_id
int setup_id
string redeemer
Uint8Array recipient_script
int amount_sats
string status
int created_at
string nbtc_pkg
string nbtc_contract
SuiNet sui_network
}
class RedeemRequestWithNetwork {
int redeem_id
string btc_network
}
RedeemService --> SuiClient : uses
RedeemService --> BtcIndexerRpcI : calls via Service
RedeemService --> Storage : uses
BtcIndexerRpc --> Indexer : delegates
Indexer --> Storage : uses
Indexer --> Electrs : uses
SuiClientImp --> RedeemRequest : builds Move calls
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@sourcery-ai title |
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
sui-indexerIndexerStorage.upsertRedeemInputsyou removed the length mismatch check and error handling; consider keeping a guard (or at least an assertion) whenutxoIds.length !== dwalletIds.lengthso bad input doesn’t silently insert inconsistent rows. - In
RedeemService.broadcastRedeemyou rely on// @ts-expect-errorand a defaultbtc_networkofregtest; it would be more robust to extend theRedeemRequest/row type to includebtc_networkexplicitly and avoid the type suppression and implicit defaulting.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `sui-indexer` `IndexerStorage.upsertRedeemInputs` you removed the length mismatch check and error handling; consider keeping a guard (or at least an assertion) when `utxoIds.length !== dwalletIds.length` so bad input doesn’t silently insert inconsistent rows.
- In `RedeemService.broadcastRedeem` you rely on `// @ts-expect-error` and a default `btc_network` of `regtest`; it would be more robust to extend the `RedeemRequest`/row type to include `btc_network` explicitly and avoid the type suppression and implicit defaulting.
## Individual Comments
### Comment 1
<location> `packages/sui-indexer/src/storage.ts:192-201` </location>
<code_context>
+ upsertRedeemInputs(redeemId: number, utxoIds: number[], dwalletIds: string[]): Promise<void> {
</code_context>
<issue_to_address>
**issue (bug_risk):** Reintroduced upsertRedeemInputs without length validation or error logging, which can now silently bind undefined values.
The previous version validated `utxoIds.length === dwalletIds.length` and logged structured errors on batch failure. The new version removes both the guard and error handling, so mismatched lengths can bind `dwalletIds[i]` as `undefined`, and D1 failures are dropped by the `.then()` chain. Please reintroduce input validation and either return `this.db.batch(batch)` directly or add a `.catch` that calls `logError` so failures are visible.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive redeem broadcast and confirmation flow for nBTC, enabling automated broadcasting of signed Bitcoin redemption transactions and tracking their confirmation status across the Bitcoin blockchain.
Key Changes:
- Adds broadcasting pipeline for signed nBTC redeem transactions via Electrs in btcindexer
- Implements confirmation tracking by matching Bitcoin block transactions to redeem requests
- Introduces new HTTP and RPC endpoints to query redeem status by Sui address
Reviewed changes
Copilot reviewed 21 out of 24 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/sui-indexer/src/storage.ts | Refactored upsertRedeemInputs to use promise chaining and added markRedeemInputVerified method for tracking verified inputs |
| packages/sui-indexer/src/models.ts | Added Confirmed status enum value and SignatureRecordedEventRaw interface |
| packages/sui-indexer/src/handler.ts | Added event handler for SignatureRecordedEvent to mark inputs as verified |
| packages/redeem_solver/wrangler.jsonc | Added service binding to btcindexer worker |
| packages/redeem_solver/worker-configuration.d.ts | Auto-generated types updated with BTCINDEXER service binding |
| packages/redeem_solver/src/sui_client.ts | Added getRedeemBtcTx method to fetch raw signed Bitcoin transaction from Sui |
| packages/redeem_solver/src/storage.ts | Added getSignedRedeems query to select fully verified redeems ready for broadcast |
| packages/redeem_solver/src/service.ts | Implemented broadcastReadyRedeems and broadcastRedeem methods for transaction broadcasting |
| packages/redeem_solver/src/index.ts | Integrated broadcast task into scheduler with improved error logging |
| packages/redeem_solver/package.json | Added btcindexer workspace dependency |
| packages/btcindexer/src/storage.ts | Added interface methods for redeem status updates and confirmation tracking |
| packages/btcindexer/src/cf-storage.ts | Implemented database operations for redeem broadcasting and confirmation |
| packages/btcindexer/src/btcindexer.ts | Added broadcastRedeemTx and getRedeemsBySuiAddr methods with confirmation calculation |
| packages/btcindexer/src/rpc.ts | Exposed broadcastRedeemTx RPC method with documentation |
| packages/btcindexer/src/rpc-interface.ts | Added broadcastRedeemTx to RPC interface |
| packages/btcindexer/src/rpc-mock.ts | Simplified mock implementation and added broadcastRedeemTx stub |
| packages/btcindexer/src/router.ts | Added /redeems/:address HTTP endpoint for querying redeems by Sui address |
| packages/btcindexer/src/models.ts | Added NbtcRedeemRow and NbtcRedeemResp types for redeem data structures |
| packages/btcindexer/src/electrs.ts | Added broadcastTx method for submitting transactions to Bitcoin network |
| packages/btcindexer/src/electrs.test.ts | Updated mock to include broadcastTx method |
| packages/btcindexer/src/api/client.ts | Added redeems REST path constant |
| packages/btcindexer/db/migrations/0001_initial_schema.sql | Added columns for Bitcoin transaction tracking and confirmation |
| packages/block-ingestor/worker-configuration.d.ts | Auto-generated runtime type updates (unrelated to PR changes) |
| bun.lock | Updated lockfile with new dependency resolution |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 30 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
…cc/workers into stan/broadcast-redeems
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
…cc/workers into stan/broadcast-redeems
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 33 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
packages/redeem_solver/src/rpc.ts:30
- The removed RPC method redeemsBySuiAddr has been replaced with an HTTP endpoint, but the interface change is breaking. Any existing callers using the RPC service binding to fetch redeems will fail at runtime. Consider adding a deprecation notice or migration guide, or maintaining backward compatibility with a wrapper that calls the new HTTP endpoint.
export interface RedeemSolverRpc {
finalizeRedeem: () => Promise<void>;
putRedeemTx: (setupId: number, suiTxId: string, e: RedeemRequestEventRaw) => Promise<void>;
}
/**
* RPC entrypoint for the worker.
*
* @see https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/
*/
export class RPC extends WorkerEntrypoint<Env> implements RedeemSolverRpc {
/**
* Once BTC withdraw for the Redeem Request is confirmed and finalzed, this method
* will update the DB state and remove related UTXOs.
*/
async finalizeRedeem(): Promise<void> {
return;
}
/**
* Stores a redeem request transaction emitted on Sui into the indexer storage, to be later
* tracked by the indexer to trigger solution (UTXOs) proposal in this worker scheduler.
*
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
robert-zaremba
left a comment
There was a problem hiding this comment.
- let's move the not related api away from btcindexer
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 32 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <43810037+sczembor@users.noreply.github.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Description
Closes: #229
Author Checklist
All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.
I have...
!to the type prefix if API or client breaking changeCHANGELOG.mdSummary by Sourcery
Automate nBTC redeem broadcasting and confirmation tracking across Bitcoin and Sui indexers, and expose redeem status via new APIs.
New Features:
Enhancements: