diff --git a/.gitignore b/.gitignore index c9f0add..a11b87d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ bdkpython.egg-info/ __pycache__/ libbdkffi.dylib .idea/ +.DS_Store diff --git a/README.md b/README.md index 7218b47..e3a9e95 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,25 @@ Install the latest release using ```shell pip install bdkpython ``` +
## Run the tests ```shell python -m tox ``` +
## Build the package ```shell python -m build ``` +
## Install locally ```shell pip install ./dist/bdkpython-0.0.1-py3-none-any.whl ``` +
## Known issues Note that until the fix is merged upstream in [uniffi-rs](https://github.com/mozilla/uniffi-rs), the `loadIndirect()` function in the `bdk.py` module must be replaced with the following: diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..28ca33b --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +setup( + name='bdkpython', + version='0.0.1', + packages=find_packages(where="src"), + package_dir={"": "src"}, + package_data={"bdkpython": ["*.dylib"]}, + include_package_data=True, + zip_safe=False, +) diff --git a/src/bdkpython/__init__.py b/src/bdkpython/__init__.py new file mode 100644 index 0000000..0b0ba05 --- /dev/null +++ b/src/bdkpython/__init__.py @@ -0,0 +1 @@ +from bdkpython.bdk import * diff --git a/src/bdkpython/bdk.py b/src/bdkpython/bdk.py new file mode 100644 index 0000000..b6e0679 --- /dev/null +++ b/src/bdkpython/bdk.py @@ -0,0 +1,1891 @@ +# This file was autogenerated by some hot garbage in the `uniffi` crate. +# Trust me, you don't want to mess with it! + +# Common helper code. +# +# Ideally this would live in a separate .py file where it can be unittested etc +# in isolation, and perhaps even published as a re-useable package. +# +# However, it's important that the details of how this helper code works (e.g. the +# way that different builtin types are passed across the FFI) exactly match what's +# expected by the rust code on the other side of the interface. In practice right +# now that means coming from the exact some version of `uniffi` that was used to +# compile the rust component. The easiest way to ensure this is to bundle the Python +# helpers directly inline like we're doing here. + +import os +import sys +import ctypes +import enum +import struct +import contextlib +import datetime + + +class RustBuffer(ctypes.Structure): + _fields_ = [ + ("capacity", ctypes.c_int32), + ("len", ctypes.c_int32), + ("data", ctypes.POINTER(ctypes.c_char)), + ] + + @staticmethod + def alloc(size): + return rust_call(_UniFFILib.ffi_bdk_b9b3_rustbuffer_alloc, size) + + @staticmethod + def reserve(rbuf, additional): + return rust_call(_UniFFILib.ffi_bdk_b9b3_rustbuffer_reserve, rbuf, additional) + + def free(self): + return rust_call(_UniFFILib.ffi_bdk_b9b3_rustbuffer_free, self) + + def __str__(self): + return "RustBuffer(capacity={}, len={}, data={})".format( + self.capacity, + self.len, + self.data[0:self.len] + ) + + @contextlib.contextmanager + def allocWithBuilder(): + """Context-manger to allocate a buffer using a RustBufferBuilder. + + The allocated buffer will be automatically freed if an error occurs, ensuring that + we don't accidentally leak it. + """ + builder = RustBufferBuilder() + try: + yield builder + except: + builder.discard() + raise + + @contextlib.contextmanager + def consumeWithStream(self): + """Context-manager to consume a buffer using a RustBufferStream. + + The RustBuffer will be freed once the context-manager exits, ensuring that we don't + leak it even if an error occurs. + """ + try: + s = RustBufferStream(self) + yield s + if s.remaining() != 0: + raise RuntimeError("junk data left in buffer after consuming") + finally: + self.free() + + +class ForeignBytes(ctypes.Structure): + _fields_ = [ + ("len", ctypes.c_int32), + ("data", ctypes.POINTER(ctypes.c_char)), + ] + + def __str__(self): + return "ForeignBytes(len={}, data={})".format(self.len, self.data[0:self.len]) + + +class RustBufferStream(object): + """ + Helper for structured reading of bytes from a RustBuffer + """ + + def __init__(self, rbuf): + self.rbuf = rbuf + self.offset = 0 + + def remaining(self): + return self.rbuf.len - self.offset + + def _unpack_from(self, size, format): + if self.offset + size > self.rbuf.len: + raise InternalError("read past end of rust buffer") + value = struct.unpack(format, self.rbuf.data[self.offset:self.offset+size])[0] + self.offset += size + return value + + def read(self, size): + if self.offset + size > self.rbuf.len: + raise InternalError("read past end of rust buffer") + data = self.rbuf.data[self.offset:self.offset+size] + self.offset += size + return data + + def readI8(self): + return self._unpack_from(1, ">b") + + def readU8(self): + return self._unpack_from(1, ">B") + + def readI16(self): + return self._unpack_from(2, ">h") + + def readU16(self): + return self._unpack_from(2, ">H") + + def readI32(self): + return self._unpack_from(4, ">i") + + def readU32(self): + return self._unpack_from(4, ">I") + + def readI64(self): + return self._unpack_from(8, ">q") + + def readU64(self): + return self._unpack_from(8, ">Q") + + def readFloat(self): + v = self._unpack_from(4, ">f") + return v + + def readDouble(self): + return self._unpack_from(8, ">d") + + +class RustBufferBuilder(object): + """ + Helper for structured writing of bytes into a RustBuffer. + """ + + def __init__(self): + self.rbuf = RustBuffer.alloc(16) + self.rbuf.len = 0 + + def finalize(self): + rbuf = self.rbuf + self.rbuf = None + return rbuf + + def discard(self): + if self.rbuf is not None: + rbuf = self.finalize() + rbuf.free() + + @contextlib.contextmanager + def _reserve(self, numBytes): + if self.rbuf.len + numBytes > self.rbuf.capacity: + self.rbuf = RustBuffer.reserve(self.rbuf, numBytes) + yield None + self.rbuf.len += numBytes + + def _pack_into(self, size, format, value): + with self._reserve(size): + # XXX TODO: I feel like I should be able to use `struct.pack_into` here but can't figure it out. + for i, byte in enumerate(struct.pack(format, value)): + self.rbuf.data[self.rbuf.len + i] = byte + + def write(self, value): + with self._reserve(len(value)): + for i, byte in enumerate(value): + self.rbuf.data[self.rbuf.len + i] = byte + + def writeI8(self, v): + self._pack_into(1, ">b", v) + + def writeU8(self, v): + self._pack_into(1, ">B", v) + + def writeI16(self, v): + self._pack_into(2, ">h", v) + + def writeU16(self, v): + self._pack_into(2, ">H", v) + + def writeI32(self, v): + self._pack_into(4, ">i", v) + + def writeU32(self, v): + self._pack_into(4, ">I", v) + + def writeI64(self, v): + self._pack_into(8, ">q", v) + + def writeU64(self, v): + self._pack_into(8, ">Q", v) + + def writeFloat(self, v): + self._pack_into(4, ">f", v) + + def writeDouble(self, v): + self._pack_into(8, ">d", v) +# A handful of classes and functions to support the generated data structures. +# This would be a good candidate for isolating in its own ffi-support lib. + +class InternalError(Exception): + pass + +class RustCallStatus(ctypes.Structure): + """ + Error runtime. + """ + _fields_ = [ + ("code", ctypes.c_int8), + ("error_buf", RustBuffer), + ] + + # These match the values from the uniffi::rustcalls module + CALL_SUCCESS = 0 + CALL_ERROR = 1 + CALL_PANIC = 2 + + def __str__(self): + if self.code == RustCallStatus.CALL_SUCCESS: + return "RustCallStatus(CALL_SUCCESS)" + elif self.code == RustCallStatus.CALL_ERROR: + return "RustCallStatus(CALL_ERROR)" + elif self.code == RustCallStatus.CALL_PANIC: + return "RustCallStatus(CALL_SUCCESS)" + else: + return "RustCallStatus()" + +def rust_call(fn, *args): + # Call a rust function + return rust_call_with_error(None, fn, *args) + +def rust_call_with_error(error_class, fn, *args): + # Call a rust function and handle any errors + # + # This function is used for rust calls that return Result<> and therefore can set the CALL_ERROR status code. + # error_class must be set to the error class that corresponds to the result. + call_status = RustCallStatus(code=RustCallStatus.CALL_SUCCESS, error_buf=RustBuffer(0, 0, None)) + + args_with_error = args + (ctypes.byref(call_status),) + result = fn(*args_with_error) + if call_status.code == RustCallStatus.CALL_SUCCESS: + return result + elif call_status.code == RustCallStatus.CALL_ERROR: + if error_class is None: + call_status.err_buf.contents.free() + raise InternalError("rust_call_with_error: CALL_ERROR, but no error class set") + else: + raise error_class._lift(call_status.error_buf) + elif call_status.code == RustCallStatus.CALL_PANIC: + # 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 call_status.error_buf.len > 0: + msg = FfiConverterString._lift(call_status.error_buf) + else: + msg = "Unknown rust panic" + raise InternalError(msg) + else: + raise InternalError("Invalid RustCallStatus code: {}".format( + call_status.code)) + +# A function pointer for a callback as defined by UniFFI. +# Rust definition `fn(handle: u64, method: u32, args: RustBuffer, buf_ptr: *mut RustBuffer) -> int` +FOREIGN_CALLBACK_T = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_ulonglong, ctypes.c_ulong, RustBuffer, ctypes.POINTER(RustBuffer)) +# Types conforming to `Primitive` pass themselves directly over the FFI. +class Primitive: + @classmethod + def _lift(cls, value): + return value + + @classmethod + def _lower(cls, value): + return value + +# Helper class for new types that will always go through a RustBuffer. +# Classes should inherit from this and implement the `_read` static method +# and `_write` instance methods. +class ViaFfiUsingByteBuffer: + @classmethod + def _lift(cls, rbuf): + with rbuf.consumeWithStream() as stream: + return cls._read(stream) + + def _lower(self): + with RustBuffer.allocWithBuilder() as builder: + self._write(builder) + return builder.finalize() + +# Helper class for wrapper types that will always go through a RustBuffer. +# Classes should inherit from this and implement the `_read` and `_write` static methods. +class FfiConverterUsingByteBuffer: + @classmethod + def _lift(cls, rbuf): + with rbuf.consumeWithStream() as stream: + return cls._read(stream) + + @classmethod + def _lower(cls, value): + with RustBuffer.allocWithBuilder() as builder: + cls._write(value, builder) + return builder.finalize() + +# Helpers for structural types. + +class FfiConverterSequence: + @staticmethod + def _write(value, buf, writeItem): + items = len(value) + buf.writeI32(items) + for item in value: + writeItem(item, buf) + + @staticmethod + def _read(buf, readItem): + count = buf.readI32() + if count < 0: + raise InternalError("Unexpected negative sequence length") + + items = [] + while count > 0: + items.append(readItem(buf)) + count -= 1 + return items + +class FfiConverterOptional: + @staticmethod + def _write(value, buf, writeItem): + if value is None: + buf.writeU8(0) + return + + buf.writeU8(1) + writeItem(value, buf) + + @staticmethod + def _read(buf, readItem): + flag = buf.readU8() + if flag == 0: + return None + elif flag == 1: + return readItem(buf) + else: + raise InternalError("Unexpected flag byte for optional type") + +class FfiConverterDictionary: + @staticmethod + def _write(items, buf, writeItem): + buf.writeI32(len(items)) + for (key, value) in items.items(): + writeItem(key, value, buf) + + @staticmethod + def _read(buf, readItem): + count = buf.readI32() + if count < 0: + raise InternalError("Unexpected negative map size") + items = {} + while count > 0: + key, value = readItem(buf) + items[key] = value + count -= 1 + return items + +# Contains loading, initialization code, +# and the FFI Function declarations in a com.sun.jna.Library. +# This is how we find and load the dynamic library provided by the component. +# For now we just look it up by name. +# +# XXX TODO: This will probably grow some magic for resolving megazording in future. +# E.g. we might start by looking for the named component in `libuniffi.so` and if +# that fails, fall back to loading it separately from `lib${componentName}.so`. + +def loadIndirect(): + if sys.platform == "linux": + # libname = "lib{}.so" + libname = os.path.join(os.path.dirname(__file__), "lib{}.so") + elif sys.platform == "darwin": + # libname = "lib{}.dylib" + libname = os.path.join(os.path.dirname(__file__), "lib{}.dylib") + elif sys.platform.startswith("win"): + # As of python3.8, ctypes does not seem to search $PATH when loading DLLs. + # We could use `os.add_dll_directory` to configure the search path, but + # it doesn't feel right to mess with application-wide settings. Let's + # assume that the `.dll` is next to the `.py` file and load by full path. + libname = os.path.join( + os.path.dirname(__file__), + "{}.dll", + ) + return getattr(ctypes.cdll, libname.format("bdkffi")) + +# A ctypes library to expose the extern-C FFI definitions. +# This is an implementation detail which will be called internally by the public API. + +_UniFFILib = loadIndirect() +_UniFFILib.ffi_bdk_b9b3_OfflineWallet_object_free.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.ffi_bdk_b9b3_OfflineWallet_object_free.restype = None +_UniFFILib.bdk_b9b3_OfflineWallet_new.argtypes = ( + RustBuffer, + RustBuffer, + RustBuffer, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OfflineWallet_new.restype = ctypes.c_void_p +_UniFFILib.bdk_b9b3_OfflineWallet_get_new_address.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OfflineWallet_get_new_address.restype = RustBuffer +_UniFFILib.bdk_b9b3_OfflineWallet_get_last_unused_address.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OfflineWallet_get_last_unused_address.restype = RustBuffer +_UniFFILib.bdk_b9b3_OfflineWallet_get_balance.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OfflineWallet_get_balance.restype = ctypes.c_uint64 +_UniFFILib.bdk_b9b3_OfflineWallet_sign.argtypes = ( + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OfflineWallet_sign.restype = None +_UniFFILib.bdk_b9b3_OfflineWallet_get_transactions.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OfflineWallet_get_transactions.restype = RustBuffer +_UniFFILib.ffi_bdk_b9b3_OnlineWallet_object_free.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.ffi_bdk_b9b3_OnlineWallet_object_free.restype = None +_UniFFILib.bdk_b9b3_OnlineWallet_new.argtypes = ( + RustBuffer, + RustBuffer, + RustBuffer, + RustBuffer, + RustBuffer, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_new.restype = ctypes.c_void_p +_UniFFILib.bdk_b9b3_OnlineWallet_get_new_address.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_get_new_address.restype = RustBuffer +_UniFFILib.bdk_b9b3_OnlineWallet_get_last_unused_address.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_get_last_unused_address.restype = RustBuffer +_UniFFILib.bdk_b9b3_OnlineWallet_get_balance.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_get_balance.restype = ctypes.c_uint64 +_UniFFILib.bdk_b9b3_OnlineWallet_sign.argtypes = ( + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_sign.restype = None +_UniFFILib.bdk_b9b3_OnlineWallet_get_transactions.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_get_transactions.restype = RustBuffer +_UniFFILib.bdk_b9b3_OnlineWallet_get_network.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_get_network.restype = RustBuffer +_UniFFILib.bdk_b9b3_OnlineWallet_sync.argtypes = ( + ctypes.c_void_p, + ctypes.c_uint64, + RustBuffer, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_sync.restype = None +_UniFFILib.bdk_b9b3_OnlineWallet_broadcast.argtypes = ( + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_OnlineWallet_broadcast.restype = RustBuffer +_UniFFILib.ffi_bdk_b9b3_PartiallySignedBitcoinTransaction_object_free.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.ffi_bdk_b9b3_PartiallySignedBitcoinTransaction_object_free.restype = None +_UniFFILib.bdk_b9b3_PartiallySignedBitcoinTransaction_new.argtypes = ( + ctypes.c_void_p, + RustBuffer, + ctypes.c_uint64, + RustBuffer, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_PartiallySignedBitcoinTransaction_new.restype = ctypes.c_void_p +_UniFFILib.ffi_bdk_b9b3_BdkProgress_init_callback.argtypes = ( + FOREIGN_CALLBACK_T, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.ffi_bdk_b9b3_BdkProgress_init_callback.restype = None +_UniFFILib.bdk_b9b3_generate_extended_key.argtypes = ( + RustBuffer, + RustBuffer, + RustBuffer, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_generate_extended_key.restype = RustBuffer +_UniFFILib.bdk_b9b3_restore_extended_key.argtypes = ( + RustBuffer, + RustBuffer, + RustBuffer, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.bdk_b9b3_restore_extended_key.restype = RustBuffer +_UniFFILib.ffi_bdk_b9b3_rustbuffer_alloc.argtypes = ( + ctypes.c_int32, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.ffi_bdk_b9b3_rustbuffer_alloc.restype = RustBuffer +_UniFFILib.ffi_bdk_b9b3_rustbuffer_from_bytes.argtypes = ( + ForeignBytes, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.ffi_bdk_b9b3_rustbuffer_from_bytes.restype = RustBuffer +_UniFFILib.ffi_bdk_b9b3_rustbuffer_free.argtypes = ( + RustBuffer, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.ffi_bdk_b9b3_rustbuffer_free.restype = None +_UniFFILib.ffi_bdk_b9b3_rustbuffer_reserve.argtypes = ( + RustBuffer, + ctypes.c_int32, + ctypes.POINTER(RustCallStatus), +) +_UniFFILib.ffi_bdk_b9b3_rustbuffer_reserve.restype = RustBuffer + +# Public interface members begin here. + +import threading + +class ConcurrentHandleMap: + """ + A map where inserting, getting and removing data is synchronized with a lock. + """ + + def __init__(self): + # type Handle = int + self._left_map = {} # type: Dict[Handle, Any] + self._right_map = {} # type: Dict[Any, Handle] + + self._lock = threading.Lock() + self._current_handle = 0 + self._stride = 1 + + + def insert(self, obj): + with self._lock: + if obj in self._right_map: + return self._right_map[obj] + else: + handle = self._current_handle + self._current_handle += self._stride + self._left_map[handle] = obj + self._right_map[obj] = handle + return handle + + def get(self, handle): + with self._lock: + return self._left_map.get(handle) + + def remove(self, handle): + with self._lock: + if handle in self._left_map: + obj = self._left_map.pop(handle) + del self._right_map[obj] + return obj + +# Magic number for the Rust proxy to call using the same mechanism as every other method, +# to free the callback once it's dropped by Rust. +IDX_CALLBACK_FREE = 0 + +class FfiConverterCallbackInterface: + _handle_map = ConcurrentHandleMap() + + def __init__(self, cb): + self._foreign_callback = cb + + def drop(self, handle): + self.__class__._handle_map.remove(handle) + + @classmethod + def _lift(cls, handle): + obj = cls._handle_map.get(handle) + if not obj: + raise InternalError("The object in the handle map has been dropped already") + + return obj + + @classmethod + def _read(cls, buf): + handle = buf.readU64() + cls._lift(handle) + + @classmethod + def _lower(cls, cb): + handle = cls._handle_map.insert(cb) + return handle + + @classmethod + def _write(cls, cb, buf): + buf.writeU64(cls._lower(cb)) + + + +class Network(ViaFfiUsingByteBuffer, enum.Enum): + BITCOIN = 1 + TESTNET = 2 + SIGNET = 3 + REGTEST = 4 + + + @staticmethod + def _read(buf): + variant = buf.readI32() + if variant == 1: + return Network.BITCOIN + if variant == 2: + return Network.TESTNET + if variant == 3: + return Network.SIGNET + if variant == 4: + return Network.REGTEST + + raise InternalError("Raw enum value doesn't match any cases") + + def _write(self, buf): + if self is Network.BITCOIN: + i = 1 + buf.writeI32(1) + if self is Network.TESTNET: + i = 2 + buf.writeI32(2) + if self is Network.SIGNET: + i = 3 + buf.writeI32(3) + if self is Network.REGTEST: + i = 4 + buf.writeI32(4) + + + + + + + +class DatabaseConfig(ViaFfiUsingByteBuffer, object): + def __init__(self): + raise RuntimeError("DatabaseConfig cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + class MEMORY(object): + def __init__(self,junk ): + + self.junk = junk + + + def __str__(self): + return "DatabaseConfig.MEMORY(junk={} )".format(self.junk ) + + def __eq__(self, other): + if not other.is_memory(): + return False + if self.junk != other.junk: + return False + return True + class SLED(object): + def __init__(self,config ): + + self.config = config + + + def __str__(self): + return "DatabaseConfig.SLED(config={} )".format(self.config ) + + def __eq__(self, other): + if not other.is_sled(): + return False + if self.config != other.config: + return False + return True + + + # For each variant, we have an `is_NAME` method for easily checking + # whether an instance is that variant. + def is_memory(self): + return isinstance(self, DatabaseConfig.MEMORY) + def is_sled(self): + return isinstance(self, DatabaseConfig.SLED) + + + @classmethod + def _read(cls, buf): + variant = buf.readI32() + + if variant == 1: + return cls.MEMORY( + junk=FfiConverterString._read(buf), + ) + if variant == 2: + return cls.SLED( + config=SledDbConfiguration._read(buf), + ) + + raise InternalError("Raw enum value doesn't match any cases") + + def _write(self, buf): + if self.is_memory(): + buf.writeI32(1) + FfiConverterString._write(self.junk, buf) + if self.is_sled(): + buf.writeI32(2) + self.config._write(buf) + + +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +DatabaseConfig.MEMORY = type("DatabaseConfig.MEMORY", (DatabaseConfig.MEMORY, DatabaseConfig,), {}) +DatabaseConfig.SLED = type("DatabaseConfig.SLED", (DatabaseConfig.SLED, DatabaseConfig,), {}) + + + + + + + +class Transaction(ViaFfiUsingByteBuffer, object): + def __init__(self): + raise RuntimeError("Transaction cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + class UNCONFIRMED(object): + def __init__(self,details ): + + self.details = details + + + def __str__(self): + return "Transaction.UNCONFIRMED(details={} )".format(self.details ) + + def __eq__(self, other): + if not other.is_unconfirmed(): + return False + if self.details != other.details: + return False + return True + class CONFIRMED(object): + def __init__(self,details, confirmation ): + + self.details = details + self.confirmation = confirmation + + + def __str__(self): + return "Transaction.CONFIRMED(details={}, confirmation={} )".format(self.details, self.confirmation ) + + def __eq__(self, other): + if not other.is_confirmed(): + return False + if self.details != other.details: + return False + if self.confirmation != other.confirmation: + return False + return True + + + # For each variant, we have an `is_NAME` method for easily checking + # whether an instance is that variant. + def is_unconfirmed(self): + return isinstance(self, Transaction.UNCONFIRMED) + def is_confirmed(self): + return isinstance(self, Transaction.CONFIRMED) + + + @classmethod + def _read(cls, buf): + variant = buf.readI32() + + if variant == 1: + return cls.UNCONFIRMED( + details=TransactionDetails._read(buf), + ) + if variant == 2: + return cls.CONFIRMED( + details=TransactionDetails._read(buf), + confirmation=Confirmation._read(buf), + ) + + raise InternalError("Raw enum value doesn't match any cases") + + def _write(self, buf): + if self.is_unconfirmed(): + buf.writeI32(1) + self.details._write(buf) + if self.is_confirmed(): + buf.writeI32(2) + self.details._write(buf) + self.confirmation._write(buf) + + +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +Transaction.UNCONFIRMED = type("Transaction.UNCONFIRMED", (Transaction.UNCONFIRMED, Transaction,), {}) +Transaction.CONFIRMED = type("Transaction.CONFIRMED", (Transaction.CONFIRMED, Transaction,), {}) + + + + + + + +class BlockchainConfig(ViaFfiUsingByteBuffer, object): + def __init__(self): + raise RuntimeError("BlockchainConfig cannot be instantiated directly") + + # Each enum variant is a nested class of the enum itself. + class ELECTRUM(object): + def __init__(self,config ): + + self.config = config + + + def __str__(self): + return "BlockchainConfig.ELECTRUM(config={} )".format(self.config ) + + def __eq__(self, other): + if not other.is_electrum(): + return False + if self.config != other.config: + return False + return True + class ESPLORA(object): + def __init__(self,config ): + + self.config = config + + + def __str__(self): + return "BlockchainConfig.ESPLORA(config={} )".format(self.config ) + + def __eq__(self, other): + if not other.is_esplora(): + return False + if self.config != other.config: + return False + return True + + + # For each variant, we have an `is_NAME` method for easily checking + # whether an instance is that variant. + def is_electrum(self): + return isinstance(self, BlockchainConfig.ELECTRUM) + def is_esplora(self): + return isinstance(self, BlockchainConfig.ESPLORA) + + + @classmethod + def _read(cls, buf): + variant = buf.readI32() + + if variant == 1: + return cls.ELECTRUM( + config=ElectrumConfig._read(buf), + ) + if variant == 2: + return cls.ESPLORA( + config=EsploraConfig._read(buf), + ) + + raise InternalError("Raw enum value doesn't match any cases") + + def _write(self, buf): + if self.is_electrum(): + buf.writeI32(1) + self.config._write(buf) + if self.is_esplora(): + buf.writeI32(2) + self.config._write(buf) + + +# Now, a little trick - we make each nested variant class be a subclass of the main +# enum class, so that method calls and instance checks etc will work intuitively. +# We might be able to do this a little more neatly with a metaclass, but this'll do. +BlockchainConfig.ELECTRUM = type("BlockchainConfig.ELECTRUM", (BlockchainConfig.ELECTRUM, BlockchainConfig,), {}) +BlockchainConfig.ESPLORA = type("BlockchainConfig.ESPLORA", (BlockchainConfig.ESPLORA, BlockchainConfig,), {}) + + + + + + +class MnemonicType(ViaFfiUsingByteBuffer, enum.Enum): + WORDS12 = 1 + WORDS15 = 2 + WORDS18 = 3 + WORDS21 = 4 + WORDS24 = 5 + + + @staticmethod + def _read(buf): + variant = buf.readI32() + if variant == 1: + return MnemonicType.WORDS12 + if variant == 2: + return MnemonicType.WORDS15 + if variant == 3: + return MnemonicType.WORDS18 + if variant == 4: + return MnemonicType.WORDS21 + if variant == 5: + return MnemonicType.WORDS24 + + raise InternalError("Raw enum value doesn't match any cases") + + def _write(self, buf): + if self is MnemonicType.WORDS12: + i = 1 + buf.writeI32(1) + if self is MnemonicType.WORDS15: + i = 2 + buf.writeI32(2) + if self is MnemonicType.WORDS18: + i = 3 + buf.writeI32(3) + if self is MnemonicType.WORDS21: + i = 4 + buf.writeI32(4) + if self is MnemonicType.WORDS24: + i = 5 + buf.writeI32(5) + + + + + +def generate_extended_key(network,mnemonic_type,password): + network = network + mnemonic_type = mnemonic_type + password = (None if password is None else password) + _retval = rust_call_with_error(BdkError,_UniFFILib.bdk_b9b3_generate_extended_key,network._lower(),mnemonic_type._lower(),FfiConverterOptionalString._lower(password)) + return ExtendedKeyInfo._lift(_retval) + + + +def restore_extended_key(network,mnemonic,password): + network = network + mnemonic = mnemonic + password = (None if password is None else password) + _retval = rust_call_with_error(BdkError,_UniFFILib.bdk_b9b3_restore_extended_key,network._lower(),FfiConverterString._lower(mnemonic),FfiConverterOptionalString._lower(password)) + return ExtendedKeyInfo._lift(_retval) + + + +class OfflineWallet(object): + def __init__(self, descriptor,network,database_config): + descriptor = descriptor + network = network + database_config = database_config + self._pointer = rust_call_with_error(BdkError,_UniFFILib.bdk_b9b3_OfflineWallet_new,FfiConverterString._lower(descriptor),network._lower(),database_config._lower()) + + def __del__(self): + # In case of partial initialization of instances. + pointer = getattr(self, "_pointer", None) + if pointer is not None: + rust_call(_UniFFILib.ffi_bdk_b9b3_OfflineWallet_object_free, pointer) + + # Used by alternative constructors or any methods which return this type. + @classmethod + def _make_instance_(cls, pointer): + # Lightly yucky way to bypass the usual __init__ logic + # and just create a new instance with the required pointer. + inst = cls.__new__(cls) + inst._pointer = pointer + return inst + + + + def get_new_address(self, ): + _retval = rust_call(_UniFFILib.bdk_b9b3_OfflineWallet_get_new_address,self._pointer,) + return FfiConverterString._lift(_retval) + + def get_last_unused_address(self, ): + _retval = rust_call(_UniFFILib.bdk_b9b3_OfflineWallet_get_last_unused_address,self._pointer,) + return FfiConverterString._lift(_retval) + + def get_balance(self, ): + _retval = rust_call_with_error( + BdkError,_UniFFILib.bdk_b9b3_OfflineWallet_get_balance,self._pointer,) + return FfiConverterUInt64._lift(_retval) + + def sign(self, psbt): + psbt = psbt + rust_call_with_error( + BdkError,_UniFFILib.bdk_b9b3_OfflineWallet_sign,self._pointer,psbt._lower()) + + def get_transactions(self, ): + _retval = rust_call_with_error( + BdkError,_UniFFILib.bdk_b9b3_OfflineWallet_get_transactions,self._pointer,) + return FfiConverterSequenceEnumTransaction._lift(_retval) + + + + @classmethod + def _read(cls, buf): + ptr = buf.readU64() + if ptr == 0: + raise InternalError("Raw pointer value was null") + return cls._lift(ptr) + + @classmethod + def _write(cls, value, buf): + if not isinstance(value, OfflineWallet): + raise TypeError("Expected OfflineWallet instance, {} found".format(value.__class__.__name__)) + buf.writeU64(value._lower()) + + @classmethod + def _lift(cls, pointer): + return cls._make_instance_(pointer) + + def _lower(self): + return self._pointer + + +class OnlineWallet(object): + def __init__(self, descriptor,change_descriptor,network,database_config,blockchain_config): + descriptor = descriptor + change_descriptor = (None if change_descriptor is None else change_descriptor) + network = network + database_config = database_config + blockchain_config = blockchain_config + self._pointer = rust_call_with_error(BdkError,_UniFFILib.bdk_b9b3_OnlineWallet_new,FfiConverterString._lower(descriptor),FfiConverterOptionalString._lower(change_descriptor),network._lower(),database_config._lower(),blockchain_config._lower()) + + def __del__(self): + # In case of partial initialization of instances. + pointer = getattr(self, "_pointer", None) + if pointer is not None: + rust_call(_UniFFILib.ffi_bdk_b9b3_OnlineWallet_object_free, pointer) + + # Used by alternative constructors or any methods which return this type. + @classmethod + def _make_instance_(cls, pointer): + # Lightly yucky way to bypass the usual __init__ logic + # and just create a new instance with the required pointer. + inst = cls.__new__(cls) + inst._pointer = pointer + return inst + + + + def get_new_address(self, ): + _retval = rust_call(_UniFFILib.bdk_b9b3_OnlineWallet_get_new_address,self._pointer,) + return FfiConverterString._lift(_retval) + + def get_last_unused_address(self, ): + _retval = rust_call(_UniFFILib.bdk_b9b3_OnlineWallet_get_last_unused_address,self._pointer,) + return FfiConverterString._lift(_retval) + + def get_balance(self, ): + _retval = rust_call_with_error( + BdkError,_UniFFILib.bdk_b9b3_OnlineWallet_get_balance,self._pointer,) + return FfiConverterUInt64._lift(_retval) + + def sign(self, psbt): + psbt = psbt + rust_call_with_error( + BdkError,_UniFFILib.bdk_b9b3_OnlineWallet_sign,self._pointer,psbt._lower()) + + def get_transactions(self, ): + _retval = rust_call_with_error( + BdkError,_UniFFILib.bdk_b9b3_OnlineWallet_get_transactions,self._pointer,) + return FfiConverterSequenceEnumTransaction._lift(_retval) + + def get_network(self, ): + _retval = rust_call(_UniFFILib.bdk_b9b3_OnlineWallet_get_network,self._pointer,) + return Network._lift(_retval) + + def sync(self, progress_update,max_address_param): + progress_update = progress_update + max_address_param = (None if max_address_param is None else int(max_address_param)) + rust_call_with_error( + BdkError,_UniFFILib.bdk_b9b3_OnlineWallet_sync,self._pointer,FfiConverterCallbackInterfaceBdkProgress._lower(progress_update),FfiConverterOptionalUInt32._lower(max_address_param)) + + def broadcast(self, psbt): + psbt = psbt + _retval = rust_call_with_error( + BdkError,_UniFFILib.bdk_b9b3_OnlineWallet_broadcast,self._pointer,psbt._lower()) + return Transaction._lift(_retval) + + + + @classmethod + def _read(cls, buf): + ptr = buf.readU64() + if ptr == 0: + raise InternalError("Raw pointer value was null") + return cls._lift(ptr) + + @classmethod + def _write(cls, value, buf): + if not isinstance(value, OnlineWallet): + raise TypeError("Expected OnlineWallet instance, {} found".format(value.__class__.__name__)) + buf.writeU64(value._lower()) + + @classmethod + def _lift(cls, pointer): + return cls._make_instance_(pointer) + + def _lower(self): + return self._pointer + + +class PartiallySignedBitcoinTransaction(object): + def __init__(self, wallet,recipient,amount,fee_rate): + wallet = wallet + recipient = recipient + amount = int(amount) + fee_rate = (None if fee_rate is None else float(fee_rate)) + self._pointer = rust_call_with_error(BdkError,_UniFFILib.bdk_b9b3_PartiallySignedBitcoinTransaction_new,wallet._lower(),FfiConverterString._lower(recipient),FfiConverterUInt64._lower(amount),FfiConverterOptionalFloat._lower(fee_rate)) + + def __del__(self): + # In case of partial initialization of instances. + pointer = getattr(self, "_pointer", None) + if pointer is not None: + rust_call(_UniFFILib.ffi_bdk_b9b3_PartiallySignedBitcoinTransaction_object_free, pointer) + + # Used by alternative constructors or any methods which return this type. + @classmethod + def _make_instance_(cls, pointer): + # Lightly yucky way to bypass the usual __init__ logic + # and just create a new instance with the required pointer. + inst = cls.__new__(cls) + inst._pointer = pointer + return inst + + + + + + @classmethod + def _read(cls, buf): + ptr = buf.readU64() + if ptr == 0: + raise InternalError("Raw pointer value was null") + return cls._lift(ptr) + + @classmethod + def _write(cls, value, buf): + if not isinstance(value, PartiallySignedBitcoinTransaction): + raise TypeError("Expected PartiallySignedBitcoinTransaction instance, {} found".format(value.__class__.__name__)) + buf.writeU64(value._lower()) + + @classmethod + def _lift(cls, pointer): + return cls._make_instance_(pointer) + + def _lower(self): + return self._pointer + +class SledDbConfiguration(ViaFfiUsingByteBuffer, object): + def __init__(self,path, tree_name ): + self.path = path + self.tree_name = tree_name + + def __str__(self): + return "SledDbConfiguration(path={}, tree_name={} )".format(self.path, self.tree_name ) + + def __eq__(self, other): + if self.path != other.path: + return False + if self.tree_name != other.tree_name: + return False + return True + + @staticmethod + def _read(buf): + return SledDbConfiguration( + path=FfiConverterString._read(buf), + tree_name=FfiConverterString._read(buf) + ) + + def _write(self, buf): + FfiConverterString._write(self.path, buf) + FfiConverterString._write(self.tree_name, buf) + +class TransactionDetails(ViaFfiUsingByteBuffer, object): + def __init__(self,fees, received, sent, txid ): + self.fees = fees + self.received = received + self.sent = sent + self.txid = txid + + def __str__(self): + return "TransactionDetails(fees={}, received={}, sent={}, txid={} )".format(self.fees, self.received, self.sent, self.txid ) + + def __eq__(self, other): + if self.fees != other.fees: + return False + if self.received != other.received: + return False + if self.sent != other.sent: + return False + if self.txid != other.txid: + return False + return True + + @staticmethod + def _read(buf): + return TransactionDetails( + fees=FfiConverterOptionalUInt64._read(buf), + received=FfiConverterUInt64._read(buf), + sent=FfiConverterUInt64._read(buf), + txid=FfiConverterString._read(buf) + ) + + def _write(self, buf): + FfiConverterOptionalUInt64._write(self.fees, buf) + FfiConverterUInt64._write(self.received, buf) + FfiConverterUInt64._write(self.sent, buf) + FfiConverterString._write(self.txid, buf) + +class Confirmation(ViaFfiUsingByteBuffer, object): + def __init__(self,height, timestamp ): + self.height = height + self.timestamp = timestamp + + def __str__(self): + return "Confirmation(height={}, timestamp={} )".format(self.height, self.timestamp ) + + def __eq__(self, other): + if self.height != other.height: + return False + if self.timestamp != other.timestamp: + return False + return True + + @staticmethod + def _read(buf): + return Confirmation( + height=FfiConverterUInt32._read(buf), + timestamp=FfiConverterUInt64._read(buf) + ) + + def _write(self, buf): + FfiConverterUInt32._write(self.height, buf) + FfiConverterUInt64._write(self.timestamp, buf) + +class ElectrumConfig(ViaFfiUsingByteBuffer, object): + def __init__(self,url, socks5, retry, timeout, stop_gap ): + self.url = url + self.socks5 = socks5 + self.retry = retry + self.timeout = timeout + self.stop_gap = stop_gap + + def __str__(self): + return "ElectrumConfig(url={}, socks5={}, retry={}, timeout={}, stop_gap={} )".format(self.url, self.socks5, self.retry, self.timeout, self.stop_gap ) + + def __eq__(self, other): + if self.url != other.url: + return False + if self.socks5 != other.socks5: + return False + if self.retry != other.retry: + return False + if self.timeout != other.timeout: + return False + if self.stop_gap != other.stop_gap: + return False + return True + + @staticmethod + def _read(buf): + return ElectrumConfig( + url=FfiConverterString._read(buf), + socks5=FfiConverterOptionalString._read(buf), + retry=FfiConverterUInt8._read(buf), + timeout=FfiConverterOptionalUInt8._read(buf), + stop_gap=FfiConverterUInt64._read(buf) + ) + + def _write(self, buf): + FfiConverterString._write(self.url, buf) + FfiConverterOptionalString._write(self.socks5, buf) + FfiConverterUInt8._write(self.retry, buf) + FfiConverterOptionalUInt8._write(self.timeout, buf) + FfiConverterUInt64._write(self.stop_gap, buf) + +class EsploraConfig(ViaFfiUsingByteBuffer, object): + def __init__(self,base_url, proxy, timeout_read, timeout_write, stop_gap ): + self.base_url = base_url + self.proxy = proxy + self.timeout_read = timeout_read + self.timeout_write = timeout_write + self.stop_gap = stop_gap + + def __str__(self): + return "EsploraConfig(base_url={}, proxy={}, timeout_read={}, timeout_write={}, stop_gap={} )".format(self.base_url, self.proxy, self.timeout_read, self.timeout_write, self.stop_gap ) + + def __eq__(self, other): + if self.base_url != other.base_url: + return False + if self.proxy != other.proxy: + return False + if self.timeout_read != other.timeout_read: + return False + if self.timeout_write != other.timeout_write: + return False + if self.stop_gap != other.stop_gap: + return False + return True + + @staticmethod + def _read(buf): + return EsploraConfig( + base_url=FfiConverterString._read(buf), + proxy=FfiConverterOptionalString._read(buf), + timeout_read=FfiConverterUInt64._read(buf), + timeout_write=FfiConverterUInt64._read(buf), + stop_gap=FfiConverterUInt64._read(buf) + ) + + def _write(self, buf): + FfiConverterString._write(self.base_url, buf) + FfiConverterOptionalString._write(self.proxy, buf) + FfiConverterUInt64._write(self.timeout_read, buf) + FfiConverterUInt64._write(self.timeout_write, buf) + FfiConverterUInt64._write(self.stop_gap, buf) + +class ExtendedKeyInfo(ViaFfiUsingByteBuffer, object): + def __init__(self,mnemonic, xprv, fingerprint ): + self.mnemonic = mnemonic + self.xprv = xprv + self.fingerprint = fingerprint + + def __str__(self): + return "ExtendedKeyInfo(mnemonic={}, xprv={}, fingerprint={} )".format(self.mnemonic, self.xprv, self.fingerprint ) + + def __eq__(self, other): + if self.mnemonic != other.mnemonic: + return False + if self.xprv != other.xprv: + return False + if self.fingerprint != other.fingerprint: + return False + return True + + @staticmethod + def _read(buf): + return ExtendedKeyInfo( + mnemonic=FfiConverterString._read(buf), + xprv=FfiConverterString._read(buf), + fingerprint=FfiConverterString._read(buf) + ) + + def _write(self, buf): + FfiConverterString._write(self.mnemonic, buf) + FfiConverterString._write(self.xprv, buf) + FfiConverterString._write(self.fingerprint, buf) + +class BdkError(ViaFfiUsingByteBuffer): + + # Each variant is a nested class of the error itself. + # It just carries a string error message, so no special implementation is necessary. + class InvalidU32Bytes(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(1) + message = str(self) + FfiConverterString._write(message, buf) + class Generic(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(2) + message = str(self) + FfiConverterString._write(message, buf) + class ScriptDoesntHaveAddressForm(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(3) + message = str(self) + FfiConverterString._write(message, buf) + class NoRecipients(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(4) + message = str(self) + FfiConverterString._write(message, buf) + class NoUtxosSelected(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(5) + message = str(self) + FfiConverterString._write(message, buf) + class OutputBelowDustLimit(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(6) + message = str(self) + FfiConverterString._write(message, buf) + class InsufficientFunds(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(7) + message = str(self) + FfiConverterString._write(message, buf) + class BnBTotalTriesExceeded(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(8) + message = str(self) + FfiConverterString._write(message, buf) + class BnBNoExactMatch(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(9) + message = str(self) + FfiConverterString._write(message, buf) + class UnknownUtxo(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(10) + message = str(self) + FfiConverterString._write(message, buf) + class TransactionNotFound(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(11) + message = str(self) + FfiConverterString._write(message, buf) + class TransactionConfirmed(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(12) + message = str(self) + FfiConverterString._write(message, buf) + class IrreplaceableTransaction(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(13) + message = str(self) + FfiConverterString._write(message, buf) + class FeeRateTooLow(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(14) + message = str(self) + FfiConverterString._write(message, buf) + class FeeTooLow(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(15) + message = str(self) + FfiConverterString._write(message, buf) + class FeeRateUnavailable(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(16) + message = str(self) + FfiConverterString._write(message, buf) + class MissingKeyOrigin(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(17) + message = str(self) + FfiConverterString._write(message, buf) + class Key(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(18) + message = str(self) + FfiConverterString._write(message, buf) + class ChecksumMismatch(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(19) + message = str(self) + FfiConverterString._write(message, buf) + class SpendingPolicyRequired(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(20) + message = str(self) + FfiConverterString._write(message, buf) + class InvalidPolicyPathError(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(21) + message = str(self) + FfiConverterString._write(message, buf) + class Signer(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(22) + message = str(self) + FfiConverterString._write(message, buf) + class InvalidNetwork(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(23) + message = str(self) + FfiConverterString._write(message, buf) + class InvalidProgressValue(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(24) + message = str(self) + FfiConverterString._write(message, buf) + class ProgressUpdateError(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(25) + message = str(self) + FfiConverterString._write(message, buf) + class InvalidOutpoint(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(26) + message = str(self) + FfiConverterString._write(message, buf) + class Descriptor(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(27) + message = str(self) + FfiConverterString._write(message, buf) + class AddressValidator(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(28) + message = str(self) + FfiConverterString._write(message, buf) + class Encode(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(29) + message = str(self) + FfiConverterString._write(message, buf) + class Miniscript(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(30) + message = str(self) + FfiConverterString._write(message, buf) + class Bip32(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(31) + message = str(self) + FfiConverterString._write(message, buf) + class Secp256k1(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(32) + message = str(self) + FfiConverterString._write(message, buf) + class Json(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(33) + message = str(self) + FfiConverterString._write(message, buf) + class Hex(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(34) + message = str(self) + FfiConverterString._write(message, buf) + class Psbt(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(35) + message = str(self) + FfiConverterString._write(message, buf) + class PsbtParse(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(36) + message = str(self) + FfiConverterString._write(message, buf) + class Electrum(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(37) + message = str(self) + FfiConverterString._write(message, buf) + class Esplora(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(38) + message = str(self) + FfiConverterString._write(message, buf) + class Sled(ViaFfiUsingByteBuffer, Exception): + def _write(self, buf): + buf.writeI32(39) + message = str(self) + FfiConverterString._write(message, buf) + + @classmethod + def _read(cls, buf): + variant = buf.readI32() + if variant == 1: + return cls.InvalidU32Bytes(FfiConverterString._read(buf)) + if variant == 2: + return cls.Generic(FfiConverterString._read(buf)) + if variant == 3: + return cls.ScriptDoesntHaveAddressForm(FfiConverterString._read(buf)) + if variant == 4: + return cls.NoRecipients(FfiConverterString._read(buf)) + if variant == 5: + return cls.NoUtxosSelected(FfiConverterString._read(buf)) + if variant == 6: + return cls.OutputBelowDustLimit(FfiConverterString._read(buf)) + if variant == 7: + return cls.InsufficientFunds(FfiConverterString._read(buf)) + if variant == 8: + return cls.BnBTotalTriesExceeded(FfiConverterString._read(buf)) + if variant == 9: + return cls.BnBNoExactMatch(FfiConverterString._read(buf)) + if variant == 10: + return cls.UnknownUtxo(FfiConverterString._read(buf)) + if variant == 11: + return cls.TransactionNotFound(FfiConverterString._read(buf)) + if variant == 12: + return cls.TransactionConfirmed(FfiConverterString._read(buf)) + if variant == 13: + return cls.IrreplaceableTransaction(FfiConverterString._read(buf)) + if variant == 14: + return cls.FeeRateTooLow(FfiConverterString._read(buf)) + if variant == 15: + return cls.FeeTooLow(FfiConverterString._read(buf)) + if variant == 16: + return cls.FeeRateUnavailable(FfiConverterString._read(buf)) + if variant == 17: + return cls.MissingKeyOrigin(FfiConverterString._read(buf)) + if variant == 18: + return cls.Key(FfiConverterString._read(buf)) + if variant == 19: + return cls.ChecksumMismatch(FfiConverterString._read(buf)) + if variant == 20: + return cls.SpendingPolicyRequired(FfiConverterString._read(buf)) + if variant == 21: + return cls.InvalidPolicyPathError(FfiConverterString._read(buf)) + if variant == 22: + return cls.Signer(FfiConverterString._read(buf)) + if variant == 23: + return cls.InvalidNetwork(FfiConverterString._read(buf)) + if variant == 24: + return cls.InvalidProgressValue(FfiConverterString._read(buf)) + if variant == 25: + return cls.ProgressUpdateError(FfiConverterString._read(buf)) + if variant == 26: + return cls.InvalidOutpoint(FfiConverterString._read(buf)) + if variant == 27: + return cls.Descriptor(FfiConverterString._read(buf)) + if variant == 28: + return cls.AddressValidator(FfiConverterString._read(buf)) + if variant == 29: + return cls.Encode(FfiConverterString._read(buf)) + if variant == 30: + return cls.Miniscript(FfiConverterString._read(buf)) + if variant == 31: + return cls.Bip32(FfiConverterString._read(buf)) + if variant == 32: + return cls.Secp256k1(FfiConverterString._read(buf)) + if variant == 33: + return cls.Json(FfiConverterString._read(buf)) + if variant == 34: + return cls.Hex(FfiConverterString._read(buf)) + if variant == 35: + return cls.Psbt(FfiConverterString._read(buf)) + if variant == 36: + return cls.PsbtParse(FfiConverterString._read(buf)) + if variant == 37: + return cls.Electrum(FfiConverterString._read(buf)) + if variant == 38: + return cls.Esplora(FfiConverterString._read(buf)) + if variant == 39: + return cls.Sled(FfiConverterString._read(buf)) + + raise InternalError("Raw enum value doesn't match any cases") + + +# Declaration and FfiConverters for BdkProgress Callback Interface + +class BdkProgress: + def update(progress,message): + raise NotImplementedError + + + +def py_foreignCallbackCallbackInterfaceBdkProgress(handle, method, args, buf_ptr): + + def invoke_update(python_callback, args): + rval = None + with args.consumeWithStream() as buf: + rval = python_callback.update( + FfiConverterFloat._read(buf), + FfiConverterOptionalString._read(buf) + ) + return RustBuffer.alloc(0) + # TODO catch errors and report them back to Rust. + # https://github.com/mozilla/uniffi-rs/issues/351 + + + cb = FfiConverterCallbackInterfaceBdkProgress._lift(handle) + if not cb: + raise InternalError("No callback in handlemap; this is a Uniffi bug") + + if method == IDX_CALLBACK_FREE: + FfiConverterCallbackInterfaceBdkProgress.drop(handle) + # No return value. + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return 0 + + if method == 1: + buf_ptr[0] = invoke_update(cb, args) + # Value written to out buffer. + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return 1 + + + # This should never happen, because an out of bounds method index won't + # ever be used. Once we can catch errors, we should return an InternalException. + # https://github.com/mozilla/uniffi-rs/issues/351 + + # An unexpected error happened. + # See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` + return -1 + +# We need to keep this function reference alive: +# if they get GC'd while in use then UniFFI internals could attempt to call a function +# that is in freed memory. +# That would be...uh...bad. Yeah, that's the word. Bad. +foreignCallbackCallbackInterfaceBdkProgress = FOREIGN_CALLBACK_T(py_foreignCallbackCallbackInterfaceBdkProgress) + +# The FfiConverter which transforms the Callbacks in to Handles to pass to Rust. +rust_call(lambda err: _UniFFILib.ffi_bdk_b9b3_BdkProgress_init_callback(foreignCallbackCallbackInterfaceBdkProgress, err)) +FfiConverterCallbackInterfaceBdkProgress = FfiConverterCallbackInterface(foreignCallbackCallbackInterfaceBdkProgress) +class FfiConverterUInt8(Primitive): + @staticmethod + def _read(buf): + return FfiConverterUInt8._lift(buf.readU8()) + + @staticmethod + def _write(value, buf): + buf.writeU8(FfiConverterUInt8._lower(value)) +class FfiConverterUInt32(Primitive): + @staticmethod + def _read(buf): + return FfiConverterUInt32._lift(buf.readU32()) + + @staticmethod + def _write(value, buf): + buf.writeU32(FfiConverterUInt32._lower(value)) +class FfiConverterUInt64(Primitive): + @staticmethod + def _read(buf): + return FfiConverterUInt64._lift(buf.readU64()) + + @staticmethod + def _write(value, buf): + buf.writeU64(FfiConverterUInt64._lower(value)) +class FfiConverterFloat(Primitive): + @staticmethod + def _read(buf): + return FfiConverterFloat._lift(buf.readFloat()) + + @staticmethod + def _write(value, buf): + buf.writeFloat(FfiConverterFloat._lower(value)) +class FfiConverterString: + @staticmethod + def _read(buf): + size = buf.readI32() + if size < 0: + raise InternalError("Unexpected negative string length") + utf8Bytes = buf.read(size) + return utf8Bytes.decode("utf-8") + + @staticmethod + def _write(value, buf): + utf8Bytes = value.encode("utf-8") + buf.writeI32(len(utf8Bytes)) + buf.write(utf8Bytes) + + @staticmethod + def _lift(buf): + with buf.consumeWithStream() as stream: + return stream.read(stream.remaining()).decode("utf-8") + + @staticmethod + def _lower(value): + with RustBuffer.allocWithBuilder() as builder: + builder.write(value.encode("utf-8")) + return builder.finalize() +# Helper code for OfflineWallet class is found in ObjectTemplate.py +# Helper code for OnlineWallet class is found in ObjectTemplate.py +# Helper code for PartiallySignedBitcoinTransaction class is found in ObjectTemplate.py +# Helper code for Confirmation record is found in RecordTemplate.py +# Helper code for ElectrumConfig record is found in RecordTemplate.py +# Helper code for EsploraConfig record is found in RecordTemplate.py +# Helper code for ExtendedKeyInfo record is found in RecordTemplate.py +# Helper code for SledDbConfiguration record is found in RecordTemplate.py +# Helper code for TransactionDetails record is found in RecordTemplate.py +# Helper code for BlockchainConfig enum is found in EnumTemplate.py +# Helper code for DatabaseConfig enum is found in EnumTemplate.py +# Helper code for MnemonicType enum is found in EnumTemplate.py +# Helper code for Network enum is found in EnumTemplate.py +# Helper code for Transaction enum is found in EnumTemplate.py +# Helper code for BdkError error is found in ErrorTemplate.py + + +class FfiConverterOptionalUInt8(FfiConverterUsingByteBuffer): + @staticmethod + def _write(value, buf): + FfiConverterOptional._write(value, buf, lambda v, buf: FfiConverterUInt8._write(v, buf)) + + @staticmethod + def _read(buf): + return FfiConverterOptional._read(buf, lambda buf: FfiConverterUInt8._read(buf)) + + +class FfiConverterOptionalUInt32(FfiConverterUsingByteBuffer): + @staticmethod + def _write(value, buf): + FfiConverterOptional._write(value, buf, lambda v, buf: FfiConverterUInt32._write(v, buf)) + + @staticmethod + def _read(buf): + return FfiConverterOptional._read(buf, lambda buf: FfiConverterUInt32._read(buf)) + + +class FfiConverterOptionalUInt64(FfiConverterUsingByteBuffer): + @staticmethod + def _write(value, buf): + FfiConverterOptional._write(value, buf, lambda v, buf: FfiConverterUInt64._write(v, buf)) + + @staticmethod + def _read(buf): + return FfiConverterOptional._read(buf, lambda buf: FfiConverterUInt64._read(buf)) + + +class FfiConverterOptionalFloat(FfiConverterUsingByteBuffer): + @staticmethod + def _write(value, buf): + FfiConverterOptional._write(value, buf, lambda v, buf: FfiConverterFloat._write(v, buf)) + + @staticmethod + def _read(buf): + return FfiConverterOptional._read(buf, lambda buf: FfiConverterFloat._read(buf)) + + +class FfiConverterOptionalString(FfiConverterUsingByteBuffer): + @staticmethod + def _write(value, buf): + FfiConverterOptional._write(value, buf, lambda v, buf: FfiConverterString._write(v, buf)) + + @staticmethod + def _read(buf): + return FfiConverterOptional._read(buf, lambda buf: FfiConverterString._read(buf)) + + +class FfiConverterSequenceEnumTransaction(FfiConverterUsingByteBuffer): + @staticmethod + def _write(value, buf): + FfiConverterSequence._write(value, buf, lambda v, buf: v._write(buf)) + + @staticmethod + def _read(buf): + return FfiConverterSequence._read(buf, lambda buf: Transaction._read(buf)) + +__all__ = [ + "InternalError", + "Network", + "DatabaseConfig", + "Transaction", + "BlockchainConfig", + "MnemonicType", + "SledDbConfiguration", + "TransactionDetails", + "Confirmation", + "ElectrumConfig", + "EsploraConfig", + "ExtendedKeyInfo", + "generate_extended_key", + "restore_extended_key", + "OfflineWallet", + "OnlineWallet", + "PartiallySignedBitcoinTransaction", + "BdkError", + "BdkProgress", +] + diff --git a/tests/test_bdk.py b/tests/test_bdk.py new file mode 100644 index 0000000..85a6542 --- /dev/null +++ b/tests/test_bdk.py @@ -0,0 +1,33 @@ +import bdkpython as bdk + + +# taken from bdk test suite @ https://github.com/bitcoindevkit/bdk/blob/master/src/descriptor/template.rs#L676 +descriptor = "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/84h/0h/0h/0/*)" +config = bdk.DatabaseConfig.MEMORY("") +client = bdk.BlockchainConfig.ELECTRUM( + bdk.ElectrumConfig( + "ssl://electrum.blockstream.info:60002", + None, + 5, + None, + 100 + ) +) + + +def test_address_BIP84_testnet(): + wallet = bdk.OfflineWallet(descriptor, bdk.Network.TESTNET, config) + address = wallet.get_new_address() + assert address == "tb1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvvztyse" + +# def test_wallet_balance(): +# wallet = bdk.OnlineWallet( +# descriptor=descriptor, +# change_descriptor=descriptor, +# network=bdk.Network.TESTNET, +# database_config=config, +# blockchain_config=client +# ) +# wallet.sync() +# balance = wallet.get_balance() +# assert balance > 0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..95f6045 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = + py39 + +[testenv] +deps = + pytest + bdk +commands = + pytest --verbose --override-ini console_output_style=count