Skip to content

Commit 5e5d895

Browse files
authored
Merge pull request #1003 from yyforyongyu/add-op-watch
wtxmgr: add method `OutputsToWatch`
2 parents 8e790bf + 8d3d081 commit 5e5d895

File tree

2 files changed

+193
-99
lines changed

2 files changed

+193
-99
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ coverage.txt
55
*.swp
66
.vscode
77
.DS_Store
8+
.aider*

wtxmgr/tx.go

Lines changed: 192 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -805,121 +805,214 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error {
805805
return putMinedBalance(ns, minedBalance)
806806
}
807807

808-
// UnspentOutputs returns all unspent received transaction outputs.
809-
// The order is undefined.
810-
func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) {
811-
var unspent []Credit
808+
// fetchCredits retrieves credits from the store based on the provided filters.
809+
// It iterates over both mined (unspent) and unmined credits.
810+
//
811+
// Parameters:
812+
// - ns: The database bucket to read from.
813+
// - includeLocked: If true, credits locked by LockOutput are included.
814+
// - includeSpentByUnmined: If true, credits spent by unmined transactions
815+
// are included.
816+
// - populateFullDetails: If true, all fields of the Credit struct are
817+
// populated. Otherwise, only OutPoint and PkScript are populated.
818+
func (s *Store) fetchCredits(ns walletdb.ReadBucket, includeLocked bool,
819+
includeSpentByUnmined bool,
820+
populateFullDetails bool) ([]Credit, error) {
821+
822+
var credits []Credit
823+
now := s.clock.Now() // Cache current time for lock checks
824+
825+
// Iterate over mined unspent credits (bucketUnspent).
826+
unspentBucket := ns.NestedReadBucket(bucketUnspent)
827+
if unspentBucket != nil {
828+
err := unspentBucket.ForEach(func(k, v []byte) error {
829+
var op wire.OutPoint
830+
err := readCanonicalOutPoint(k, &op)
831+
if err != nil {
832+
return err
833+
}
812834

813-
var op wire.OutPoint
814-
var block Block
815-
err := ns.NestedReadBucket(bucketUnspent).ForEach(func(k, v []byte) error {
816-
err := readCanonicalOutPoint(k, &op)
817-
if err != nil {
818-
return err
819-
}
835+
// Check if locked, skip if necessary.
836+
if !includeLocked {
837+
_, _, isLocked := isLockedOutput(ns, op, now)
838+
if isLocked {
839+
return nil
840+
}
841+
}
820842

821-
// Skip the output if it's locked.
822-
_, _, isLocked := isLockedOutput(ns, op, s.clock.Now())
823-
if isLocked {
824-
return nil
825-
}
843+
// Check if spent by unmined, skip if necessary.
844+
if !includeSpentByUnmined {
845+
if existsRawUnminedInput(ns, k) != nil {
846+
return nil
847+
}
848+
}
826849

827-
if existsRawUnminedInput(ns, k) != nil {
828-
// Output is spent by an unmined transaction.
829-
// Skip this k/v pair.
830-
return nil
831-
}
850+
// Fetch the transaction record to get PkScript and
851+
// potentially other details.
852+
var block Block
853+
err = readUnspentBlock(v, &block)
854+
if err != nil {
855+
return err
856+
}
832857

833-
err = readUnspentBlock(v, &block)
834-
if err != nil {
835-
return err
836-
}
858+
// TODO(jrick): reading the entire transaction should
859+
// be avoidable. Creating the credit only requires the
860+
// output amount and pkScript.
861+
rec, err := fetchTxRecord(ns, &op.Hash, &block)
862+
if err != nil {
863+
// Wrap the error for context.
864+
return fmt.Errorf("unable to retrieve tx %v "+
865+
"for mined credit: %w", op.Hash, err)
866+
}
837867

838-
blockTime, err := fetchBlockTime(ns, block.Height)
839-
if err != nil {
840-
return err
841-
}
842-
// TODO(jrick): reading the entire transaction should
843-
// be avoidable. Creating the credit only requires the
844-
// output amount and pkScript.
845-
rec, err := fetchTxRecord(ns, &op.Hash, &block)
868+
txOut := rec.MsgTx.TxOut[op.Index]
869+
cred := Credit{
870+
OutPoint: op,
871+
PkScript: txOut.PkScript,
872+
}
873+
874+
// Populate full details if requested.
875+
if populateFullDetails {
876+
blockTime, err := fetchBlockTime(
877+
ns, block.Height,
878+
)
879+
if err != nil {
880+
// Wrap the error for context.
881+
return fmt.Errorf("unable to fetch "+
882+
"block time for height %d: %w",
883+
block.Height, err)
884+
}
885+
886+
cred.BlockMeta = BlockMeta{
887+
Block: block,
888+
Time: blockTime,
889+
}
890+
cred.Amount = btcutil.Amount(txOut.Value)
891+
cred.Received = rec.Received
892+
cred.FromCoinBase = blockchain.IsCoinBaseTx(
893+
&rec.MsgTx,
894+
)
895+
}
896+
897+
credits = append(credits, cred)
898+
return nil
899+
})
846900
if err != nil {
847-
return fmt.Errorf("unable to retrieve transaction %v: "+
848-
"%w", op.Hash, err)
849-
}
850-
txOut := rec.MsgTx.TxOut[op.Index]
851-
cred := Credit{
852-
OutPoint: op,
853-
BlockMeta: BlockMeta{
854-
Block: block,
855-
Time: blockTime,
856-
},
857-
Amount: btcutil.Amount(txOut.Value),
858-
PkScript: txOut.PkScript,
859-
Received: rec.Received,
860-
FromCoinBase: blockchain.IsCoinBaseTx(&rec.MsgTx),
861-
}
862-
unspent = append(unspent, cred)
863-
return nil
864-
})
865-
if err != nil {
866-
if _, ok := err.(Error); ok {
867-
return nil, err
901+
// Check if it's already a storeError, otherwise wrap
902+
// it.
903+
if _, ok := err.(Error); ok {
904+
return nil, err
905+
}
906+
907+
str := "failed iterating unspent bucket"
908+
return nil, storeError(ErrDatabase, str, err)
868909
}
869-
str := "failed iterating unspent bucket"
870-
return nil, storeError(ErrDatabase, str, err)
871910
}
872911

873-
err = ns.NestedReadBucket(bucketUnminedCredits).ForEach(func(k, v []byte) error {
874-
if err := readCanonicalOutPoint(k, &op); err != nil {
875-
return err
876-
}
912+
// Iterate over unmined credits (bucketUnminedCredits).
913+
unminedCreditsBucket := ns.NestedReadBucket(bucketUnminedCredits)
914+
if unminedCreditsBucket != nil {
915+
err := unminedCreditsBucket.ForEach(func(k, v []byte) error {
916+
var op wire.OutPoint
917+
if err := readCanonicalOutPoint(k, &op); err != nil {
918+
return err
919+
}
877920

878-
// Skip the output if it's locked.
879-
_, _, isLocked := isLockedOutput(ns, op, s.clock.Now())
880-
if isLocked {
881-
return nil
882-
}
921+
// Check if locked, skip if necessary.
922+
if !includeLocked {
923+
_, _, isLocked := isLockedOutput(ns, op, now)
924+
if isLocked {
925+
return nil
926+
}
927+
}
883928

884-
if existsRawUnminedInput(ns, k) != nil {
885-
// Output is spent by an unmined transaction.
886-
// Skip to next unmined credit.
887-
return nil
888-
}
929+
// Check if spent by unmined, skip if necessary.
930+
if !includeSpentByUnmined {
931+
if existsRawUnminedInput(ns, k) != nil {
932+
return nil
933+
}
934+
}
889935

890-
// TODO(jrick): Reading/parsing the entire transaction record
891-
// just for the output amount and script can be avoided.
892-
recVal := existsRawUnmined(ns, op.Hash[:])
893-
var rec TxRecord
894-
err = readRawTxRecord(&op.Hash, recVal, &rec)
895-
if err != nil {
896-
return fmt.Errorf("unable to retrieve raw transaction "+
897-
"%v: %w", op.Hash, err)
898-
}
936+
// Fetch the transaction record to get PkScript and
937+
// potentially other details.
938+
recVal := existsRawUnmined(ns, op.Hash[:])
899939

900-
txOut := rec.MsgTx.TxOut[op.Index]
901-
cred := Credit{
902-
OutPoint: op,
903-
BlockMeta: BlockMeta{
904-
Block: Block{Height: -1},
905-
},
906-
Amount: btcutil.Amount(txOut.Value),
907-
PkScript: txOut.PkScript,
908-
Received: rec.Received,
909-
FromCoinBase: blockchain.IsCoinBaseTx(&rec.MsgTx),
910-
}
911-
unspent = append(unspent, cred)
912-
return nil
913-
})
914-
if err != nil {
915-
if _, ok := err.(Error); ok {
916-
return nil, err
940+
// existsRawUnmined should always return a value for a
941+
// key in bucketUnminedCredits, but check defensively.
942+
if recVal == nil {
943+
log.Warnf("Unmined credit %v points to "+
944+
"non-existent unmined tx record %v", op,
945+
op.Hash)
946+
947+
// Skip this credit as its tx record is missing.
948+
return nil
949+
}
950+
951+
var rec TxRecord
952+
err := readRawTxRecord(&op.Hash, recVal, &rec)
953+
if err != nil {
954+
// Wrap the error for context.
955+
return fmt.Errorf("unable to retrieve raw tx "+
956+
"%v for unmined credit: %w", op.Hash,
957+
err)
958+
}
959+
960+
txOut := rec.MsgTx.TxOut[op.Index]
961+
cred := Credit{
962+
OutPoint: op,
963+
PkScript: txOut.PkScript,
964+
}
965+
966+
// Populate full details if requested.
967+
if populateFullDetails {
968+
cred.BlockMeta = BlockMeta{
969+
// Unmined height.
970+
Block: Block{Height: -1},
971+
}
972+
cred.Amount = btcutil.Amount(txOut.Value)
973+
cred.Received = rec.Received
974+
cred.FromCoinBase = blockchain.IsCoinBaseTx(
975+
&rec.MsgTx,
976+
)
977+
}
978+
979+
credits = append(credits, cred)
980+
return nil
981+
})
982+
if err != nil {
983+
// Check if it's already a storeError, otherwise wrap
984+
// it.
985+
if _, ok := err.(Error); ok {
986+
return nil, err
987+
}
988+
str := "failed iterating unmined credits bucket"
989+
return nil, storeError(ErrDatabase, str, err)
917990
}
918-
str := "failed iterating unmined credits bucket"
919-
return nil, storeError(ErrDatabase, str, err)
920991
}
921992

922-
return unspent, nil
993+
return credits, nil
994+
}
995+
996+
// OutputsToWatch returns a list of outputs to monitor during the wallet's
997+
// startup. The returned items are similar to UnspentOutputs, exccept the
998+
// locked outputs and unmined credits are also returned here. In addition, we
999+
// only set the field `OutPoint` and `PkScript` for the `Credit`, as these are
1000+
// the only fields used during the rescan.
1001+
func (s *Store) OutputsToWatch(ns walletdb.ReadBucket) ([]Credit, error) {
1002+
// OutputsToWatch needs all known outputs (mined and unmined),
1003+
// including locked ones and those spent by other unmined txs,
1004+
// but only requires minimal details (OutPoint, PkScript).
1005+
return s.fetchCredits(ns, true, true, false)
1006+
}
1007+
1008+
// UnspentOutputs returns all unspent received transaction outputs.
1009+
// The order is undefined.
1010+
func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) {
1011+
// UnspentOutputs needs outputs that are actually spendable:
1012+
// - Not locked.
1013+
// - Not spent by an unmined transaction.
1014+
// It requires full credit details.
1015+
return s.fetchCredits(ns, false, false, true)
9231016
}
9241017

9251018
// Balance returns the spendable wallet balance (total value of all unspent

0 commit comments

Comments
 (0)