From 0c14162efb12bb4032e8b84b889760e521f41955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Tue, 9 Jan 2024 21:40:43 +0800 Subject: [PATCH 01/48] chore: update gitignore to exclude Cargo files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 967bcd2e..9e9a397d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ profile.tmp profile.cov utreexod + +# Cargo +target/ +Cargo.lock From 681fd7eab1bb457c7bbe6a1c0aaee4522b66d836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Tue, 9 Jan 2024 21:43:12 +0800 Subject: [PATCH 02/48] bdkwallet: initial work on BDK wallet --- Cargo.toml | 7 + bdkwallet/bdkgo/bdkgo.c | 8 + bdkwallet/bdkgo/bdkgo.go | 1491 +++++++++++++++++++++++++++ bdkwallet/bdkgo/bdkgo.h | 490 +++++++++ bdkwallet/bdkgo_crate/Cargo.toml | 23 + bdkwallet/bdkgo_crate/build.rs | 3 + bdkwallet/bdkgo_crate/src/bdkgo.udl | 77 ++ bdkwallet/bdkgo_crate/src/lib.rs | 311 ++++++ bdkwallet/log.go | 24 + bdkwallet/manager.go | 80 ++ bdkwallet/wallet.go | 158 +++ bdkwallet/wallet_test.go | 67 ++ buildbdkgo.sh | 3 + log.go | 4 + server.go | 16 + 15 files changed, 2762 insertions(+) create mode 100644 Cargo.toml create mode 100644 bdkwallet/bdkgo/bdkgo.c create mode 100644 bdkwallet/bdkgo/bdkgo.go create mode 100644 bdkwallet/bdkgo/bdkgo.h create mode 100644 bdkwallet/bdkgo_crate/Cargo.toml create mode 100644 bdkwallet/bdkgo_crate/build.rs create mode 100644 bdkwallet/bdkgo_crate/src/bdkgo.udl create mode 100644 bdkwallet/bdkgo_crate/src/lib.rs create mode 100644 bdkwallet/log.go create mode 100644 bdkwallet/manager.go create mode 100644 bdkwallet/wallet.go create mode 100644 bdkwallet/wallet_test.go create mode 100755 buildbdkgo.sh diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..67a1f185 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] + +members = [ + "bdkwallet/bdkgo_crate", +] + +resolver = "2" diff --git a/bdkwallet/bdkgo/bdkgo.c b/bdkwallet/bdkgo/bdkgo.c new file mode 100644 index 00000000..2bd64c01 --- /dev/null +++ b/bdkwallet/bdkgo/bdkgo.c @@ -0,0 +1,8 @@ +#include + +// This file exists beacause of +// https://github.com/golang/go/issues/11263 + +void cgo_rust_task_callback_bridge_bdkgo(RustTaskCallback cb, const void * taskData, int8_t status) { + cb(taskData, status); +} \ No newline at end of file diff --git a/bdkwallet/bdkgo/bdkgo.go b/bdkwallet/bdkgo/bdkgo.go new file mode 100644 index 00000000..5d55023b --- /dev/null +++ b/bdkwallet/bdkgo/bdkgo.go @@ -0,0 +1,1491 @@ +package bdkgo + +// #include +import "C" + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math" + "runtime" + "sync/atomic" + "unsafe" +) + +type RustBuffer = C.RustBuffer + +type RustBufferI interface { + AsReader() *bytes.Reader + Free() + ToGoBytes() []byte + Data() unsafe.Pointer + Len() int + Capacity() int +} + +func RustBufferFromExternal(b RustBufferI) RustBuffer { + return RustBuffer{ + capacity: C.int(b.Capacity()), + len: C.int(b.Len()), + data: (*C.uchar)(b.Data()), + } +} + +func (cb RustBuffer) Capacity() int { + return int(cb.capacity) +} + +func (cb RustBuffer) Len() int { + return int(cb.len) +} + +func (cb RustBuffer) Data() unsafe.Pointer { + return unsafe.Pointer(cb.data) +} + +func (cb RustBuffer) AsReader() *bytes.Reader { + b := unsafe.Slice((*byte)(cb.data), C.int(cb.len)) + return bytes.NewReader(b) +} + +func (cb RustBuffer) Free() { + rustCall(func(status *C.RustCallStatus) bool { + C.ffi_bdkgo_rustbuffer_free(cb, status) + return false + }) +} + +func (cb RustBuffer) ToGoBytes() []byte { + return C.GoBytes(unsafe.Pointer(cb.data), C.int(cb.len)) +} + +func stringToRustBuffer(str string) RustBuffer { + return bytesToRustBuffer([]byte(str)) +} + +func bytesToRustBuffer(b []byte) RustBuffer { + if len(b) == 0 { + return RustBuffer{} + } + // We can pass the pointer along here, as it is pinned + // for the duration of this call + foreign := C.ForeignBytes{ + len: C.int(len(b)), + data: (*C.uchar)(unsafe.Pointer(&b[0])), + } + + return rustCall(func(status *C.RustCallStatus) RustBuffer { + return C.ffi_bdkgo_rustbuffer_from_bytes(foreign, status) + }) +} + +type BufLifter[GoType any] interface { + Lift(value RustBufferI) GoType +} + +type BufLowerer[GoType any] interface { + Lower(value GoType) RustBuffer +} + +type FfiConverter[GoType any, FfiType any] interface { + Lift(value FfiType) GoType + Lower(value GoType) FfiType +} + +type BufReader[GoType any] interface { + Read(reader io.Reader) GoType +} + +type BufWriter[GoType any] interface { + Write(writer io.Writer, value GoType) +} + +type FfiRustBufConverter[GoType any, FfiType any] interface { + FfiConverter[GoType, FfiType] + BufReader[GoType] +} + +func LowerIntoRustBuffer[GoType any](bufWriter BufWriter[GoType], value GoType) RustBuffer { + // This might be not the most efficient way but it does not require knowing allocation size + // beforehand + var buffer bytes.Buffer + bufWriter.Write(&buffer, value) + + bytes, err := io.ReadAll(&buffer) + if err != nil { + panic(fmt.Errorf("reading written data: %w", err)) + } + return bytesToRustBuffer(bytes) +} + +func LiftFromRustBuffer[GoType any](bufReader BufReader[GoType], rbuf RustBufferI) GoType { + defer rbuf.Free() + reader := rbuf.AsReader() + item := bufReader.Read(reader) + if reader.Len() > 0 { + // TODO: Remove this + leftover, _ := io.ReadAll(reader) + panic(fmt.Errorf("Junk remaining in buffer after lifting: %s", string(leftover))) + } + return item +} + +func rustCallWithError[U any](converter BufLifter[error], callback func(*C.RustCallStatus) U) (U, error) { + var status C.RustCallStatus + returnValue := callback(&status) + err := checkCallStatus(converter, status) + + return returnValue, err +} + +func checkCallStatus(converter BufLifter[error], status C.RustCallStatus) error { + switch status.code { + case 0: + return nil + case 1: + return converter.Lift(status.errorBuf) + case 2: + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if status.errorBuf.len > 0 { + panic(fmt.Errorf("%s", FfiConverterStringINSTANCE.Lift(status.errorBuf))) + } else { + panic(fmt.Errorf("Rust panicked while handling Rust panic")) + } + default: + return fmt.Errorf("unknown status code: %d", status.code) + } +} + +func checkCallStatusUnknown(status C.RustCallStatus) error { + switch status.code { + case 0: + return nil + case 1: + panic(fmt.Errorf("function not returning an error returned an error")) + case 2: + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if status.errorBuf.len > 0 { + panic(fmt.Errorf("%s", FfiConverterStringINSTANCE.Lift(status.errorBuf))) + } else { + panic(fmt.Errorf("Rust panicked while handling Rust panic")) + } + default: + return fmt.Errorf("unknown status code: %d", status.code) + } +} + +func rustCall[U any](callback func(*C.RustCallStatus) U) U { + returnValue, err := rustCallWithError(nil, callback) + if err != nil { + panic(err) + } + return returnValue +} + +func writeInt8(writer io.Writer, value int8) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeUint8(writer io.Writer, value uint8) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeInt16(writer io.Writer, value int16) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeUint16(writer io.Writer, value uint16) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeInt32(writer io.Writer, value int32) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeUint32(writer io.Writer, value uint32) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeInt64(writer io.Writer, value int64) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeUint64(writer io.Writer, value uint64) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeFloat32(writer io.Writer, value float32) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeFloat64(writer io.Writer, value float64) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func readInt8(reader io.Reader) int8 { + var result int8 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readUint8(reader io.Reader) uint8 { + var result uint8 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readInt16(reader io.Reader) int16 { + var result int16 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readUint16(reader io.Reader) uint16 { + var result uint16 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readInt32(reader io.Reader) int32 { + var result int32 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readUint32(reader io.Reader) uint32 { + var result uint32 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readInt64(reader io.Reader) int64 { + var result int64 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readUint64(reader io.Reader) uint64 { + var result uint64 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readFloat32(reader io.Reader) float32 { + var result float32 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readFloat64(reader io.Reader) float64 { + var result float64 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func init() { + + uniffiCheckChecksums() +} + +func uniffiCheckChecksums() { + // Get the bindings contract version from our ComponentInterface + bindingsContractVersion := 24 + // Get the scaffolding contract version by calling the into the dylib + scaffoldingContractVersion := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint32_t { + return C.ffi_bdkgo_uniffi_contract_version(uniffiStatus) + }) + if bindingsContractVersion != int(scaffoldingContractVersion) { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: UniFFI contract version mismatch") + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_block_hash(uniffiStatus) + }) + if checksum != 33317 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_block_hash: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_apply_block(uniffiStatus) + }) + if checksum != 60553 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_apply_block: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_balance(uniffiStatus) + }) + if checksum != 26195 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_balance: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_fresh_address(uniffiStatus) + }) + if checksum != 39819 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_fresh_address: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_genesis_hash(uniffiStatus) + }) + if checksum != 65013 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_genesis_hash: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_last_unused_address(uniffiStatus) + }) + if checksum != 34780 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_last_unused_address: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_peek_address(uniffiStatus) + }) + if checksum != 60510 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_peek_address: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_recent_blocks(uniffiStatus) + }) + if checksum != 57902 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_recent_blocks: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_constructor_block_new(uniffiStatus) + }) + if checksum != 33914 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_constructor_block_new: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_constructor_wallet_create_new(uniffiStatus) + }) + if checksum != 23490 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_constructor_wallet_create_new: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_constructor_wallet_load(uniffiStatus) + }) + if checksum != 64083 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_constructor_wallet_load: UniFFI API checksum mismatch") + } + } +} + +type FfiConverterUint32 struct{} + +var FfiConverterUint32INSTANCE = FfiConverterUint32{} + +func (FfiConverterUint32) Lower(value uint32) C.uint32_t { + return C.uint32_t(value) +} + +func (FfiConverterUint32) Write(writer io.Writer, value uint32) { + writeUint32(writer, value) +} + +func (FfiConverterUint32) Lift(value C.uint32_t) uint32 { + return uint32(value) +} + +func (FfiConverterUint32) Read(reader io.Reader) uint32 { + return readUint32(reader) +} + +type FfiDestroyerUint32 struct{} + +func (FfiDestroyerUint32) Destroy(_ uint32) {} + +type FfiConverterUint64 struct{} + +var FfiConverterUint64INSTANCE = FfiConverterUint64{} + +func (FfiConverterUint64) Lower(value uint64) C.uint64_t { + return C.uint64_t(value) +} + +func (FfiConverterUint64) Write(writer io.Writer, value uint64) { + writeUint64(writer, value) +} + +func (FfiConverterUint64) Lift(value C.uint64_t) uint64 { + return uint64(value) +} + +func (FfiConverterUint64) Read(reader io.Reader) uint64 { + return readUint64(reader) +} + +type FfiDestroyerUint64 struct{} + +func (FfiDestroyerUint64) Destroy(_ uint64) {} + +type FfiConverterString struct{} + +var FfiConverterStringINSTANCE = FfiConverterString{} + +func (FfiConverterString) Lift(rb RustBufferI) string { + defer rb.Free() + reader := rb.AsReader() + b, err := io.ReadAll(reader) + if err != nil { + panic(fmt.Errorf("reading reader: %w", err)) + } + return string(b) +} + +func (FfiConverterString) Read(reader io.Reader) string { + length := readInt32(reader) + buffer := make([]byte, length) + read_length, err := reader.Read(buffer) + if err != nil { + panic(err) + } + if read_length != int(length) { + panic(fmt.Errorf("bad read length when reading string, expected %d, read %d", length, read_length)) + } + return string(buffer) +} + +func (FfiConverterString) Lower(value string) RustBuffer { + return stringToRustBuffer(value) +} + +func (FfiConverterString) Write(writer io.Writer, value string) { + if len(value) > math.MaxInt32 { + panic("String is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + write_length, err := io.WriteString(writer, value) + if err != nil { + panic(err) + } + if write_length != len(value) { + panic(fmt.Errorf("bad write length when writing string, expected %d, written %d", len(value), write_length)) + } +} + +type FfiDestroyerString struct{} + +func (FfiDestroyerString) Destroy(_ string) {} + +type FfiConverterBytes struct{} + +var FfiConverterBytesINSTANCE = FfiConverterBytes{} + +func (c FfiConverterBytes) Lower(value []byte) RustBuffer { + return LowerIntoRustBuffer[[]byte](c, value) +} + +func (c FfiConverterBytes) Write(writer io.Writer, value []byte) { + if len(value) > math.MaxInt32 { + panic("[]byte is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + write_length, err := writer.Write(value) + if err != nil { + panic(err) + } + if write_length != len(value) { + panic(fmt.Errorf("bad write length when writing []byte, expected %d, written %d", len(value), write_length)) + } +} + +func (c FfiConverterBytes) Lift(rb RustBufferI) []byte { + return LiftFromRustBuffer[[]byte](c, rb) +} + +func (c FfiConverterBytes) Read(reader io.Reader) []byte { + length := readInt32(reader) + buffer := make([]byte, length) + read_length, err := reader.Read(buffer) + if err != nil { + panic(err) + } + if read_length != int(length) { + panic(fmt.Errorf("bad read length when reading []byte, expected %d, read %d", length, read_length)) + } + return buffer +} + +type FfiDestroyerBytes struct{} + +func (FfiDestroyerBytes) Destroy(_ []byte) {} + +// Below is an implementation of synchronization requirements outlined in the link. +// https://github.com/mozilla/uniffi-rs/blob/0dc031132d9493ca812c3af6e7dd60ad2ea95bf0/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt#L31 + +type FfiObject struct { + pointer unsafe.Pointer + callCounter atomic.Int64 + freeFunction func(unsafe.Pointer, *C.RustCallStatus) + destroyed atomic.Bool +} + +func newFfiObject(pointer unsafe.Pointer, freeFunction func(unsafe.Pointer, *C.RustCallStatus)) FfiObject { + return FfiObject{ + pointer: pointer, + freeFunction: freeFunction, + } +} + +func (ffiObject *FfiObject) incrementPointer(debugName string) unsafe.Pointer { + for { + counter := ffiObject.callCounter.Load() + if counter <= -1 { + panic(fmt.Errorf("%v object has already been destroyed", debugName)) + } + if counter == math.MaxInt64 { + panic(fmt.Errorf("%v object call counter would overflow", debugName)) + } + if ffiObject.callCounter.CompareAndSwap(counter, counter+1) { + break + } + } + + return ffiObject.pointer +} + +func (ffiObject *FfiObject) decrementPointer() { + if ffiObject.callCounter.Add(-1) == -1 { + ffiObject.freeRustArcPtr() + } +} + +func (ffiObject *FfiObject) destroy() { + if ffiObject.destroyed.CompareAndSwap(false, true) { + if ffiObject.callCounter.Add(-1) == -1 { + ffiObject.freeRustArcPtr() + } + } +} + +func (ffiObject *FfiObject) freeRustArcPtr() { + rustCall(func(status *C.RustCallStatus) int32 { + ffiObject.freeFunction(ffiObject.pointer, status) + return 0 + }) +} + +type Block struct { + ffiObject FfiObject +} + +func NewBlock(b []byte) *Block { + return FfiConverterBlockINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_bdkgo_fn_constructor_block_new(FfiConverterBytesINSTANCE.Lower(b), _uniffiStatus) + })) +} + +func (_self *Block) Hash() []byte { + _pointer := _self.ffiObject.incrementPointer("*Block") + defer _self.ffiObject.decrementPointer() + return FfiConverterBytesINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_block_hash( + _pointer, _uniffiStatus) + })) +} + +func (object *Block) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterBlock struct{} + +var FfiConverterBlockINSTANCE = FfiConverterBlock{} + +func (c FfiConverterBlock) Lift(pointer unsafe.Pointer) *Block { + result := &Block{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_bdkgo_fn_free_block(pointer, status) + }), + } + runtime.SetFinalizer(result, (*Block).Destroy) + return result +} + +func (c FfiConverterBlock) Read(reader io.Reader) *Block { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterBlock) Lower(value *Block) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*Block") + defer value.ffiObject.decrementPointer() + return pointer +} + +func (c FfiConverterBlock) Write(writer io.Writer, value *Block) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerBlock struct{} + +func (_ FfiDestroyerBlock) Destroy(value *Block) { + value.Destroy() +} + +type Wallet struct { + ffiObject FfiObject +} + +func WalletCreateNew(dbPath string, network string, genesisHash []byte) (*Wallet, error) { + _uniffiRV, _uniffiErr := rustCallWithError(FfiConverterTypeCreateNewError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_bdkgo_fn_constructor_wallet_create_new(FfiConverterStringINSTANCE.Lower(dbPath), FfiConverterStringINSTANCE.Lower(network), FfiConverterBytesINSTANCE.Lower(genesisHash), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue *Wallet + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterWalletINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} +func WalletLoad(dbPath string) (*Wallet, error) { + _uniffiRV, _uniffiErr := rustCallWithError(FfiConverterTypeLoadError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_bdkgo_fn_constructor_wallet_load(FfiConverterStringINSTANCE.Lower(dbPath), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue *Wallet + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterWalletINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} + +func (_self *Wallet) ApplyBlock(height uint32, blockBytes *Block) error { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + _, _uniffiErr := rustCallWithError(FfiConverterTypeApplyBlockError{}, func(_uniffiStatus *C.RustCallStatus) bool { + C.uniffi_bdkgo_fn_method_wallet_apply_block( + _pointer, FfiConverterUint32INSTANCE.Lower(height), FfiConverterBlockINSTANCE.Lower(blockBytes), _uniffiStatus) + return false + }) + return _uniffiErr +} + +func (_self *Wallet) Balance() Balance { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + return FfiConverterTypeBalanceINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_balance( + _pointer, _uniffiStatus) + })) +} + +func (_self *Wallet) FreshAddress() (AddressInfo, error) { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + _uniffiRV, _uniffiErr := rustCallWithError(FfiConverterTypeDatabaseError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_fresh_address( + _pointer, _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue AddressInfo + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterTypeAddressInfoINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} + +func (_self *Wallet) GenesisHash() []byte { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + return FfiConverterBytesINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_genesis_hash( + _pointer, _uniffiStatus) + })) +} + +func (_self *Wallet) LastUnusedAddress() (AddressInfo, error) { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + _uniffiRV, _uniffiErr := rustCallWithError(FfiConverterTypeDatabaseError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_last_unused_address( + _pointer, _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue AddressInfo + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterTypeAddressInfoINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} + +func (_self *Wallet) PeekAddress(index uint32) (AddressInfo, error) { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + _uniffiRV, _uniffiErr := rustCallWithError(FfiConverterTypeDatabaseError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_peek_address( + _pointer, FfiConverterUint32INSTANCE.Lower(index), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue AddressInfo + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterTypeAddressInfoINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} + +func (_self *Wallet) RecentBlocks(count uint32) []BlockId { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + return FfiConverterSequenceTypeBlockIdINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_recent_blocks( + _pointer, FfiConverterUint32INSTANCE.Lower(count), _uniffiStatus) + })) +} + +func (object *Wallet) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterWallet struct{} + +var FfiConverterWalletINSTANCE = FfiConverterWallet{} + +func (c FfiConverterWallet) Lift(pointer unsafe.Pointer) *Wallet { + result := &Wallet{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_bdkgo_fn_free_wallet(pointer, status) + }), + } + runtime.SetFinalizer(result, (*Wallet).Destroy) + return result +} + +func (c FfiConverterWallet) Read(reader io.Reader) *Wallet { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterWallet) Lower(value *Wallet) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*Wallet") + defer value.ffiObject.decrementPointer() + return pointer +} + +func (c FfiConverterWallet) Write(writer io.Writer, value *Wallet) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerWallet struct{} + +func (_ FfiDestroyerWallet) Destroy(value *Wallet) { + value.Destroy() +} + +type AddressInfo struct { + Index uint32 + Address string +} + +func (r *AddressInfo) Destroy() { + FfiDestroyerUint32{}.Destroy(r.Index) + FfiDestroyerString{}.Destroy(r.Address) +} + +type FfiConverterTypeAddressInfo struct{} + +var FfiConverterTypeAddressInfoINSTANCE = FfiConverterTypeAddressInfo{} + +func (c FfiConverterTypeAddressInfo) Lift(rb RustBufferI) AddressInfo { + return LiftFromRustBuffer[AddressInfo](c, rb) +} + +func (c FfiConverterTypeAddressInfo) Read(reader io.Reader) AddressInfo { + return AddressInfo{ + FfiConverterUint32INSTANCE.Read(reader), + FfiConverterStringINSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeAddressInfo) Lower(value AddressInfo) RustBuffer { + return LowerIntoRustBuffer[AddressInfo](c, value) +} + +func (c FfiConverterTypeAddressInfo) Write(writer io.Writer, value AddressInfo) { + FfiConverterUint32INSTANCE.Write(writer, value.Index) + FfiConverterStringINSTANCE.Write(writer, value.Address) +} + +type FfiDestroyerTypeAddressInfo struct{} + +func (_ FfiDestroyerTypeAddressInfo) Destroy(value AddressInfo) { + value.Destroy() +} + +type Balance struct { + Immature uint64 + TrustedPending uint64 + UntrustedPending uint64 + Confirmed uint64 +} + +func (r *Balance) Destroy() { + FfiDestroyerUint64{}.Destroy(r.Immature) + FfiDestroyerUint64{}.Destroy(r.TrustedPending) + FfiDestroyerUint64{}.Destroy(r.UntrustedPending) + FfiDestroyerUint64{}.Destroy(r.Confirmed) +} + +type FfiConverterTypeBalance struct{} + +var FfiConverterTypeBalanceINSTANCE = FfiConverterTypeBalance{} + +func (c FfiConverterTypeBalance) Lift(rb RustBufferI) Balance { + return LiftFromRustBuffer[Balance](c, rb) +} + +func (c FfiConverterTypeBalance) Read(reader io.Reader) Balance { + return Balance{ + FfiConverterUint64INSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeBalance) Lower(value Balance) RustBuffer { + return LowerIntoRustBuffer[Balance](c, value) +} + +func (c FfiConverterTypeBalance) Write(writer io.Writer, value Balance) { + FfiConverterUint64INSTANCE.Write(writer, value.Immature) + FfiConverterUint64INSTANCE.Write(writer, value.TrustedPending) + FfiConverterUint64INSTANCE.Write(writer, value.UntrustedPending) + FfiConverterUint64INSTANCE.Write(writer, value.Confirmed) +} + +type FfiDestroyerTypeBalance struct{} + +func (_ FfiDestroyerTypeBalance) Destroy(value Balance) { + value.Destroy() +} + +type BlockId struct { + Height uint32 + Hash []byte +} + +func (r *BlockId) Destroy() { + FfiDestroyerUint32{}.Destroy(r.Height) + FfiDestroyerBytes{}.Destroy(r.Hash) +} + +type FfiConverterTypeBlockId struct{} + +var FfiConverterTypeBlockIdINSTANCE = FfiConverterTypeBlockId{} + +func (c FfiConverterTypeBlockId) Lift(rb RustBufferI) BlockId { + return LiftFromRustBuffer[BlockId](c, rb) +} + +func (c FfiConverterTypeBlockId) Read(reader io.Reader) BlockId { + return BlockId{ + FfiConverterUint32INSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeBlockId) Lower(value BlockId) RustBuffer { + return LowerIntoRustBuffer[BlockId](c, value) +} + +func (c FfiConverterTypeBlockId) Write(writer io.Writer, value BlockId) { + FfiConverterUint32INSTANCE.Write(writer, value.Height) + FfiConverterBytesINSTANCE.Write(writer, value.Hash) +} + +type FfiDestroyerTypeBlockId struct{} + +func (_ FfiDestroyerTypeBlockId) Destroy(value BlockId) { + value.Destroy() +} + +type ApplyBlockError struct { + err error +} + +func (err ApplyBlockError) Error() string { + return fmt.Sprintf("ApplyBlockError: %s", err.err.Error()) +} + +func (err ApplyBlockError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrApplyBlockErrorDecodeBlock = fmt.Errorf("ApplyBlockErrorDecodeBlock") +var ErrApplyBlockErrorCannotConnect = fmt.Errorf("ApplyBlockErrorCannotConnect") +var ErrApplyBlockErrorDatabase = fmt.Errorf("ApplyBlockErrorDatabase") + +// Variant structs +type ApplyBlockErrorDecodeBlock struct { + message string +} + +func NewApplyBlockErrorDecodeBlock() *ApplyBlockError { + return &ApplyBlockError{ + err: &ApplyBlockErrorDecodeBlock{}, + } +} + +func (err ApplyBlockErrorDecodeBlock) Error() string { + return fmt.Sprintf("DecodeBlock: %s", err.message) +} + +func (self ApplyBlockErrorDecodeBlock) Is(target error) bool { + return target == ErrApplyBlockErrorDecodeBlock +} + +type ApplyBlockErrorCannotConnect struct { + message string +} + +func NewApplyBlockErrorCannotConnect() *ApplyBlockError { + return &ApplyBlockError{ + err: &ApplyBlockErrorCannotConnect{}, + } +} + +func (err ApplyBlockErrorCannotConnect) Error() string { + return fmt.Sprintf("CannotConnect: %s", err.message) +} + +func (self ApplyBlockErrorCannotConnect) Is(target error) bool { + return target == ErrApplyBlockErrorCannotConnect +} + +type ApplyBlockErrorDatabase struct { + message string +} + +func NewApplyBlockErrorDatabase() *ApplyBlockError { + return &ApplyBlockError{ + err: &ApplyBlockErrorDatabase{}, + } +} + +func (err ApplyBlockErrorDatabase) Error() string { + return fmt.Sprintf("Database: %s", err.message) +} + +func (self ApplyBlockErrorDatabase) Is(target error) bool { + return target == ErrApplyBlockErrorDatabase +} + +type FfiConverterTypeApplyBlockError struct{} + +var FfiConverterTypeApplyBlockErrorINSTANCE = FfiConverterTypeApplyBlockError{} + +func (c FfiConverterTypeApplyBlockError) Lift(eb RustBufferI) error { + return LiftFromRustBuffer[error](c, eb) +} + +func (c FfiConverterTypeApplyBlockError) Lower(value *ApplyBlockError) RustBuffer { + return LowerIntoRustBuffer[*ApplyBlockError](c, value) +} + +func (c FfiConverterTypeApplyBlockError) Read(reader io.Reader) error { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &ApplyBlockError{&ApplyBlockErrorDecodeBlock{message}} + case 2: + return &ApplyBlockError{&ApplyBlockErrorCannotConnect{message}} + case 3: + return &ApplyBlockError{&ApplyBlockErrorDatabase{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterTypeApplyBlockError.Read()", errorID)) + } + +} + +func (c FfiConverterTypeApplyBlockError) Write(writer io.Writer, value *ApplyBlockError) { + switch variantValue := value.err.(type) { + case *ApplyBlockErrorDecodeBlock: + writeInt32(writer, 1) + case *ApplyBlockErrorCannotConnect: + writeInt32(writer, 2) + case *ApplyBlockErrorDatabase: + writeInt32(writer, 3) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterTypeApplyBlockError.Write", value)) + } +} + +type CreateNewError struct { + err error +} + +func (err CreateNewError) Error() string { + return fmt.Sprintf("CreateNewError: %s", err.err.Error()) +} + +func (err CreateNewError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrCreateNewErrorParseNetwork = fmt.Errorf("CreateNewErrorParseNetwork") +var ErrCreateNewErrorParseGenesisHash = fmt.Errorf("CreateNewErrorParseGenesisHash") +var ErrCreateNewErrorDatabase = fmt.Errorf("CreateNewErrorDatabase") +var ErrCreateNewErrorWallet = fmt.Errorf("CreateNewErrorWallet") + +// Variant structs +type CreateNewErrorParseNetwork struct { + message string +} + +func NewCreateNewErrorParseNetwork() *CreateNewError { + return &CreateNewError{ + err: &CreateNewErrorParseNetwork{}, + } +} + +func (err CreateNewErrorParseNetwork) Error() string { + return fmt.Sprintf("ParseNetwork: %s", err.message) +} + +func (self CreateNewErrorParseNetwork) Is(target error) bool { + return target == ErrCreateNewErrorParseNetwork +} + +type CreateNewErrorParseGenesisHash struct { + message string +} + +func NewCreateNewErrorParseGenesisHash() *CreateNewError { + return &CreateNewError{ + err: &CreateNewErrorParseGenesisHash{}, + } +} + +func (err CreateNewErrorParseGenesisHash) Error() string { + return fmt.Sprintf("ParseGenesisHash: %s", err.message) +} + +func (self CreateNewErrorParseGenesisHash) Is(target error) bool { + return target == ErrCreateNewErrorParseGenesisHash +} + +type CreateNewErrorDatabase struct { + message string +} + +func NewCreateNewErrorDatabase() *CreateNewError { + return &CreateNewError{ + err: &CreateNewErrorDatabase{}, + } +} + +func (err CreateNewErrorDatabase) Error() string { + return fmt.Sprintf("Database: %s", err.message) +} + +func (self CreateNewErrorDatabase) Is(target error) bool { + return target == ErrCreateNewErrorDatabase +} + +type CreateNewErrorWallet struct { + message string +} + +func NewCreateNewErrorWallet() *CreateNewError { + return &CreateNewError{ + err: &CreateNewErrorWallet{}, + } +} + +func (err CreateNewErrorWallet) Error() string { + return fmt.Sprintf("Wallet: %s", err.message) +} + +func (self CreateNewErrorWallet) Is(target error) bool { + return target == ErrCreateNewErrorWallet +} + +type FfiConverterTypeCreateNewError struct{} + +var FfiConverterTypeCreateNewErrorINSTANCE = FfiConverterTypeCreateNewError{} + +func (c FfiConverterTypeCreateNewError) Lift(eb RustBufferI) error { + return LiftFromRustBuffer[error](c, eb) +} + +func (c FfiConverterTypeCreateNewError) Lower(value *CreateNewError) RustBuffer { + return LowerIntoRustBuffer[*CreateNewError](c, value) +} + +func (c FfiConverterTypeCreateNewError) Read(reader io.Reader) error { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &CreateNewError{&CreateNewErrorParseNetwork{message}} + case 2: + return &CreateNewError{&CreateNewErrorParseGenesisHash{message}} + case 3: + return &CreateNewError{&CreateNewErrorDatabase{message}} + case 4: + return &CreateNewError{&CreateNewErrorWallet{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterTypeCreateNewError.Read()", errorID)) + } + +} + +func (c FfiConverterTypeCreateNewError) Write(writer io.Writer, value *CreateNewError) { + switch variantValue := value.err.(type) { + case *CreateNewErrorParseNetwork: + writeInt32(writer, 1) + case *CreateNewErrorParseGenesisHash: + writeInt32(writer, 2) + case *CreateNewErrorDatabase: + writeInt32(writer, 3) + case *CreateNewErrorWallet: + writeInt32(writer, 4) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterTypeCreateNewError.Write", value)) + } +} + +type DatabaseError struct { + err error +} + +func (err DatabaseError) Error() string { + return fmt.Sprintf("DatabaseError: %s", err.err.Error()) +} + +func (err DatabaseError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrDatabaseErrorWrite = fmt.Errorf("DatabaseErrorWrite") + +// Variant structs +type DatabaseErrorWrite struct { + message string +} + +func NewDatabaseErrorWrite() *DatabaseError { + return &DatabaseError{ + err: &DatabaseErrorWrite{}, + } +} + +func (err DatabaseErrorWrite) Error() string { + return fmt.Sprintf("Write: %s", err.message) +} + +func (self DatabaseErrorWrite) Is(target error) bool { + return target == ErrDatabaseErrorWrite +} + +type FfiConverterTypeDatabaseError struct{} + +var FfiConverterTypeDatabaseErrorINSTANCE = FfiConverterTypeDatabaseError{} + +func (c FfiConverterTypeDatabaseError) Lift(eb RustBufferI) error { + return LiftFromRustBuffer[error](c, eb) +} + +func (c FfiConverterTypeDatabaseError) Lower(value *DatabaseError) RustBuffer { + return LowerIntoRustBuffer[*DatabaseError](c, value) +} + +func (c FfiConverterTypeDatabaseError) Read(reader io.Reader) error { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &DatabaseError{&DatabaseErrorWrite{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterTypeDatabaseError.Read()", errorID)) + } + +} + +func (c FfiConverterTypeDatabaseError) Write(writer io.Writer, value *DatabaseError) { + switch variantValue := value.err.(type) { + case *DatabaseErrorWrite: + writeInt32(writer, 1) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterTypeDatabaseError.Write", value)) + } +} + +type LoadError struct { + err error +} + +func (err LoadError) Error() string { + return fmt.Sprintf("LoadError: %s", err.err.Error()) +} + +func (err LoadError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrLoadErrorDatabase = fmt.Errorf("LoadErrorDatabase") +var ErrLoadErrorParseHeader = fmt.Errorf("LoadErrorParseHeader") +var ErrLoadErrorHeaderVersion = fmt.Errorf("LoadErrorHeaderVersion") +var ErrLoadErrorWallet = fmt.Errorf("LoadErrorWallet") + +// Variant structs +type LoadErrorDatabase struct { + message string +} + +func NewLoadErrorDatabase() *LoadError { + return &LoadError{ + err: &LoadErrorDatabase{}, + } +} + +func (err LoadErrorDatabase) Error() string { + return fmt.Sprintf("Database: %s", err.message) +} + +func (self LoadErrorDatabase) Is(target error) bool { + return target == ErrLoadErrorDatabase +} + +type LoadErrorParseHeader struct { + message string +} + +func NewLoadErrorParseHeader() *LoadError { + return &LoadError{ + err: &LoadErrorParseHeader{}, + } +} + +func (err LoadErrorParseHeader) Error() string { + return fmt.Sprintf("ParseHeader: %s", err.message) +} + +func (self LoadErrorParseHeader) Is(target error) bool { + return target == ErrLoadErrorParseHeader +} + +type LoadErrorHeaderVersion struct { + message string +} + +func NewLoadErrorHeaderVersion() *LoadError { + return &LoadError{ + err: &LoadErrorHeaderVersion{}, + } +} + +func (err LoadErrorHeaderVersion) Error() string { + return fmt.Sprintf("HeaderVersion: %s", err.message) +} + +func (self LoadErrorHeaderVersion) Is(target error) bool { + return target == ErrLoadErrorHeaderVersion +} + +type LoadErrorWallet struct { + message string +} + +func NewLoadErrorWallet() *LoadError { + return &LoadError{ + err: &LoadErrorWallet{}, + } +} + +func (err LoadErrorWallet) Error() string { + return fmt.Sprintf("Wallet: %s", err.message) +} + +func (self LoadErrorWallet) Is(target error) bool { + return target == ErrLoadErrorWallet +} + +type FfiConverterTypeLoadError struct{} + +var FfiConverterTypeLoadErrorINSTANCE = FfiConverterTypeLoadError{} + +func (c FfiConverterTypeLoadError) Lift(eb RustBufferI) error { + return LiftFromRustBuffer[error](c, eb) +} + +func (c FfiConverterTypeLoadError) Lower(value *LoadError) RustBuffer { + return LowerIntoRustBuffer[*LoadError](c, value) +} + +func (c FfiConverterTypeLoadError) Read(reader io.Reader) error { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &LoadError{&LoadErrorDatabase{message}} + case 2: + return &LoadError{&LoadErrorParseHeader{message}} + case 3: + return &LoadError{&LoadErrorHeaderVersion{message}} + case 4: + return &LoadError{&LoadErrorWallet{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterTypeLoadError.Read()", errorID)) + } + +} + +func (c FfiConverterTypeLoadError) Write(writer io.Writer, value *LoadError) { + switch variantValue := value.err.(type) { + case *LoadErrorDatabase: + writeInt32(writer, 1) + case *LoadErrorParseHeader: + writeInt32(writer, 2) + case *LoadErrorHeaderVersion: + writeInt32(writer, 3) + case *LoadErrorWallet: + writeInt32(writer, 4) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterTypeLoadError.Write", value)) + } +} + +type FfiConverterSequenceTypeBlockId struct{} + +var FfiConverterSequenceTypeBlockIdINSTANCE = FfiConverterSequenceTypeBlockId{} + +func (c FfiConverterSequenceTypeBlockId) Lift(rb RustBufferI) []BlockId { + return LiftFromRustBuffer[[]BlockId](c, rb) +} + +func (c FfiConverterSequenceTypeBlockId) Read(reader io.Reader) []BlockId { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]BlockId, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterTypeBlockIdINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceTypeBlockId) Lower(value []BlockId) RustBuffer { + return LowerIntoRustBuffer[[]BlockId](c, value) +} + +func (c FfiConverterSequenceTypeBlockId) Write(writer io.Writer, value []BlockId) { + if len(value) > math.MaxInt32 { + panic("[]BlockId is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterTypeBlockIdINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceTypeBlockId struct{} + +func (FfiDestroyerSequenceTypeBlockId) Destroy(sequence []BlockId) { + for _, value := range sequence { + FfiDestroyerTypeBlockId{}.Destroy(value) + } +} diff --git a/bdkwallet/bdkgo/bdkgo.h b/bdkwallet/bdkgo/bdkgo.h new file mode 100644 index 00000000..c36d5a6b --- /dev/null +++ b/bdkwallet/bdkgo/bdkgo.h @@ -0,0 +1,490 @@ + + +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + + + +#include +#include + +// The following structs are used to implement the lowest level +// of the FFI, and thus useful to multiple uniffied crates. +// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. +#ifdef UNIFFI_SHARED_H + // We also try to prevent mixing versions of shared uniffi header structs. + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V6 + #ifndef UNIFFI_SHARED_HEADER_V6 + #error Combining helper code from multiple versions of uniffi is not supported + #endif // ndef UNIFFI_SHARED_HEADER_V6 +#else +#define UNIFFI_SHARED_H +#define UNIFFI_SHARED_HEADER_V6 +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V6 in this file. ⚠️ + +typedef struct RustBuffer { + int32_t capacity; + int32_t len; + uint8_t *data; +} RustBuffer; + +typedef int32_t (*ForeignCallback)(uint64_t, int32_t, uint8_t *, int32_t, RustBuffer *); + +// Task defined in Rust that Go executes +typedef void (*RustTaskCallback)(const void *, int8_t); + +// Callback to execute Rust tasks using a Go routine +// +// Args: +// executor: ForeignExecutor lowered into a uint64_t value +// delay: Delay in MS +// task: RustTaskCallback to call +// task_data: data to pass the task callback +typedef int8_t (*ForeignExecutorCallback)(uint64_t, uint32_t, RustTaskCallback, void *); + +typedef struct ForeignBytes { + int32_t len; + const uint8_t *data; +} ForeignBytes; + +// Error definitions +typedef struct RustCallStatus { + int8_t code; + RustBuffer errorBuf; +} RustCallStatus; + +// Continuation callback for UniFFI Futures +typedef void (*RustFutureContinuation)(void * , int8_t); + +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V6 in this file. ⚠️ +#endif // def UNIFFI_SHARED_H + +// Needed because we can't execute the callback directly from go. +void cgo_rust_task_callback_bridge_bdkgo(RustTaskCallback, const void *, int8_t); + +int8_t uniffiForeignExecutorCallbackbdkgo(uint64_t, uint32_t, RustTaskCallback, void*); + +void uniffiFutureContinuationCallbackbdkgo(void*, int8_t); + +void uniffi_bdkgo_fn_free_block( + void* ptr, + RustCallStatus* out_status +); + +void* uniffi_bdkgo_fn_constructor_block_new( + RustBuffer b, + RustCallStatus* out_status +); + +RustBuffer uniffi_bdkgo_fn_method_block_hash( + void* ptr, + RustCallStatus* out_status +); + +void uniffi_bdkgo_fn_free_wallet( + void* ptr, + RustCallStatus* out_status +); + +void* uniffi_bdkgo_fn_constructor_wallet_create_new( + RustBuffer db_path, + RustBuffer network, + RustBuffer genesis_hash, + RustCallStatus* out_status +); + +void* uniffi_bdkgo_fn_constructor_wallet_load( + RustBuffer db_path, + RustCallStatus* out_status +); + +void uniffi_bdkgo_fn_method_wallet_apply_block( + void* ptr, + uint32_t height, + void* block_bytes, + RustCallStatus* out_status +); + +RustBuffer uniffi_bdkgo_fn_method_wallet_balance( + void* ptr, + RustCallStatus* out_status +); + +RustBuffer uniffi_bdkgo_fn_method_wallet_fresh_address( + void* ptr, + RustCallStatus* out_status +); + +RustBuffer uniffi_bdkgo_fn_method_wallet_genesis_hash( + void* ptr, + RustCallStatus* out_status +); + +RustBuffer uniffi_bdkgo_fn_method_wallet_last_unused_address( + void* ptr, + RustCallStatus* out_status +); + +RustBuffer uniffi_bdkgo_fn_method_wallet_peek_address( + void* ptr, + uint32_t index, + RustCallStatus* out_status +); + +RustBuffer uniffi_bdkgo_fn_method_wallet_recent_blocks( + void* ptr, + uint32_t count, + RustCallStatus* out_status +); + +RustBuffer ffi_bdkgo_rustbuffer_alloc( + int32_t size, + RustCallStatus* out_status +); + +RustBuffer ffi_bdkgo_rustbuffer_from_bytes( + ForeignBytes bytes, + RustCallStatus* out_status +); + +void ffi_bdkgo_rustbuffer_free( + RustBuffer buf, + RustCallStatus* out_status +); + +RustBuffer ffi_bdkgo_rustbuffer_reserve( + RustBuffer buf, + int32_t additional, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_continuation_callback_set( + RustFutureContinuation callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_u8( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_u8( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_u8( + void* handle, + RustCallStatus* out_status +); + +uint8_t ffi_bdkgo_rust_future_complete_u8( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_i8( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_i8( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_i8( + void* handle, + RustCallStatus* out_status +); + +int8_t ffi_bdkgo_rust_future_complete_i8( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_u16( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_u16( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_u16( + void* handle, + RustCallStatus* out_status +); + +uint16_t ffi_bdkgo_rust_future_complete_u16( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_i16( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_i16( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_i16( + void* handle, + RustCallStatus* out_status +); + +int16_t ffi_bdkgo_rust_future_complete_i16( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_u32( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_u32( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_u32( + void* handle, + RustCallStatus* out_status +); + +uint32_t ffi_bdkgo_rust_future_complete_u32( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_i32( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_i32( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_i32( + void* handle, + RustCallStatus* out_status +); + +int32_t ffi_bdkgo_rust_future_complete_i32( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_u64( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_u64( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_u64( + void* handle, + RustCallStatus* out_status +); + +uint64_t ffi_bdkgo_rust_future_complete_u64( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_i64( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_i64( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_i64( + void* handle, + RustCallStatus* out_status +); + +int64_t ffi_bdkgo_rust_future_complete_i64( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_f32( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_f32( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_f32( + void* handle, + RustCallStatus* out_status +); + +float ffi_bdkgo_rust_future_complete_f32( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_f64( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_f64( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_f64( + void* handle, + RustCallStatus* out_status +); + +double ffi_bdkgo_rust_future_complete_f64( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_pointer( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_pointer( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_pointer( + void* handle, + RustCallStatus* out_status +); + +void* ffi_bdkgo_rust_future_complete_pointer( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_rust_buffer( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_rust_buffer( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_rust_buffer( + void* handle, + RustCallStatus* out_status +); + +RustBuffer ffi_bdkgo_rust_future_complete_rust_buffer( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_poll_void( + void* handle, + void* uniffi_callback, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_cancel_void( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_free_void( + void* handle, + RustCallStatus* out_status +); + +void ffi_bdkgo_rust_future_complete_void( + void* handle, + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_block_hash( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_wallet_apply_block( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_wallet_balance( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_wallet_fresh_address( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_wallet_genesis_hash( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_wallet_last_unused_address( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_wallet_peek_address( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_wallet_recent_blocks( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_constructor_block_new( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_constructor_wallet_create_new( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_constructor_wallet_load( + RustCallStatus* out_status +); + +uint32_t ffi_bdkgo_uniffi_contract_version( + RustCallStatus* out_status +); + + + diff --git a/bdkwallet/bdkgo_crate/Cargo.toml b/bdkwallet/bdkgo_crate/Cargo.toml new file mode 100644 index 00000000..e685ce47 --- /dev/null +++ b/bdkwallet/bdkgo_crate/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bdkgo" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib", "cdylib", "lib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +uniffi = "=0.25.0" +weedle2 = "=4.0.0" +thiserror = "1.0" +bincode = "1" +serde = { version = "1", features = ["derive"] } +bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "fbd1d656187859fcb7b84b177e7e9498c15537ea", features = ["keys-bip39"] } +bdk_file_store = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "fbd1d656187859fcb7b84b177e7e9498c15537ea" } +rand = "0.8.5" + +[build-dependencies] +uniffi = { version = "=0.25.0", features = [ "build" ] } +weedle2 = "=4.0.0" diff --git a/bdkwallet/bdkgo_crate/build.rs b/bdkwallet/bdkgo_crate/build.rs new file mode 100644 index 00000000..74144e7f --- /dev/null +++ b/bdkwallet/bdkgo_crate/build.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::generate_scaffolding("src/bdkgo.udl").unwrap(); +} diff --git a/bdkwallet/bdkgo_crate/src/bdkgo.udl b/bdkwallet/bdkgo_crate/src/bdkgo.udl new file mode 100644 index 00000000..fe2fe989 --- /dev/null +++ b/bdkwallet/bdkgo_crate/src/bdkgo.udl @@ -0,0 +1,77 @@ +namespace bdkgo {}; + +[Error] +enum CreateNewError { + "ParseNetwork", + "ParseGenesisHash", + "Database", + "Wallet", +}; + +[Error] +enum LoadError { + "Database", + "ParseHeader", + "HeaderVersion", + "Wallet", +}; + +[Error] +enum DatabaseError { + "Write", +}; + +[Error] +enum ApplyBlockError { + "DecodeBlock", + "CannotConnect", + "Database", +}; + +dictionary AddressInfo { + u32 index; + string address; +}; + +dictionary Balance { + u64 immature; + u64 trusted_pending; + u64 untrusted_pending; + u64 confirmed; +}; + +interface Wallet { + [Name=create_new, Throws=CreateNewError] + constructor(string db_path, string network, bytes genesis_hash); + + [Name=load, Throws=LoadError] + constructor(string db_path); + + [Throws=DatabaseError] + AddressInfo last_unused_address(); + + [Throws=DatabaseError] + AddressInfo fresh_address(); + + [Throws=DatabaseError] + AddressInfo peek_address(u32 index); + + Balance balance(); + + bytes genesis_hash(); + + sequence recent_blocks(u32 count); + + [Throws=ApplyBlockError] + void apply_block(u32 height, Block block_bytes); +}; + +interface Block { + constructor([ByRef] bytes b); + bytes hash(); +}; + +dictionary BlockId { + u32 height; + bytes hash; +}; diff --git a/bdkwallet/bdkgo_crate/src/lib.rs b/bdkwallet/bdkgo_crate/src/lib.rs new file mode 100644 index 00000000..15b7aad0 --- /dev/null +++ b/bdkwallet/bdkgo_crate/src/lib.rs @@ -0,0 +1,311 @@ +use std::{ + io::Read, + str::FromStr, + sync::{Arc, Mutex}, +}; + +pub use bdk::wallet::Balance; +use bdk::{ + bitcoin::{ + self, consensus::Decodable, hashes::Hash, network::constants::ParseNetworkError, BlockHash, + Network, + }, + keys::{DerivableKey, ExtendedKey}, + template::Bip86, + wallet::AddressIndex, + KeychainKind, +}; +use bincode::Options; +use rand::RngCore; + +uniffi::include_scaffolding!("bdkgo"); + +const DB_MAGIC: &str = "utreexod.bdk.345e94cf"; +const DB_MAGIC_LEN: usize = DB_MAGIC.len(); +const ENTROPY_LEN: usize = 16; // 12 words + +type BdkWallet = bdk::Wallet>; + +fn bincode_config() -> impl bincode::Options { + bincode::options().with_fixint_encoding() +} + +#[derive(Debug, thiserror::Error)] +pub enum CreateNewError { + #[error("failed to parse network type string: {0}")] + ParseNetwork(ParseNetworkError), + #[error("failed to parse genesis hash: {0}")] + ParseGenesisHash(bdk::bitcoin::hashes::Error), + #[error("failed to create new db file: {0}")] + Database(bdk_file_store::FileError<'static>), + #[error("failed to init wallet: {0}")] + Wallet(bdk::wallet::NewError), +} + +#[derive(Debug, thiserror::Error)] +pub enum LoadError { + #[error("failed to load db: {0}")] + Database(bdk_file_store::FileError<'static>), + #[error("failed to decode wallet header: {0}")] + ParseHeader(bincode::Error), + #[error("wallet header version unsupported")] + HeaderVersion, + #[error("failed to init wallet: {0}")] + Wallet(bdk::wallet::LoadError), +} + +#[derive(Debug, thiserror::Error)] +pub enum DatabaseError { + #[error("failed to write to db: {0}")] + Write(std::io::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum ApplyBlockError { + #[error("failed to decode block: {0}")] + DecodeBlock(bdk::bitcoin::consensus::encode::Error), + #[error("block cannot connect with wallet's chain: {0}")] + CannotConnect(bdk::chain::local_chain::CannotConnectError), + #[error("failed to write block to db: {0}")] + Database(std::io::Error), +} + +pub struct AddressInfo { + pub index: u32, + pub address: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct WalletHeader { + pub version: [u8; DB_MAGIC_LEN], + pub entropy: [u8; ENTROPY_LEN], + pub network: Network, +} + +impl WalletHeader { + pub fn new(network: Network) -> Self { + let mut version = [0_u8; DB_MAGIC_LEN]; + version.copy_from_slice(DB_MAGIC.as_bytes()); + let mut entropy = [0_u8; ENTROPY_LEN]; + rand::thread_rng().fill_bytes(&mut entropy); + Self { + version, + entropy, + network, + } + } + + pub fn encode(&mut self) -> Vec { + self.version.copy_from_slice(DB_MAGIC.as_bytes()); + let b = bincode_config() + .serialize(&self) + .expect("bincode must serialize"); + let l = (b.len() as u32).to_le_bytes(); + l.into_iter().chain(b).collect::>() + } + + pub fn decode(mut r: R) -> Result<(Self, Vec), LoadError> { + let mut l_buf = [0_u8; 4]; + r.read_exact(&mut l_buf) + .map_err(|err| LoadError::Database(bdk_file_store::FileError::Io(err)))?; + let l = u32::from_le_bytes(l_buf); + let mut b = vec![0; l as usize]; + r.read_exact(&mut b) + .map_err(|err| LoadError::Database(bdk_file_store::FileError::Io(err)))?; + + let header = bincode_config() + .deserialize::(&b) + .map_err(LoadError::ParseHeader)?; + if header.version != DB_MAGIC.as_bytes() { + return Err(LoadError::HeaderVersion); + } + + let header_b = l_buf.into_iter().chain(b).collect::>(); + Ok((header, header_b)) + } + + pub fn descriptor(&self, keychain: KeychainKind) -> Bip86> { + let mnemonic = + bdk::keys::bip39::Mnemonic::from_entropy(&self.entropy).expect("must get mnemonic"); + let mut ext_key: bdk::keys::ExtendedKey = mnemonic + .into_extended_key() + .expect("must become extended key"); + match &mut ext_key { + ExtendedKey::Private((key, _)) => key.network = self.network, + ExtendedKey::Public((key, _)) => key.network = self.network, + }; + Bip86(ext_key, keychain) + } +} + +pub struct Wallet { + inner: Mutex, +} + +impl Wallet { + pub fn create_new( + db_path: String, + network: String, + genesis_hash: Vec, + ) -> Result { + let network = Network::from_str(&network).map_err(CreateNewError::ParseNetwork)?; + let genesis_hash = + BlockHash::from_slice(&genesis_hash).map_err(CreateNewError::ParseGenesisHash)?; + + let mut db_header = WalletHeader::new(network); + let db_header_bytes = Box::new(db_header.encode()).leak(); + + let db = match bdk_file_store::Store::create_new(db_header_bytes, &db_path) { + Ok(db) => db, + Err(err) => return Err(CreateNewError::Database(err)), + }; + + let bdk_wallet = match bdk::Wallet::new_with_genesis_hash( + db_header.descriptor(KeychainKind::External), + Some(db_header.descriptor(KeychainKind::Internal)), + db, + network, + genesis_hash, + ) { + Ok(w) => w, + Err(err) => { + let _ = std::fs::remove_file(db_path); + return Err(CreateNewError::Wallet(err)); + } + }; + + Ok(Self { + inner: Mutex::new(bdk_wallet), + }) + } + + pub fn load(db_path: String) -> Result { + let file = std::fs::File::open(&db_path) + .map_err(|err| LoadError::Database(bdk_file_store::FileError::Io(err)))?; + let (db_header, db_header_bytes) = WalletHeader::decode(file)?; + let db_header_bytes = Box::new(db_header_bytes).leak(); + let db = + bdk_file_store::Store::open(db_header_bytes, db_path).map_err(LoadError::Database)?; + let bdk_wallet = bdk::Wallet::load( + db_header.descriptor(KeychainKind::External), + Some(db_header.descriptor(KeychainKind::Internal)), + db, + ) + .map_err(LoadError::Wallet)?; + + Ok(Self { + inner: Mutex::new(bdk_wallet), + }) + } + + fn address(self: Arc, index: AddressIndex) -> Result { + let mut wallet = self.inner.lock().unwrap(); + let address_info = wallet + .try_get_address(index) + .map_err(DatabaseError::Write)?; + Ok(AddressInfo { + index: address_info.index, + address: address_info.address.to_string(), + }) + } + + pub fn last_unused_address(self: Arc) -> Result { + self.address(AddressIndex::LastUnused) + } + + pub fn fresh_address(self: Arc) -> Result { + self.address(AddressIndex::New) + } + + pub fn peek_address(self: Arc, index: u32) -> Result { + self.address(AddressIndex::Peek(index)) + } + + pub fn balance(self: Arc) -> bdk::wallet::Balance { + let wallet = self.inner.lock().unwrap(); + wallet.get_balance() + } + + pub fn genesis_hash(self: Arc) -> Vec { + self.inner + .lock() + .unwrap() + .local_chain() + .genesis_hash() + .to_byte_array() + .to_vec() + } + + pub fn recent_blocks(self: Arc, count: u32) -> Vec { + let tip = self.inner.lock().unwrap().latest_checkpoint(); + tip.into_iter() + .take(count as _) + .map(|cp| BlockId { + height: cp.height(), + hash: cp.hash().to_byte_array().to_vec(), + }) + .collect() + } + + pub fn apply_block( + self: Arc, + height: u32, + block: Arc, + ) -> Result<(), ApplyBlockError> { + let mut wallet = self.inner.lock().unwrap(); + eprintln!("lib.rs: Got wallet"); + + let tip = wallet.latest_checkpoint(); + eprintln!("lib.rs: Got tip {:?}", tip.block_id()); + eprintln!( + "lib.rs: Found genesis from tip: {:?}", + tip.clone().into_iter().last().unwrap().block_id() + ); + if tip.height() == 0 { + wallet + .apply_block_connected_to(&block.0, height, tip.block_id()) + .map_err(|err| match err { + bdk::chain::local_chain::ApplyHeaderError::InconsistentBlocks => { + eprintln!("lib.rs: Inconsistent block!"); + unreachable!("cannot happen") + } + bdk::chain::local_chain::ApplyHeaderError::CannotConnect(err) => { + ApplyBlockError::CannotConnect(err) + } + })?; + } else { + eprintln!("lib.rs: Cloning out of block..."); + let block = block.0.clone(); + eprintln!("lib.rs: Actually applying..."); + wallet.apply_block(&block, height).map_err(|err| { + eprintln!("lib.rs: Failed to apply block: {}", err); + ApplyBlockError::CannotConnect(err) + })?; + } + + eprintln!("lib.rs: Commiting to db!"); + wallet.commit().map_err(ApplyBlockError::Database)?; + Ok(()) + } +} + +pub struct Block(bitcoin::Block); + +impl Block { + pub fn new(b: &[u8]) -> Self { + let mut reader = b; + Block( + bitcoin::Block::consensus_decode_from_finite_reader(&mut reader) + .expect("must decode block"), + ) + } + + pub fn hash(&self) -> Vec { + self.0.block_hash().as_byte_array().to_vec() + } +} + +pub struct BlockId { + pub height: u32, + pub hash: Vec, +} diff --git a/bdkwallet/log.go b/bdkwallet/log.go new file mode 100644 index 00000000..32ac4554 --- /dev/null +++ b/bdkwallet/log.go @@ -0,0 +1,24 @@ +package bdkwallet + +import "github.com/btcsuite/btclog" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + DisableLog() +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + log = btclog.Disabled +} + +// UseLogger uses a specified Logger to output package logging info. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/bdkwallet/manager.go b/bdkwallet/manager.go new file mode 100644 index 00000000..f7066509 --- /dev/null +++ b/bdkwallet/manager.go @@ -0,0 +1,80 @@ +package bdkwallet + +import ( + "os" + "path/filepath" + + "github.com/utreexo/utreexod/blockchain" + "github.com/utreexo/utreexod/btcutil" + "github.com/utreexo/utreexod/chaincfg" + "github.com/utreexo/utreexod/mempool" +) + +var defaultWalletPath = "bdkwallet" +var defaultWalletFileName = "default.dat" + +// ManagerConfig is a configuration struct used to +type ManagerConfig struct { + Chain *blockchain.BlockChain + TxMemPool *mempool.TxPool + ChainParams *chaincfg.Params + DataDir string +} + +type Manager struct { + config ManagerConfig + wallet Wallet // wallet does not need a mutex as it's done in Rust +} + +func NewManager(config ManagerConfig) (*Manager, error) { + log.Debug("Creating new BDK Wallet Manager!") + walletDir := filepath.Join(config.DataDir, defaultWalletPath) + if _, err := os.Stat(walletDir); err != nil { + if !os.IsNotExist(err) { + return nil, err + } + os.MkdirAll(walletDir, os.ModePerm) + } + + dbPath := filepath.Join(walletDir, defaultWalletFileName) + var wallet *Wallet + if _, err := os.Stat(dbPath); err != nil { + if !os.IsNotExist(err) { + return nil, err + } + if wallet, err = Create(dbPath, config.ChainParams); err != nil { + return nil, err + } + } else { + if wallet, err = Load(dbPath); err != nil { + return nil, err + } + } + + m := Manager{ + config: config, + wallet: *wallet, + } + if config.Chain != nil { + // Subscribe to new blocks/reorged blocks. + config.Chain.Subscribe(m.handleBlockchainNotification) + } + + return &m, nil +} + +func (m *Manager) handleBlockchainNotification(notification *blockchain.Notification) { + switch notification.Type { + // A block has been accepted into the block chain. + case blockchain.NTBlockConnected: + block, ok := notification.Data.(*btcutil.Block) + if !ok { + log.Warnf("Chain connected notification is not a block.") + break + } + err := m.wallet.ApplyBlock(block) + if err != nil { + log.Warnf("Couldn't apply block to the wallet. %v", err) + } + } +} diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go new file mode 100644 index 00000000..490128ac --- /dev/null +++ b/bdkwallet/wallet.go @@ -0,0 +1,158 @@ +package bdkwallet + +//#cgo LDFLAGS: ./target/release/libbdkgo.a -ldl -lm +import "C" + +import ( + "bytes" + + "github.com/utreexo/utreexod/bdkwallet/bdkgo" + "github.com/utreexo/utreexod/btcutil" + "github.com/utreexo/utreexod/chaincfg" + "github.com/utreexo/utreexod/chaincfg/chainhash" + "github.com/utreexo/utreexod/wire" +) + +// Balance in satoshis. +type Balance struct { + bdkgo.Balance +} + +// TrustedSpendable are funds that are safe to spend. +func (b *Balance) TrustedSpendable() uint64 { + return b.Confirmed + b.TrustedPending +} + +// Total is the total funds of the wallet. +func (b *Balance) Total() uint64 { + return b.Immature + b.TrustedPending + b.UntrustedPending + b.Confirmed +} + +type BlockId struct { + bdkgo.BlockId +} + +func (id *BlockId) Height() uint32 { + return id.BlockId.Height +} + +func (id *BlockId) Hash() chainhash.Hash { + return *(*[32]byte)(id.BlockId.Hash) +} + +// Wallet is a BDK wallet. +type Wallet struct { + inner bdkgo.Wallet +} + +// Create creates a fresh new wallet. +func Create(dbPath string, chainParams *chaincfg.Params) (*Wallet, error) { + // used for the address format + // this is parsed as `bitcoin::Network` in rust + // supported strings: bitcoin, testnet, signet, regtest + // https://docs.rs/bitcoin/latest/bitcoin/network/enum.Network.html + network := chainParams.Name + log.Infof("Creating wallet with network: %v", network) + switch network { + case "mainnet": + network = "bitcoin" + case "testnet3": + network = "testnet" + } + + genesisHash := chainParams.GenesisHash.CloneBytes() + + inner, err := bdkgo.WalletCreateNew(dbPath, network, genesisHash) + if err != nil { + return nil, err + } + return &Wallet{*inner}, nil +} + +// Load loads an existing wallet from file. +func Load(dbPath string) (*Wallet, error) { + inner, err := bdkgo.WalletLoad(dbPath) + if err != nil { + return nil, err + } + return &Wallet{*inner}, nil +} + +// UnusedAddress returns the earliest address which have not received any funds. +func (w *Wallet) UnusedAddress() (uint32, btcutil.Address, error) { + info, err := w.inner.LastUnusedAddress() + if err != nil { + return info.Index, nil, err + } + addr, err := btcutil.DecodeAddress(info.Address, nil) + if err != nil { + return info.Index, nil, err + } + return info.Index, addr, nil +} + +// FreshAddress always returns a new address. This means it always increments +// the last derivation index even though the previous derivation indexes have +// not received funds. +func (w *Wallet) FreshAddress() (uint32, btcutil.Address, error) { + info, err := w.inner.FreshAddress() + if err != nil { + return info.Index, nil, err + } + addr, err := btcutil.DecodeAddress(info.Address, nil) + if err != nil { + return info.Index, nil, err + } + return info.Index, addr, nil +} + +// PeekAddress previews the address at the derivation index. This does not +// increment the last revealed index. +func (w *Wallet) PeekAddress(index uint32) (uint32, btcutil.Address, error) { + info, err := w.inner.PeekAddress(index) + if err != nil { + return info.Index, nil, err + } + addr, err := btcutil.DecodeAddress(info.Address, nil) + if err != nil { + return info.Index, nil, err + } + return info.Index, addr, nil +} + +// Balance returns the balance of the wallet. +func (w *Wallet) Balance() Balance { + return Balance{w.inner.Balance()} +} + +// RecentBlocks returns the most recent blocks +func (w *Wallet) RecentBlocks(count uint32) []BlockId { + generatedCodeBlocks := w.inner.RecentBlocks(count) + out := make([]BlockId, 0, len(generatedCodeBlocks)) + for _, block := range generatedCodeBlocks { + out = append(out, BlockId{block}) + } + return out +} + +// ApplyBlock updates the wallet with the given block. +func (w *Wallet) ApplyBlock(block *btcutil.Block) error { + log.Infof("Got block [%v:%v]", block.Height(), block.Hash()) + var b bytes.Buffer + if err := block.MsgBlock().BtcEncode(&b, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { + return err + } + + bb := bdkgo.NewBlock(b.Bytes()[:b.Len()]) + bheight := uint32(block.Height()) + bhash, err := chainhash.NewHash(bb.Hash()) + if err != nil { + return err + } + log.Infof("Applying [%v:%v] from %v bytes", bheight, bhash, b.Len()) + err = w.inner.ApplyBlock(bheight, bb) + log.Info("Applied block!") + bb = nil + log.Info("Block deleted?") + return err +} diff --git a/bdkwallet/wallet_test.go b/bdkwallet/wallet_test.go new file mode 100644 index 00000000..fb2cae83 --- /dev/null +++ b/bdkwallet/wallet_test.go @@ -0,0 +1,67 @@ +package bdkwallet + +import ( + "path/filepath" + "testing" + + "github.com/utreexo/utreexod/chaincfg" +) + +func TestCreateAndLoad(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "bdk.db") + + { + wallet, err := Create(dbPath, &chaincfg.MainNetParams) + if err != nil { + t.Fatalf("failed to create db: %v", err) + } + + freshI, freshAddr, err := wallet.FreshAddress() + if err != nil { + t.Fatalf("failed to get fresh address: %v", err) + } + if freshI != 0 { + t.Fatal("fresh address of new wallet should be 0") + } + t.Logf("address at index 0: %v", freshAddr) + + unusedI, unusedAddr, err := wallet.UnusedAddress() + if err != nil { + t.Fatalf("failed to get unused address: %v", err) + } + if unusedI != freshI { + t.Fatalf("the fresh address is not used so unused index should be 0") + } + if unusedAddr.String() != freshAddr.String() { + t.Fatalf("unused addr should be the same as fresh addr: unused=%v", unusedAddr) + } + + // derive 4 more addresses (total 5 addresses) + for i := 1; i < 5; i++ { + addrI, _, err := wallet.FreshAddress() + if err != nil { + t.Fatalf("failed to derive addr %v: %v", i, err) + } + if i != int(addrI) { + t.Fatalf("derived addr index is unexpected") + } + } + } + + // load wallet and see what happens + { + wallet, err := Load(dbPath) + if err != nil { + t.Fatalf("failed to load wallet: %v", err) + } + + addrI, _, err := wallet.FreshAddress() + if err != nil { + t.Fatalf("failed to get fresh addr: %v", err) + } + + if addrI != 5 { + t.Fatalf("this should be 5: %v", addrI) + } + } +} diff --git a/buildbdkgo.sh b/buildbdkgo.sh new file mode 100755 index 00000000..bb4423c7 --- /dev/null +++ b/buildbdkgo.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo build --release && uniffi-bindgen-go -o bdkwallet bdkwallet/bdkgo_crate/src/bdkgo.udl diff --git a/log.go b/log.go index 95349560..02f006a3 100644 --- a/log.go +++ b/log.go @@ -11,6 +11,7 @@ import ( "path/filepath" "github.com/utreexo/utreexod/addrmgr" + "github.com/utreexo/utreexod/bdkwallet" "github.com/utreexo/utreexod/blockchain" "github.com/utreexo/utreexod/blockchain/indexers" "github.com/utreexo/utreexod/connmgr" @@ -73,6 +74,7 @@ var ( txmpLog = backendLog.Logger("TXMP") wlltLog = backendLog.Logger("WLLT") elecLog = backendLog.Logger("ELEC") + bdkwLog = backendLog.Logger("BDKW") ) // Initialize package-global logger variables. @@ -90,6 +92,7 @@ func init() { mempool.UseLogger(txmpLog) wallet.UseLogger(wlltLog) electrum.UseLogger(elecLog) + bdkwallet.UseLogger(bdkwLog) } // subsystemLoggers maps each subsystem identifier to its associated logger. @@ -111,6 +114,7 @@ var subsystemLoggers = map[string]btclog.Logger{ "TXMP": txmpLog, "WLLT": wlltLog, "ELEC": elecLog, + "BDKW": bdkwLog, } // initLogRotator initializes the logging rotater to write logs to logFile and diff --git a/server.go b/server.go index f18da840..6a0079e3 100644 --- a/server.go +++ b/server.go @@ -23,6 +23,7 @@ import ( "time" "github.com/utreexo/utreexod/addrmgr" + "github.com/utreexo/utreexod/bdkwallet" "github.com/utreexo/utreexod/blockchain" "github.com/utreexo/utreexod/blockchain/indexers" "github.com/utreexo/utreexod/btcutil" @@ -267,6 +268,9 @@ type server struct { // the database and the watch only wallet and serves them to the connected client. electrumServer *electrum.ElectrumServer + // bdkWallet keeps track of a wallet + bdkWallet *bdkwallet.Manager + // cfCheckptCaches stores a cached slice of filter headers for cfcheckpt // messages for each filter type. cfCheckptCaches map[wire.FilterType][]cfHeaderKV @@ -3533,6 +3537,18 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, } } + // auto create bdk wallet manager + // TODO: We should let the user disable this! + s.bdkWallet, err = bdkwallet.NewManager(bdkwallet.ManagerConfig{ + Chain: s.chain, + TxMemPool: s.txMemPool, + ChainParams: chainParams, + DataDir: cfg.DataDir, + }) + if err != nil { + return nil, err + } + if !cfg.DisableRPC { // Setup listeners for the configured RPC listen addresses and // TLS settings. From b6c849366f54f6b0a7feb718d286aeccaee4b5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 25 Jan 2024 14:36:08 +0900 Subject: [PATCH 03/48] main: enfore rust toolchain version --- rust-toolchain.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 rust-toolchain.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..b023ced5 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.72.0" +components = ["clippy", "rustfmt"] From bf84ba8fe699eef5d84a7cded0e349e67b36984b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 25 Jan 2024 18:25:31 +0900 Subject: [PATCH 04/48] bdkwallet: workaround for golang uniffi bug The generated golang code was being too aggressive with destroying uniffi objects. --- bdkwallet/bdkgo/bdkgo.go | 106 ++++++---------------------- bdkwallet/bdkgo/bdkgo.h | 34 +++------ bdkwallet/bdkgo_crate/Cargo.toml | 6 +- bdkwallet/bdkgo_crate/src/bdkgo.udl | 9 +-- bdkwallet/bdkgo_crate/src/lib.rs | 97 ++++++++++++------------- bdkwallet/manager.go | 6 +- bdkwallet/wallet.go | 23 +++--- 7 files changed, 100 insertions(+), 181 deletions(-) diff --git a/bdkwallet/bdkgo/bdkgo.go b/bdkwallet/bdkgo/bdkgo.go index 5d55023b..ef9afbd6 100644 --- a/bdkwallet/bdkgo/bdkgo.go +++ b/bdkwallet/bdkgo/bdkgo.go @@ -344,20 +344,11 @@ func uniffiCheckChecksums() { // If this happens try cleaning and rebuilding your project panic("bdkgo: UniFFI contract version mismatch") } - { - checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_bdkgo_checksum_method_block_hash(uniffiStatus) - }) - if checksum != 33317 { - // If this happens try cleaning and rebuilding your project - panic("bdkgo: uniffi_bdkgo_checksum_method_block_hash: UniFFI API checksum mismatch") - } - } { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_method_wallet_apply_block(uniffiStatus) }) - if checksum != 60553 { + if checksum != 20455 { // If this happens try cleaning and rebuilding your project panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_apply_block: UniFFI API checksum mismatch") } @@ -389,6 +380,15 @@ func uniffiCheckChecksums() { panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_genesis_hash: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_increment_reference_counter(uniffiStatus) + }) + if checksum != 61284 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_increment_reference_counter: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_method_wallet_last_unused_address(uniffiStatus) @@ -416,15 +416,6 @@ func uniffiCheckChecksums() { panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_recent_blocks: UniFFI API checksum mismatch") } } - { - checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_bdkgo_checksum_constructor_block_new(uniffiStatus) - }) - if checksum != 33914 { - // If this happens try cleaning and rebuilding your project - panic("bdkgo: uniffi_bdkgo_checksum_constructor_block_new: UniFFI API checksum mismatch") - } - } { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_constructor_wallet_create_new(uniffiStatus) @@ -642,69 +633,6 @@ func (ffiObject *FfiObject) freeRustArcPtr() { }) } -type Block struct { - ffiObject FfiObject -} - -func NewBlock(b []byte) *Block { - return FfiConverterBlockINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_bdkgo_fn_constructor_block_new(FfiConverterBytesINSTANCE.Lower(b), _uniffiStatus) - })) -} - -func (_self *Block) Hash() []byte { - _pointer := _self.ffiObject.incrementPointer("*Block") - defer _self.ffiObject.decrementPointer() - return FfiConverterBytesINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return C.uniffi_bdkgo_fn_method_block_hash( - _pointer, _uniffiStatus) - })) -} - -func (object *Block) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterBlock struct{} - -var FfiConverterBlockINSTANCE = FfiConverterBlock{} - -func (c FfiConverterBlock) Lift(pointer unsafe.Pointer) *Block { - result := &Block{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_bdkgo_fn_free_block(pointer, status) - }), - } - runtime.SetFinalizer(result, (*Block).Destroy) - return result -} - -func (c FfiConverterBlock) Read(reader io.Reader) *Block { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) -} - -func (c FfiConverterBlock) Lower(value *Block) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*Block") - defer value.ffiObject.decrementPointer() - return pointer -} - -func (c FfiConverterBlock) Write(writer io.Writer, value *Block) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) -} - -type FfiDestroyerBlock struct{} - -func (_ FfiDestroyerBlock) Destroy(value *Block) { - value.Destroy() -} - type Wallet struct { ffiObject FfiObject } @@ -732,12 +660,12 @@ func WalletLoad(dbPath string) (*Wallet, error) { } } -func (_self *Wallet) ApplyBlock(height uint32, blockBytes *Block) error { +func (_self *Wallet) ApplyBlock(height uint32, blockBytes []byte) error { _pointer := _self.ffiObject.incrementPointer("*Wallet") defer _self.ffiObject.decrementPointer() _, _uniffiErr := rustCallWithError(FfiConverterTypeApplyBlockError{}, func(_uniffiStatus *C.RustCallStatus) bool { C.uniffi_bdkgo_fn_method_wallet_apply_block( - _pointer, FfiConverterUint32INSTANCE.Lower(height), FfiConverterBlockINSTANCE.Lower(blockBytes), _uniffiStatus) + _pointer, FfiConverterUint32INSTANCE.Lower(height), FfiConverterBytesINSTANCE.Lower(blockBytes), _uniffiStatus) return false }) return _uniffiErr @@ -776,6 +704,16 @@ func (_self *Wallet) GenesisHash() []byte { })) } +func (_self *Wallet) IncrementReferenceCounter() { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + rustCall(func(_uniffiStatus *C.RustCallStatus) bool { + C.uniffi_bdkgo_fn_method_wallet_increment_reference_counter( + _pointer, _uniffiStatus) + return false + }) +} + func (_self *Wallet) LastUnusedAddress() (AddressInfo, error) { _pointer := _self.ffiObject.incrementPointer("*Wallet") defer _self.ffiObject.decrementPointer() diff --git a/bdkwallet/bdkgo/bdkgo.h b/bdkwallet/bdkgo/bdkgo.h index c36d5a6b..e4d44aee 100644 --- a/bdkwallet/bdkgo/bdkgo.h +++ b/bdkwallet/bdkgo/bdkgo.h @@ -68,21 +68,6 @@ int8_t uniffiForeignExecutorCallbackbdkgo(uint64_t, uint32_t, RustTaskCallback, void uniffiFutureContinuationCallbackbdkgo(void*, int8_t); -void uniffi_bdkgo_fn_free_block( - void* ptr, - RustCallStatus* out_status -); - -void* uniffi_bdkgo_fn_constructor_block_new( - RustBuffer b, - RustCallStatus* out_status -); - -RustBuffer uniffi_bdkgo_fn_method_block_hash( - void* ptr, - RustCallStatus* out_status -); - void uniffi_bdkgo_fn_free_wallet( void* ptr, RustCallStatus* out_status @@ -103,7 +88,7 @@ void* uniffi_bdkgo_fn_constructor_wallet_load( void uniffi_bdkgo_fn_method_wallet_apply_block( void* ptr, uint32_t height, - void* block_bytes, + RustBuffer block_bytes, RustCallStatus* out_status ); @@ -122,6 +107,11 @@ RustBuffer uniffi_bdkgo_fn_method_wallet_genesis_hash( RustCallStatus* out_status ); +void uniffi_bdkgo_fn_method_wallet_increment_reference_counter( + void* ptr, + RustCallStatus* out_status +); + RustBuffer uniffi_bdkgo_fn_method_wallet_last_unused_address( void* ptr, RustCallStatus* out_status @@ -438,10 +428,6 @@ void ffi_bdkgo_rust_future_complete_void( RustCallStatus* out_status ); -uint16_t uniffi_bdkgo_checksum_method_block_hash( - RustCallStatus* out_status -); - uint16_t uniffi_bdkgo_checksum_method_wallet_apply_block( RustCallStatus* out_status ); @@ -458,19 +444,19 @@ uint16_t uniffi_bdkgo_checksum_method_wallet_genesis_hash( RustCallStatus* out_status ); -uint16_t uniffi_bdkgo_checksum_method_wallet_last_unused_address( +uint16_t uniffi_bdkgo_checksum_method_wallet_increment_reference_counter( RustCallStatus* out_status ); -uint16_t uniffi_bdkgo_checksum_method_wallet_peek_address( +uint16_t uniffi_bdkgo_checksum_method_wallet_last_unused_address( RustCallStatus* out_status ); -uint16_t uniffi_bdkgo_checksum_method_wallet_recent_blocks( +uint16_t uniffi_bdkgo_checksum_method_wallet_peek_address( RustCallStatus* out_status ); -uint16_t uniffi_bdkgo_checksum_constructor_block_new( +uint16_t uniffi_bdkgo_checksum_method_wallet_recent_blocks( RustCallStatus* out_status ); diff --git a/bdkwallet/bdkgo_crate/Cargo.toml b/bdkwallet/bdkgo_crate/Cargo.toml index e685ce47..ffe9c1e8 100644 --- a/bdkwallet/bdkgo_crate/Cargo.toml +++ b/bdkwallet/bdkgo_crate/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = ["staticlib", "cdylib", "lib"] +crate-type = ["staticlib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -14,8 +14,8 @@ weedle2 = "=4.0.0" thiserror = "1.0" bincode = "1" serde = { version = "1", features = ["derive"] } -bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "fbd1d656187859fcb7b84b177e7e9498c15537ea", features = ["keys-bip39"] } -bdk_file_store = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "fbd1d656187859fcb7b84b177e7e9498c15537ea" } +bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "07116df54156315b4f1fc67647a9b8118e464d43", features = ["keys-bip39"] } +bdk_file_store = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "07116df54156315b4f1fc67647a9b8118e464d43" } rand = "0.8.5" [build-dependencies] diff --git a/bdkwallet/bdkgo_crate/src/bdkgo.udl b/bdkwallet/bdkgo_crate/src/bdkgo.udl index fe2fe989..2eff940f 100644 --- a/bdkwallet/bdkgo_crate/src/bdkgo.udl +++ b/bdkwallet/bdkgo_crate/src/bdkgo.udl @@ -47,6 +47,8 @@ interface Wallet { [Name=load, Throws=LoadError] constructor(string db_path); + void increment_reference_counter(); + [Throws=DatabaseError] AddressInfo last_unused_address(); @@ -63,12 +65,7 @@ interface Wallet { sequence recent_blocks(u32 count); [Throws=ApplyBlockError] - void apply_block(u32 height, Block block_bytes); -}; - -interface Block { - constructor([ByRef] bytes b); - bytes hash(); + void apply_block(u32 height, [ByRef] bytes block_bytes); }; dictionary BlockId { diff --git a/bdkwallet/bdkgo_crate/src/lib.rs b/bdkwallet/bdkgo_crate/src/lib.rs index 15b7aad0..7b13907f 100644 --- a/bdkwallet/bdkgo_crate/src/lib.rs +++ b/bdkwallet/bdkgo_crate/src/lib.rs @@ -17,6 +17,7 @@ use bdk::{ }; use bincode::Options; use rand::RngCore; +use uniffi::deps::bytes::Buf; uniffi::include_scaffolding!("bdkgo"); @@ -138,11 +139,28 @@ impl WalletHeader { } } +type WalletInner = Mutex; + pub struct Wallet { - inner: Mutex, + inner: WalletInner, } impl Wallet { + /// Increments the `Arc` pointer exposed via uniffi. + /// + /// This is due to a bug with golang uniffi where decrementing this counter is too aggressive. + /// The caveat of this is that `Wallet` will never be destroyed. This is an okay sacrifice as + /// typically you want to keep the wallet for the lifetime of the node. + pub fn increment_reference_counter(self: &Arc) { + + let count = Arc::strong_count(self); + eprintln!("The pointer reference count is: {}", count); + + unsafe { + Arc::increment_strong_count(Arc::into_raw(Arc::clone(self))); + } + } + pub fn create_new( db_path: String, network: String, @@ -153,13 +171,9 @@ impl Wallet { BlockHash::from_slice(&genesis_hash).map_err(CreateNewError::ParseGenesisHash)?; let mut db_header = WalletHeader::new(network); - let db_header_bytes = Box::new(db_header.encode()).leak(); - - let db = match bdk_file_store::Store::create_new(db_header_bytes, &db_path) { - Ok(db) => db, - Err(err) => return Err(CreateNewError::Database(err)), - }; - + let db_header_bytes = db_header.encode(); + let db = + bdk_file_store::Store::create_new(&db_header_bytes, &db_path).expect("must create db"); let bdk_wallet = match bdk::Wallet::new_with_genesis_hash( db_header.descriptor(KeychainKind::External), Some(db_header.descriptor(KeychainKind::Internal)), @@ -174,18 +188,15 @@ impl Wallet { } }; - Ok(Self { - inner: Mutex::new(bdk_wallet), - }) + let inner = Mutex::new(bdk_wallet); + Ok(Self { inner }) } pub fn load(db_path: String) -> Result { let file = std::fs::File::open(&db_path) .map_err(|err| LoadError::Database(bdk_file_store::FileError::Io(err)))?; let (db_header, db_header_bytes) = WalletHeader::decode(file)?; - let db_header_bytes = Box::new(db_header_bytes).leak(); - let db = - bdk_file_store::Store::open(db_header_bytes, db_path).map_err(LoadError::Database)?; + let db = bdk_file_store::Store::open(&db_header_bytes, db_path).expect("must load db"); let bdk_wallet = bdk::Wallet::load( db_header.descriptor(KeychainKind::External), Some(db_header.descriptor(KeychainKind::Internal)), @@ -193,12 +204,12 @@ impl Wallet { ) .map_err(LoadError::Wallet)?; - Ok(Self { - inner: Mutex::new(bdk_wallet), - }) + let inner = Mutex::new(bdk_wallet); + Ok(Self { inner }) } fn address(self: Arc, index: AddressIndex) -> Result { + self.increment_reference_counter(); let mut wallet = self.inner.lock().unwrap(); let address_info = wallet .try_get_address(index) @@ -210,23 +221,28 @@ impl Wallet { } pub fn last_unused_address(self: Arc) -> Result { + self.increment_reference_counter(); self.address(AddressIndex::LastUnused) } pub fn fresh_address(self: Arc) -> Result { + self.increment_reference_counter(); self.address(AddressIndex::New) } pub fn peek_address(self: Arc, index: u32) -> Result { + self.increment_reference_counter(); self.address(AddressIndex::Peek(index)) } pub fn balance(self: Arc) -> bdk::wallet::Balance { + self.increment_reference_counter(); let wallet = self.inner.lock().unwrap(); wallet.get_balance() } pub fn genesis_hash(self: Arc) -> Vec { + self.increment_reference_counter(); self.inner .lock() .unwrap() @@ -237,6 +253,7 @@ impl Wallet { } pub fn recent_blocks(self: Arc, count: u32) -> Vec { + self.increment_reference_counter(); let tip = self.inner.lock().unwrap().latest_checkpoint(); tip.into_iter() .take(count as _) @@ -250,23 +267,23 @@ impl Wallet { pub fn apply_block( self: Arc, height: u32, - block: Arc, + block_bytes: &[u8], ) -> Result<(), ApplyBlockError> { + self.increment_reference_counter(); + let mut wallet = self.inner.lock().unwrap(); - eprintln!("lib.rs: Got wallet"); + + let mut reader = block_bytes.reader(); + let block = bitcoin::Block::consensus_decode_from_finite_reader(&mut reader) + .map_err(ApplyBlockError::DecodeBlock)?; let tip = wallet.latest_checkpoint(); - eprintln!("lib.rs: Got tip {:?}", tip.block_id()); - eprintln!( - "lib.rs: Found genesis from tip: {:?}", - tip.clone().into_iter().last().unwrap().block_id() - ); + if tip.height() == 0 { wallet - .apply_block_connected_to(&block.0, height, tip.block_id()) + .apply_block_connected_to(&block, height, tip.block_id()) .map_err(|err| match err { bdk::chain::local_chain::ApplyHeaderError::InconsistentBlocks => { - eprintln!("lib.rs: Inconsistent block!"); unreachable!("cannot happen") } bdk::chain::local_chain::ApplyHeaderError::CannotConnect(err) => { @@ -274,35 +291,15 @@ impl Wallet { } })?; } else { - eprintln!("lib.rs: Cloning out of block..."); - let block = block.0.clone(); - eprintln!("lib.rs: Actually applying..."); - wallet.apply_block(&block, height).map_err(|err| { - eprintln!("lib.rs: Failed to apply block: {}", err); - ApplyBlockError::CannotConnect(err) - })?; + wallet + .apply_block(&block, height) + .map_err(|err| ApplyBlockError::CannotConnect(err))?; } - - eprintln!("lib.rs: Commiting to db!"); wallet.commit().map_err(ApplyBlockError::Database)?; Ok(()) } -} -pub struct Block(bitcoin::Block); - -impl Block { - pub fn new(b: &[u8]) -> Self { - let mut reader = b; - Block( - bitcoin::Block::consensus_decode_from_finite_reader(&mut reader) - .expect("must decode block"), - ) - } - - pub fn hash(&self) -> Vec { - self.0.block_hash().as_byte_array().to_vec() - } + //pub fn create_tx(self: Arc, recipients: Vec<>) } pub struct BlockId { diff --git a/bdkwallet/manager.go b/bdkwallet/manager.go index f7066509..b4c9b969 100644 --- a/bdkwallet/manager.go +++ b/bdkwallet/manager.go @@ -27,7 +27,7 @@ type Manager struct { } func NewManager(config ManagerConfig) (*Manager, error) { - log.Debug("Creating new BDK Wallet Manager!") + log.Info("Starting the BDK wallet manager.") walletDir := filepath.Join(config.DataDir, defaultWalletPath) if _, err := os.Stat(walletDir); err != nil { if !os.IsNotExist(err) { @@ -70,11 +70,11 @@ func (m *Manager) handleBlockchainNotification(notification *blockchain.Notifica block, ok := notification.Data.(*btcutil.Block) if !ok { log.Warnf("Chain connected notification is not a block.") - break + return } err := m.wallet.ApplyBlock(block) if err != nil { - log.Warnf("Couldn't apply block to the wallet. %v", err) + log.Criticalf("Couldn't apply block to the wallet. %v", err) } } } diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go index 490128ac..a346386b 100644 --- a/bdkwallet/wallet.go +++ b/bdkwallet/wallet.go @@ -66,6 +66,11 @@ func Create(dbPath string, chainParams *chaincfg.Params) (*Wallet, error) { if err != nil { return nil, err } + + // This increments the reference count of the Arc pointer in rust. We are + // doing this due to a bug with uniffi-bindgen-go's generated code + // decrementing this count too aggressively. + inner.IncrementReferenceCounter() return &Wallet{*inner}, nil } @@ -75,6 +80,11 @@ func Load(dbPath string) (*Wallet, error) { if err != nil { return nil, err } + + // This increments the reference count of the Arc pointer in rust. We are + // doing this due to a bug with uniffi-bindgen-go's generated code + // decrementing this count too aggressively. + inner.IncrementReferenceCounter() return &Wallet{*inner}, nil } @@ -137,22 +147,13 @@ func (w *Wallet) RecentBlocks(count uint32) []BlockId { // ApplyBlock updates the wallet with the given block. func (w *Wallet) ApplyBlock(block *btcutil.Block) error { - log.Infof("Got block [%v:%v]", block.Height(), block.Hash()) var b bytes.Buffer if err := block.MsgBlock().BtcEncode(&b, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { return err } - bb := bdkgo.NewBlock(b.Bytes()[:b.Len()]) bheight := uint32(block.Height()) - bhash, err := chainhash.NewHash(bb.Hash()) - if err != nil { - return err - } - log.Infof("Applying [%v:%v] from %v bytes", bheight, bhash, b.Len()) - err = w.inner.ApplyBlock(bheight, bb) - log.Info("Applied block!") - bb = nil - log.Info("Block deleted?") + err := w.inner.ApplyBlock(bheight, b.Bytes()) + log.Infof("Applied block [%v:%v]", block.Height(), block.Hash()) return err } From e6462ab57dc8cdee07f18b494b7e485c03e87421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 25 Jan 2024 18:26:19 +0900 Subject: [PATCH 05/48] main: add install cmd to rust build script --- buildbdkgo.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/buildbdkgo.sh b/buildbdkgo.sh index bb4423c7..51b81c6e 100755 --- a/buildbdkgo.sh +++ b/buildbdkgo.sh @@ -1,3 +1,5 @@ #!/bin/bash -cargo build --release && uniffi-bindgen-go -o bdkwallet bdkwallet/bdkgo_crate/src/bdkgo.udl +cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.2.0+v0.25.0 && \ +cargo build --release && \ +uniffi-bindgen-go -o bdkwallet bdkwallet/bdkgo_crate/src/bdkgo.udl From 4d9198fab8a224a11caa59290ad3b2723331eb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 26 Jan 2024 16:20:07 +0900 Subject: [PATCH 06/48] main: Add signet folder to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9e9a397d..18be0c3d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ utreexod # Cargo target/ Cargo.lock + +# Running stuff +/signet From c641c3f3e18d9d7da4daeec3ce79b981230e08ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sat, 27 Jan 2024 02:58:36 +0900 Subject: [PATCH 07/48] bdkwallet: apply mempool, list txs & utxos --- bdkwallet/bdkgo/bdkgo.go | 688 ++++++++++++++++++++++++++++ bdkwallet/bdkgo/bdkgo.h | 48 ++ bdkwallet/bdkgo_crate/src/bdkgo.udl | 45 ++ bdkwallet/bdkgo_crate/src/lib.rs | 202 +++++++- bdkwallet/manager.go | 6 + bdkwallet/wallet.go | 79 ++++ server.go | 4 + 7 files changed, 1047 insertions(+), 25 deletions(-) diff --git a/bdkwallet/bdkgo/bdkgo.go b/bdkwallet/bdkgo/bdkgo.go index ef9afbd6..ba5d2bb8 100644 --- a/bdkwallet/bdkgo/bdkgo.go +++ b/bdkwallet/bdkgo/bdkgo.go @@ -353,6 +353,15 @@ func uniffiCheckChecksums() { panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_apply_block: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_apply_mempool(uniffiStatus) + }) + if checksum != 23416 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_apply_mempool: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_method_wallet_balance(uniffiStatus) @@ -362,6 +371,15 @@ func uniffiCheckChecksums() { panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_balance: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_create_tx(uniffiStatus) + }) + if checksum != 54855 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_create_tx: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_method_wallet_fresh_address(uniffiStatus) @@ -398,6 +416,15 @@ func uniffiCheckChecksums() { panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_last_unused_address: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_mnemonic_words(uniffiStatus) + }) + if checksum != 3138 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_mnemonic_words: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_method_wallet_peek_address(uniffiStatus) @@ -416,6 +443,24 @@ func uniffiCheckChecksums() { panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_recent_blocks: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_transactions(uniffiStatus) + }) + if checksum != 39598 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_transactions: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_bdkgo_checksum_method_wallet_utxos(uniffiStatus) + }) + if checksum != 53540 { + // If this happens try cleaning and rebuilding your project + panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_utxos: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_constructor_wallet_create_new(uniffiStatus) @@ -484,6 +529,61 @@ type FfiDestroyerUint64 struct{} func (FfiDestroyerUint64) Destroy(_ uint64) {} +type FfiConverterFloat32 struct{} + +var FfiConverterFloat32INSTANCE = FfiConverterFloat32{} + +func (FfiConverterFloat32) Lower(value float32) C.float { + return C.float(value) +} + +func (FfiConverterFloat32) Write(writer io.Writer, value float32) { + writeFloat32(writer, value) +} + +func (FfiConverterFloat32) Lift(value C.float) float32 { + return float32(value) +} + +func (FfiConverterFloat32) Read(reader io.Reader) float32 { + return readFloat32(reader) +} + +type FfiDestroyerFloat32 struct{} + +func (FfiDestroyerFloat32) Destroy(_ float32) {} + +type FfiConverterBool struct{} + +var FfiConverterBoolINSTANCE = FfiConverterBool{} + +func (FfiConverterBool) Lower(value bool) C.int8_t { + if value { + return C.int8_t(1) + } + return C.int8_t(0) +} + +func (FfiConverterBool) Write(writer io.Writer, value bool) { + if value { + writeInt8(writer, 1) + } else { + writeInt8(writer, 0) + } +} + +func (FfiConverterBool) Lift(value C.int8_t) bool { + return value != 0 +} + +func (FfiConverterBool) Read(reader io.Reader) bool { + return readInt8(reader) != 0 +} + +type FfiDestroyerBool struct{} + +func (FfiDestroyerBool) Destroy(_ bool) {} + type FfiConverterString struct{} var FfiConverterStringINSTANCE = FfiConverterString{} @@ -671,6 +771,16 @@ func (_self *Wallet) ApplyBlock(height uint32, blockBytes []byte) error { return _uniffiErr } +func (_self *Wallet) ApplyMempool(txs []MempoolTx) { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + rustCall(func(_uniffiStatus *C.RustCallStatus) bool { + C.uniffi_bdkgo_fn_method_wallet_apply_mempool( + _pointer, FfiConverterSequenceTypeMempoolTxINSTANCE.Lower(txs), _uniffiStatus) + return false + }) +} + func (_self *Wallet) Balance() Balance { _pointer := _self.ffiObject.incrementPointer("*Wallet") defer _self.ffiObject.decrementPointer() @@ -680,6 +790,21 @@ func (_self *Wallet) Balance() Balance { })) } +func (_self *Wallet) CreateTx(feerate float32, recipients []Recipient) ([]byte, error) { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + _uniffiRV, _uniffiErr := rustCallWithError(FfiConverterTypeCreateTxError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_create_tx( + _pointer, FfiConverterFloat32INSTANCE.Lower(feerate), FfiConverterSequenceTypeRecipientINSTANCE.Lower(recipients), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue []byte + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterBytesINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} + func (_self *Wallet) FreshAddress() (AddressInfo, error) { _pointer := _self.ffiObject.incrementPointer("*Wallet") defer _self.ffiObject.decrementPointer() @@ -729,6 +854,15 @@ func (_self *Wallet) LastUnusedAddress() (AddressInfo, error) { } } +func (_self *Wallet) MnemonicWords() []string { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + return FfiConverterSequenceStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_mnemonic_words( + _pointer, _uniffiStatus) + })) +} + func (_self *Wallet) PeekAddress(index uint32) (AddressInfo, error) { _pointer := _self.ffiObject.incrementPointer("*Wallet") defer _self.ffiObject.decrementPointer() @@ -753,6 +887,24 @@ func (_self *Wallet) RecentBlocks(count uint32) []BlockId { })) } +func (_self *Wallet) Transactions() []TxInfo { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + return FfiConverterSequenceTypeTxInfoINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_transactions( + _pointer, _uniffiStatus) + })) +} + +func (_self *Wallet) Utxos() []UtxoInfo { + _pointer := _self.ffiObject.incrementPointer("*Wallet") + defer _self.ffiObject.decrementPointer() + return FfiConverterSequenceTypeUtxoInfoINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_utxos( + _pointer, _uniffiStatus) + })) +} + func (object *Wallet) Destroy() { runtime.SetFinalizer(object, nil) object.ffiObject.destroy() @@ -925,6 +1077,198 @@ func (_ FfiDestroyerTypeBlockId) Destroy(value BlockId) { value.Destroy() } +type MempoolTx struct { + Tx []byte + AddedUnix uint64 +} + +func (r *MempoolTx) Destroy() { + FfiDestroyerBytes{}.Destroy(r.Tx) + FfiDestroyerUint64{}.Destroy(r.AddedUnix) +} + +type FfiConverterTypeMempoolTx struct{} + +var FfiConverterTypeMempoolTxINSTANCE = FfiConverterTypeMempoolTx{} + +func (c FfiConverterTypeMempoolTx) Lift(rb RustBufferI) MempoolTx { + return LiftFromRustBuffer[MempoolTx](c, rb) +} + +func (c FfiConverterTypeMempoolTx) Read(reader io.Reader) MempoolTx { + return MempoolTx{ + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeMempoolTx) Lower(value MempoolTx) RustBuffer { + return LowerIntoRustBuffer[MempoolTx](c, value) +} + +func (c FfiConverterTypeMempoolTx) Write(writer io.Writer, value MempoolTx) { + FfiConverterBytesINSTANCE.Write(writer, value.Tx) + FfiConverterUint64INSTANCE.Write(writer, value.AddedUnix) +} + +type FfiDestroyerTypeMempoolTx struct{} + +func (_ FfiDestroyerTypeMempoolTx) Destroy(value MempoolTx) { + value.Destroy() +} + +type Recipient struct { + ScriptPubkey []byte + Amount uint64 +} + +func (r *Recipient) Destroy() { + FfiDestroyerBytes{}.Destroy(r.ScriptPubkey) + FfiDestroyerUint64{}.Destroy(r.Amount) +} + +type FfiConverterTypeRecipient struct{} + +var FfiConverterTypeRecipientINSTANCE = FfiConverterTypeRecipient{} + +func (c FfiConverterTypeRecipient) Lift(rb RustBufferI) Recipient { + return LiftFromRustBuffer[Recipient](c, rb) +} + +func (c FfiConverterTypeRecipient) Read(reader io.Reader) Recipient { + return Recipient{ + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeRecipient) Lower(value Recipient) RustBuffer { + return LowerIntoRustBuffer[Recipient](c, value) +} + +func (c FfiConverterTypeRecipient) Write(writer io.Writer, value Recipient) { + FfiConverterBytesINSTANCE.Write(writer, value.ScriptPubkey) + FfiConverterUint64INSTANCE.Write(writer, value.Amount) +} + +type FfiDestroyerTypeRecipient struct{} + +func (_ FfiDestroyerTypeRecipient) Destroy(value Recipient) { + value.Destroy() +} + +type TxInfo struct { + Txid []byte + Tx []byte + Spent uint64 + Received uint64 + Confirmations *uint32 +} + +func (r *TxInfo) Destroy() { + FfiDestroyerBytes{}.Destroy(r.Txid) + FfiDestroyerBytes{}.Destroy(r.Tx) + FfiDestroyerUint64{}.Destroy(r.Spent) + FfiDestroyerUint64{}.Destroy(r.Received) + FfiDestroyerOptionalUint32{}.Destroy(r.Confirmations) +} + +type FfiConverterTypeTxInfo struct{} + +var FfiConverterTypeTxInfoINSTANCE = FfiConverterTypeTxInfo{} + +func (c FfiConverterTypeTxInfo) Lift(rb RustBufferI) TxInfo { + return LiftFromRustBuffer[TxInfo](c, rb) +} + +func (c FfiConverterTypeTxInfo) Read(reader io.Reader) TxInfo { + return TxInfo{ + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + FfiConverterOptionalUint32INSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeTxInfo) Lower(value TxInfo) RustBuffer { + return LowerIntoRustBuffer[TxInfo](c, value) +} + +func (c FfiConverterTypeTxInfo) Write(writer io.Writer, value TxInfo) { + FfiConverterBytesINSTANCE.Write(writer, value.Txid) + FfiConverterBytesINSTANCE.Write(writer, value.Tx) + FfiConverterUint64INSTANCE.Write(writer, value.Spent) + FfiConverterUint64INSTANCE.Write(writer, value.Received) + FfiConverterOptionalUint32INSTANCE.Write(writer, value.Confirmations) +} + +type FfiDestroyerTypeTxInfo struct{} + +func (_ FfiDestroyerTypeTxInfo) Destroy(value TxInfo) { + value.Destroy() +} + +type UtxoInfo struct { + Txid []byte + Vout uint32 + Amount uint64 + ScriptPubkey []byte + IsChange bool + DerivationIndex uint32 + Confirmations *uint32 +} + +func (r *UtxoInfo) Destroy() { + FfiDestroyerBytes{}.Destroy(r.Txid) + FfiDestroyerUint32{}.Destroy(r.Vout) + FfiDestroyerUint64{}.Destroy(r.Amount) + FfiDestroyerBytes{}.Destroy(r.ScriptPubkey) + FfiDestroyerBool{}.Destroy(r.IsChange) + FfiDestroyerUint32{}.Destroy(r.DerivationIndex) + FfiDestroyerOptionalUint32{}.Destroy(r.Confirmations) +} + +type FfiConverterTypeUtxoInfo struct{} + +var FfiConverterTypeUtxoInfoINSTANCE = FfiConverterTypeUtxoInfo{} + +func (c FfiConverterTypeUtxoInfo) Lift(rb RustBufferI) UtxoInfo { + return LiftFromRustBuffer[UtxoInfo](c, rb) +} + +func (c FfiConverterTypeUtxoInfo) Read(reader io.Reader) UtxoInfo { + return UtxoInfo{ + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterUint32INSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterBoolINSTANCE.Read(reader), + FfiConverterUint32INSTANCE.Read(reader), + FfiConverterOptionalUint32INSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeUtxoInfo) Lower(value UtxoInfo) RustBuffer { + return LowerIntoRustBuffer[UtxoInfo](c, value) +} + +func (c FfiConverterTypeUtxoInfo) Write(writer io.Writer, value UtxoInfo) { + FfiConverterBytesINSTANCE.Write(writer, value.Txid) + FfiConverterUint32INSTANCE.Write(writer, value.Vout) + FfiConverterUint64INSTANCE.Write(writer, value.Amount) + FfiConverterBytesINSTANCE.Write(writer, value.ScriptPubkey) + FfiConverterBoolINSTANCE.Write(writer, value.IsChange) + FfiConverterUint32INSTANCE.Write(writer, value.DerivationIndex) + FfiConverterOptionalUint32INSTANCE.Write(writer, value.Confirmations) +} + +type FfiDestroyerTypeUtxoInfo struct{} + +func (_ FfiDestroyerTypeUtxoInfo) Destroy(value UtxoInfo) { + value.Destroy() +} + type ApplyBlockError struct { err error } @@ -1178,6 +1522,98 @@ func (c FfiConverterTypeCreateNewError) Write(writer io.Writer, value *CreateNew } } +type CreateTxError struct { + err error +} + +func (err CreateTxError) Error() string { + return fmt.Sprintf("CreateTxError: %s", err.err.Error()) +} + +func (err CreateTxError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrCreateTxErrorCreateTx = fmt.Errorf("CreateTxErrorCreateTx") +var ErrCreateTxErrorSignTx = fmt.Errorf("CreateTxErrorSignTx") + +// Variant structs +type CreateTxErrorCreateTx struct { + message string +} + +func NewCreateTxErrorCreateTx() *CreateTxError { + return &CreateTxError{ + err: &CreateTxErrorCreateTx{}, + } +} + +func (err CreateTxErrorCreateTx) Error() string { + return fmt.Sprintf("CreateTx: %s", err.message) +} + +func (self CreateTxErrorCreateTx) Is(target error) bool { + return target == ErrCreateTxErrorCreateTx +} + +type CreateTxErrorSignTx struct { + message string +} + +func NewCreateTxErrorSignTx() *CreateTxError { + return &CreateTxError{ + err: &CreateTxErrorSignTx{}, + } +} + +func (err CreateTxErrorSignTx) Error() string { + return fmt.Sprintf("SignTx: %s", err.message) +} + +func (self CreateTxErrorSignTx) Is(target error) bool { + return target == ErrCreateTxErrorSignTx +} + +type FfiConverterTypeCreateTxError struct{} + +var FfiConverterTypeCreateTxErrorINSTANCE = FfiConverterTypeCreateTxError{} + +func (c FfiConverterTypeCreateTxError) Lift(eb RustBufferI) error { + return LiftFromRustBuffer[error](c, eb) +} + +func (c FfiConverterTypeCreateTxError) Lower(value *CreateTxError) RustBuffer { + return LowerIntoRustBuffer[*CreateTxError](c, value) +} + +func (c FfiConverterTypeCreateTxError) Read(reader io.Reader) error { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &CreateTxError{&CreateTxErrorCreateTx{message}} + case 2: + return &CreateTxError{&CreateTxErrorSignTx{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterTypeCreateTxError.Read()", errorID)) + } + +} + +func (c FfiConverterTypeCreateTxError) Write(writer io.Writer, value *CreateTxError) { + switch variantValue := value.err.(type) { + case *CreateTxErrorCreateTx: + writeInt32(writer, 1) + case *CreateTxErrorSignTx: + writeInt32(writer, 2) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterTypeCreateTxError.Write", value)) + } +} + type DatabaseError struct { err error } @@ -1385,6 +1821,86 @@ func (c FfiConverterTypeLoadError) Write(writer io.Writer, value *LoadError) { } } +type FfiConverterOptionalUint32 struct{} + +var FfiConverterOptionalUint32INSTANCE = FfiConverterOptionalUint32{} + +func (c FfiConverterOptionalUint32) Lift(rb RustBufferI) *uint32 { + return LiftFromRustBuffer[*uint32](c, rb) +} + +func (_ FfiConverterOptionalUint32) Read(reader io.Reader) *uint32 { + if readInt8(reader) == 0 { + return nil + } + temp := FfiConverterUint32INSTANCE.Read(reader) + return &temp +} + +func (c FfiConverterOptionalUint32) Lower(value *uint32) RustBuffer { + return LowerIntoRustBuffer[*uint32](c, value) +} + +func (_ FfiConverterOptionalUint32) Write(writer io.Writer, value *uint32) { + if value == nil { + writeInt8(writer, 0) + } else { + writeInt8(writer, 1) + FfiConverterUint32INSTANCE.Write(writer, *value) + } +} + +type FfiDestroyerOptionalUint32 struct{} + +func (_ FfiDestroyerOptionalUint32) Destroy(value *uint32) { + if value != nil { + FfiDestroyerUint32{}.Destroy(*value) + } +} + +type FfiConverterSequenceString struct{} + +var FfiConverterSequenceStringINSTANCE = FfiConverterSequenceString{} + +func (c FfiConverterSequenceString) Lift(rb RustBufferI) []string { + return LiftFromRustBuffer[[]string](c, rb) +} + +func (c FfiConverterSequenceString) Read(reader io.Reader) []string { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]string, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterStringINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceString) Lower(value []string) RustBuffer { + return LowerIntoRustBuffer[[]string](c, value) +} + +func (c FfiConverterSequenceString) Write(writer io.Writer, value []string) { + if len(value) > math.MaxInt32 { + panic("[]string is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterStringINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceString struct{} + +func (FfiDestroyerSequenceString) Destroy(sequence []string) { + for _, value := range sequence { + FfiDestroyerString{}.Destroy(value) + } +} + type FfiConverterSequenceTypeBlockId struct{} var FfiConverterSequenceTypeBlockIdINSTANCE = FfiConverterSequenceTypeBlockId{} @@ -1427,3 +1943,175 @@ func (FfiDestroyerSequenceTypeBlockId) Destroy(sequence []BlockId) { FfiDestroyerTypeBlockId{}.Destroy(value) } } + +type FfiConverterSequenceTypeMempoolTx struct{} + +var FfiConverterSequenceTypeMempoolTxINSTANCE = FfiConverterSequenceTypeMempoolTx{} + +func (c FfiConverterSequenceTypeMempoolTx) Lift(rb RustBufferI) []MempoolTx { + return LiftFromRustBuffer[[]MempoolTx](c, rb) +} + +func (c FfiConverterSequenceTypeMempoolTx) Read(reader io.Reader) []MempoolTx { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]MempoolTx, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterTypeMempoolTxINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceTypeMempoolTx) Lower(value []MempoolTx) RustBuffer { + return LowerIntoRustBuffer[[]MempoolTx](c, value) +} + +func (c FfiConverterSequenceTypeMempoolTx) Write(writer io.Writer, value []MempoolTx) { + if len(value) > math.MaxInt32 { + panic("[]MempoolTx is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterTypeMempoolTxINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceTypeMempoolTx struct{} + +func (FfiDestroyerSequenceTypeMempoolTx) Destroy(sequence []MempoolTx) { + for _, value := range sequence { + FfiDestroyerTypeMempoolTx{}.Destroy(value) + } +} + +type FfiConverterSequenceTypeRecipient struct{} + +var FfiConverterSequenceTypeRecipientINSTANCE = FfiConverterSequenceTypeRecipient{} + +func (c FfiConverterSequenceTypeRecipient) Lift(rb RustBufferI) []Recipient { + return LiftFromRustBuffer[[]Recipient](c, rb) +} + +func (c FfiConverterSequenceTypeRecipient) Read(reader io.Reader) []Recipient { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]Recipient, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterTypeRecipientINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceTypeRecipient) Lower(value []Recipient) RustBuffer { + return LowerIntoRustBuffer[[]Recipient](c, value) +} + +func (c FfiConverterSequenceTypeRecipient) Write(writer io.Writer, value []Recipient) { + if len(value) > math.MaxInt32 { + panic("[]Recipient is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterTypeRecipientINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceTypeRecipient struct{} + +func (FfiDestroyerSequenceTypeRecipient) Destroy(sequence []Recipient) { + for _, value := range sequence { + FfiDestroyerTypeRecipient{}.Destroy(value) + } +} + +type FfiConverterSequenceTypeTxInfo struct{} + +var FfiConverterSequenceTypeTxInfoINSTANCE = FfiConverterSequenceTypeTxInfo{} + +func (c FfiConverterSequenceTypeTxInfo) Lift(rb RustBufferI) []TxInfo { + return LiftFromRustBuffer[[]TxInfo](c, rb) +} + +func (c FfiConverterSequenceTypeTxInfo) Read(reader io.Reader) []TxInfo { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]TxInfo, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterTypeTxInfoINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceTypeTxInfo) Lower(value []TxInfo) RustBuffer { + return LowerIntoRustBuffer[[]TxInfo](c, value) +} + +func (c FfiConverterSequenceTypeTxInfo) Write(writer io.Writer, value []TxInfo) { + if len(value) > math.MaxInt32 { + panic("[]TxInfo is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterTypeTxInfoINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceTypeTxInfo struct{} + +func (FfiDestroyerSequenceTypeTxInfo) Destroy(sequence []TxInfo) { + for _, value := range sequence { + FfiDestroyerTypeTxInfo{}.Destroy(value) + } +} + +type FfiConverterSequenceTypeUtxoInfo struct{} + +var FfiConverterSequenceTypeUtxoInfoINSTANCE = FfiConverterSequenceTypeUtxoInfo{} + +func (c FfiConverterSequenceTypeUtxoInfo) Lift(rb RustBufferI) []UtxoInfo { + return LiftFromRustBuffer[[]UtxoInfo](c, rb) +} + +func (c FfiConverterSequenceTypeUtxoInfo) Read(reader io.Reader) []UtxoInfo { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]UtxoInfo, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterTypeUtxoInfoINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceTypeUtxoInfo) Lower(value []UtxoInfo) RustBuffer { + return LowerIntoRustBuffer[[]UtxoInfo](c, value) +} + +func (c FfiConverterSequenceTypeUtxoInfo) Write(writer io.Writer, value []UtxoInfo) { + if len(value) > math.MaxInt32 { + panic("[]UtxoInfo is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterTypeUtxoInfoINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceTypeUtxoInfo struct{} + +func (FfiDestroyerSequenceTypeUtxoInfo) Destroy(sequence []UtxoInfo) { + for _, value := range sequence { + FfiDestroyerTypeUtxoInfo{}.Destroy(value) + } +} diff --git a/bdkwallet/bdkgo/bdkgo.h b/bdkwallet/bdkgo/bdkgo.h index e4d44aee..601e6839 100644 --- a/bdkwallet/bdkgo/bdkgo.h +++ b/bdkwallet/bdkgo/bdkgo.h @@ -92,11 +92,24 @@ void uniffi_bdkgo_fn_method_wallet_apply_block( RustCallStatus* out_status ); +void uniffi_bdkgo_fn_method_wallet_apply_mempool( + void* ptr, + RustBuffer txs, + RustCallStatus* out_status +); + RustBuffer uniffi_bdkgo_fn_method_wallet_balance( void* ptr, RustCallStatus* out_status ); +RustBuffer uniffi_bdkgo_fn_method_wallet_create_tx( + void* ptr, + float feerate, + RustBuffer recipients, + RustCallStatus* out_status +); + RustBuffer uniffi_bdkgo_fn_method_wallet_fresh_address( void* ptr, RustCallStatus* out_status @@ -117,6 +130,11 @@ RustBuffer uniffi_bdkgo_fn_method_wallet_last_unused_address( RustCallStatus* out_status ); +RustBuffer uniffi_bdkgo_fn_method_wallet_mnemonic_words( + void* ptr, + RustCallStatus* out_status +); + RustBuffer uniffi_bdkgo_fn_method_wallet_peek_address( void* ptr, uint32_t index, @@ -129,6 +147,16 @@ RustBuffer uniffi_bdkgo_fn_method_wallet_recent_blocks( RustCallStatus* out_status ); +RustBuffer uniffi_bdkgo_fn_method_wallet_transactions( + void* ptr, + RustCallStatus* out_status +); + +RustBuffer uniffi_bdkgo_fn_method_wallet_utxos( + void* ptr, + RustCallStatus* out_status +); + RustBuffer ffi_bdkgo_rustbuffer_alloc( int32_t size, RustCallStatus* out_status @@ -432,10 +460,18 @@ uint16_t uniffi_bdkgo_checksum_method_wallet_apply_block( RustCallStatus* out_status ); +uint16_t uniffi_bdkgo_checksum_method_wallet_apply_mempool( + RustCallStatus* out_status +); + uint16_t uniffi_bdkgo_checksum_method_wallet_balance( RustCallStatus* out_status ); +uint16_t uniffi_bdkgo_checksum_method_wallet_create_tx( + RustCallStatus* out_status +); + uint16_t uniffi_bdkgo_checksum_method_wallet_fresh_address( RustCallStatus* out_status ); @@ -452,6 +488,10 @@ uint16_t uniffi_bdkgo_checksum_method_wallet_last_unused_address( RustCallStatus* out_status ); +uint16_t uniffi_bdkgo_checksum_method_wallet_mnemonic_words( + RustCallStatus* out_status +); + uint16_t uniffi_bdkgo_checksum_method_wallet_peek_address( RustCallStatus* out_status ); @@ -460,6 +500,14 @@ uint16_t uniffi_bdkgo_checksum_method_wallet_recent_blocks( RustCallStatus* out_status ); +uint16_t uniffi_bdkgo_checksum_method_wallet_transactions( + RustCallStatus* out_status +); + +uint16_t uniffi_bdkgo_checksum_method_wallet_utxos( + RustCallStatus* out_status +); + uint16_t uniffi_bdkgo_checksum_constructor_wallet_create_new( RustCallStatus* out_status ); diff --git a/bdkwallet/bdkgo_crate/src/bdkgo.udl b/bdkwallet/bdkgo_crate/src/bdkgo.udl index 2eff940f..23a80c53 100644 --- a/bdkwallet/bdkgo_crate/src/bdkgo.udl +++ b/bdkwallet/bdkgo_crate/src/bdkgo.udl @@ -28,6 +28,12 @@ enum ApplyBlockError { "Database", }; +[Error] +enum CreateTxError { + "CreateTx", + "SignTx", +}; + dictionary AddressInfo { u32 index; string address; @@ -66,9 +72,48 @@ interface Wallet { [Throws=ApplyBlockError] void apply_block(u32 height, [ByRef] bytes block_bytes); + + void apply_mempool(sequence txs); + + [Throws=CreateTxError] + bytes create_tx(f32 feerate, sequence recipients); + + sequence mnemonic_words(); + + sequence transactions(); + + sequence utxos(); +}; + +dictionary Recipient { + bytes script_pubkey; + u64 amount; }; dictionary BlockId { u32 height; bytes hash; }; + +dictionary TxInfo { + bytes txid; + bytes tx; + u64 spent; + u64 received; + u32? confirmations; +}; + +dictionary UtxoInfo { + bytes txid; + u32 vout; + u64 amount; + bytes script_pubkey; + boolean is_change; + u32 derivation_index; + u32? confirmations; +}; + +dictionary MempoolTx { + bytes tx; + u64 added_unix; +}; diff --git a/bdkwallet/bdkgo_crate/src/lib.rs b/bdkwallet/bdkgo_crate/src/lib.rs index 7b13907f..4e03a184 100644 --- a/bdkwallet/bdkgo_crate/src/lib.rs +++ b/bdkwallet/bdkgo_crate/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + cmp::Reverse, io::Read, str::FromStr, sync::{Arc, Mutex}, @@ -7,13 +8,16 @@ use std::{ pub use bdk::wallet::Balance; use bdk::{ bitcoin::{ - self, consensus::Decodable, hashes::Hash, network::constants::ParseNetworkError, BlockHash, - Network, + self, + consensus::{Decodable, Encodable}, + hashes::Hash, + network::constants::ParseNetworkError, + BlockHash, Network, ScriptBuf, Transaction, }, keys::{DerivableKey, ExtendedKey}, template::Bip86, wallet::AddressIndex, - KeychainKind, + FeeRate, KeychainKind, SignOptions, }; use bincode::Options; use rand::RngCore; @@ -71,6 +75,14 @@ pub enum ApplyBlockError { Database(std::io::Error), } +#[derive(Debug, thiserror::Error)] +pub enum CreateTxError { + #[error("failed to create tx: {0}")] + CreateTx(bdk::wallet::error::CreateTxError), + #[error("failed to sign tx: {0}")] + SignTx(bdk::wallet::signer::SignerError), +} + pub struct AddressInfo { pub index: u32, pub address: String, @@ -137,12 +149,17 @@ impl WalletHeader { }; Bip86(ext_key, keychain) } -} -type WalletInner = Mutex; + pub fn mnemonic_words(&self) -> Vec { + let mnemonic = + bdk::keys::bip39::Mnemonic::from_entropy(&self.entropy).expect("must get mnemonic"); + mnemonic.word_iter().map(|w| w.to_string()).collect() + } +} pub struct Wallet { - inner: WalletInner, + inner: Mutex, + header: Mutex, } impl Wallet { @@ -152,13 +169,7 @@ impl Wallet { /// The caveat of this is that `Wallet` will never be destroyed. This is an okay sacrifice as /// typically you want to keep the wallet for the lifetime of the node. pub fn increment_reference_counter(self: &Arc) { - - let count = Arc::strong_count(self); - eprintln!("The pointer reference count is: {}", count); - - unsafe { - Arc::increment_strong_count(Arc::into_raw(Arc::clone(self))); - } + unsafe { Arc::increment_strong_count(Arc::into_raw(Arc::clone(self))) } } pub fn create_new( @@ -170,13 +181,13 @@ impl Wallet { let genesis_hash = BlockHash::from_slice(&genesis_hash).map_err(CreateNewError::ParseGenesisHash)?; - let mut db_header = WalletHeader::new(network); - let db_header_bytes = db_header.encode(); + let mut header = WalletHeader::new(network); + let header_bytes = header.encode(); let db = - bdk_file_store::Store::create_new(&db_header_bytes, &db_path).expect("must create db"); + bdk_file_store::Store::create_new(&header_bytes, &db_path).expect("must create db"); let bdk_wallet = match bdk::Wallet::new_with_genesis_hash( - db_header.descriptor(KeychainKind::External), - Some(db_header.descriptor(KeychainKind::Internal)), + header.descriptor(KeychainKind::External), + Some(header.descriptor(KeychainKind::Internal)), db, network, genesis_hash, @@ -189,23 +200,25 @@ impl Wallet { }; let inner = Mutex::new(bdk_wallet); - Ok(Self { inner }) + let header = Mutex::new(header); + Ok(Self { inner, header }) } pub fn load(db_path: String) -> Result { let file = std::fs::File::open(&db_path) .map_err(|err| LoadError::Database(bdk_file_store::FileError::Io(err)))?; - let (db_header, db_header_bytes) = WalletHeader::decode(file)?; - let db = bdk_file_store::Store::open(&db_header_bytes, db_path).expect("must load db"); + let (header, header_bytes) = WalletHeader::decode(file)?; + let db = bdk_file_store::Store::open(&header_bytes, db_path).expect("must load db"); let bdk_wallet = bdk::Wallet::load( - db_header.descriptor(KeychainKind::External), - Some(db_header.descriptor(KeychainKind::Internal)), + header.descriptor(KeychainKind::External), + Some(header.descriptor(KeychainKind::Internal)), db, ) .map_err(LoadError::Wallet)?; let inner = Mutex::new(bdk_wallet); - Ok(Self { inner }) + let header = Mutex::new(header); + Ok(Self { inner, header }) } fn address(self: Arc, index: AddressIndex) -> Result { @@ -299,10 +312,149 @@ impl Wallet { Ok(()) } - //pub fn create_tx(self: Arc, recipients: Vec<>) + pub fn apply_mempool(self: Arc, txs: Vec) { + self.increment_reference_counter(); + let mut wallet = self.inner.lock().unwrap(); + let txs = txs + .into_iter() + .map(|mtx| { + ( + Transaction::consensus_decode_from_finite_reader(&mut mtx.tx.reader()) + .expect("must decode tx"), + mtx.added_unix, + ) + }) + .collect::>(); + wallet.apply_unconfirmed_txs(txs.iter().map(|(tx, added)| (tx, *added))) + // TODO: Do we need to commit to persistence after receiving memory txs? + } + + pub fn create_tx( + self: Arc, + feerate: f32, + recipients: Vec, + ) -> Result, CreateTxError> { + self.increment_reference_counter(); + let mut wallet = self.inner.lock().unwrap(); + let mut psbt = wallet + .build_tx() + .set_recipients( + recipients + .into_iter() + .map(|r| (ScriptBuf::from_bytes(r.script_pubkey), r.amount)) + .collect(), + ) + .fee_rate(FeeRate::from_sat_per_vb(feerate)) + .enable_rbf() + .clone() + .finish() + .map_err(CreateTxError::CreateTx)?; + let is_finalized = wallet + .sign(&mut psbt, SignOptions::default()) + .map_err(CreateTxError::SignTx)?; + assert!(is_finalized, "tx should always be finalized"); + + let mut raw_bytes = Vec::::new(); + psbt.extract_tx() + .consensus_encode(&mut raw_bytes) + .expect("must encode tx"); + Ok(raw_bytes) + } + + pub fn mnemonic_words(self: Arc) -> Vec { + self.increment_reference_counter(); + self.header.lock().unwrap().mnemonic_words() + } + + pub fn transactions(self: Arc) -> Vec { + self.increment_reference_counter(); + let wallet = self.inner.lock().unwrap(); + let height = wallet.latest_checkpoint().height(); + let mut txs = wallet + .transactions() + .map(|ctx| { + let txid = ctx.tx_node.txid.to_byte_array().to_vec(); + let mut tx = Vec::::new(); + ctx.tx_node + .tx + .consensus_encode(&mut tx) + .expect("must encode"); + let (spent, received) = wallet.sent_and_received(ctx.tx_node.tx); + let confirmations = ctx + .chain_position + .confirmation_height_upper_bound() + .map(|conf_height| height.saturating_sub(conf_height)); + TxInfo { + txid, + tx, + spent, + received, + confirmations, + } + }) + .collect::>(); + txs.sort_unstable_by_key(|tx| Reverse(tx.confirmations)); + txs + } + + pub fn utxos(self: Arc) -> Vec { + self.increment_reference_counter(); + let wallet = self.inner.lock().unwrap(); + let wallet_height = wallet.latest_checkpoint().height(); + let mut utxos = wallet + .list_unspent() + .map(|utxo| UtxoInfo { + txid: utxo.outpoint.txid.to_byte_array().to_vec(), + vout: utxo.outpoint.vout, + amount: utxo.txout.value, + script_pubkey: utxo.txout.script_pubkey.to_bytes(), + is_change: utxo.keychain == KeychainKind::Internal, + derivation_index: utxo.derivation_index, + confirmations: match utxo.confirmation_time { + bdk::chain::ConfirmationTime::Confirmed { height, .. } => { + Some(wallet_height.saturating_sub(height)) + } + bdk::chain::ConfirmationTime::Unconfirmed { .. } => None, + }, + }) + .collect::>(); + utxos.sort_unstable_by_key(|utxo| Reverse(utxo.confirmations)); + utxos + } +} + +pub struct Recipient { + pub script_pubkey: Vec, + pub amount: u64, } pub struct BlockId { pub height: u32, pub hash: Vec, } + +pub struct TxInfo { + pub txid: Vec, + pub tx: Vec, + /// Sum of inputs spending from owned script pubkeys. + pub spent: u64, + /// Sum of outputs containing owned script pubkeys. + pub received: u64, + /// How confirmed is this transaction? + pub confirmations: Option, +} + +pub struct UtxoInfo { + pub txid: Vec, + pub vout: u32, + pub amount: u64, + pub script_pubkey: Vec, + pub is_change: bool, + pub derivation_index: u32, + pub confirmations: Option, +} + +pub struct MempoolTx { + pub tx: Vec, + pub added_unix: u64, +} diff --git a/bdkwallet/manager.go b/bdkwallet/manager.go index b4c9b969..039ad79d 100644 --- a/bdkwallet/manager.go +++ b/bdkwallet/manager.go @@ -63,6 +63,12 @@ func NewManager(config ManagerConfig) (*Manager, error) { return &m, nil } +func (m *Manager) NotifyNewTransactions(txns []*mempool.TxDesc) { + if err := m.wallet.ApplyMempoolTransactions(txns); err != nil { + log.Errorf("Failed to apply mempool txs to the wallet. %v", err) + } +} + func (m *Manager) handleBlockchainNotification(notification *blockchain.Notification) { switch notification.Type { // A block has been accepted into the block chain. diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go index a346386b..424fe8a1 100644 --- a/bdkwallet/wallet.go +++ b/bdkwallet/wallet.go @@ -5,14 +5,18 @@ import "C" import ( "bytes" + "errors" "github.com/utreexo/utreexod/bdkwallet/bdkgo" "github.com/utreexo/utreexod/btcutil" "github.com/utreexo/utreexod/chaincfg" "github.com/utreexo/utreexod/chaincfg/chainhash" + "github.com/utreexo/utreexod/mempool" "github.com/utreexo/utreexod/wire" ) +var ErrNoRecipient = errors.New("must have atleast one recipient") + // Balance in satoshis. type Balance struct { bdkgo.Balance @@ -40,6 +44,31 @@ func (id *BlockId) Hash() chainhash.Hash { return *(*[32]byte)(id.BlockId.Hash) } +type Recipient struct { + Amount btcutil.Amount + Address btcutil.Address +} + +type TxInfo struct{ bdkgo.TxInfo } + +func (tx *TxInfo) Txid() chainhash.Hash { + return *(*[32]byte)(tx.TxInfo.Txid) +} + +func (tx *TxInfo) Tx() btcutil.Tx { + var msgTx wire.MsgTx + if err := msgTx.BtcDecode(bytes.NewReader(tx.TxInfo.Tx), wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { + panic("Must decode tx from rust.") + } + return *btcutil.NewTx(&msgTx) +} + +type UtxoInfo struct{ bdkgo.UtxoInfo } + +func (utxo *UtxoInfo) Txid() chainhash.Hash { + return *(*[32]byte)(utxo.UtxoInfo.Txid) +} + // Wallet is a BDK wallet. type Wallet struct { inner bdkgo.Wallet @@ -157,3 +186,53 @@ func (w *Wallet) ApplyBlock(block *btcutil.Block) error { log.Infof("Applied block [%v:%v]", block.Height(), block.Hash()) return err } + +func (w *Wallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { + genTxns := make([]bdkgo.MempoolTx, 0, len(txns)) + for _, tx := range txns { + var txb bytes.Buffer + if err := tx.Tx.MsgTx().BtcEncode(&txb, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { + return err + } + genTxns = append(genTxns, bdkgo.MempoolTx{ + Tx: txb.Bytes(), + AddedUnix: uint64(tx.Added.Unix()), + }) + } + w.inner.ApplyMempool(genTxns) + log.Infof("Applied %v mempool transactions.", len(txns)) + return nil +} + +func (w *Wallet) CreateTx(feerate float32, recipients []Recipient) ([]byte, error) { + genRecipients := make([]bdkgo.Recipient, 0, len(recipients)) + for _, r := range recipients { + genRecipients = append(genRecipients, bdkgo.Recipient{ + ScriptPubkey: r.Address.ScriptAddress(), + Amount: uint64(r.Amount), + }) + } + return w.inner.CreateTx(feerate, genRecipients) +} + +func (w *Wallet) MnemonicWords() []string { + return w.inner.MnemonicWords() +} + +func (w *Wallet) Transactions() []TxInfo { + genOut := w.inner.Transactions() + out := make([]TxInfo, 0, len(genOut)) + for _, info := range genOut { + out = append(out, TxInfo{info}) + } + return out +} + +func (w *Wallet) Utxos() []UtxoInfo { + genOut := w.inner.Utxos() + out := make([]UtxoInfo, 0, len(genOut)) + for _, info := range genOut { + out = append(out, UtxoInfo{info}) + } + return out +} diff --git a/server.go b/server.go index 6a0079e3..ed729eb3 100644 --- a/server.go +++ b/server.go @@ -1509,6 +1509,10 @@ func (s *server) AnnounceNewTransactions(txns []*mempool.TxDesc) { s.rpcServer.NotifyNewTransactions(txns) } + if s.bdkWallet != nil { + s.bdkWallet.NotifyNewTransactions(txns) + } + if s.watchOnlyWallet != nil { s.watchOnlyWallet.NotifyNewTransactions(txns) } From 69e9dfb84fc3b9f038241cce73f3841ce0f54cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sat, 27 Jan 2024 17:28:23 +0900 Subject: [PATCH 08/48] bdkwallet: add docs for wallet.go --- bdkwallet/wallet.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go index 424fe8a1..1a48d5b8 100644 --- a/bdkwallet/wallet.go +++ b/bdkwallet/wallet.go @@ -32,23 +32,28 @@ func (b *Balance) Total() uint64 { return b.Immature + b.TrustedPending + b.UntrustedPending + b.Confirmed } +// BlockId consists of a block height and a block hash. This identifies a block. type BlockId struct { bdkgo.BlockId } +// Height gets the block height. func (id *BlockId) Height() uint32 { return id.BlockId.Height } +// Hash gets the block hash. func (id *BlockId) Hash() chainhash.Hash { return *(*[32]byte)(id.BlockId.Hash) } +// Recipient specifies the intended amount and destination address for a transaction output. type Recipient struct { Amount btcutil.Amount Address btcutil.Address } +// TxInfo is information on a given transaction. type TxInfo struct{ bdkgo.TxInfo } func (tx *TxInfo) Txid() chainhash.Hash { @@ -63,6 +68,7 @@ func (tx *TxInfo) Tx() btcutil.Tx { return *btcutil.NewTx(&msgTx) } +// UtxoInfo is information on a given transaction. type UtxoInfo struct{ bdkgo.UtxoInfo } func (utxo *UtxoInfo) Txid() chainhash.Hash { @@ -187,6 +193,7 @@ func (w *Wallet) ApplyBlock(block *btcutil.Block) error { return err } +// ApplyMempoolTransactions updates the wallet with the given mempool transactions. func (w *Wallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { genTxns := make([]bdkgo.MempoolTx, 0, len(txns)) for _, tx := range txns { @@ -204,6 +211,7 @@ func (w *Wallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { return nil } +// CreateTx creates and signs a transaction spending from the wallet. func (w *Wallet) CreateTx(feerate float32, recipients []Recipient) ([]byte, error) { genRecipients := make([]bdkgo.Recipient, 0, len(recipients)) for _, r := range recipients { @@ -215,10 +223,12 @@ func (w *Wallet) CreateTx(feerate float32, recipients []Recipient) ([]byte, erro return w.inner.CreateTx(feerate, genRecipients) } +// MnemonicWords returns the mnemonic words to backup the wallet. func (w *Wallet) MnemonicWords() []string { return w.inner.MnemonicWords() } +// Transactions returns the list of wallet transactions. func (w *Wallet) Transactions() []TxInfo { genOut := w.inner.Transactions() out := make([]TxInfo, 0, len(genOut)) @@ -228,6 +238,7 @@ func (w *Wallet) Transactions() []TxInfo { return out } +// Utxos returns the list of wallet UTXOs. func (w *Wallet) Utxos() []UtxoInfo { genOut := w.inner.Utxos() out := make([]UtxoInfo, 0, len(genOut)) From d5efac7c5dd967c1358a3b9cbee173b47b8839c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sat, 27 Jan 2024 18:46:58 +0900 Subject: [PATCH 09/48] bdkwallet: improve logging --- bdkwallet/bdkgo/bdkgo.go | 179 ++++++++++++++++++++++++++-- bdkwallet/bdkgo/bdkgo.h | 4 +- bdkwallet/bdkgo_crate/src/bdkgo.udl | 14 ++- bdkwallet/bdkgo_crate/src/lib.rs | 31 ++++- bdkwallet/wallet.go | 27 ++++- 5 files changed, 229 insertions(+), 26 deletions(-) diff --git a/bdkwallet/bdkgo/bdkgo.go b/bdkwallet/bdkgo/bdkgo.go index ba5d2bb8..f50b28bf 100644 --- a/bdkwallet/bdkgo/bdkgo.go +++ b/bdkwallet/bdkgo/bdkgo.go @@ -348,7 +348,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_method_wallet_apply_block(uniffiStatus) }) - if checksum != 20455 { + if checksum != 38677 { // If this happens try cleaning and rebuilding your project panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_apply_block: UniFFI API checksum mismatch") } @@ -357,7 +357,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_bdkgo_checksum_method_wallet_apply_mempool(uniffiStatus) }) - if checksum != 23416 { + if checksum != 15979 { // If this happens try cleaning and rebuilding your project panic("bdkgo: uniffi_bdkgo_checksum_method_wallet_apply_mempool: UniFFI API checksum mismatch") } @@ -760,25 +760,34 @@ func WalletLoad(dbPath string) (*Wallet, error) { } } -func (_self *Wallet) ApplyBlock(height uint32, blockBytes []byte) error { +func (_self *Wallet) ApplyBlock(height uint32, blockBytes []byte) (ApplyResult, error) { _pointer := _self.ffiObject.incrementPointer("*Wallet") defer _self.ffiObject.decrementPointer() - _, _uniffiErr := rustCallWithError(FfiConverterTypeApplyBlockError{}, func(_uniffiStatus *C.RustCallStatus) bool { - C.uniffi_bdkgo_fn_method_wallet_apply_block( + _uniffiRV, _uniffiErr := rustCallWithError(FfiConverterTypeApplyBlockError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_apply_block( _pointer, FfiConverterUint32INSTANCE.Lower(height), FfiConverterBytesINSTANCE.Lower(blockBytes), _uniffiStatus) - return false }) - return _uniffiErr + if _uniffiErr != nil { + var _uniffiDefaultValue ApplyResult + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterTypeApplyResultINSTANCE.Lift(_uniffiRV), _uniffiErr + } } -func (_self *Wallet) ApplyMempool(txs []MempoolTx) { +func (_self *Wallet) ApplyMempool(txs []MempoolTx) (ApplyResult, error) { _pointer := _self.ffiObject.incrementPointer("*Wallet") defer _self.ffiObject.decrementPointer() - rustCall(func(_uniffiStatus *C.RustCallStatus) bool { - C.uniffi_bdkgo_fn_method_wallet_apply_mempool( + _uniffiRV, _uniffiErr := rustCallWithError(FfiConverterTypeApplyMempoolError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_bdkgo_fn_method_wallet_apply_mempool( _pointer, FfiConverterSequenceTypeMempoolTxINSTANCE.Lower(txs), _uniffiStatus) - return false }) + if _uniffiErr != nil { + var _uniffiDefaultValue ApplyResult + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterTypeApplyResultINSTANCE.Lift(_uniffiRV), _uniffiErr + } } func (_self *Wallet) Balance() Balance { @@ -989,6 +998,42 @@ func (_ FfiDestroyerTypeAddressInfo) Destroy(value AddressInfo) { value.Destroy() } +type ApplyResult struct { + RelevantTxids [][]byte +} + +func (r *ApplyResult) Destroy() { + FfiDestroyerSequenceBytes{}.Destroy(r.RelevantTxids) +} + +type FfiConverterTypeApplyResult struct{} + +var FfiConverterTypeApplyResultINSTANCE = FfiConverterTypeApplyResult{} + +func (c FfiConverterTypeApplyResult) Lift(rb RustBufferI) ApplyResult { + return LiftFromRustBuffer[ApplyResult](c, rb) +} + +func (c FfiConverterTypeApplyResult) Read(reader io.Reader) ApplyResult { + return ApplyResult{ + FfiConverterSequenceBytesINSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeApplyResult) Lower(value ApplyResult) RustBuffer { + return LowerIntoRustBuffer[ApplyResult](c, value) +} + +func (c FfiConverterTypeApplyResult) Write(writer io.Writer, value ApplyResult) { + FfiConverterSequenceBytesINSTANCE.Write(writer, value.RelevantTxids) +} + +type FfiDestroyerTypeApplyResult struct{} + +func (_ FfiDestroyerTypeApplyResult) Destroy(value ApplyResult) { + value.Destroy() +} + type Balance struct { Immature uint64 TrustedPending uint64 @@ -1384,6 +1429,75 @@ func (c FfiConverterTypeApplyBlockError) Write(writer io.Writer, value *ApplyBlo } } +type ApplyMempoolError struct { + err error +} + +func (err ApplyMempoolError) Error() string { + return fmt.Sprintf("ApplyMempoolError: %s", err.err.Error()) +} + +func (err ApplyMempoolError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrApplyMempoolErrorDatabase = fmt.Errorf("ApplyMempoolErrorDatabase") + +// Variant structs +type ApplyMempoolErrorDatabase struct { + message string +} + +func NewApplyMempoolErrorDatabase() *ApplyMempoolError { + return &ApplyMempoolError{ + err: &ApplyMempoolErrorDatabase{}, + } +} + +func (err ApplyMempoolErrorDatabase) Error() string { + return fmt.Sprintf("Database: %s", err.message) +} + +func (self ApplyMempoolErrorDatabase) Is(target error) bool { + return target == ErrApplyMempoolErrorDatabase +} + +type FfiConverterTypeApplyMempoolError struct{} + +var FfiConverterTypeApplyMempoolErrorINSTANCE = FfiConverterTypeApplyMempoolError{} + +func (c FfiConverterTypeApplyMempoolError) Lift(eb RustBufferI) error { + return LiftFromRustBuffer[error](c, eb) +} + +func (c FfiConverterTypeApplyMempoolError) Lower(value *ApplyMempoolError) RustBuffer { + return LowerIntoRustBuffer[*ApplyMempoolError](c, value) +} + +func (c FfiConverterTypeApplyMempoolError) Read(reader io.Reader) error { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &ApplyMempoolError{&ApplyMempoolErrorDatabase{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterTypeApplyMempoolError.Read()", errorID)) + } + +} + +func (c FfiConverterTypeApplyMempoolError) Write(writer io.Writer, value *ApplyMempoolError) { + switch variantValue := value.err.(type) { + case *ApplyMempoolErrorDatabase: + writeInt32(writer, 1) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterTypeApplyMempoolError.Write", value)) + } +} + type CreateNewError struct { err error } @@ -1901,6 +2015,49 @@ func (FfiDestroyerSequenceString) Destroy(sequence []string) { } } +type FfiConverterSequenceBytes struct{} + +var FfiConverterSequenceBytesINSTANCE = FfiConverterSequenceBytes{} + +func (c FfiConverterSequenceBytes) Lift(rb RustBufferI) [][]byte { + return LiftFromRustBuffer[[][]byte](c, rb) +} + +func (c FfiConverterSequenceBytes) Read(reader io.Reader) [][]byte { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([][]byte, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterBytesINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceBytes) Lower(value [][]byte) RustBuffer { + return LowerIntoRustBuffer[[][]byte](c, value) +} + +func (c FfiConverterSequenceBytes) Write(writer io.Writer, value [][]byte) { + if len(value) > math.MaxInt32 { + panic("[][]byte is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterBytesINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceBytes struct{} + +func (FfiDestroyerSequenceBytes) Destroy(sequence [][]byte) { + for _, value := range sequence { + FfiDestroyerBytes{}.Destroy(value) + } +} + type FfiConverterSequenceTypeBlockId struct{} var FfiConverterSequenceTypeBlockIdINSTANCE = FfiConverterSequenceTypeBlockId{} diff --git a/bdkwallet/bdkgo/bdkgo.h b/bdkwallet/bdkgo/bdkgo.h index 601e6839..14a5f5b2 100644 --- a/bdkwallet/bdkgo/bdkgo.h +++ b/bdkwallet/bdkgo/bdkgo.h @@ -85,14 +85,14 @@ void* uniffi_bdkgo_fn_constructor_wallet_load( RustCallStatus* out_status ); -void uniffi_bdkgo_fn_method_wallet_apply_block( +RustBuffer uniffi_bdkgo_fn_method_wallet_apply_block( void* ptr, uint32_t height, RustBuffer block_bytes, RustCallStatus* out_status ); -void uniffi_bdkgo_fn_method_wallet_apply_mempool( +RustBuffer uniffi_bdkgo_fn_method_wallet_apply_mempool( void* ptr, RustBuffer txs, RustCallStatus* out_status diff --git a/bdkwallet/bdkgo_crate/src/bdkgo.udl b/bdkwallet/bdkgo_crate/src/bdkgo.udl index 23a80c53..66df3092 100644 --- a/bdkwallet/bdkgo_crate/src/bdkgo.udl +++ b/bdkwallet/bdkgo_crate/src/bdkgo.udl @@ -28,6 +28,11 @@ enum ApplyBlockError { "Database", }; +[Error] +enum ApplyMempoolError { + "Database", +}; + [Error] enum CreateTxError { "CreateTx", @@ -71,9 +76,10 @@ interface Wallet { sequence recent_blocks(u32 count); [Throws=ApplyBlockError] - void apply_block(u32 height, [ByRef] bytes block_bytes); + ApplyResult apply_block(u32 height, [ByRef] bytes block_bytes); - void apply_mempool(sequence txs); + [Throws=ApplyMempoolError] + ApplyResult apply_mempool(sequence txs); [Throws=CreateTxError] bytes create_tx(f32 feerate, sequence recipients); @@ -117,3 +123,7 @@ dictionary MempoolTx { bytes tx; u64 added_unix; }; + +dictionary ApplyResult { + sequence relevant_txids; +}; diff --git a/bdkwallet/bdkgo_crate/src/lib.rs b/bdkwallet/bdkgo_crate/src/lib.rs index 4e03a184..116fc5c3 100644 --- a/bdkwallet/bdkgo_crate/src/lib.rs +++ b/bdkwallet/bdkgo_crate/src/lib.rs @@ -75,6 +75,12 @@ pub enum ApplyBlockError { Database(std::io::Error), } +#[derive(Debug, thiserror::Error)] +pub enum ApplyMempoolError { + #[error("failed to write mempool txs to db: {0}")] + Database(std::io::Error), +} + #[derive(Debug, thiserror::Error)] pub enum CreateTxError { #[error("failed to create tx: {0}")] @@ -281,7 +287,7 @@ impl Wallet { self: Arc, height: u32, block_bytes: &[u8], - ) -> Result<(), ApplyBlockError> { + ) -> Result { self.increment_reference_counter(); let mut wallet = self.inner.lock().unwrap(); @@ -308,11 +314,12 @@ impl Wallet { .apply_block(&block, height) .map_err(|err| ApplyBlockError::CannotConnect(err))?; } + let res = ApplyResult::new(&wallet); wallet.commit().map_err(ApplyBlockError::Database)?; - Ok(()) + Ok(res) } - pub fn apply_mempool(self: Arc, txs: Vec) { + pub fn apply_mempool(self: Arc, txs: Vec) -> Result { self.increment_reference_counter(); let mut wallet = self.inner.lock().unwrap(); let txs = txs @@ -325,8 +332,11 @@ impl Wallet { ) }) .collect::>(); - wallet.apply_unconfirmed_txs(txs.iter().map(|(tx, added)| (tx, *added))) - // TODO: Do we need to commit to persistence after receiving memory txs? + wallet.apply_unconfirmed_txs(txs.iter().map(|(tx, added)| (tx, *added))); + + let res = ApplyResult::new(&wallet); + wallet.commit().map_err(ApplyMempoolError::Database)?; + Ok(res) } pub fn create_tx( @@ -458,3 +468,14 @@ pub struct MempoolTx { pub tx: Vec, pub added_unix: u64, } + +pub struct ApplyResult { + pub relevant_txids: Vec>, +} + +impl ApplyResult { + pub fn new(wallet: &BdkWallet) -> Self { + let relevant_txids = wallet.staged().indexed_tx_graph.graph.txs.iter().map(|tx| tx.txid().to_byte_array().to_vec()).collect::>(); + Self { relevant_txids } + } +} diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go index 1a48d5b8..473a1655 100644 --- a/bdkwallet/wallet.go +++ b/bdkwallet/wallet.go @@ -186,15 +186,24 @@ func (w *Wallet) ApplyBlock(block *btcutil.Block) error { if err := block.MsgBlock().BtcEncode(&b, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { return err } - bheight := uint32(block.Height()) - err := w.inner.ApplyBlock(bheight, b.Bytes()) - log.Infof("Applied block [%v:%v]", block.Height(), block.Hash()) - return err + res, err := w.inner.ApplyBlock(bheight, b.Bytes()) + if err != nil { + return err + } + bhash := block.Hash() + for _, genTxid := range res.RelevantTxids { + txid := *(*[32]byte)(genTxid) + log.Infof("Found relevant tx %v in block %v:%v.", txid, bheight, bhash) + } + return nil } // ApplyMempoolTransactions updates the wallet with the given mempool transactions. func (w *Wallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { + if len(txns) == 0 { + return nil + } genTxns := make([]bdkgo.MempoolTx, 0, len(txns)) for _, tx := range txns { var txb bytes.Buffer @@ -206,8 +215,14 @@ func (w *Wallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { AddedUnix: uint64(tx.Added.Unix()), }) } - w.inner.ApplyMempool(genTxns) - log.Infof("Applied %v mempool transactions.", len(txns)) + res, err := w.inner.ApplyMempool(genTxns) + if err != nil { + return err + } + for _, genTxid := range res.RelevantTxids { + txid := *(*[32]byte)(genTxid) + log.Infof("Found relevant tx %v in mempool.", txid) + } return nil } From 30f41ca3fbb363a7170572692fd29ba033fa1798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sat, 27 Jan 2024 19:43:00 +0900 Subject: [PATCH 10/48] main,bdkwallet: implement safe startup logic for BDK wallet --- bdkwallet/manager.go | 24 ++++++++++++++++++------ config.go | 1 + server.go | 21 +++++++++++---------- utreexod.go | 16 ++++++++++++++++ 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/bdkwallet/manager.go b/bdkwallet/manager.go index 039ad79d..5195ea0d 100644 --- a/bdkwallet/manager.go +++ b/bdkwallet/manager.go @@ -26,14 +26,25 @@ type Manager struct { wallet Wallet // wallet does not need a mutex as it's done in Rust } -func NewManager(config ManagerConfig) (*Manager, error) { - log.Info("Starting the BDK wallet manager.") - walletDir := filepath.Join(config.DataDir, defaultWalletPath) +func WalletDir(dataDir string) string { + return filepath.Join(dataDir, defaultWalletPath) +} + +func DoesWalletDirExist(dataDir string) (bool, error) { + walletDir := WalletDir(dataDir) if _, err := os.Stat(walletDir); err != nil { - if !os.IsNotExist(err) { - return nil, err + if os.IsNotExist(err) { + return false, nil } - os.MkdirAll(walletDir, os.ModePerm) + return false, err + } + return true, nil +} + +func NewManager(config ManagerConfig) (*Manager, error) { + walletDir := WalletDir(config.DataDir) + if err := os.MkdirAll(walletDir, os.ModePerm); err != nil { + return nil, err } dbPath := filepath.Join(walletDir, defaultWalletFileName) @@ -60,6 +71,7 @@ func NewManager(config ManagerConfig) (*Manager, error) { config.Chain.Subscribe(m.handleBlockchainNotification) } + log.Info("Started the BDK wallet manager.") return &m, nil } diff --git a/config.go b/config.go index df18ccf5..a94cbd1b 100644 --- a/config.go +++ b/config.go @@ -216,6 +216,7 @@ type config struct { RegisterAddressToWatchOnlyWallet []string `long:"registeraddresstowatchonlywallet" description:"Registers addresses to be watched to the watch only wallet. Must have --watchonlywallet enabled"` RegisterExtendedPubKeysToWatchOnlyWallet []string `long:"registerextendedpubkeystowatchonlywallet" description:"Registers extended pubkeys to be watched to the watch only wallet. Must have --watchonlywallet enabled."` RegisterExtendedPubKeysWithAddrTypeToWatchOnlyWallet []string `long:"registerextendedpubkeyswithaddresstypetowatchonlywallet" description:"Registers extended pubkeys to be watched to the watch only wallet and let's the user override the hd type of the extended public key. Must have --watchonlywallet enabled. Format: ':
. Supported address types: '{p2pkh, p2wpkh, p2sh}'"` + BdkWallet bool `long:"bdkwallet" description:"Enable the BDK wallet."` // Electrum server options. ElectrumListeners []string `long:"electrumlisteners" description:"Interface/port for the electrum server to listen to. (default 50001). Electrum server is only enabled when --watchonlywallet is enabled"` diff --git a/server.go b/server.go index ed729eb3..f6faa02e 100644 --- a/server.go +++ b/server.go @@ -3541,16 +3541,17 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, } } - // auto create bdk wallet manager - // TODO: We should let the user disable this! - s.bdkWallet, err = bdkwallet.NewManager(bdkwallet.ManagerConfig{ - Chain: s.chain, - TxMemPool: s.txMemPool, - ChainParams: chainParams, - DataDir: cfg.DataDir, - }) - if err != nil { - return nil, err + if cfg.BdkWallet { + // Setup BDK wallet if it is enabled. + s.bdkWallet, err = bdkwallet.NewManager(bdkwallet.ManagerConfig{ + Chain: s.chain, + TxMemPool: s.txMemPool, + ChainParams: chainParams, + DataDir: cfg.DataDir, + }) + if err != nil { + return nil, err + } } if !cfg.DisableRPC { diff --git a/utreexod.go b/utreexod.go index d2b1cc0a..31c35acd 100644 --- a/utreexod.go +++ b/utreexod.go @@ -16,6 +16,7 @@ import ( "runtime/pprof" "runtime/trace" + "github.com/utreexo/utreexod/bdkwallet" "github.com/utreexo/utreexod/blockchain" "github.com/utreexo/utreexod/blockchain/indexers" "github.com/utreexo/utreexod/database" @@ -349,6 +350,21 @@ func btcdMain(serverChan chan<- *server) error { btcdLog.Error(err) return err } + + // If the node had been run with wallet enabled but it now re-started with wallet disabled. + var walletDirExists bool + if walletDirExists, err = bdkwallet.DoesWalletDirExist(cfg.DataDir); err != nil { + btcdLog.Error(err) + return err + } + if walletDirExists && !cfg.BdkWallet { + err = fmt.Errorf("BDK wallet is disabled but this node has been previously "+ + "started with BDK wallet enabled with walletdir of \"%v\". Please "+ + "completely remove the walletdir to start the node without wallet.", + bdkwallet.WalletDir(cfg.DataDir)) + btcdLog.Error(err) + return err + } } // Check if the user had been pruned before and do basic checks on indexers. From 74a1ee4bc028c4ff6dd49965ccedc56baf89feec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 29 Jan 2024 14:54:12 +0900 Subject: [PATCH 11/48] bdkwallet: implement conditional building only build with the BDK wallet if the 'bdkwallet' build tag is set. --- bdkwallet/bdkwallet.go | 231 +++++++++++++++ .../{wallet_test.go => bdkwallet_test.go} | 11 +- bdkwallet/manager.go | 21 +- bdkwallet/wallet.go | 278 ++++-------------- 4 files changed, 322 insertions(+), 219 deletions(-) create mode 100644 bdkwallet/bdkwallet.go rename bdkwallet/{wallet_test.go => bdkwallet_test.go} (88%) diff --git a/bdkwallet/bdkwallet.go b/bdkwallet/bdkwallet.go new file mode 100644 index 00000000..9e8f8f1c --- /dev/null +++ b/bdkwallet/bdkwallet.go @@ -0,0 +1,231 @@ +//go:build bdkwallet + +package bdkwallet + +//#cgo LDFLAGS: ./target/release/libbdkgo.a -ldl -lm +import "C" + +import ( + "bytes" + + "github.com/utreexo/utreexod/bdkwallet/bdkgo" + "github.com/utreexo/utreexod/btcutil" + "github.com/utreexo/utreexod/chaincfg" + "github.com/utreexo/utreexod/mempool" + "github.com/utreexo/utreexod/wire" +) + +func init() { + walletFactory = &BDKWalletFactory{} +} + +type BDKWalletFactory struct{} + +func (*BDKWalletFactory) Create(dbPath string, chainParams *chaincfg.Params) (Wallet, error) { + // used for the address format + // this is parsed as `bitcoin::Network` in rust + // supported strings: bitcoin, testnet, signet, regtest + // https://docs.rs/bitcoin/latest/bitcoin/network/enum.Network.html + network := chainParams.Name + log.Infof("Creating wallet with network: %v", network) + switch network { + case "mainnet": + network = "bitcoin" + case "testnet3": + network = "testnet" + } + + genesisHash := chainParams.GenesisHash.CloneBytes() + + inner, err := bdkgo.WalletCreateNew(dbPath, network, genesisHash) + if err != nil { + return nil, err + } + + // This increments the reference count of the Arc pointer in rust. We are + // doing this due to a bug with uniffi-bindgen-go's generated code + // decrementing this count too aggressively. + inner.IncrementReferenceCounter() + return &BDKWallet{*inner}, nil +} + +func (*BDKWalletFactory) Load(dbPath string) (Wallet, error) { + inner, err := bdkgo.WalletLoad(dbPath) + if err != nil { + return nil, err + } + + // This increments the reference count of the Arc pointer in rust. We are + // doing this due to a bug with uniffi-bindgen-go's generated code + // decrementing this count too aggressively. + inner.IncrementReferenceCounter() + return &BDKWallet{*inner}, nil +} + +// Wallet is a BDK wallet. +type BDKWallet struct { + inner bdkgo.Wallet +} + +// UnusedAddress returns the earliest address which have not received any funds. +func (w *BDKWallet) UnusedAddress() (uint, btcutil.Address, error) { + info, err := w.inner.LastUnusedAddress() + if err != nil { + return uint(info.Index), nil, err + } + addr, err := btcutil.DecodeAddress(info.Address, nil) + if err != nil { + return uint(info.Index), nil, err + } + return uint(info.Index), addr, nil +} + +// FreshAddress always returns a new address. This means it always increments +// the last derivation index even though the previous derivation indexes have +// not received funds. +func (w *BDKWallet) FreshAddress() (uint, btcutil.Address, error) { + info, err := w.inner.FreshAddress() + if err != nil { + return uint(info.Index), nil, err + } + addr, err := btcutil.DecodeAddress(info.Address, nil) + if err != nil { + return uint(info.Index), nil, err + } + return uint(info.Index), addr, nil +} + +// PeekAddress previews the address at the derivation index. This does not +// increment the last revealed index. +func (w *BDKWallet) PeekAddress(index uint32) (uint, btcutil.Address, error) { + info, err := w.inner.PeekAddress(uint32(index)) + if err != nil { + return uint(info.Index), nil, err + } + addr, err := btcutil.DecodeAddress(info.Address, nil) + if err != nil { + return uint(info.Index), nil, err + } + return uint(info.Index), addr, nil +} + +// Balance returns the balance of the wallet. +func (w *BDKWallet) Balance() Balance { + balance := w.inner.Balance() + return Balance{ + Immature: btcutil.Amount(balance.Immature), + TrustedPending: btcutil.Amount(balance.TrustedPending), + UntrustedPending: btcutil.Amount(balance.UntrustedPending), + Confirmed: btcutil.Amount(balance.Confirmed), + } +} + +// RecentBlocks returns the most recent blocks +func (w *BDKWallet) RecentBlocks(count uint32) []BlockId { + generatedCodeBlocks := w.inner.RecentBlocks(uint32(count)) + out := make([]BlockId, 0, len(generatedCodeBlocks)) + for _, block := range generatedCodeBlocks { + out = append(out, BlockId{ + Height: uint(block.Height), + Hash: hashFromBytes(block.Hash), + }) + } + return out +} + +// ApplyBlock updates the wallet with the given block. +func (w *BDKWallet) ApplyBlock(block *btcutil.Block) error { + var b bytes.Buffer + if err := block.MsgBlock().BtcEncode(&b, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { + return err + } + bheight := uint32(block.Height()) + res, err := w.inner.ApplyBlock(bheight, b.Bytes()) + if err != nil { + return err + } + bhash := block.Hash() + for _, genTxid := range res.RelevantTxids { + txid := hashFromBytes(genTxid) + log.Infof("Found relevant tx %v in block %v:%v.", txid, bheight, bhash) + } + return nil +} + +// ApplyMempoolTransactions updates the wallet with the given mempool transactions. +func (w *BDKWallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { + if len(txns) == 0 { + return nil + } + genTxns := make([]bdkgo.MempoolTx, 0, len(txns)) + for _, tx := range txns { + var txb bytes.Buffer + if err := tx.Tx.MsgTx().BtcEncode(&txb, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { + return err + } + genTxns = append(genTxns, bdkgo.MempoolTx{ + Tx: txb.Bytes(), + AddedUnix: uint64(tx.Added.Unix()), + }) + } + res, err := w.inner.ApplyMempool(genTxns) + if err != nil { + return err + } + for _, genTxid := range res.RelevantTxids { + txid := *(*[32]byte)(genTxid) + log.Infof("Found relevant tx %v in mempool.", txid) + } + return nil +} + +// CreateTx creates and signs a transaction spending from the wallet. +func (w *BDKWallet) CreateTx(feerate float32, recipients []Recipient) ([]byte, error) { + genRecipients := make([]bdkgo.Recipient, 0, len(recipients)) + for _, r := range recipients { + genRecipients = append(genRecipients, bdkgo.Recipient{ + ScriptPubkey: r.Address.ScriptAddress(), + Amount: uint64(r.Amount), + }) + } + return w.inner.CreateTx(feerate, genRecipients) +} + +// MnemonicWords returns the mnemonic words to backup the wallet. +func (w *BDKWallet) MnemonicWords() []string { + return w.inner.MnemonicWords() +} + +// Transactions returns the list of wallet transactions. +func (w *BDKWallet) Transactions() []TxInfo { + genOut := w.inner.Transactions() + out := make([]TxInfo, 0, len(genOut)) + for _, info := range genOut { + out = append(out, TxInfo{ + Txid: hashFromBytes(info.Txid), + Tx: txFromBytes(info.Tx), + Spent: btcutil.Amount(info.Spent), + Received: btcutil.Amount(info.Received), + Confirmations: info.Confirmations, + }) + } + return out +} + +// Utxos returns the list of wallet UTXOs. +func (w *BDKWallet) UTXOs() []UTXOInfo { + genOut := w.inner.Utxos() + out := make([]UTXOInfo, 0, len(genOut)) + for _, info := range genOut { + out = append(out, UTXOInfo{ + Txid: hashFromBytes(info.Txid), + Vout: uint(info.Vout), + Amount: btcutil.Amount(info.Amount), + ScriptPubKey: info.ScriptPubkey, + IsChange: info.IsChange, + DerivationIndex: uint(info.DerivationIndex), + Confirmations: info.Confirmations, + }) + } + return out +} diff --git a/bdkwallet/wallet_test.go b/bdkwallet/bdkwallet_test.go similarity index 88% rename from bdkwallet/wallet_test.go rename to bdkwallet/bdkwallet_test.go index fb2cae83..b06766a8 100644 --- a/bdkwallet/wallet_test.go +++ b/bdkwallet/bdkwallet_test.go @@ -1,3 +1,5 @@ +//go:build bdkwallet + package bdkwallet import ( @@ -8,10 +10,15 @@ import ( ) func TestCreateAndLoad(t *testing.T) { + factory, err := factory() + if err != nil { + t.Fatal(err) + } + dbPath := filepath.Join(t.TempDir(), "bdk.db") { - wallet, err := Create(dbPath, &chaincfg.MainNetParams) + wallet, err := factory.Create(dbPath, &chaincfg.MainNetParams) if err != nil { t.Fatalf("failed to create db: %v", err) } @@ -50,7 +57,7 @@ func TestCreateAndLoad(t *testing.T) { // load wallet and see what happens { - wallet, err := Load(dbPath) + wallet, err := factory.Load(dbPath) if err != nil { t.Fatalf("failed to load wallet: %v", err) } diff --git a/bdkwallet/manager.go b/bdkwallet/manager.go index 5195ea0d..3e8f2f35 100644 --- a/bdkwallet/manager.go +++ b/bdkwallet/manager.go @@ -42,29 +42,34 @@ func DoesWalletDirExist(dataDir string) (bool, error) { } func NewManager(config ManagerConfig) (*Manager, error) { + factory, err := factory() + if err != nil { + return nil, err + } + walletDir := WalletDir(config.DataDir) if err := os.MkdirAll(walletDir, os.ModePerm); err != nil { return nil, err } dbPath := filepath.Join(walletDir, defaultWalletFileName) - var wallet *Wallet + var wallet Wallet if _, err := os.Stat(dbPath); err != nil { if !os.IsNotExist(err) { return nil, err } - if wallet, err = Create(dbPath, config.ChainParams); err != nil { + if wallet, err = factory.Create(dbPath, config.ChainParams); err != nil { return nil, err } } else { - if wallet, err = Load(dbPath); err != nil { + if wallet, err = factory.Load(dbPath); err != nil { return nil, err } } m := Manager{ config: config, - wallet: *wallet, + wallet: wallet, } if config.Chain != nil { // Subscribe to new blocks/reorged blocks. @@ -76,12 +81,20 @@ func NewManager(config ManagerConfig) (*Manager, error) { } func (m *Manager) NotifyNewTransactions(txns []*mempool.TxDesc) { + if m.wallet == nil { + return + } + if err := m.wallet.ApplyMempoolTransactions(txns); err != nil { log.Errorf("Failed to apply mempool txs to the wallet. %v", err) } } func (m *Manager) handleBlockchainNotification(notification *blockchain.Notification) { + if m.wallet == nil { + return + } + switch notification.Type { // A block has been accepted into the block chain. case blockchain.NTBlockConnected: diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go index 473a1655..315dffd3 100644 --- a/bdkwallet/wallet.go +++ b/bdkwallet/wallet.go @@ -1,13 +1,9 @@ package bdkwallet -//#cgo LDFLAGS: ./target/release/libbdkgo.a -ldl -lm -import "C" - import ( "bytes" "errors" - "github.com/utreexo/utreexod/bdkwallet/bdkgo" "github.com/utreexo/utreexod/btcutil" "github.com/utreexo/utreexod/chaincfg" "github.com/utreexo/utreexod/chaincfg/chainhash" @@ -15,36 +11,61 @@ import ( "github.com/utreexo/utreexod/wire" ) -var ErrNoRecipient = errors.New("must have atleast one recipient") +var ( + ErrNoRecipient = errors.New("must have atleast one recipient") + ErrNoBDK = errors.New("utreexod must be built with the 'bdkwallet' tag to enable the BDK wallet") +) + +var walletFactory WalletFactory + +func factory() (WalletFactory, error) { + if walletFactory == nil { + return nil, ErrNoBDK + } + return walletFactory, nil +} + +type WalletFactory interface { + Create(dbPath string, chainParams *chaincfg.Params) (Wallet, error) + Load(dbPath string) (Wallet, error) +} + +type Wallet interface { + UnusedAddress() (uint, btcutil.Address, error) + FreshAddress() (uint, btcutil.Address, error) + PeekAddress(index uint32) (uint, btcutil.Address, error) + Balance() Balance + RecentBlocks(count uint32) []BlockId + ApplyBlock(block *btcutil.Block) error + ApplyMempoolTransactions(txns []*mempool.TxDesc) error + CreateTx(feerate float32, recipients []Recipient) ([]byte, error) + MnemonicWords() []string + Transactions() []TxInfo + UTXOs() []UTXOInfo +} // Balance in satoshis. type Balance struct { - bdkgo.Balance + Immature btcutil.Amount + TrustedPending btcutil.Amount + UntrustedPending btcutil.Amount + Confirmed btcutil.Amount } // TrustedSpendable are funds that are safe to spend. -func (b *Balance) TrustedSpendable() uint64 { +func (b *Balance) TrustedSpendable() btcutil.Amount { return b.Confirmed + b.TrustedPending } // Total is the total funds of the wallet. -func (b *Balance) Total() uint64 { +func (b *Balance) Total() btcutil.Amount { return b.Immature + b.TrustedPending + b.UntrustedPending + b.Confirmed } // BlockId consists of a block height and a block hash. This identifies a block. type BlockId struct { - bdkgo.BlockId -} - -// Height gets the block height. -func (id *BlockId) Height() uint32 { - return id.BlockId.Height -} - -// Hash gets the block hash. -func (id *BlockId) Hash() chainhash.Hash { - return *(*[32]byte)(id.BlockId.Hash) + Height uint + Hash chainhash.Hash } // Recipient specifies the intended amount and destination address for a transaction output. @@ -54,211 +75,42 @@ type Recipient struct { } // TxInfo is information on a given transaction. -type TxInfo struct{ bdkgo.TxInfo } - -func (tx *TxInfo) Txid() chainhash.Hash { - return *(*[32]byte)(tx.TxInfo.Txid) -} - -func (tx *TxInfo) Tx() btcutil.Tx { - var msgTx wire.MsgTx - if err := msgTx.BtcDecode(bytes.NewReader(tx.TxInfo.Tx), wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { - panic("Must decode tx from rust.") - } - return *btcutil.NewTx(&msgTx) +type TxInfo struct { + Txid chainhash.Hash + Tx btcutil.Tx + Spent btcutil.Amount + Received btcutil.Amount + Confirmations *uint32 } // UtxoInfo is information on a given transaction. -type UtxoInfo struct{ bdkgo.UtxoInfo } - -func (utxo *UtxoInfo) Txid() chainhash.Hash { - return *(*[32]byte)(utxo.UtxoInfo.Txid) -} - -// Wallet is a BDK wallet. -type Wallet struct { - inner bdkgo.Wallet -} - -// Create creates a fresh new wallet. -func Create(dbPath string, chainParams *chaincfg.Params) (*Wallet, error) { - // used for the address format - // this is parsed as `bitcoin::Network` in rust - // supported strings: bitcoin, testnet, signet, regtest - // https://docs.rs/bitcoin/latest/bitcoin/network/enum.Network.html - network := chainParams.Name - log.Infof("Creating wallet with network: %v", network) - switch network { - case "mainnet": - network = "bitcoin" - case "testnet3": - network = "testnet" - } - - genesisHash := chainParams.GenesisHash.CloneBytes() - - inner, err := bdkgo.WalletCreateNew(dbPath, network, genesisHash) - if err != nil { - return nil, err - } - - // This increments the reference count of the Arc pointer in rust. We are - // doing this due to a bug with uniffi-bindgen-go's generated code - // decrementing this count too aggressively. - inner.IncrementReferenceCounter() - return &Wallet{*inner}, nil -} - -// Load loads an existing wallet from file. -func Load(dbPath string) (*Wallet, error) { - inner, err := bdkgo.WalletLoad(dbPath) - if err != nil { - return nil, err - } - - // This increments the reference count of the Arc pointer in rust. We are - // doing this due to a bug with uniffi-bindgen-go's generated code - // decrementing this count too aggressively. - inner.IncrementReferenceCounter() - return &Wallet{*inner}, nil +type UTXOInfo struct { + Txid chainhash.Hash + Vout uint + Amount btcutil.Amount + ScriptPubKey []byte + IsChange bool + DerivationIndex uint + Confirmations *uint32 } -// UnusedAddress returns the earliest address which have not received any funds. -func (w *Wallet) UnusedAddress() (uint32, btcutil.Address, error) { - info, err := w.inner.LastUnusedAddress() - if err != nil { - return info.Index, nil, err - } - addr, err := btcutil.DecodeAddress(info.Address, nil) - if err != nil { - return info.Index, nil, err - } - return info.Index, addr, nil +func hashFromBytes(b []byte) chainhash.Hash { + return *(*[32]byte)(b) } -// FreshAddress always returns a new address. This means it always increments -// the last derivation index even though the previous derivation indexes have -// not received funds. -func (w *Wallet) FreshAddress() (uint32, btcutil.Address, error) { - info, err := w.inner.FreshAddress() - if err != nil { - return info.Index, nil, err - } - addr, err := btcutil.DecodeAddress(info.Address, nil) - if err != nil { - return info.Index, nil, err - } - return info.Index, addr, nil -} - -// PeekAddress previews the address at the derivation index. This does not -// increment the last revealed index. -func (w *Wallet) PeekAddress(index uint32) (uint32, btcutil.Address, error) { - info, err := w.inner.PeekAddress(index) - if err != nil { - return info.Index, nil, err - } - addr, err := btcutil.DecodeAddress(info.Address, nil) - if err != nil { - return info.Index, nil, err - } - return info.Index, addr, nil -} - -// Balance returns the balance of the wallet. -func (w *Wallet) Balance() Balance { - return Balance{w.inner.Balance()} -} - -// RecentBlocks returns the most recent blocks -func (w *Wallet) RecentBlocks(count uint32) []BlockId { - generatedCodeBlocks := w.inner.RecentBlocks(count) - out := make([]BlockId, 0, len(generatedCodeBlocks)) - for _, block := range generatedCodeBlocks { - out = append(out, BlockId{block}) - } - return out -} - -// ApplyBlock updates the wallet with the given block. -func (w *Wallet) ApplyBlock(block *btcutil.Block) error { - var b bytes.Buffer - if err := block.MsgBlock().BtcEncode(&b, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { - return err - } - bheight := uint32(block.Height()) - res, err := w.inner.ApplyBlock(bheight, b.Bytes()) - if err != nil { - return err - } - bhash := block.Hash() - for _, genTxid := range res.RelevantTxids { - txid := *(*[32]byte)(genTxid) - log.Infof("Found relevant tx %v in block %v:%v.", txid, bheight, bhash) +func txFromBytes(b []byte) btcutil.Tx { + var msgTx wire.MsgTx + if err := msgTx.BtcDecode(bytes.NewReader(b), wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { + panic("must decode tx consensus bytes from rust") } - return nil + return *btcutil.NewTx(&msgTx) } -// ApplyMempoolTransactions updates the wallet with the given mempool transactions. -func (w *Wallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { - if len(txns) == 0 { +func uintPointerFromUint32Pointer(v *uint32) *uint { + if v == nil { return nil } - genTxns := make([]bdkgo.MempoolTx, 0, len(txns)) - for _, tx := range txns { - var txb bytes.Buffer - if err := tx.Tx.MsgTx().BtcEncode(&txb, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { - return err - } - genTxns = append(genTxns, bdkgo.MempoolTx{ - Tx: txb.Bytes(), - AddedUnix: uint64(tx.Added.Unix()), - }) - } - res, err := w.inner.ApplyMempool(genTxns) - if err != nil { - return err - } - for _, genTxid := range res.RelevantTxids { - txid := *(*[32]byte)(genTxid) - log.Infof("Found relevant tx %v in mempool.", txid) - } - return nil -} - -// CreateTx creates and signs a transaction spending from the wallet. -func (w *Wallet) CreateTx(feerate float32, recipients []Recipient) ([]byte, error) { - genRecipients := make([]bdkgo.Recipient, 0, len(recipients)) - for _, r := range recipients { - genRecipients = append(genRecipients, bdkgo.Recipient{ - ScriptPubkey: r.Address.ScriptAddress(), - Amount: uint64(r.Amount), - }) - } - return w.inner.CreateTx(feerate, genRecipients) -} - -// MnemonicWords returns the mnemonic words to backup the wallet. -func (w *Wallet) MnemonicWords() []string { - return w.inner.MnemonicWords() -} - -// Transactions returns the list of wallet transactions. -func (w *Wallet) Transactions() []TxInfo { - genOut := w.inner.Transactions() - out := make([]TxInfo, 0, len(genOut)) - for _, info := range genOut { - out = append(out, TxInfo{info}) - } - return out -} -// Utxos returns the list of wallet UTXOs. -func (w *Wallet) Utxos() []UtxoInfo { - genOut := w.inner.Utxos() - out := make([]UtxoInfo, 0, len(genOut)) - for _, info := range genOut { - out = append(out, UtxoInfo{info}) - } - return out + v2 := uint(*v) + return &v2 } From aeb2485a78c5f451e1bdb1e2ab149a90ec3ca85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 29 Jan 2024 15:55:29 +0900 Subject: [PATCH 12/48] bdkwallet: fix confirmations logic + docs --- bdkwallet/bdkgo/bdkgo.go | 53 +++++------------------------ bdkwallet/bdkgo_crate/src/bdkgo.udl | 4 +-- bdkwallet/bdkgo_crate/src/lib.rs | 24 +++++++++---- bdkwallet/bdkwallet.go | 4 +-- bdkwallet/wallet.go | 29 +++++++++------- 5 files changed, 46 insertions(+), 68 deletions(-) diff --git a/bdkwallet/bdkgo/bdkgo.go b/bdkwallet/bdkgo/bdkgo.go index f50b28bf..1c1130c2 100644 --- a/bdkwallet/bdkgo/bdkgo.go +++ b/bdkwallet/bdkgo/bdkgo.go @@ -1207,7 +1207,7 @@ type TxInfo struct { Tx []byte Spent uint64 Received uint64 - Confirmations *uint32 + Confirmations uint32 } func (r *TxInfo) Destroy() { @@ -1215,7 +1215,7 @@ func (r *TxInfo) Destroy() { FfiDestroyerBytes{}.Destroy(r.Tx) FfiDestroyerUint64{}.Destroy(r.Spent) FfiDestroyerUint64{}.Destroy(r.Received) - FfiDestroyerOptionalUint32{}.Destroy(r.Confirmations) + FfiDestroyerUint32{}.Destroy(r.Confirmations) } type FfiConverterTypeTxInfo struct{} @@ -1232,7 +1232,7 @@ func (c FfiConverterTypeTxInfo) Read(reader io.Reader) TxInfo { FfiConverterBytesINSTANCE.Read(reader), FfiConverterUint64INSTANCE.Read(reader), FfiConverterUint64INSTANCE.Read(reader), - FfiConverterOptionalUint32INSTANCE.Read(reader), + FfiConverterUint32INSTANCE.Read(reader), } } @@ -1245,7 +1245,7 @@ func (c FfiConverterTypeTxInfo) Write(writer io.Writer, value TxInfo) { FfiConverterBytesINSTANCE.Write(writer, value.Tx) FfiConverterUint64INSTANCE.Write(writer, value.Spent) FfiConverterUint64INSTANCE.Write(writer, value.Received) - FfiConverterOptionalUint32INSTANCE.Write(writer, value.Confirmations) + FfiConverterUint32INSTANCE.Write(writer, value.Confirmations) } type FfiDestroyerTypeTxInfo struct{} @@ -1261,7 +1261,7 @@ type UtxoInfo struct { ScriptPubkey []byte IsChange bool DerivationIndex uint32 - Confirmations *uint32 + Confirmations uint32 } func (r *UtxoInfo) Destroy() { @@ -1271,7 +1271,7 @@ func (r *UtxoInfo) Destroy() { FfiDestroyerBytes{}.Destroy(r.ScriptPubkey) FfiDestroyerBool{}.Destroy(r.IsChange) FfiDestroyerUint32{}.Destroy(r.DerivationIndex) - FfiDestroyerOptionalUint32{}.Destroy(r.Confirmations) + FfiDestroyerUint32{}.Destroy(r.Confirmations) } type FfiConverterTypeUtxoInfo struct{} @@ -1290,7 +1290,7 @@ func (c FfiConverterTypeUtxoInfo) Read(reader io.Reader) UtxoInfo { FfiConverterBytesINSTANCE.Read(reader), FfiConverterBoolINSTANCE.Read(reader), FfiConverterUint32INSTANCE.Read(reader), - FfiConverterOptionalUint32INSTANCE.Read(reader), + FfiConverterUint32INSTANCE.Read(reader), } } @@ -1305,7 +1305,7 @@ func (c FfiConverterTypeUtxoInfo) Write(writer io.Writer, value UtxoInfo) { FfiConverterBytesINSTANCE.Write(writer, value.ScriptPubkey) FfiConverterBoolINSTANCE.Write(writer, value.IsChange) FfiConverterUint32INSTANCE.Write(writer, value.DerivationIndex) - FfiConverterOptionalUint32INSTANCE.Write(writer, value.Confirmations) + FfiConverterUint32INSTANCE.Write(writer, value.Confirmations) } type FfiDestroyerTypeUtxoInfo struct{} @@ -1935,43 +1935,6 @@ func (c FfiConverterTypeLoadError) Write(writer io.Writer, value *LoadError) { } } -type FfiConverterOptionalUint32 struct{} - -var FfiConverterOptionalUint32INSTANCE = FfiConverterOptionalUint32{} - -func (c FfiConverterOptionalUint32) Lift(rb RustBufferI) *uint32 { - return LiftFromRustBuffer[*uint32](c, rb) -} - -func (_ FfiConverterOptionalUint32) Read(reader io.Reader) *uint32 { - if readInt8(reader) == 0 { - return nil - } - temp := FfiConverterUint32INSTANCE.Read(reader) - return &temp -} - -func (c FfiConverterOptionalUint32) Lower(value *uint32) RustBuffer { - return LowerIntoRustBuffer[*uint32](c, value) -} - -func (_ FfiConverterOptionalUint32) Write(writer io.Writer, value *uint32) { - if value == nil { - writeInt8(writer, 0) - } else { - writeInt8(writer, 1) - FfiConverterUint32INSTANCE.Write(writer, *value) - } -} - -type FfiDestroyerOptionalUint32 struct{} - -func (_ FfiDestroyerOptionalUint32) Destroy(value *uint32) { - if value != nil { - FfiDestroyerUint32{}.Destroy(*value) - } -} - type FfiConverterSequenceString struct{} var FfiConverterSequenceStringINSTANCE = FfiConverterSequenceString{} diff --git a/bdkwallet/bdkgo_crate/src/bdkgo.udl b/bdkwallet/bdkgo_crate/src/bdkgo.udl index 66df3092..ff9de0a5 100644 --- a/bdkwallet/bdkgo_crate/src/bdkgo.udl +++ b/bdkwallet/bdkgo_crate/src/bdkgo.udl @@ -106,7 +106,7 @@ dictionary TxInfo { bytes tx; u64 spent; u64 received; - u32? confirmations; + u32 confirmations; }; dictionary UtxoInfo { @@ -116,7 +116,7 @@ dictionary UtxoInfo { bytes script_pubkey; boolean is_change; u32 derivation_index; - u32? confirmations; + u32 confirmations; }; dictionary MempoolTx { diff --git a/bdkwallet/bdkgo_crate/src/lib.rs b/bdkwallet/bdkgo_crate/src/lib.rs index 116fc5c3..4d010c98 100644 --- a/bdkwallet/bdkgo_crate/src/lib.rs +++ b/bdkwallet/bdkgo_crate/src/lib.rs @@ -319,7 +319,10 @@ impl Wallet { Ok(res) } - pub fn apply_mempool(self: Arc, txs: Vec) -> Result { + pub fn apply_mempool( + self: Arc, + txs: Vec, + ) -> Result { self.increment_reference_counter(); let mut wallet = self.inner.lock().unwrap(); let txs = txs @@ -393,7 +396,7 @@ impl Wallet { let confirmations = ctx .chain_position .confirmation_height_upper_bound() - .map(|conf_height| height.saturating_sub(conf_height)); + .map_or(0, |conf_height| (1 + height).saturating_sub(conf_height)); TxInfo { txid, tx, @@ -422,9 +425,9 @@ impl Wallet { derivation_index: utxo.derivation_index, confirmations: match utxo.confirmation_time { bdk::chain::ConfirmationTime::Confirmed { height, .. } => { - Some(wallet_height.saturating_sub(height)) + (1 + wallet_height).saturating_sub(height) } - bdk::chain::ConfirmationTime::Unconfirmed { .. } => None, + bdk::chain::ConfirmationTime::Unconfirmed { .. } => 0, }, }) .collect::>(); @@ -451,7 +454,7 @@ pub struct TxInfo { /// Sum of outputs containing owned script pubkeys. pub received: u64, /// How confirmed is this transaction? - pub confirmations: Option, + pub confirmations: u32, } pub struct UtxoInfo { @@ -461,7 +464,7 @@ pub struct UtxoInfo { pub script_pubkey: Vec, pub is_change: bool, pub derivation_index: u32, - pub confirmations: Option, + pub confirmations: u32, } pub struct MempoolTx { @@ -475,7 +478,14 @@ pub struct ApplyResult { impl ApplyResult { pub fn new(wallet: &BdkWallet) -> Self { - let relevant_txids = wallet.staged().indexed_tx_graph.graph.txs.iter().map(|tx| tx.txid().to_byte_array().to_vec()).collect::>(); + let relevant_txids = wallet + .staged() + .indexed_tx_graph + .graph + .txs + .iter() + .map(|tx| tx.txid().to_byte_array().to_vec()) + .collect::>(); Self { relevant_txids } } } diff --git a/bdkwallet/bdkwallet.go b/bdkwallet/bdkwallet.go index 9e8f8f1c..57f222f3 100644 --- a/bdkwallet/bdkwallet.go +++ b/bdkwallet/bdkwallet.go @@ -206,7 +206,7 @@ func (w *BDKWallet) Transactions() []TxInfo { Tx: txFromBytes(info.Tx), Spent: btcutil.Amount(info.Spent), Received: btcutil.Amount(info.Received), - Confirmations: info.Confirmations, + Confirmations: uint(info.Confirmations), }) } return out @@ -224,7 +224,7 @@ func (w *BDKWallet) UTXOs() []UTXOInfo { ScriptPubKey: info.ScriptPubkey, IsChange: info.IsChange, DerivationIndex: uint(info.DerivationIndex), - Confirmations: info.Confirmations, + Confirmations: uint(info.Confirmations), }) } return out diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go index 315dffd3..4da7f1f4 100644 --- a/bdkwallet/wallet.go +++ b/bdkwallet/wallet.go @@ -16,8 +16,10 @@ var ( ErrNoBDK = errors.New("utreexod must be built with the 'bdkwallet' tag to enable the BDK wallet") ) +// walletFactory is nil unless we build with the 'bdkwallet' build tag. var walletFactory WalletFactory +// factory returns the wallet factory (if it exists). Otherwise, an error will be returned. func factory() (WalletFactory, error) { if walletFactory == nil { return nil, ErrNoBDK @@ -25,11 +27,14 @@ func factory() (WalletFactory, error) { return walletFactory, nil } +// WalletFactory creates wallets. type WalletFactory interface { Create(dbPath string, chainParams *chaincfg.Params) (Wallet, error) Load(dbPath string) (Wallet, error) } +// Wallet tracks addresses and transactions sending/receiving to/from those addresses. The wallet is +// updated by incoming blocks and new mempool transactions. type Wallet interface { UnusedAddress() (uint, btcutil.Address, error) FreshAddress() (uint, btcutil.Address, error) @@ -46,10 +51,10 @@ type Wallet interface { // Balance in satoshis. type Balance struct { - Immature btcutil.Amount - TrustedPending btcutil.Amount - UntrustedPending btcutil.Amount - Confirmed btcutil.Amount + Immature btcutil.Amount // immature coinbase balance + TrustedPending btcutil.Amount // unconfirmed balance that is part of our change keychain + UntrustedPending btcutil.Amount // unconfirmed balance that is part of our public keychain + Confirmed btcutil.Amount // confirmed balance } // TrustedSpendable are funds that are safe to spend. @@ -64,23 +69,23 @@ func (b *Balance) Total() btcutil.Amount { // BlockId consists of a block height and a block hash. This identifies a block. type BlockId struct { - Height uint - Hash chainhash.Hash + Height uint // block height + Hash chainhash.Hash // block hash } // Recipient specifies the intended amount and destination address for a transaction output. type Recipient struct { - Amount btcutil.Amount - Address btcutil.Address + Amount btcutil.Amount // amount to send + Address btcutil.Address // recipient address to send to } // TxInfo is information on a given transaction. type TxInfo struct { Txid chainhash.Hash Tx btcutil.Tx - Spent btcutil.Amount - Received btcutil.Amount - Confirmations *uint32 + Spent btcutil.Amount // sum of owned inputs + Received btcutil.Amount // sum of owned outputs + Confirmations uint // number of confirmations for this tx } // UtxoInfo is information on a given transaction. @@ -91,7 +96,7 @@ type UTXOInfo struct { ScriptPubKey []byte IsChange bool DerivationIndex uint - Confirmations *uint32 + Confirmations uint // number of confirmations for this utxo } func hashFromBytes(b []byte) chainhash.Hash { From a5f0b10dd6a772122c37bf940c0f9551fc453ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 29 Jan 2024 16:29:09 +0900 Subject: [PATCH 13/48] main: add Makefile --- Makefile | 29 +++++++++++++++++++++++++++++ buildbdkgo.sh | 5 ----- 2 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 Makefile delete mode 100755 buildbdkgo.sh diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..73a1ae4c --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +help: ## Display help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +install-rust: ## Install rust if not installed + if ! type cargo &> /dev/null; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh; fi + +install-uniffi-bindgen-go: install-rust ## Install uniffi-bindgen-go if not installed + if ! type uniffi-bindgen-go &> /dev/null; then cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.2.0+v0.25.0; fi + +build-bdk: install-uniffi-bindgen-go ## Build BDK static library + uniffi-bindgen-go -o bdkwallet bdkwallet/bdkgo_crate/src/bdkgo.udl + cargo build --release + +build-utreexod: build-bdk ## Build utreexod with all features + go build ./... --tags=bdkwallet + +build-utreexod-without-bdk: ## Build utreexod without BDK wallet + go build ./... + +test: + cargo test + sh ./goclean.sh + +all: build-bdk build-utreexod + +clean: + rm utreexod + go clean + cargo clean diff --git a/buildbdkgo.sh b/buildbdkgo.sh deleted file mode 100755 index 51b81c6e..00000000 --- a/buildbdkgo.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.2.0+v0.25.0 && \ -cargo build --release && \ -uniffi-bindgen-go -o bdkwallet bdkwallet/bdkgo_crate/src/bdkgo.udl From 5e9009906ba434a607b76db6482e14bf009542f2 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Mon, 29 Jan 2024 21:36:07 +0900 Subject: [PATCH 14/48] main: don't run linter on generated files --- goclean.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/goclean.sh b/goclean.sh index e77d580b..cadfaa7a 100755 --- a/goclean.sh +++ b/goclean.sh @@ -14,4 +14,5 @@ golangci-lint run --deadline=10m --disable-all \ --enable=gofmt \ --enable=vet \ --enable=gosimple \ ---enable=unconvert +--enable=unconvert \ +--skip-dirs=bdkwallet/bdkgo # these are generated files From 675027c3d76c4e8d831dec513d7f26d1b2fa6230 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 17:20:12 +0900 Subject: [PATCH 15/48] bdkwallet: export Wallet and add comments --- bdkwallet/manager.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bdkwallet/manager.go b/bdkwallet/manager.go index 3e8f2f35..614cfbb2 100644 --- a/bdkwallet/manager.go +++ b/bdkwallet/manager.go @@ -21,9 +21,14 @@ type ManagerConfig struct { DataDir string } +// Manager handles the configuration and handling data in between the utreexo node +// and the bdk wallet library. type Manager struct { config ManagerConfig - wallet Wallet // wallet does not need a mutex as it's done in Rust + + // Wallet is the underlying wallet that calls out to the + // bdk rust library. + Wallet Wallet // wallet does not need a mutex as it's done in Rust } func WalletDir(dataDir string) string { @@ -69,7 +74,7 @@ func NewManager(config ManagerConfig) (*Manager, error) { m := Manager{ config: config, - wallet: wallet, + Wallet: wallet, } if config.Chain != nil { // Subscribe to new blocks/reorged blocks. @@ -81,17 +86,17 @@ func NewManager(config ManagerConfig) (*Manager, error) { } func (m *Manager) NotifyNewTransactions(txns []*mempool.TxDesc) { - if m.wallet == nil { + if m.Wallet == nil { return } - if err := m.wallet.ApplyMempoolTransactions(txns); err != nil { + if err := m.Wallet.ApplyMempoolTransactions(txns); err != nil { log.Errorf("Failed to apply mempool txs to the wallet. %v", err) } } func (m *Manager) handleBlockchainNotification(notification *blockchain.Notification) { - if m.wallet == nil { + if m.Wallet == nil { return } @@ -103,7 +108,7 @@ func (m *Manager) handleBlockchainNotification(notification *blockchain.Notifica log.Warnf("Chain connected notification is not a block.") return } - err := m.wallet.ApplyBlock(block) + err := m.Wallet.ApplyBlock(block) if err != nil { log.Criticalf("Couldn't apply block to the wallet. %v", err) } From 4e6fc6fc935c7e6fb406b2a560b23a04c46dd59f Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 17:21:33 +0900 Subject: [PATCH 16/48] main: clarify that we do handle wallet commands, just not btcwallet commands --- rpcserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcserver.go b/rpcserver.go index 2c09d09e..a5332c92 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -120,7 +120,7 @@ var ( // command is recognized as a wallet command. ErrRPCNoWallet = &btcjson.RPCError{ Code: btcjson.ErrRPCNoWallet, - Message: "This implementation does not implement wallet commands", + Message: "This implementation does not implement btcwallet commands", } ) From ccfca83587c04eabe3b4e00588d7074a14f84ef3 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 17:23:14 +0900 Subject: [PATCH 17/48] main: add bdkwallet to the rpcserver --- rpcserver.go | 4 ++++ server.go | 1 + 2 files changed, 5 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index a5332c92..671ddee3 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -29,6 +29,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/websocket" + "github.com/utreexo/utreexod/bdkwallet" "github.com/utreexo/utreexod/blockchain" "github.com/utreexo/utreexod/blockchain/indexers" "github.com/utreexo/utreexod/btcjson" @@ -5248,6 +5249,9 @@ type rpcserverConfig struct { // WatchOnlyWallet keeps track of relevant utxos and its utreexo proof // for the given addresses and xpubs. WatchOnlyWallet *wallet.WatchOnlyWalletManager + + // BDKWallet is the underlying bdk wallet that is a part of this node. + BDKWallet *bdkwallet.Manager } // newRPCServer returns a new instance of the rpcServer struct. diff --git a/server.go b/server.go index f6faa02e..f188d5b4 100644 --- a/server.go +++ b/server.go @@ -3585,6 +3585,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, FlatUtreexoProofIndex: s.flatUtreexoProofIndex, FeeEstimator: s.feeEstimator, WatchOnlyWallet: s.watchOnlyWallet, + BDKWallet: s.bdkWallet, }) if err != nil { return nil, err From 8ae10ac6707a33c2b796a94066c927d56ff8bfb6 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 17:47:07 +0900 Subject: [PATCH 18/48] btcjson: add support for unusedaddress command --- btcjson/chainsvrcmds.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 4f43444e..3e6565d9 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -1081,6 +1081,15 @@ func NewSubmitBlockCmd(hexBlock string, options *SubmitBlockOptions) *SubmitBloc } } +// UnusedAddressCmd defines the unusedaddress JSON-RPC command. +type UnusedAddressCmd struct{} + +// NewUnusedAddressCmd returns a new instance which can be used to issue an unusedaddress +// JSON-RPC command. +func NewUnusedAddressCmd() *UnusedAddressCmd { + return &UnusedAddressCmd{} +} + // UptimeCmd defines the uptime JSON-RPC command. type UptimeCmd struct{} @@ -1225,6 +1234,7 @@ func init() { MustRegisterCmd("signmessagewithprivkey", (*SignMessageWithPrivKeyCmd)(nil), flags) MustRegisterCmd("stop", (*StopCmd)(nil), flags) MustRegisterCmd("submitblock", (*SubmitBlockCmd)(nil), flags) + MustRegisterCmd("unusedaddress", (*UnusedAddressCmd)(nil), flags) MustRegisterCmd("uptime", (*UptimeCmd)(nil), flags) MustRegisterCmd("validateaddress", (*ValidateAddressCmd)(nil), flags) MustRegisterCmd("verifychain", (*VerifyChainCmd)(nil), flags) From 6fea917f52205c39bfcfa96422c5f84eab3d643f Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 17:47:41 +0900 Subject: [PATCH 19/48] btcjson: add bdkaddressresult bdkaddressresult is the result of the get address commands that bdk wallet supports. There are 3 commands (unusedaddress, freshaddress, peekaddress) but they all return the same thing. --- btcjson/chainsvrresults.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 59041c5d..ef81ab37 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -909,3 +909,10 @@ type ProveWatchOnlyChainTipInclusionVerboseResult struct { HashesProven []string `json:"hashesproven"` Hex string `json:"hex"` } + +// BDKAddressResult models the data for all rpc calls that the bdk wallet returns. +// This includes the following commands: unusedaddress, freshaddress, and peekaddress. +type BDKAddressResult struct { + Index int `json:"index"` + Address string `json:"address"` +} From 4c8dad1b445a782af07a8f4c3bfc7df46b209347 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 17:24:29 +0900 Subject: [PATCH 20/48] main: add unusedaddress support to rpcserver --- rpcserver.go | 15 +++++++++++++++ rpcserverhelp.go | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index 671ddee3..d226169c 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -186,6 +186,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "signmessagewithprivkey": handleSignMessageWithPrivKey, "stop": handleStop, "submitblock": handleSubmitBlock, + "unusedaddress": handleUnusedAddress, "uptime": handleUptime, "validateaddress": handleValidateAddress, "verifychain": handleVerifyChain, @@ -4152,6 +4153,20 @@ func handleSubmitBlock(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) return nil, nil } +// handleUnusedAddress implements the unusedaddress command. +func handleUnusedAddress(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + index, address, err := s.cfg.BDKWallet.Wallet.UnusedAddress() + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Failed to retrieve unused address: " + err.Error(), + } + } + + result := btcjson.BDKAddressResult{Index: int(index), Address: address.String()} + return result, nil +} + // handleUptime implements the uptime command. func handleUptime(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { return time.Now().Unix() - s.cfg.StartupTime, nil diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 98cd068b..3dfc422f 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -100,6 +100,10 @@ var helpDescsEnUS = map[string]string{ "txrawdecoderesult-vin": "The transaction inputs as JSON objects", "txrawdecoderesult-vout": "The transaction outputs as JSON objects", + // BDKAddressResult help. + "bdkaddressresult-index": "BIP86 index of the address", + "bdkaddressresult-address": "Taproot address that you can receive funds to", + // DecodeRawTransactionCmd help. "decoderawtransaction--synopsis": "Returns a JSON object representing the provided serialized, hex-encoded transaction.", "decoderawtransaction-hextx": "Serialized, hex-encoded transaction", @@ -791,6 +795,9 @@ var helpDescsEnUS = map[string]string{ "rescannedblock-hash": "Hash of the matching block.", "rescannedblock-transactions": "List of matching transactions, serialized and hex-encoded.", + // UnusedAddressCmd help. + "unusedaddress--synopsis": "Returns an address that never received funds from the bdkwallet.", + // Uptime help. "uptime--synopsis": "Returns the total uptime of the server.", "uptime--result0": "The number of seconds that the server has been running", @@ -868,6 +875,7 @@ var rpcResultTypes = map[string][]interface{}{ "signmessagewithprivkey": {(*string)(nil)}, "stop": {(*string)(nil)}, "submitblock": {nil, (*string)(nil)}, + "unusedaddress": {(*btcjson.BDKAddressResult)(nil)}, "uptime": {(*int64)(nil)}, "validateaddress": {(*btcjson.ValidateAddressChainResult)(nil)}, "verifychain": {(*bool)(nil)}, From 0f7709bf25dc9c53750ee7185308296fb17561eb Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 17:40:54 +0900 Subject: [PATCH 21/48] btcjson: add support for freshaddress command --- btcjson/chainsvrcmds.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 3e6565d9..51b9022d 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -149,6 +149,15 @@ type FundRawTransactionOpts struct { EstimateMode *EstimateSmartFeeMode `json:"estimate_mode,omitempty"` } +// FreshAddressCmd defines the freshaddress JSON-RPC command +type FreshAddressCmd struct{} + +// NewFreshAddressCmd returns a new instance which can be used to issue a +// freshaddress JSON-RPC command. +func NewFreshAddressCmd() *FreshAddressCmd { + return &FreshAddressCmd{} +} + // FundRawTransactionCmd defines the fundrawtransaction JSON-RPC command type FundRawTransactionCmd struct { HexTx string @@ -1180,6 +1189,7 @@ func init() { MustRegisterCmd("decoderawtransaction", (*DecodeRawTransactionCmd)(nil), flags) MustRegisterCmd("decodescript", (*DecodeScriptCmd)(nil), flags) MustRegisterCmd("deriveaddresses", (*DeriveAddressesCmd)(nil), flags) + MustRegisterCmd("freshaddress", (*FreshAddressCmd)(nil), flags) MustRegisterCmd("fundrawtransaction", (*FundRawTransactionCmd)(nil), flags) MustRegisterCmd("getaddednodeinfo", (*GetAddedNodeInfoCmd)(nil), flags) MustRegisterCmd("getbestblockhash", (*GetBestBlockHashCmd)(nil), flags) From b8852fb2ef3184009b911f7aadef63630c6503e9 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 17:44:09 +0900 Subject: [PATCH 22/48] main: add freshaddress support to rpcserver --- rpcserver.go | 15 +++++++++++++++ rpcserverhelp.go | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index d226169c..d6dca349 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -138,6 +138,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "decoderawtransaction": handleDecodeRawTransaction, "decodescript": handleDecodeScript, "estimatefee": handleEstimateFee, + "freshaddress": handleFreshAddress, "generate": handleGenerate, "getaddednodeinfo": handleGetAddedNodeInfo, "getbestblock": handleGetBestBlock, @@ -901,6 +902,20 @@ func handleEstimateFee(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) return float64(feeRate), nil } +// handleFreshAddress implements the freshaddress command. +func handleFreshAddress(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + index, address, err := s.cfg.BDKWallet.Wallet.FreshAddress() + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Failed to retrieve new address: " + err.Error(), + } + } + + result := btcjson.BDKAddressResult{Index: int(index), Address: address.String()} + return result, nil +} + // handleGenerate handles generate commands. func handleGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { // Respond with an error if there are no addresses to pay the diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 3dfc422f..f5f5f83f 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -128,6 +128,10 @@ var helpDescsEnUS = map[string]string{ "estimatefee--result0": "Estimated fee per kilobyte in satoshis for a block to " + "be mined in the next NumBlocks blocks.", + // FreshAddressCmd help. + "freshaddress--synopsis": "Returns an address of the next derivation index regardless of if the " + + "preivous derivation address has received funds or not.", + // GenerateCmd help "generate--synopsis": "Generates a set number of blocks (simnet or regtest only) and returns a JSON\n" + " array of their hashes.", @@ -827,6 +831,7 @@ var rpcResultTypes = map[string][]interface{}{ "decoderawtransaction": {(*btcjson.TxRawDecodeResult)(nil)}, "decodescript": {(*btcjson.DecodeScriptResult)(nil)}, "estimatefee": {(*float64)(nil)}, + "freshaddress": {(*btcjson.BDKAddressResult)(nil)}, "generate": {(*[]string)(nil)}, "getaddednodeinfo": {(*[]string)(nil), (*[]btcjson.GetAddedNodeInfoResult)(nil)}, "getbestblock": {(*btcjson.GetBestBlockResult)(nil)}, From caccd7d1824aa50db4c0e25d107ea3effa283f3d Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 18:04:40 +0900 Subject: [PATCH 23/48] btcjson: add peekaddress command --- btcjson/chainsvrcmds.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 51b9022d..64749ecc 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -830,6 +830,17 @@ func NewInvalidateBlockCmd(blockHash string) *InvalidateBlockCmd { } } +// PeekAddressCmd defines the peekaddress JSON-RPC command. +type PeekAddressCmd struct { + Index uint32 +} + +// NewPeekAddressCmd returns a new instance which can be used to issue a peekaddress JSON-RPC +// command. +func NewPeekAddressCmd() *PeekAddressCmd { + return &PeekAddressCmd{} +} + // PingCmd defines the ping JSON-RPC command. type PingCmd struct{} @@ -1232,6 +1243,7 @@ func init() { MustRegisterCmd("getwatchonlybalance", (*GetWatchOnlyBalanceCmd)(nil), flags) MustRegisterCmd("help", (*HelpCmd)(nil), flags) MustRegisterCmd("invalidateblock", (*InvalidateBlockCmd)(nil), flags) + MustRegisterCmd("peekaddress", (*PeekAddressCmd)(nil), flags) MustRegisterCmd("ping", (*PingCmd)(nil), flags) MustRegisterCmd("preciousblock", (*PreciousBlockCmd)(nil), flags) MustRegisterCmd("proveutxochaintipinclusion", (*ProveUtxoChainTipInclusionCmd)(nil), flags) From 6bbf5dcdb13b61ce1d3f85ad02aaed8ae6ba2578 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 18:05:10 +0900 Subject: [PATCH 24/48] main: add peekaddress command support to rpcserver --- rpcserver.go | 18 ++++++++++++++++++ rpcserverhelp.go | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index d6dca349..e1916b04 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -176,6 +176,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "invalidateblock": handleInvalidateBlock, "help": handleHelp, "node": handleNode, + "peekaddress": handlePeekAddress, "ping": handlePing, "proveutxochaintipinclusion": handleProveUtxoChainTipInclusion, "provewatchonlychaintipinclusion": handleProveWatchOnlyChainTipInclusion, @@ -2987,6 +2988,23 @@ func handleHelp(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (inter return help, nil } +// handlePeekAddress implements the peekaddress command. +func handlePeekAddress(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.PeekAddressCmd) + + index, address, err := s.cfg.BDKWallet.Wallet.PeekAddress(c.Index) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: fmt.Sprintf("Failed to retrieve address at index %v: %v", + c.Index, err), + } + } + + result := btcjson.BDKAddressResult{Index: int(index), Address: address.String()} + return result, nil +} + // handlePing implements the ping command. func handlePing(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { // Ask server to ping \o_ diff --git a/rpcserverhelp.go b/rpcserverhelp.go index f5f5f83f..106ea5c6 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -608,6 +608,10 @@ var helpDescsEnUS = map[string]string{ "help--result0": "List of commands", "help--result1": "Help for specified command", + // PeekAddressCmd help. + "peekaddress--synopsis": "Returns an address of the desired derivation index", + "peekaddress-index": "The desired derivation index you want to fetch the address at", + // PingCmd help. "ping--synopsis": "Queues a ping to be sent to each connected peer.\n" + "Ping times are provided by getpeerinfo via the pingtime and pingwait fields.", @@ -869,6 +873,7 @@ var rpcResultTypes = map[string][]interface{}{ "node": nil, "help": {(*string)(nil), (*string)(nil)}, "invalidateblock": nil, + "peekaddress": {(*btcjson.BDKAddressResult)(nil)}, "ping": nil, "proveutxochaintipinclusion": {(*btcjson.ProveUtxoChainTipInclusionVerboseResult)(nil)}, "provewatchonlychaintipinclusion": {(*btcjson.ProveWatchOnlyChainTipInclusionVerboseResult)(nil)}, From 9d2b740fc1ed2a14e67379d0cac863aee6b94400 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 18:21:22 +0900 Subject: [PATCH 25/48] btcjson: add support for balance command --- btcjson/chainsvrcmds.go | 10 ++++++++++ btcjson/chainsvrresults.go | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 64749ecc..b196871c 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -48,6 +48,15 @@ func NewAddNodeCmd(addr string, subCmd AddNodeSubCmd) *AddNodeCmd { } } +// BalanceCmd defines the balance JSON-RPC command. +type BalanceCmd struct{} + +// NewBalanceCmd returns a new instance which can be used to issue an balance +// JSON-RPC command. +func NewBalanceCmd() *BalanceCmd { + return &BalanceCmd{} +} + // TransactionInput represents the inputs to a transaction. Specifically a // transaction hash and output number pair. type TransactionInput struct { @@ -1196,6 +1205,7 @@ func init() { flags := UsageFlag(0) MustRegisterCmd("addnode", (*AddNodeCmd)(nil), flags) + MustRegisterCmd("balance", (*BalanceCmd)(nil), flags) MustRegisterCmd("createrawtransaction", (*CreateRawTransactionCmd)(nil), flags) MustRegisterCmd("decoderawtransaction", (*DecodeRawTransactionCmd)(nil), flags) MustRegisterCmd("decodescript", (*DecodeScriptCmd)(nil), flags) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index ef81ab37..ca84907a 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -15,6 +15,18 @@ import ( "github.com/utreexo/utreexod/wire" ) +// BalanceResult models the data from the balance command. +type BalanceResult struct { + // Immature is the coinbase balance that's not been confirmed 100 times. + Immature int64 `json:"immature"` + // TrustedPending is the balance that's part of our change keychain. + TrustedPending int64 `json:"trustedpending"` + // UntrustedPending is the balance that's part of our public keychain. + UntrustedPending int64 `json:"untrustedpending"` + // Confirmed is the confirmed balance. + Confirmed int64 `json:"confirmed"` +} + // GetBlockHeaderVerboseResult models the data from the getblockheader command when // the verbose flag is set. When the verbose flag is not set, getblockheader // returns a hex-encoded string. From 545bba00e99fdb3964ba1ee4a10651eb6b2c37fb Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 18:21:51 +0900 Subject: [PATCH 26/48] main: add balance command support to the rpcserver --- rpcserver.go | 12 ++++++++++++ rpcserverhelp.go | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index e1916b04..58de17b5 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -133,6 +133,7 @@ type commandHandler func(*rpcServer, interface{}, <-chan struct{}) (interface{}, var rpcHandlers map[string]commandHandler var rpcHandlersBeforeInit = map[string]commandHandler{ "addnode": handleAddNode, + "balance": handleBalance, "createrawtransaction": handleCreateRawTransaction, "debuglevel": handleDebugLevel, "decoderawtransaction": handleDecodeRawTransaction, @@ -416,6 +417,17 @@ func handleAddNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (in return nil, nil } +// handleBalance handles the balance command. +func handleBalance(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + balance := s.cfg.BDKWallet.Wallet.Balance() + return btcjson.BalanceResult{ + Immature: int64(balance.Immature), + TrustedPending: int64(balance.TrustedPending), + UntrustedPending: int64(balance.UntrustedPending), + Confirmed: int64(balance.Confirmed), + }, nil +} + // handleNode handles node commands. func handleNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.NodeCmd) diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 106ea5c6..d1f7c4a7 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -34,6 +34,15 @@ var helpDescsEnUS = map[string]string{ "addnode-addr": "IP address and port of the peer to operate on", "addnode-subcmd": "'add' to add a persistent peer, 'remove' to remove a persistent peer, or 'onetry' to try a single connection to a peer", + // BalanceCmd help. + "balance--synopsis": "Retrieves the balance from the underlying bdkwallet.", + + // BalanceResult help. + "balanceresult-immature": "The coinbase balance that's not been confirmed 100 times and therefore can't be spent.", + "balanceresult-trustedpending": "The balance that's part of our change keychain.", + "balanceresult-untrustedpending": "The balance that's part of our public keychain.", + "balanceresult-confirmed": "The confirmed balance.", + // NodeCmd help. "node--synopsis": "Attempts to add or remove a peer.", "node-subcmd": "'disconnect' to remove all matching non-persistent peers, 'remove' to remove a persistent peer, or 'connect' to connect to a peer", @@ -830,6 +839,7 @@ var helpDescsEnUS = map[string]string{ // pointer to the type (or nil to indicate no return value). var rpcResultTypes = map[string][]interface{}{ "addnode": nil, + "balance": {(*btcjson.BalanceResult)(nil)}, "createrawtransaction": {(*string)(nil)}, "debuglevel": {(*string)(nil), (*string)(nil)}, "decoderawtransaction": {(*btcjson.TxRawDecodeResult)(nil)}, From 166286362e40f411f6c5bcfddd73e713a3804ffc Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 22:31:25 +0900 Subject: [PATCH 27/48] main: add bdkwallet tag on the tests --- goclean.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/goclean.sh b/goclean.sh index cadfaa7a..e5b819bb 100755 --- a/goclean.sh +++ b/goclean.sh @@ -7,7 +7,7 @@ set -ex -go test -short -race -tags="rpctest" ./... +go test -short -race -tags="rpctest" -tags="bdkwallet" ./... # Automatic checks golangci-lint run --deadline=10m --disable-all \ From d99cdcb91928dfc64950b4e1c5f2564c6b3f08d3 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 22:33:14 +0900 Subject: [PATCH 28/48] btcjson: add support for createtransactionfrombdkwallet command --- btcjson/chainsvrcmds.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index b196871c..4edd789a 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -64,6 +64,27 @@ type TransactionInput struct { Vout uint32 `json:"vout"` } +// Recipient is the recipient information needed to create a transaction from the bdk wallet. +type Recipient struct { + Amount int64 `json:"amount"` + Address string `json:"address"` +} + +// CreateTransactionFromBDKWalletCmd defines the createtransactionfrombdkwallet JSON-RPC command. +type CreateTransactionFromBDKWalletCmd struct { + FeeRate float32 + Recipients []Recipient +} + +// NewCreateTransactionFromBDKWalletCmd returns a new instance which can be used to issue +// a createtransactionfrombdkwallet command. +func NewCreateTransactionFromBDKWalletCmd(feeRate float32, recipients []Recipient) *CreateTransactionFromBDKWalletCmd { + return &CreateTransactionFromBDKWalletCmd{ + FeeRate: feeRate, + Recipients: recipients, + } +} + // CreateRawTransactionCmd defines the createrawtransaction JSON-RPC command. type CreateRawTransactionCmd struct { Inputs []TransactionInput @@ -1206,6 +1227,7 @@ func init() { MustRegisterCmd("addnode", (*AddNodeCmd)(nil), flags) MustRegisterCmd("balance", (*BalanceCmd)(nil), flags) + MustRegisterCmd("createtransactionfrombdkwallet", (*CreateTransactionFromBDKWalletCmd)(nil), flags) MustRegisterCmd("createrawtransaction", (*CreateRawTransactionCmd)(nil), flags) MustRegisterCmd("decoderawtransaction", (*DecodeRawTransactionCmd)(nil), flags) MustRegisterCmd("decodescript", (*DecodeScriptCmd)(nil), flags) From 140dd4bec364351dc93a0842903b07be85e5a8da Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Wed, 31 Jan 2024 17:50:18 +0900 Subject: [PATCH 29/48] btcjson: add support for createtransactionfrombdkwalletresult --- btcjson/chainsvrresults.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index ca84907a..311d53a2 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -928,3 +928,10 @@ type BDKAddressResult struct { Index int `json:"index"` Address string `json:"address"` } + +// CreateTransactionFromBDKWalletResult models the data from the +// createtransactionfrombdkwallet command. +type CreateTransactionFromBDKWalletResult struct { + TxHash string `json:"txhash"` + RawBytes string `json:"rawbytes"` +} From 9256bb13ada668d8803439288ed5332e80256dc4 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 30 Jan 2024 22:34:28 +0900 Subject: [PATCH 30/48] main: add createtransactionfrombdkwallet support to rpcserver --- rpcserver.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ rpcserverhelp.go | 14 +++++++++ 2 files changed, 89 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index 58de17b5..9c33cf55 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -134,6 +134,7 @@ var rpcHandlers map[string]commandHandler var rpcHandlersBeforeInit = map[string]commandHandler{ "addnode": handleAddNode, "balance": handleBalance, + "createtransactionfrombdkwallet": handleCreateTransactionFromBDKWallet, "createrawtransaction": handleCreateRawTransaction, "debuglevel": handleDebugLevel, "decoderawtransaction": handleDecodeRawTransaction, @@ -534,6 +535,80 @@ func peerExists(connMgr rpcserverConnManager, addr string, nodeID int32) bool { return false } +// handleCreateTransactionFromBDKWallet handles createtransactionfrombdkwallet command. +func handleCreateTransactionFromBDKWallet(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + // Before doing anything, check that the bdk wallet is active. + if s.cfg.BDKWallet == nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "BDK wallet must be enabled. (--bdkwallet)", + } + } + + c := cmd.(*btcjson.CreateTransactionFromBDKWalletCmd) + + recipients := make([]bdkwallet.Recipient, len(c.Recipients)) + for i := range recipients { + addr, err := btcutil.DecodeAddress(c.Recipients[i].Address, s.cfg.ChainParams) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: fmt.Sprintf("Couldn't decode address %v. %v", + c.Recipients[i].Address, err), + } + } + + recipients[i].Address = addr.String() + recipients[i].Amount = btcutil.Amount(c.Recipients[i].Amount) + } + + bytes, err := s.cfg.BDKWallet.Wallet.CreateTx(c.FeeRate, recipients) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: fmt.Sprintf("Failed to create tx. %v", err), + } + } + + tx, err := btcutil.NewTxFromBytes(bytes) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: fmt.Sprintf("Failed to create tx. %v", err), + } + } + + // The below code just broadcasts the tx without checking the validity. + // Since bdk isn't handling the utreexo proofs, there's no way for a utreexo + // node to check the transaction's validity. + // + // This is also better in general as mempool txs aren't being saved so on a restart + // the validation will fail for spending txs that aren't yet spent. + // We just trust bdk that it's doing things right. + txD := &mempool.TxDesc{ + TxDesc: mining.TxDesc{Tx: tx}, + } + + // Generate and relay inventory vectors. + s.cfg.ConnMgr.RelayTransactions([]*mempool.TxDesc{txD}) + + // Notify both websocket and getblocktemplate long poll clients of all + // newly accepted transactions. + s.NotifyNewTransactions([]*mempool.TxDesc{txD}) + + // Keep track of the tx so that they can be rebroadcast + // if they don't make their way into a block. + iv := wire.NewInvVect(wire.InvTypeTx, txD.Tx.Hash()) + s.cfg.ConnMgr.AddRebroadcastInventory(iv, txD) + + res := btcjson.CreateTransactionFromBDKWalletResult{ + TxHash: tx.Hash().String(), + RawBytes: hex.EncodeToString(bytes), + } + + return res, nil +} + // messageToHex serializes a message to the wire protocol encoding using the // latest protocol version and returns a hex-encoded string of the result. func messageToHex(msg wire.Message) (string, error) { diff --git a/rpcserverhelp.go b/rpcserverhelp.go index d1f7c4a7..523b9695 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -43,6 +43,19 @@ var helpDescsEnUS = map[string]string{ "balanceresult-untrustedpending": "The balance that's part of our public keychain.", "balanceresult-confirmed": "The confirmed balance.", + // Recipient help. + "recipient-amount": "The amount in satoshis to send to the recipient.", + "recipient-address": "The address of the recipient.", + + // CreateTransactionFromBDKWalletCmd help. + "createtransactionfrombdkwallet--synopsis": "Creates and returns a hex encoded transaction from the underlying bdk walllet that's ready to broadcast", + "createtransactionfrombdkwallet-feerate": "Feerate in satoshis that this tx will be paying", + "createtransactionfrombdkwallet-recipients": "List of recipients that this tx will be paying", + + // CreateTransactionFromBDKWalletResult help. + "createtransactionfrombdkwalletresult-txhash": "Txid of the transaction", + "createtransactionfrombdkwalletresult-rawbytes": "Hex-encoded bytes of the serialized transaction", + // NodeCmd help. "node--synopsis": "Attempts to add or remove a peer.", "node-subcmd": "'disconnect' to remove all matching non-persistent peers, 'remove' to remove a persistent peer, or 'connect' to connect to a peer", @@ -841,6 +854,7 @@ var rpcResultTypes = map[string][]interface{}{ "addnode": nil, "balance": {(*btcjson.BalanceResult)(nil)}, "createrawtransaction": {(*string)(nil)}, + "createtransactionfrombdkwallet": {(*btcjson.CreateTransactionFromBDKWalletResult)(nil)}, "debuglevel": {(*string)(nil), (*string)(nil)}, "decoderawtransaction": {(*btcjson.TxRawDecodeResult)(nil)}, "decodescript": {(*btcjson.DecodeScriptResult)(nil)}, From e5711d6d61f936f35a6eb129c0f2c1736da8c5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 31 Jan 2024 00:15:42 +0800 Subject: [PATCH 31/48] bdkwallet: pass in human-readable address to rust Instead of binary bytes of the expected spk. --- bdkwallet/bdkgo/bdkgo.go | 39 +++++++++++++++++++++++------ bdkwallet/bdkgo_crate/src/bdkgo.udl | 3 ++- bdkwallet/bdkgo_crate/src/lib.rs | 26 ++++++++++++------- bdkwallet/bdkwallet.go | 4 +-- bdkwallet/wallet.go | 4 +-- 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/bdkwallet/bdkgo/bdkgo.go b/bdkwallet/bdkgo/bdkgo.go index 1c1130c2..73e2db1e 100644 --- a/bdkwallet/bdkgo/bdkgo.go +++ b/bdkwallet/bdkgo/bdkgo.go @@ -1163,12 +1163,12 @@ func (_ FfiDestroyerTypeMempoolTx) Destroy(value MempoolTx) { } type Recipient struct { - ScriptPubkey []byte - Amount uint64 + Address string + Amount uint64 } func (r *Recipient) Destroy() { - FfiDestroyerBytes{}.Destroy(r.ScriptPubkey) + FfiDestroyerString{}.Destroy(r.Address) FfiDestroyerUint64{}.Destroy(r.Amount) } @@ -1182,7 +1182,7 @@ func (c FfiConverterTypeRecipient) Lift(rb RustBufferI) Recipient { func (c FfiConverterTypeRecipient) Read(reader io.Reader) Recipient { return Recipient{ - FfiConverterBytesINSTANCE.Read(reader), + FfiConverterStringINSTANCE.Read(reader), FfiConverterUint64INSTANCE.Read(reader), } } @@ -1192,7 +1192,7 @@ func (c FfiConverterTypeRecipient) Lower(value Recipient) RustBuffer { } func (c FfiConverterTypeRecipient) Write(writer io.Writer, value Recipient) { - FfiConverterBytesINSTANCE.Write(writer, value.ScriptPubkey) + FfiConverterStringINSTANCE.Write(writer, value.Address) FfiConverterUint64INSTANCE.Write(writer, value.Amount) } @@ -1649,10 +1649,29 @@ func (err CreateTxError) Unwrap() error { } // Err* are used for checking error type with `errors.Is` +var ErrCreateTxErrorInvalidAddress = fmt.Errorf("CreateTxErrorInvalidAddress") var ErrCreateTxErrorCreateTx = fmt.Errorf("CreateTxErrorCreateTx") var ErrCreateTxErrorSignTx = fmt.Errorf("CreateTxErrorSignTx") // Variant structs +type CreateTxErrorInvalidAddress struct { + message string +} + +func NewCreateTxErrorInvalidAddress() *CreateTxError { + return &CreateTxError{ + err: &CreateTxErrorInvalidAddress{}, + } +} + +func (err CreateTxErrorInvalidAddress) Error() string { + return fmt.Sprintf("InvalidAddress: %s", err.message) +} + +func (self CreateTxErrorInvalidAddress) Is(target error) bool { + return target == ErrCreateTxErrorInvalidAddress +} + type CreateTxErrorCreateTx struct { message string } @@ -1707,8 +1726,10 @@ func (c FfiConverterTypeCreateTxError) Read(reader io.Reader) error { message := FfiConverterStringINSTANCE.Read(reader) switch errorID { case 1: - return &CreateTxError{&CreateTxErrorCreateTx{message}} + return &CreateTxError{&CreateTxErrorInvalidAddress{message}} case 2: + return &CreateTxError{&CreateTxErrorCreateTx{message}} + case 3: return &CreateTxError{&CreateTxErrorSignTx{message}} default: panic(fmt.Sprintf("Unknown error code %d in FfiConverterTypeCreateTxError.Read()", errorID)) @@ -1718,10 +1739,12 @@ func (c FfiConverterTypeCreateTxError) Read(reader io.Reader) error { func (c FfiConverterTypeCreateTxError) Write(writer io.Writer, value *CreateTxError) { switch variantValue := value.err.(type) { - case *CreateTxErrorCreateTx: + case *CreateTxErrorInvalidAddress: writeInt32(writer, 1) - case *CreateTxErrorSignTx: + case *CreateTxErrorCreateTx: writeInt32(writer, 2) + case *CreateTxErrorSignTx: + writeInt32(writer, 3) default: _ = variantValue panic(fmt.Sprintf("invalid error value `%v` in FfiConverterTypeCreateTxError.Write", value)) diff --git a/bdkwallet/bdkgo_crate/src/bdkgo.udl b/bdkwallet/bdkgo_crate/src/bdkgo.udl index ff9de0a5..4c2879d9 100644 --- a/bdkwallet/bdkgo_crate/src/bdkgo.udl +++ b/bdkwallet/bdkgo_crate/src/bdkgo.udl @@ -35,6 +35,7 @@ enum ApplyMempoolError { [Error] enum CreateTxError { + "InvalidAddress", "CreateTx", "SignTx", }; @@ -92,7 +93,7 @@ interface Wallet { }; dictionary Recipient { - bytes script_pubkey; + string address; u64 amount; }; diff --git a/bdkwallet/bdkgo_crate/src/lib.rs b/bdkwallet/bdkgo_crate/src/lib.rs index 4d010c98..2ed52857 100644 --- a/bdkwallet/bdkgo_crate/src/lib.rs +++ b/bdkwallet/bdkgo_crate/src/lib.rs @@ -12,7 +12,7 @@ use bdk::{ consensus::{Decodable, Encodable}, hashes::Hash, network::constants::ParseNetworkError, - BlockHash, Network, ScriptBuf, Transaction, + Address, BlockHash, Network, Transaction, }, keys::{DerivableKey, ExtendedKey}, template::Bip86, @@ -83,6 +83,8 @@ pub enum ApplyMempoolError { #[derive(Debug, thiserror::Error)] pub enum CreateTxError { + #[error("recipient address is invalid: {0}")] + InvalidAddress(bdk::bitcoin::address::Error), #[error("failed to create tx: {0}")] CreateTx(bdk::wallet::error::CreateTxError), #[error("failed to sign tx: {0}")] @@ -312,7 +314,7 @@ impl Wallet { } else { wallet .apply_block(&block, height) - .map_err(|err| ApplyBlockError::CannotConnect(err))?; + .map_err(ApplyBlockError::CannotConnect)?; } let res = ApplyResult::new(&wallet); wallet.commit().map_err(ApplyBlockError::Database)?; @@ -349,14 +351,20 @@ impl Wallet { ) -> Result, CreateTxError> { self.increment_reference_counter(); let mut wallet = self.inner.lock().unwrap(); + let recipients = recipients + .into_iter() + .map(|r| -> Result<_, _> { + let addr = Address::from_str(&r.address) + .map_err(CreateTxError::InvalidAddress)? + .require_network(wallet.network()) + .map_err(CreateTxError::InvalidAddress)?; + Ok((addr.script_pubkey(), r.amount)) + }) + .collect::, _>>()?; + let mut psbt = wallet .build_tx() - .set_recipients( - recipients - .into_iter() - .map(|r| (ScriptBuf::from_bytes(r.script_pubkey), r.amount)) - .collect(), - ) + .set_recipients(recipients) .fee_rate(FeeRate::from_sat_per_vb(feerate)) .enable_rbf() .clone() @@ -437,7 +445,7 @@ impl Wallet { } pub struct Recipient { - pub script_pubkey: Vec, + pub address: String, pub amount: u64, } diff --git a/bdkwallet/bdkwallet.go b/bdkwallet/bdkwallet.go index 57f222f3..8acaeb25 100644 --- a/bdkwallet/bdkwallet.go +++ b/bdkwallet/bdkwallet.go @@ -184,8 +184,8 @@ func (w *BDKWallet) CreateTx(feerate float32, recipients []Recipient) ([]byte, e genRecipients := make([]bdkgo.Recipient, 0, len(recipients)) for _, r := range recipients { genRecipients = append(genRecipients, bdkgo.Recipient{ - ScriptPubkey: r.Address.ScriptAddress(), - Amount: uint64(r.Amount), + Address: r.Address, + Amount: uint64(r.Amount), }) } return w.inner.CreateTx(feerate, genRecipients) diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go index 4da7f1f4..17d2aea5 100644 --- a/bdkwallet/wallet.go +++ b/bdkwallet/wallet.go @@ -75,8 +75,8 @@ type BlockId struct { // Recipient specifies the intended amount and destination address for a transaction output. type Recipient struct { - Amount btcutil.Amount // amount to send - Address btcutil.Address // recipient address to send to + Amount btcutil.Amount // amount to send + Address string // recipient address to send to (in human-readable form) } // TxInfo is information on a given transaction. From ffd9a72a2bd4f0e72c2b52f00406ff126bf4124b Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Wed, 31 Jan 2024 15:37:19 +0900 Subject: [PATCH 32/48] btcjson: add getmnemonicwords command --- btcjson/chainsvrcmds.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 4edd789a..8af4007b 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -606,6 +606,15 @@ func NewGetMiningInfoCmd() *GetMiningInfoCmd { return &GetMiningInfoCmd{} } +// GetMnemonicWordsCmd defines the getmnemonicwords JSON-RPC command. +type GetMnemonicWordsCmd struct{} + +// NewGetMiningInfoCmd returns a new instance which can be used to issue a +// getmnemonicwords JSON-RPC command. +func NewGetMnemonicWordsCmd() *GetMnemonicWordsCmd { + return &GetMnemonicWordsCmd{} +} + // GetNetworkInfoCmd defines the getnetworkinfo JSON-RPC command. type GetNetworkInfoCmd struct{} @@ -1257,6 +1266,7 @@ func init() { MustRegisterCmd("getmempoolentry", (*GetMempoolEntryCmd)(nil), flags) MustRegisterCmd("getmempoolinfo", (*GetMempoolInfoCmd)(nil), flags) MustRegisterCmd("getmininginfo", (*GetMiningInfoCmd)(nil), flags) + MustRegisterCmd("getmnemonicwords", (*GetMnemonicWordsCmd)(nil), flags) MustRegisterCmd("getnetworkinfo", (*GetNetworkInfoCmd)(nil), flags) MustRegisterCmd("getnettotals", (*GetNetTotalsCmd)(nil), flags) MustRegisterCmd("gettxtotals", (*GetTxTotalsCmd)(nil), flags) From 69f00b654f75238b2049c7f8c666ee97da578117 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Wed, 31 Jan 2024 16:36:15 +0900 Subject: [PATCH 33/48] main: add getmnemonicwords command to rpcserver --- rpcserver.go | 7 +++++++ rpcserverhelp.go | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index 9c33cf55..e15bb6f6 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -163,6 +163,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "getinfo": handleGetInfo, "getmempoolinfo": handleGetMempoolInfo, "getmininginfo": handleGetMiningInfo, + "getmnemonicwords": handleGetMnemonicWords, "getnettotals": handleGetNetTotals, "gettxtotals": handleGetTxTotals, "getnetworkhashps": handleGetNetworkHashPS, @@ -2535,6 +2536,12 @@ func handleGetMiningInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{ return &result, nil } +// handleGetMnemonicWords implements the getmnemonicwords command. +func handleGetMnemonicWords(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + words := s.cfg.BDKWallet.Wallet.MnemonicWords() + return words, nil +} + // handleGetNetTotals implements the getnettotals command. func handleGetNetTotals(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { totalBytesRecv, totalBytesSent := s.cfg.ConnMgr.NetTotals() diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 523b9695..195eeeab 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -481,6 +481,10 @@ var helpDescsEnUS = map[string]string{ // GetMiningInfoCmd help. "getmininginfo--synopsis": "Returns a JSON object containing mining-related information.", + // GetMnemonicWordsCmd help. + "getmnemonicwords--synopsis": "Returns the mnemonic words of the bdk wallet.", + "getmnemonicwords--result0": "12 mnemonic words of the bdk wallet.", + // GetNetworkHashPSCmd help. "getnetworkhashps--synopsis": "Returns the estimated network hashes per second for the block heights provided by the parameters.", "getnetworkhashps-blocks": "The number of blocks, or -1 for blocks since last difficulty change", @@ -882,6 +886,7 @@ var rpcResultTypes = map[string][]interface{}{ "getinfo": {(*btcjson.InfoChainResult)(nil)}, "getmempoolinfo": {(*btcjson.GetMempoolInfoResult)(nil)}, "getmininginfo": {(*btcjson.GetMiningInfoResult)(nil)}, + "getmnemonicwords": {(*[]string)(nil)}, "getnettotals": {(*btcjson.GetNetTotalsResult)(nil)}, "gettxtotals": {(*btcjson.GetTxTotalsResult)(nil)}, "getutreexoproof": {(*btcjson.GetUtreexoProofVerboseResult)(nil)}, From 48a5385dfb1f41893e36dbdebceacc5a2d800e0c Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Wed, 31 Jan 2024 17:25:31 +0900 Subject: [PATCH 34/48] btcjson: add listbdktransactions command --- btcjson/chainsvrcmds.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 8af4007b..02284cb1 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -856,6 +856,15 @@ func NewHelpCmd(command *string) *HelpCmd { } } +// ListBDKTransactionsCmd defines the listbdktransactions JSON-RPC command. +type ListBDKTransactionsCmd struct{} + +// NewListBDKTransactionsCmd returns a new instance which can be used to issue a +// listbdktransactions JSON-RPC command. +func NewListBDKTransactionsCmd() *ListBDKTransactionsCmd { + return &ListBDKTransactionsCmd{} +} + // InvalidateBlockCmd defines the invalidateblock JSON-RPC command. type InvalidateBlockCmd struct { BlockHash string @@ -1284,6 +1293,7 @@ func init() { MustRegisterCmd("getwork", (*GetWorkCmd)(nil), flags) MustRegisterCmd("getwatchonlybalance", (*GetWatchOnlyBalanceCmd)(nil), flags) MustRegisterCmd("help", (*HelpCmd)(nil), flags) + MustRegisterCmd("listbdktransactions", (*ListBDKTransactionsCmd)(nil), flags) MustRegisterCmd("invalidateblock", (*InvalidateBlockCmd)(nil), flags) MustRegisterCmd("peekaddress", (*PeekAddressCmd)(nil), flags) MustRegisterCmd("ping", (*PingCmd)(nil), flags) From 46788c97e112ad85d50e8de4cb842161e5c3ab5e Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Wed, 31 Jan 2024 17:28:49 +0900 Subject: [PATCH 35/48] btcjson: add support for listbdktransactions result --- btcjson/chainsvrresults.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 311d53a2..81f2cc2b 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -27,6 +27,15 @@ type BalanceResult struct { Confirmed int64 `json:"confirmed"` } +// ListBDKTransactionsResult models the data from the listbdktransactions command. +type ListBDKTransactionsResult struct { + Txid string `json:"txid"` // txid of the tx + RawBytes string `json:"rawbytes"` // hex encoded raw tx + Spent int64 `json:"spent"` // sum of owned inputs + Received int64 `json:"received"` // sum of owned outputs + Confirmations uint `json:"confirmations"` // number of confirmations for this tx +} + // GetBlockHeaderVerboseResult models the data from the getblockheader command when // the verbose flag is set. When the verbose flag is not set, getblockheader // returns a hex-encoded string. From 18369f1f0d877a29c31fd94041a8b377ef96575a Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Wed, 31 Jan 2024 17:34:30 +0900 Subject: [PATCH 36/48] main: add support for listbdktransactions command in rpcserver --- rpcserver.go | 34 ++++++++++++++++++++++++++++++++++ rpcserverhelp.go | 11 +++++++++++ 2 files changed, 45 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index e15bb6f6..ed823431 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -178,6 +178,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "getwatchonlybalance": handleGetWatchOnlyBalance, "invalidateblock": handleInvalidateBlock, "help": handleHelp, + "listbdktransactions": handleListBDKTransactions, "node": handleNode, "peekaddress": handlePeekAddress, "ping": handlePing, @@ -3082,6 +3083,39 @@ func handleHelp(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (inter return help, nil } +// handleListBDKTransactions handles listbdktransactions commands. +func handleListBDKTransactions(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + txs, err := s.cfg.BDKWallet.Wallet.Transactions() + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Unable to fetch transactions " + err.Error(), + } + } + + res := make([]btcjson.ListBDKTransactionsResult, len(txs)) + for i := range res { + + var buf bytes.Buffer + err := txs[i].Tx.MsgTx().Serialize(&buf) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: "Unable to serialize tx " + err.Error(), + } + } + res[i] = btcjson.ListBDKTransactionsResult{ + Txid: txs[i].Txid.String(), + RawBytes: hex.EncodeToString(buf.Bytes()), + Spent: int64(txs[i].Spent), + Received: int64(txs[i].Received), + Confirmations: txs[i].Confirmations, + } + } + + return res, nil +} + // handlePeekAddress implements the peekaddress command. func handlePeekAddress(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.PeekAddressCmd) diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 195eeeab..3c60c71b 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -634,6 +634,16 @@ var helpDescsEnUS = map[string]string{ "help--result0": "List of commands", "help--result1": "Help for specified command", + // ListBDKTransactionsCmd help. + "listbdktransactions--synopsis": "Returns a list of all the relevant transactions the bdk wallet is holding onto", + + // ListBDKTransactionsResult help. + "listbdktransactionsresult-txid": "The transaction hash of the relevant transaction", + "listbdktransactionsresult-rawbytes": "The consensus encoded transaction in hex.", + "listbdktransactionsresult-spent": "The sum of satoshis that was spent in this tx.", + "listbdktransactionsresult-received": "The sum of satoshis that was received in this tx.", + "listbdktransactionsresult-confirmations": "The amount of blockchain confirmations for this tx.", + // PeekAddressCmd help. "peekaddress--synopsis": "Returns an address of the desired derivation index", "peekaddress-index": "The desired derivation index you want to fetch the address at", @@ -902,6 +912,7 @@ var rpcResultTypes = map[string][]interface{}{ "node": nil, "help": {(*string)(nil), (*string)(nil)}, "invalidateblock": nil, + "listbdktransactions": {(*[]btcjson.ListBDKTransactionsResult)(nil)}, "peekaddress": {(*btcjson.BDKAddressResult)(nil)}, "ping": nil, "proveutxochaintipinclusion": {(*btcjson.ProveUtxoChainTipInclusionVerboseResult)(nil)}, From 65f56180fc770a57e2bb4d15c12ba5d89c8bfee9 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Wed, 31 Jan 2024 17:45:53 +0900 Subject: [PATCH 37/48] main: notify bdkwallet and watchonly wallet for rpcserver rpcserver.NotifyNewTransactions() were not notifying the bdkwallet and the watchonly wallet of new transactions that were submitted to the node previously. --- rpcserver.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index ed823431..1ff4413c 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -4681,6 +4681,14 @@ func (s *rpcServer) NotifyNewTransactions(txns []*mempool.TxDesc) { // Potentially notify any getblocktemplate long poll clients // about stale block templates due to the new transaction. s.gbtWorkState.NotifyMempoolTx(s.cfg.TxMemPool.LastUpdated()) + + if s.cfg.BDKWallet != nil { + s.cfg.BDKWallet.NotifyNewTransactions(txns) + } + + if s.cfg.WatchOnlyWallet != nil { + s.cfg.WatchOnlyWallet.NotifyNewTransactions(txns) + } } } From c2836bd54e667d88ec1d879621b98e0085857ce7 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Wed, 31 Jan 2024 17:57:31 +0900 Subject: [PATCH 38/48] bdkwallet: general fixup Just generally making the code idiomatic. --- bdkwallet/bdkwallet.go | 28 +++++++++++++++++----------- bdkwallet/wallet.go | 12 +----------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/bdkwallet/bdkwallet.go b/bdkwallet/bdkwallet.go index 8acaeb25..3e1b5c59 100644 --- a/bdkwallet/bdkwallet.go +++ b/bdkwallet/bdkwallet.go @@ -7,6 +7,7 @@ import "C" import ( "bytes" + "encoding/hex" "github.com/utreexo/utreexod/bdkwallet/bdkgo" "github.com/utreexo/utreexod/btcutil" @@ -144,10 +145,9 @@ func (w *BDKWallet) ApplyBlock(block *btcutil.Block) error { if err != nil { return err } - bhash := block.Hash() - for _, genTxid := range res.RelevantTxids { - txid := hashFromBytes(genTxid) - log.Infof("Found relevant tx %v in block %v:%v.", txid, bheight, bhash) + for _, txid := range res.RelevantTxids { + log.Infof("Found relevant tx %v in block %v:%v.", + hex.EncodeToString(txid), bheight, block.Hash().String()) } return nil } @@ -160,7 +160,7 @@ func (w *BDKWallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { genTxns := make([]bdkgo.MempoolTx, 0, len(txns)) for _, tx := range txns { var txb bytes.Buffer - if err := tx.Tx.MsgTx().BtcEncode(&txb, wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { + if err := tx.Tx.MsgTx().BtcEncode(&txb, 0, wire.WitnessEncoding); err != nil { return err } genTxns = append(genTxns, bdkgo.MempoolTx{ @@ -172,9 +172,8 @@ func (w *BDKWallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { if err != nil { return err } - for _, genTxid := range res.RelevantTxids { - txid := *(*[32]byte)(genTxid) - log.Infof("Found relevant tx %v in mempool.", txid) + for _, txid := range res.RelevantTxids { + log.Infof("Found relevant tx %v in mempool.", hex.EncodeToString(txid)) } return nil } @@ -197,19 +196,26 @@ func (w *BDKWallet) MnemonicWords() []string { } // Transactions returns the list of wallet transactions. -func (w *BDKWallet) Transactions() []TxInfo { +func (w *BDKWallet) Transactions() ([]TxInfo, error) { genOut := w.inner.Transactions() out := make([]TxInfo, 0, len(genOut)) + for _, info := range genOut { + tx, err := btcutil.NewTxFromBytes(info.Tx) + if err != nil { + return nil, err + } + out = append(out, TxInfo{ Txid: hashFromBytes(info.Txid), - Tx: txFromBytes(info.Tx), + Tx: *tx, Spent: btcutil.Amount(info.Spent), Received: btcutil.Amount(info.Received), Confirmations: uint(info.Confirmations), }) } - return out + + return out, nil } // Utxos returns the list of wallet UTXOs. diff --git a/bdkwallet/wallet.go b/bdkwallet/wallet.go index 17d2aea5..e7387311 100644 --- a/bdkwallet/wallet.go +++ b/bdkwallet/wallet.go @@ -1,14 +1,12 @@ package bdkwallet import ( - "bytes" "errors" "github.com/utreexo/utreexod/btcutil" "github.com/utreexo/utreexod/chaincfg" "github.com/utreexo/utreexod/chaincfg/chainhash" "github.com/utreexo/utreexod/mempool" - "github.com/utreexo/utreexod/wire" ) var ( @@ -45,7 +43,7 @@ type Wallet interface { ApplyMempoolTransactions(txns []*mempool.TxDesc) error CreateTx(feerate float32, recipients []Recipient) ([]byte, error) MnemonicWords() []string - Transactions() []TxInfo + Transactions() ([]TxInfo, error) UTXOs() []UTXOInfo } @@ -103,14 +101,6 @@ func hashFromBytes(b []byte) chainhash.Hash { return *(*[32]byte)(b) } -func txFromBytes(b []byte) btcutil.Tx { - var msgTx wire.MsgTx - if err := msgTx.BtcDecode(bytes.NewReader(b), wire.FeeFilterVersion, wire.WitnessEncoding); err != nil { - panic("must decode tx consensus bytes from rust") - } - return *btcutil.NewTx(&msgTx) -} - func uintPointerFromUint32Pointer(v *uint32) *uint { if v == nil { return nil From 964d57259b6ffa392030c29e2d10e48e376374a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 31 Jan 2024 18:02:23 +0800 Subject: [PATCH 39/48] main: fix Makefile --- .github/workflows/go.yml | 15 +++++++++++++-- Makefile | 14 +++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5d0f5a09..6dcc92b6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -13,6 +13,10 @@ jobs: strategy: matrix: go: ["1.20", "1.21"] + rust: + - version: stable + clippy: true + - version: 1.72.0 # As specified in rust-toolchain steps: - uses: actions/checkout@v3 @@ -21,11 +25,18 @@ jobs: with: go-version: ${{ matrix.go }} + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust.version }} + override: true + profile: minimal + - name: Build - run: go build -v ./... + run: make all - name: Install Linters run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 - name: Test - run: sh ./goclean.sh + run: make test diff --git a/Makefile b/Makefile index 73a1ae4c..571a6635 100644 --- a/Makefile +++ b/Makefile @@ -2,28 +2,28 @@ help: ## Display help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' install-rust: ## Install rust if not installed - if ! type cargo &> /dev/null; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh; fi + if ! type cargo; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ; fi install-uniffi-bindgen-go: install-rust ## Install uniffi-bindgen-go if not installed - if ! type uniffi-bindgen-go &> /dev/null; then cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.2.0+v0.25.0; fi + if ! type uniffi-bindgen-go; then cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.2.0+v0.25.0 ; fi build-bdk: install-uniffi-bindgen-go ## Build BDK static library uniffi-bindgen-go -o bdkwallet bdkwallet/bdkgo_crate/src/bdkgo.udl cargo build --release build-utreexod: build-bdk ## Build utreexod with all features - go build ./... --tags=bdkwallet + go build --tags=bdkwallet -o . ./... build-utreexod-without-bdk: ## Build utreexod without BDK wallet - go build ./... + go build -o . ./... -test: +test: build-bdk ## Run all test cargo test sh ./goclean.sh -all: build-bdk build-utreexod +all: build-bdk build-utreexod ## Build all -clean: +clean: ## Clean all rm utreexod go clean cargo clean From 9398196084cb526e83657674ec3e94c8cce0cf71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 31 Jan 2024 21:18:09 +0800 Subject: [PATCH 40/48] bdkwallet: update BDK --- bdkwallet/bdkgo_crate/Cargo.toml | 4 ++-- bdkwallet/bdkgo_crate/src/lib.rs | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bdkwallet/bdkgo_crate/Cargo.toml b/bdkwallet/bdkgo_crate/Cargo.toml index ffe9c1e8..2e62b5fa 100644 --- a/bdkwallet/bdkgo_crate/Cargo.toml +++ b/bdkwallet/bdkgo_crate/Cargo.toml @@ -14,8 +14,8 @@ weedle2 = "=4.0.0" thiserror = "1.0" bincode = "1" serde = { version = "1", features = ["derive"] } -bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "07116df54156315b4f1fc67647a9b8118e464d43", features = ["keys-bip39"] } -bdk_file_store = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "07116df54156315b4f1fc67647a9b8118e464d43" } +bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "070fffb95cddb9153f326d5662f206d25ecbcc7c", features = ["keys-bip39"] } +bdk_file_store = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "070fffb95cddb9153f326d5662f206d25ecbcc7c" } rand = "0.8.5" [build-dependencies] diff --git a/bdkwallet/bdkgo_crate/src/lib.rs b/bdkwallet/bdkgo_crate/src/lib.rs index 2ed52857..8fb383f0 100644 --- a/bdkwallet/bdkgo_crate/src/lib.rs +++ b/bdkwallet/bdkgo_crate/src/lib.rs @@ -42,7 +42,7 @@ pub enum CreateNewError { #[error("failed to parse genesis hash: {0}")] ParseGenesisHash(bdk::bitcoin::hashes::Error), #[error("failed to create new db file: {0}")] - Database(bdk_file_store::FileError<'static>), + Database(bdk_file_store::FileError), #[error("failed to init wallet: {0}")] Wallet(bdk::wallet::NewError), } @@ -50,7 +50,7 @@ pub enum CreateNewError { #[derive(Debug, thiserror::Error)] pub enum LoadError { #[error("failed to load db: {0}")] - Database(bdk_file_store::FileError<'static>), + Database(bdk_file_store::FileError), #[error("failed to decode wallet header: {0}")] ParseHeader(bincode::Error), #[error("wallet header version unsupported")] @@ -191,8 +191,8 @@ impl Wallet { let mut header = WalletHeader::new(network); let header_bytes = header.encode(); - let db = - bdk_file_store::Store::create_new(&header_bytes, &db_path).expect("must create db"); + let db = bdk_file_store::Store::create_new(&header_bytes, &db_path) + .map_err(CreateNewError::Database)?; let bdk_wallet = match bdk::Wallet::new_with_genesis_hash( header.descriptor(KeychainKind::External), Some(header.descriptor(KeychainKind::Internal)), @@ -216,7 +216,8 @@ impl Wallet { let file = std::fs::File::open(&db_path) .map_err(|err| LoadError::Database(bdk_file_store::FileError::Io(err)))?; let (header, header_bytes) = WalletHeader::decode(file)?; - let db = bdk_file_store::Store::open(&header_bytes, db_path).expect("must load db"); + let db = + bdk_file_store::Store::open(&header_bytes, db_path).map_err(LoadError::Database)?; let bdk_wallet = bdk::Wallet::load( header.descriptor(KeychainKind::External), Some(header.descriptor(KeychainKind::Internal)), From 66df88b10b489a5fec0e8138cfccc0c87ac4580c Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Thu, 1 Feb 2024 01:26:20 +0900 Subject: [PATCH 41/48] btcjson: add support for rebroadcastunconfirmedbdktxs --- btcjson/chainsvrcmds.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 02284cb1..7a78c54d 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -968,6 +968,16 @@ func NewRegisterAddressesToWatchOnlyWalletCmd(addresses []string) *RegisterAddre } } +// RebroadcastUnconfirmedBDKTxsCmd defines the rebroadcastunconfirmedbdktxs JSON-RPC +// command. +type RebroadcastUnconfirmedBDKTxsCmd struct{} + +// NewRebroadcastUnconfirmedBDKTxsCmd returns a new instance which can be used to issue a +// registeraddressestowatchonlywallet JSON-RPC command. +func NewRebroadcastUnconfirmedBDKTxsCmd() *RebroadcastUnconfirmedBDKTxsCmd { + return &RebroadcastUnconfirmedBDKTxsCmd{} +} + // SearchRawTransactionsCmd defines the searchrawtransactions JSON-RPC command. type SearchRawTransactionsCmd struct { Address string @@ -1301,6 +1311,7 @@ func init() { MustRegisterCmd("proveutxochaintipinclusion", (*ProveUtxoChainTipInclusionCmd)(nil), flags) MustRegisterCmd("provewatchonlychaintipinclusion", (*ProveWatchOnlyChainTipInclusionCmd)(nil), flags) MustRegisterCmd("registeraddressestowatchonlywallet", (*RegisterAddressesToWatchOnlyWalletCmd)(nil), flags) + MustRegisterCmd("rebroadcastunconfirmedbdktxs", (*RebroadcastUnconfirmedBDKTxsCmd)(nil), flags) MustRegisterCmd("reconsiderblock", (*ReconsiderBlockCmd)(nil), flags) MustRegisterCmd("searchrawtransactions", (*SearchRawTransactionsCmd)(nil), flags) MustRegisterCmd("sendrawtransaction", (*SendRawTransactionCmd)(nil), flags) From 848b2610a34ac9eda488af865d37382ac7557673 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Thu, 1 Feb 2024 01:27:13 +0900 Subject: [PATCH 42/48] main: add support for rebroadcastunconfirmedbdktxs command It rebroadcasts the unconfirmed txs that are stored on the bdk side to peers connected to the node. A node could be shut down after it has passed off the tx to bdk but before it has sent the txs off to other peers. Having this rpc makes it so that the user can manually trigger rebroadcasts. --- rpcserver.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ rpcserverhelp.go | 5 +++ 2 files changed, 93 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index 1ff4413c..8e9b5519 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -184,6 +184,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "ping": handlePing, "proveutxochaintipinclusion": handleProveUtxoChainTipInclusion, "provewatchonlychaintipinclusion": handleProveWatchOnlyChainTipInclusion, + "rebroadcastunconfirmedbdktxs": handleRebroadcastUnconfirmedBDKTxs, "reconsiderblock": handleReconsiderBlock, "registeraddressestowatchonlywallet": handleRegisterAddressesToWatchOnlyWallet, "searchrawtransactions": handleSearchRawTransactions, @@ -3773,6 +3774,93 @@ func fetchMempoolTxnsForAddress(s *rpcServer, addr btcutil.Address, numToSkip, n return mpTxns[numToSkip:rangeEnd], numToSkip } +func sendTxToMempoolSpace(rawTx string, chainParams *chaincfg.Params) (string, error) { + client := &http.Client{} + var data = strings.NewReader(rawTx) + + link := "https://mempool.space/api/tx" + switch chainParams.Name { + case chaincfg.MainNetParams.Name: + case chaincfg.TestNet3Params.Name: + link = "https://mempool.space/testnet/api/tx" + case chaincfg.SigNetParams.Name: + link = "https://mempool.space/signet/api/tx" + default: + return "", fmt.Errorf("unsupported network %v", chainParams.Name) + } + + req, err := http.NewRequest("POST", link, data) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + bodyText, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(bodyText), nil +} + +// handleRebroadcastUnconfirmedBDKTxs implements the rebroadcastunconfirmedbdktxs command. +func handleRebroadcastUnconfirmedBDKTxs(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + allTxs, err := s.cfg.BDKWallet.Wallet.Transactions() + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: fmt.Sprintf("Failed to fetch transaction from bdk wallet. %v", err), + } + } + + unconfirmedTxs := make([]bdkwallet.TxInfo, 0, len(allTxs)) + for _, tx := range allTxs { + if tx.Confirmations == 0 { + unconfirmedTxs = append(unconfirmedTxs, tx) + } + } + + // Nothing to rebroadcast. + if len(unconfirmedTxs) == 0 { + return []string{}, nil + } + + rebroadcasted := make([]string, 0, len(unconfirmedTxs)) + for _, unconfirmedTx := range unconfirmedTxs { + tx := unconfirmedTx.Tx + + // If it's already in the mempool, then we've already broadcasted it. + if s.cfg.TxMemPool.IsTransactionInPool(tx.Hash()) { + continue + } + + if s.cfg.Chain.IsUtreexoViewActive() { + // This is not ideal but since bdk doesn't handle utreexo proofs yet, there's + // no way for us to broadcast it normally unless we make some undesired changes + // to the mempool code. Just send it off to mempool.space until we can do things + // properly. + _, err = sendTxToMempoolSpace(txHexString(tx.MsgTx()), s.cfg.ChainParams) + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: fmt.Sprintf("Failed to broadcast transaction to mempool.space. %v", err), + } + } else { + err = s.rpcProcessTx(&tx, true, false) + if err != nil { + return nil, err + } + } + + rebroadcasted = append(rebroadcasted, unconfirmedTx.Tx.Hash().String()) + } + + return rebroadcasted, nil +} + // handleReconsiderBlock implements the reconsiderblock command. func handleReconsiderBlock(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.ReconsiderBlockCmd) diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 3c60c71b..80d46cf6 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -683,6 +683,10 @@ var helpDescsEnUS = map[string]string{ "Note that these are not purely hashes of txid:vout. The preimage also include Amount, PkScript, and other parts of the UTXO", "provewatchonlychaintipinclusionverboseresult-hex": "The raw hash of the entire chain-tip inclusion proof", + // RebroadcastUnconfirmedBDKTxs help. + "rebroadcastunconfirmedbdktxs--synopsis": "Rebroadcasts the unconfirmed txs in the bdk wallet to the network. Won't rebroadcast the txs already in this node's mempool.", + "rebroadcastunconfirmedbdktxs--result0": "List of txids of the rebroadcasted txs", + // SearchRawTransactionsCmd help. "searchrawtransactions--synopsis": "Returns raw data for transactions involving the passed address.\n" + "Returned transactions are pulled from both the database, and transactions currently in the mempool.\n" + @@ -917,6 +921,7 @@ var rpcResultTypes = map[string][]interface{}{ "ping": nil, "proveutxochaintipinclusion": {(*btcjson.ProveUtxoChainTipInclusionVerboseResult)(nil)}, "provewatchonlychaintipinclusion": {(*btcjson.ProveWatchOnlyChainTipInclusionVerboseResult)(nil)}, + "rebroadcastunconfirmedbdktxs": {(*[]string)(nil)}, "registeraddressestowatchonlywallet": nil, "reconsiderblock": nil, "searchrawtransactions": {(*string)(nil), (*[]btcjson.SearchRawTransactionsResult)(nil)}, From ba145962841f4d52d8741bd4e204d43d5c1439ad Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Thu, 1 Feb 2024 14:52:19 +0900 Subject: [PATCH 43/48] main: change handleCreateTransactionFromBDKWallet to send txs to mempool.space --- rpcserver.go | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/rpcserver.go b/rpcserver.go index 8e9b5519..09a2f144 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -581,29 +581,30 @@ func handleCreateTransactionFromBDKWallet(s *rpcServer, cmd interface{}, closeCh } } - // The below code just broadcasts the tx without checking the validity. - // Since bdk isn't handling the utreexo proofs, there's no way for a utreexo - // node to check the transaction's validity. - // - // This is also better in general as mempool txs aren't being saved so on a restart - // the validation will fail for spending txs that aren't yet spent. - // We just trust bdk that it's doing things right. - txD := &mempool.TxDesc{ - TxDesc: mining.TxDesc{Tx: tx}, + if s.cfg.Chain.IsUtreexoViewActive() { + // This is not ideal but since bdk doesn't handle utreexo proofs yet, there's + // no way for us to broadcast it normally unless we make some undesired changes + // to the mempool code. Just send it off to mempool.space until we can do things + // properly. + _, err = sendTxToMempoolSpace(txHexString(tx.MsgTx()), s.cfg.ChainParams) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCMisc, + Message: fmt.Sprintf("Failed to broadcast transaction to mempool.space. %v", err), + } + } + txD := &mempool.TxDesc{ + TxDesc: mining.TxDesc{Tx: tx}, + } + // Notify bdkwallet and other listeners. + s.NotifyNewTransactions([]*mempool.TxDesc{txD}) + } else { + err = s.rpcProcessTx(tx, true, false) + if err != nil { + return nil, err + } } - // Generate and relay inventory vectors. - s.cfg.ConnMgr.RelayTransactions([]*mempool.TxDesc{txD}) - - // Notify both websocket and getblocktemplate long poll clients of all - // newly accepted transactions. - s.NotifyNewTransactions([]*mempool.TxDesc{txD}) - - // Keep track of the tx so that they can be rebroadcast - // if they don't make their way into a block. - iv := wire.NewInvVect(wire.InvTypeTx, txD.Tx.Hash()) - s.cfg.ConnMgr.AddRebroadcastInventory(iv, txD) - res := btcjson.CreateTransactionFromBDKWalletResult{ TxHash: tx.Hash().String(), RawBytes: hex.EncodeToString(bytes), From 23ec5ad6e201a05f060b94c939148348ec495111 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Thu, 1 Feb 2024 14:58:17 +0900 Subject: [PATCH 44/48] btcjson: add support for listbdkutxos --- btcjson/chainsvrcmds.go | 10 ++++++++++ btcjson/chainsvrresults.go | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 7a78c54d..7cc1888b 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -865,6 +865,15 @@ func NewListBDKTransactionsCmd() *ListBDKTransactionsCmd { return &ListBDKTransactionsCmd{} } +// ListBDKUTXOsCmd defines the listbdkutxos JSON-RPC command. +type ListBDKUTXOsCmd struct{} + +// NewListBDKUTXOsCmd returns a new instance which can be used to issue a +// listbdkutxos JSON-RPC command. +func NewListBDKUTXOsCmd() *ListBDKUTXOsCmd { + return &ListBDKUTXOsCmd{} +} + // InvalidateBlockCmd defines the invalidateblock JSON-RPC command. type InvalidateBlockCmd struct { BlockHash string @@ -1304,6 +1313,7 @@ func init() { MustRegisterCmd("getwatchonlybalance", (*GetWatchOnlyBalanceCmd)(nil), flags) MustRegisterCmd("help", (*HelpCmd)(nil), flags) MustRegisterCmd("listbdktransactions", (*ListBDKTransactionsCmd)(nil), flags) + MustRegisterCmd("listbdkutxos", (*ListBDKUTXOsCmd)(nil), flags) MustRegisterCmd("invalidateblock", (*InvalidateBlockCmd)(nil), flags) MustRegisterCmd("peekaddress", (*PeekAddressCmd)(nil), flags) MustRegisterCmd("ping", (*PingCmd)(nil), flags) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 81f2cc2b..dc30867f 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -36,6 +36,17 @@ type ListBDKTransactionsResult struct { Confirmations uint `json:"confirmations"` // number of confirmations for this tx } +// ListBDKUTXOsResult models the data from the listbdkutxos command. +type ListBDKUTXOsResult struct { + Txid string `json:"txid"` + Vout uint `json:"vout"` + Amount int64 `json:"amount"` + ScriptPubKey string `json:"scriptpubkey"` + IsChange bool `json:"ischange"` + DerivationIndex uint `json:"derivationindex"` + Confirmations uint `json:"confirmations"` +} + // GetBlockHeaderVerboseResult models the data from the getblockheader command when // the verbose flag is set. When the verbose flag is not set, getblockheader // returns a hex-encoded string. From 779ca92a7db010899c5e9e30e182f850246beefc Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Thu, 1 Feb 2024 15:06:19 +0900 Subject: [PATCH 45/48] main: add listbdkutxos support to rpc --- rpcserver.go | 21 +++++++++++++++++++++ rpcserverhelp.go | 13 +++++++++++++ 2 files changed, 34 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index 09a2f144..343b865f 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -179,6 +179,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "invalidateblock": handleInvalidateBlock, "help": handleHelp, "listbdktransactions": handleListBDKTransactions, + "listbdkutxos": handleListBDKUTXOs, "node": handleNode, "peekaddress": handlePeekAddress, "ping": handlePing, @@ -3118,6 +3119,26 @@ func handleListBDKTransactions(s *rpcServer, cmd interface{}, closeChan <-chan s return res, nil } +// handleListBDKUTXOs handles handlelistbdkutxos commands. +func handleListBDKUTXOs(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + utxos := s.cfg.BDKWallet.Wallet.UTXOs() + + res := make([]btcjson.ListBDKUTXOsResult, len(utxos)) + for i := range res { + res[i] = btcjson.ListBDKUTXOsResult{ + Txid: utxos[i].Txid.String(), + Vout: utxos[i].Vout, + Amount: int64(utxos[i].Amount), + ScriptPubKey: hex.EncodeToString(utxos[i].ScriptPubKey), + IsChange: utxos[i].IsChange, + DerivationIndex: utxos[i].DerivationIndex, + Confirmations: utxos[i].Confirmations, + } + } + + return res, nil +} + // handlePeekAddress implements the peekaddress command. func handlePeekAddress(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.PeekAddressCmd) diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 80d46cf6..8333e6f7 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -644,6 +644,18 @@ var helpDescsEnUS = map[string]string{ "listbdktransactionsresult-received": "The sum of satoshis that was received in this tx.", "listbdktransactionsresult-confirmations": "The amount of blockchain confirmations for this tx.", + // ListBDKUTXOsCmd help. + "listbdkutxos--synopsis": "Returns a list of all the relevant utxos the bdk wallet is holding onto", + + // ListBDKUTXOsResult help. + "listbdkutxosresult-txid": "The txid of the relevant utxo.", + "listbdkutxosresult-vout": "The output index of the relevant utxo.", + "listbdkutxosresult-amount": "The amount in satoshis this utxo is worth.", + "listbdkutxosresult-scriptpubkey": "The script pubkey of the utxo.", + "listbdkutxosresult-ischange": "Whether or not this utxo is a change output.", + "listbdkutxosresult-derivationindex": "The derivation index of the wallet this utxo is located at.", + "listbdkutxosresult-confirmations": "The total amount of blockchain confirmations this utxo has.", + // PeekAddressCmd help. "peekaddress--synopsis": "Returns an address of the desired derivation index", "peekaddress-index": "The desired derivation index you want to fetch the address at", @@ -917,6 +929,7 @@ var rpcResultTypes = map[string][]interface{}{ "help": {(*string)(nil), (*string)(nil)}, "invalidateblock": nil, "listbdktransactions": {(*[]btcjson.ListBDKTransactionsResult)(nil)}, + "listbdkutxos": {(*[]btcjson.ListBDKUTXOsResult)(nil)}, "peekaddress": {(*btcjson.BDKAddressResult)(nil)}, "ping": nil, "proveutxochaintipinclusion": {(*btcjson.ProveUtxoChainTipInclusionVerboseResult)(nil)}, From 3a9631041e26ee015f02cbd40df83e069769e34a Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Thu, 1 Feb 2024 15:10:03 +0900 Subject: [PATCH 46/48] bdkwallet: reverse hashes Since everyone reverses the hashes, we also need to reverse them so it looks right. --- bdkwallet/bdkwallet.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/bdkwallet/bdkwallet.go b/bdkwallet/bdkwallet.go index 3e1b5c59..b079eb90 100644 --- a/bdkwallet/bdkwallet.go +++ b/bdkwallet/bdkwallet.go @@ -7,11 +7,11 @@ import "C" import ( "bytes" - "encoding/hex" "github.com/utreexo/utreexod/bdkwallet/bdkgo" "github.com/utreexo/utreexod/btcutil" "github.com/utreexo/utreexod/chaincfg" + "github.com/utreexo/utreexod/chaincfg/chainhash" "github.com/utreexo/utreexod/mempool" "github.com/utreexo/utreexod/wire" ) @@ -145,9 +145,14 @@ func (w *BDKWallet) ApplyBlock(block *btcutil.Block) error { if err != nil { return err } + for _, txid := range res.RelevantTxids { + hash, err := chainhash.NewHash(txid) + if err != nil { + return err + } log.Infof("Found relevant tx %v in block %v:%v.", - hex.EncodeToString(txid), bheight, block.Hash().String()) + hash.String(), bheight, block.Hash().String()) } return nil } @@ -173,7 +178,11 @@ func (w *BDKWallet) ApplyMempoolTransactions(txns []*mempool.TxDesc) error { return err } for _, txid := range res.RelevantTxids { - log.Infof("Found relevant tx %v in mempool.", hex.EncodeToString(txid)) + hash, err := chainhash.NewHash(txid) + if err != nil { + return err + } + log.Infof("Found relevant tx %v in mempool.", hash.String()) } return nil } From 5103c9e76837b2982f3861770dd22ecdd668a2f7 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Thu, 1 Feb 2024 16:12:46 +0900 Subject: [PATCH 47/48] main: enable bdkwallet if the wallet exists --- utreexod.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/utreexod.go b/utreexod.go index 31c35acd..5773853e 100644 --- a/utreexod.go +++ b/utreexod.go @@ -357,13 +357,8 @@ func btcdMain(serverChan chan<- *server) error { btcdLog.Error(err) return err } - if walletDirExists && !cfg.BdkWallet { - err = fmt.Errorf("BDK wallet is disabled but this node has been previously "+ - "started with BDK wallet enabled with walletdir of \"%v\". Please "+ - "completely remove the walletdir to start the node without wallet.", - bdkwallet.WalletDir(cfg.DataDir)) - btcdLog.Error(err) - return err + if walletDirExists { + cfg.BdkWallet = true } } From 8bdc98020a026e12f610834c5631e645d8ff467f Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Thu, 1 Feb 2024 16:15:56 +0900 Subject: [PATCH 48/48] main: make bdk wallet enabled by default --- config.go | 2 +- integration/getchaintips_test.go | 2 +- integration/getutreexoroots_test.go | 4 +++- integration/invalidate_reconsider_block_test.go | 2 +- integration/utreexocompactstatenode_test.go | 2 ++ server.go | 2 +- utreexod.go | 2 +- 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/config.go b/config.go index a94cbd1b..ee3f3965 100644 --- a/config.go +++ b/config.go @@ -216,7 +216,7 @@ type config struct { RegisterAddressToWatchOnlyWallet []string `long:"registeraddresstowatchonlywallet" description:"Registers addresses to be watched to the watch only wallet. Must have --watchonlywallet enabled"` RegisterExtendedPubKeysToWatchOnlyWallet []string `long:"registerextendedpubkeystowatchonlywallet" description:"Registers extended pubkeys to be watched to the watch only wallet. Must have --watchonlywallet enabled."` RegisterExtendedPubKeysWithAddrTypeToWatchOnlyWallet []string `long:"registerextendedpubkeyswithaddresstypetowatchonlywallet" description:"Registers extended pubkeys to be watched to the watch only wallet and let's the user override the hd type of the extended public key. Must have --watchonlywallet enabled. Format: ':
. Supported address types: '{p2pkh, p2wpkh, p2sh}'"` - BdkWallet bool `long:"bdkwallet" description:"Enable the BDK wallet."` + NoBdkWallet bool `long:"nobdkwallet" description:"Disable the BDK wallet."` // Electrum server options. ElectrumListeners []string `long:"electrumlisteners" description:"Interface/port for the electrum server to listen to. (default 50001). Electrum server is only enabled when --watchonlywallet is enabled"` diff --git a/integration/getchaintips_test.go b/integration/getchaintips_test.go index 9fb62bdc..f7f20b01 100644 --- a/integration/getchaintips_test.go +++ b/integration/getchaintips_test.go @@ -167,7 +167,7 @@ func TestGetChainTips(t *testing.T) { "0000000000000000000000000000000000000000000000000000" // Set up regtest chain. - r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, []string{"--noutreexo"}, "") + r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, []string{"--noutreexo", "--nobdkwallet"}, "") if err != nil { t.Fatal("TestGetChainTips fail. Unable to create primary harness: ", err) } diff --git a/integration/getutreexoroots_test.go b/integration/getutreexoroots_test.go index 82291f62..f3777276 100644 --- a/integration/getutreexoroots_test.go +++ b/integration/getutreexoroots_test.go @@ -63,6 +63,7 @@ func TestGetUtreexoRoots(t *testing.T) { utreexoProofIdxArgs := []string{ "--utreexoproofindex", "--noutreexo", + "--nobdkwallet", } utreexoProofNode, err := rpctest.New(&chaincfg.RegressionNetParams, nil, utreexoProofIdxArgs, "") if err != nil { @@ -77,6 +78,7 @@ func TestGetUtreexoRoots(t *testing.T) { flatUtreexoProofIdxArgs := []string{ "--flatutreexoproofindex", "--noutreexo", + "--nobdkwallet", } flatUtreexoProofNode, err := rpctest.New(&chaincfg.RegressionNetParams, nil, flatUtreexoProofIdxArgs, "") if err != nil { @@ -88,7 +90,7 @@ func TestGetUtreexoRoots(t *testing.T) { defer flatUtreexoProofNode.TearDown() // Set up the CSN. - csn, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + csn, err := rpctest.New(&chaincfg.RegressionNetParams, nil, []string{"--nobdkwallet"}, "") if err != nil { t.Fatal("TestGetUtreexoRoots fail. Unable to create primary harness: ", err) } diff --git a/integration/invalidate_reconsider_block_test.go b/integration/invalidate_reconsider_block_test.go index de26ee9d..7fff3b3e 100644 --- a/integration/invalidate_reconsider_block_test.go +++ b/integration/invalidate_reconsider_block_test.go @@ -9,7 +9,7 @@ import ( func TestInvalidateAndReconsiderBlock(t *testing.T) { // Set up regtest chain. - r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, []string{"--flatutreexoproofindex", "--utreexoproofindex"}, "") + r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, []string{"--flatutreexoproofindex", "--utreexoproofindex", "--nobdkwallet"}, "") if err != nil { t.Fatalf("TestInvalidateAndReconsiderBlock fail. Unable to create primary harness: %v", err) } diff --git a/integration/utreexocompactstatenode_test.go b/integration/utreexocompactstatenode_test.go index 13ca90b3..5dfaa25d 100644 --- a/integration/utreexocompactstatenode_test.go +++ b/integration/utreexocompactstatenode_test.go @@ -87,6 +87,7 @@ func TestUtreexoCSN(t *testing.T) { bridgeNodeArgs := []string{ "--flatutreexoproofindex", "--noutreexo", + "--nobdkwallet", } // Set up regtest chain for the bridge node. bridgeNode, err := rpctest.New(&chaincfg.RegressionNetParams, nil, bridgeNodeArgs, "") @@ -190,6 +191,7 @@ func TestUtreexoCSN(t *testing.T) { // Set up the CSN. csnArgs := []string{ "--watchonlywallet", + "--nobdkwallet", } for _, addr := range watchAddrs { csnArgs = append(csnArgs, fmt.Sprintf("--registeraddresstowatchonlywallet=%s", addr.String())) diff --git a/server.go b/server.go index f188d5b4..baa5fd04 100644 --- a/server.go +++ b/server.go @@ -3541,7 +3541,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, } } - if cfg.BdkWallet { + if !cfg.NoBdkWallet { // Setup BDK wallet if it is enabled. s.bdkWallet, err = bdkwallet.NewManager(bdkwallet.ManagerConfig{ Chain: s.chain, diff --git a/utreexod.go b/utreexod.go index 5773853e..1b2528c8 100644 --- a/utreexod.go +++ b/utreexod.go @@ -358,7 +358,7 @@ func btcdMain(serverChan chan<- *server) error { return err } if walletDirExists { - cfg.BdkWallet = true + cfg.NoBdkWallet = false } }