diff --git a/.gitignore b/.gitignore index 77efe9de8..10c407ebb 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ *.xcworkspace *.xcodeproj -.idea/ \ No newline at end of file +.idea/ +tags +.env diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 000000000..2f2c81f88 --- /dev/null +++ b/python/.gitignore @@ -0,0 +1,5 @@ +*.egg-info +*.eggs +build/ +__pycache__ +*.pyc diff --git a/python/bindings.c b/python/bindings.c new file mode 100644 index 000000000..2d00f7a85 --- /dev/null +++ b/python/bindings.c @@ -0,0 +1,1339 @@ +#include +#include "structmember.h" +#include "BRInt.h" +#include "BRBIP32Sequence.h" +#include "BRBIP39Mnemonic.h" +#include "BRKey.h" +#include "BRTransaction.h" +#include "BRWallet.h" + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Ints + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + typedef struct { + PyObject_HEAD + UInt256 ob_fval; + } b_UInt256; + + static PyObject *b_UInt256New(PyTypeObject *type, PyObject *args, PyObject *kwds) { + b_UInt256 *self = (b_UInt256 *)type->tp_alloc(type, 0); + if (self != NULL) { + self->ob_fval = UINT256_ZERO; + } + return (PyObject *)self; + } + +static PyObject *b_UInt256FromHex(PyObject *cls, PyObject *args, PyObject *kwds) { + PyObject *result = NULL; + char *hex = ""; + static char *kwlist[] = { "hex", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", kwlist, &hex)) { + return NULL; + } + result = PyObject_CallFunction(cls, ""); + if (result != NULL) { + ((b_UInt256 *)result)->ob_fval = u256_hex_decode(hex); + } + return result; +} + +static PyObject *b_UInt256FromHash(PyObject *cls, PyObject *args, PyObject *kwds) { + PyObject *result = NULL; + PyObject *hash = NULL; + static char *kwlist[] = { "hash", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &hash)) { + return NULL; + } + if (hash == NULL) { + PyErr_SetString(PyExc_ValueError, "NULL hash argument"); + return NULL; + } + if (!PyObject_HasAttrString(hash, "digest")) { + PyErr_SetString(PyExc_TypeError, "hash argument must have a 'digest' attribute that is an instance method"); + return NULL; + } + PyObject *digestMethod = PyObject_GetAttrString(hash, "digest"); + if (digestMethod == NULL || !PyCallable_Check(digestMethod)) { + PyErr_SetString(PyExc_TypeError, "hash argument must have a digest() method"); + Py_XDECREF(digestMethod); + return NULL; + } + + // get the result of the digest() method as a UInt256 + PyObject *digestObj = PyObject_CallFunction(digestMethod, ""); + if (digestObj == NULL || !PyBytes_Check(digestObj) || PyBytes_Size(digestObj) != 32) { + PyErr_SetString(PyExc_TypeError, "hash argument digest() method must return a bytes object with a length of 32"); + Py_XDECREF(digestMethod); + Py_XDECREF(digestObj); + return NULL; + } + + UInt256 *u256 = (void *)PyBytes_AsString(digestObj); + + result = PyObject_CallFunction(cls, ""); + if (result != NULL) { + memcpy(&((b_UInt256 *)result)->ob_fval, u256, 32); + } + Py_XDECREF(digestMethod); + Py_XDECREF(digestObj); + return result; +} + +static PyObject *b_UInt256GetHex(b_UInt256 *self, void *closure) { + return Py_BuildValue("s", u256_hex_encode(self->ob_fval)); +} + +static PyGetSetDef b_UInt256GetSetters[] = { + {"hex", + (getter)b_UInt256GetHex, NULL, + "get the hex value", + NULL}, + {NULL} +}; + + static PyMethodDef b_UInt256Methods[] = { + /* Class Methods */ + {"from_hex", (PyCFunction)b_UInt256FromHex, (METH_VARARGS | METH_KEYWORDS | METH_CLASS), + "initialize a UInt256 from a hex string"}, + {"from_hash", (PyCFunction)b_UInt256FromHash, (METH_VARARGS | METH_KEYWORDS | METH_CLASS), + "initialize a UInt256 from a hash object"}, + {NULL} + }; + + static PyTypeObject b_UInt256Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "breadwallet.UInt256", /* tp_name */ + sizeof(b_UInt256), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "UInt256 Object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + b_UInt256Methods, /* tp_methods */ + 0, /* tp_members */ + b_UInt256GetSetters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + b_UInt256New, /* tp_new */ + }; + +typedef struct { + PyObject_HEAD + UInt512 ob_fval; +} b_UInt512; + +static PyObject *b_UInt512New(PyTypeObject *type, PyObject *args, PyObject *kwds) { + b_UInt512 *self = (b_UInt512 *)type->tp_alloc(type, 0); + if (self != NULL) { + self->ob_fval = UINT512_ZERO; + } + return (PyObject *)self; +} + +static PyTypeObject b_UInt512Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "breadwallet.UInt12", /* tp_name */ + sizeof(b_UInt512), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "UInt512 Object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + b_UInt512New, /* tp_new */ +}; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Address + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +typedef struct { + PyObject_HEAD + BRAddress ob_fval; +} b_Address; + +static PyObject *b_AddressNew(PyTypeObject *type, PyObject *args, PyObject *kwds) { + b_Address *self = (b_Address *)type->tp_alloc(type, 0); + if (self != NULL) { + } + return (PyObject *)self; +} + +static int b_AddressInit(b_Address *self, PyObject *args, PyObject *kwds) { + PyObject *addy; + // parse args + static char *kwlist[] = { "str", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &addy)) { + return -1; + } + if (addy == NULL || addy == Py_None) { + return 0; // optional argument + } + if (!PyUnicode_Check(addy)) { + PyErr_SetString(PyExc_TypeError, "Address must initialized with a string."); + return -1; + } + PyObject *buf = PyUnicode_AsEncodedString(addy, "utf-8", "strict"); + if (buf == NULL) { + PyErr_SetString(PyExc_ValueError, "Error decoding unicode value as utf8"); + return -1; + } + memcpy(&self->ob_fval, PyBytes_AsString(buf), PyBytes_Size(buf)); + return 0; +} + +static PyObject *b_AddressToRepr(b_Address *self) { + return PyUnicode_FromFormat("", self->ob_fval.s); +} + +static PyObject *b_AddresToStr(b_Address *self) { + return PyUnicode_FromString(self->ob_fval.s); +} + +// forward decl because the comparison needs to check against the Address type +static PyObject *b_AddressRichCompare(PyObject *a, PyObject *b, int op); + +static PyMethodDef b_AddressMethods[] = { + {NULL} +}; + +static PyTypeObject b_AddressType = { + PyVarObject_HEAD_INIT(NULL, 0) + "breadwallet.Address", /* tp_name */ + sizeof(b_Address), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + (reprfunc)b_AddressToRepr, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + (reprfunc)b_AddresToStr, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "Address Object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + (richcmpfunc)b_AddressRichCompare, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + b_AddressMethods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)b_AddressInit, /* tp_init */ + 0, /* tp_alloc */ + b_AddressNew, /* tp_new */ +}; + +static PyObject *b_AddressRichCompare(PyObject *a, PyObject *b, int op) { + if (op != Py_EQ && op != Py_NE) { + return Py_NotImplemented; + } + if (!PyObject_IsInstance(a, (PyObject *)&b_AddressType)) { + // WHAT? + PyErr_SetString(PyExc_TypeError, "Instance is not an Address"); + return NULL; + } + b_Address *self = (b_Address *)a; + int eq = 0; + if (PyUnicode_Check(b)) { + // compared to a string + PyObject *buf = PyUnicode_AsEncodedString(b, "utf-8", "strict"); + if (buf == NULL) { + PyErr_SetString(PyExc_ValueError, "Error decoding unicode value as utf8"); + return NULL; + } + const char *addressB = PyBytes_AsString(buf); + eq = BRAddressEq(&self->ob_fval, addressB); + } else if (PyObject_IsInstance(b, (PyObject *)&b_AddressType)) { + b_Address *other = (b_Address *)b; + eq = BRAddressEq(&self->ob_fval, &other->ob_fval); + } else if (b == Py_None) { + // default to not equal + } else { + PyErr_SetString(PyExc_TypeError, "Address can only be compared to a string or another address"); + return NULL; + } + if (op == Py_NE) { + eq = !eq; + } + return eq ? Py_True : Py_False; +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Transaction + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +typedef struct { + PyObject_HEAD + BRTransaction *ob_fval; +} b_Transaction; + +static void b_TransactionDealloc(b_Transaction *self) { + BRTransactionFree(self->ob_fval); + self->ob_fval = NULL; + Py_TYPE(self)->tp_free((PyObject*)self); +} + +static PyObject *b_TransactionNew(PyTypeObject *type, PyObject *args, PyObject *kwds) { + b_Transaction *self = (b_Transaction *)type->tp_alloc(type, 0); + if (self != NULL) { + self->ob_fval = NULL; + } + return (PyObject *)self; +} + +static int b_TransactionInit(PyObject *self, PyObject *args, PyObject *kwds) { + b_Transaction *obj = (b_Transaction *)self; + obj->ob_fval = BRTransactionNew(); + return 0; +} + +static PyObject *b_TransactionToStr(PyObject *self) { + return PyUnicode_FromString(u256_hex_encode(((b_Transaction *)self)->ob_fval->txHash)); +} + +static PyMethodDef b_TransactionMethods[] = { + {NULL} +}; + +static PyTypeObject b_TransactionType = { + PyVarObject_HEAD_INIT(NULL, 0) + "breadwallet.Transaction", /* tp_name */ + sizeof(b_Transaction), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)b_TransactionDealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + (reprfunc)b_TransactionToStr, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + (reprfunc)b_TransactionToStr, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "Transaction Object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + b_TransactionMethods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)b_TransactionInit, /* tp_init */ + 0, /* tp_alloc */ + b_TransactionNew, /* tp_new */ +}; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Keys + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +typedef struct { + PyObject_HEAD + BRMasterPubKey ob_fval; +} b_MasterPubKey; + +static PyObject *b_MasterPubKeyNew(PyTypeObject *type, PyObject *args, PyObject *kwds) { + b_MasterPubKey *self = (b_MasterPubKey *)type->tp_alloc(type, 0); + if (self != NULL) { + self->ob_fval = BR_MASTER_PUBKEY_NONE; + } + return (PyObject *)self; +} + +static PyObject *b_MasterPubKeyFromPhrase(PyObject *cls, PyObject *args, PyObject *kwds) { + PyObject *result = NULL; + char *phrase = ""; + // parse args + static char *kwlist[] = { "phrase", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", kwlist, &phrase)) { + return NULL; + } + // derive + UInt512 seed = UINT512_ZERO; + BRBIP39DeriveKey(seed.u8, phrase, NULL); + // allocate + result = PyObject_CallFunction(cls, ""); + // set value + if (result != NULL) { + ((b_MasterPubKey *)result)->ob_fval = BRBIP32MasterPubKey(&seed, sizeof(seed)); + } + return result; +} + +static PyMethodDef b_MasterPubKeyMethods[] = { + /* Class Methods */ + {"from_phrase", (PyCFunction)b_MasterPubKeyFromPhrase, (METH_VARARGS | METH_KEYWORDS | METH_CLASS), + "generate a MasterPubKey from a phrase"}, + {NULL} +}; + +static PyTypeObject b_MasterPubKeyType = { + PyVarObject_HEAD_INIT(NULL, 0) + "breadwallet.MasterPubKey", /* tp_name */ + sizeof(b_MasterPubKey), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "MasterPubKey Object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + b_MasterPubKeyMethods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + b_MasterPubKeyNew, /* tp_new */ +}; + +static PyObject *b_DeriveKey(PyObject *self, PyObject *args) { + const char *phrase; + if (!PyArg_ParseTuple(args, "s", &phrase)) return NULL; + UInt512 seed = UINT512_ZERO; + BRBIP39DeriveKey(seed.u8, phrase, NULL); + b_UInt512 *obj = PyObject_New(b_UInt512, &b_UInt512Type); + obj->ob_fval = seed; + Py_INCREF(obj); + return (PyObject *)obj; +} + +typedef struct { + PyObject_HEAD + BRKey *ob_fval; +} b_Key; + +static PyObject *b_KeyNew(PyTypeObject *type, PyObject *args, PyObject *kwds) { + b_Key *self = (b_Key *)type->tp_alloc(type, 0); + if (self != NULL) { + self->ob_fval = NULL; + } + return (PyObject *)self; +} + +static PyObject *b_KeyFromBitID(PyObject *cls, PyObject *args, PyObject *kwds) { + PyObject *result = NULL; + PyObject *seedObj = NULL; + int index = 0; + char *endpoint = NULL; + // parse args + static char *kwlist[] = { "seed", "index", "endpoint", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Ois", kwlist, &seedObj, &index, &endpoint)) { + return NULL; + } + if (!PyObject_IsInstance(seedObj, (PyObject *)&b_UInt512Type)) { + PyErr_SetString(PyExc_TypeError, "seed must be an instance of UInt512"); + return NULL; + } + b_UInt512 *seed = (b_UInt512 *)seedObj; + + // create + BRKey *key = calloc(1, sizeof(BRKey)); + BRBIP32BitIDKey(key, seed->ob_fval.u8, sizeof(seed->ob_fval.u8), index, endpoint); + + // allocate + result = PyObject_CallFunction(cls, ""); + // set value + if (result != NULL) { + ((b_Key *)result)->ob_fval = key; + } + return result; +} + +static PyObject *b_KeyRecoverPubKey(PyObject *cls, PyObject *args, PyObject *kwds) { + b_Key *result = NULL; + PyObject *message = NULL; + PyObject *signature = NULL; + static char *kwlist[] = { "message", "signature", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OS", kwlist, &message, &signature)) { + return NULL; + } + if (message == NULL || message == Py_None) { + PyErr_SetString(PyExc_ValueError, "message must not be NULL"); + return NULL; + } + UInt256 *toSign; + PyObject *msgBytes; + if (PyObject_IsInstance(message, (PyObject *)&PyBytes_Type)) { + msgBytes = message; + } else if (PyCallable_Check(PyObject_GetAttrString(message, "digest"))) { + msgBytes = PyObject_CallMethod(message, "digest", ""); + if (!PyObject_IsInstance(msgBytes, (PyObject *)&PyBytes_Type)) { + PyErr_SetString(PyExc_TypeError, "digest() must return a bytes object"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "message must be either a bytes object with 32 bytes or a hash object " + "with a digest() method"); + return NULL; + } + if (PyBytes_Size(msgBytes) != 32) { + PyErr_SetString(PyExc_ValueError, "must be 32 bytes of data (a UInt256)"); + return NULL; + } + toSign = (UInt256 *)PyBytes_AsString(msgBytes); + BRKey *key = calloc(1, sizeof(BRKey)); + int keyLen = BRKeyRecoverPubKey(key, *toSign, PyBytes_AsString(signature), PyBytes_Size(signature)); + if (!keyLen) { + return Py_BuildValue(""); // unable to recover, return None + } + + result = (b_Key *)PyObject_CallFunction(cls, ""); + if (result != NULL) { + result->ob_fval = key; + } + return (PyObject *)result; +} + +static PyObject *b_KeySign(b_Key *self, PyObject *args, PyObject *kwds) { + PyObject *message; + static char *kwlist[] = { "message", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &message)) { + return NULL; + } + if (message == NULL || message == Py_None) { + PyErr_SetString(PyExc_ValueError, "message must not be NULL"); + return NULL; + } + UInt256 *toSign; + PyObject *msgBytes; + if (PyObject_IsInstance(message, (PyObject *)&PyBytes_Type)) { + msgBytes = message; + } else if (PyCallable_Check(PyObject_GetAttrString(message, "digest"))) { + msgBytes = PyObject_CallMethod(message, "digest", ""); + if (!PyObject_IsInstance(msgBytes, (PyObject *)&PyBytes_Type)) { + PyErr_SetString(PyExc_TypeError, "digest() must return a bytes object"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "message must be either a bytes object with 32 bytes or a hash object " + "with a digest() method"); + return NULL; + } + if (PyBytes_Size(msgBytes) != 32) { + PyErr_SetString(PyExc_ValueError, "must be 32 bytes of data (a UInt256)"); + return NULL; + } + + toSign = (UInt256 *)PyBytes_AsString(msgBytes); + uint8_t sig[72]; + size_t sigLen = BRKeySign(self->ob_fval, sig, sizeof(sig), *toSign); + PyObject *ret = PyBytes_FromStringAndSize((const char *)&sig, sigLen); + + return ret; +} + +static PyObject *b_KeyVerify(b_Key *self, PyObject *args, PyObject *kwds) { + PyObject *message; + PyObject *signature; + static char *kwlist[] = { "message", "signature", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OS", kwlist, &message, &signature)) { + return NULL; + } + if (message == NULL || message == Py_None) { + PyErr_SetString(PyExc_ValueError, "message must not be NULL"); + return NULL; + } + UInt256 *toSign; + PyObject *msgBytes; + if (PyObject_IsInstance(message, (PyObject *)&PyBytes_Type)) { + msgBytes = message; + } else if (PyCallable_Check(PyObject_GetAttrString(message, "digest"))) { + msgBytes = PyObject_CallMethod(message, "digest", ""); + if (!PyObject_IsInstance(msgBytes, (PyObject *)&PyBytes_Type)) { + PyErr_SetString(PyExc_TypeError, "digest() must return a bytes object"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "message must be either a bytes object with 32 bytes or a hash object " + "with a digest() method"); + return NULL; + } + if (PyBytes_Size(msgBytes) != 32) { + PyErr_SetString(PyExc_ValueError, "must be 32 bytes of data (a UInt256)"); + return NULL; + } + toSign = (UInt256 *)PyBytes_AsString(msgBytes); + int valid = BRKeyVerify(self->ob_fval, *toSign, PyBytes_AsString(signature), PyBytes_Size(signature)); + return valid ? Py_True : Py_False; +} + +static PyObject *b_KeyCompactSign(b_Key *self, PyObject *args, PyObject *kwds) { + PyObject *message; + static char *kwlist[] = { "message", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &message)) { + return NULL; + } + if (message == NULL || message == Py_None) { + PyErr_SetString(PyExc_ValueError, "message must not be NULL"); + return NULL; + } + UInt256 *toSign; + PyObject *msgBytes; + if (PyObject_IsInstance(message, (PyObject *)&PyBytes_Type)) { + msgBytes = message; + } else if (PyCallable_Check(PyObject_GetAttrString(message, "digest"))) { + msgBytes = PyObject_CallMethod(message, "digest", ""); + if (!PyObject_IsInstance(msgBytes, (PyObject *)&PyBytes_Type)) { + PyErr_SetString(PyExc_TypeError, "digest() must return a bytes object"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "message must be either a bytes object with 32 bytes or a hash object " + "with a digest() method"); + return NULL; + } + if (PyBytes_Size(msgBytes) != 32) { + PyErr_SetString(PyExc_ValueError, "must be 32 bytes of data (a UInt256)"); + return NULL; + } + + toSign = (UInt256 *)PyBytes_AsString(msgBytes); + uint8_t sig[72]; + size_t sigLen = BRKeyCompactSign(self->ob_fval, sig, sizeof(sig), *toSign); + PyObject *ret = PyBytes_FromStringAndSize((const char *)&sig, sigLen); + return ret; +} + +static PyObject *b_KeyPrivKeyIsValid(PyObject *cls, PyObject *args, PyObject *kwds) { + PyObject *result = Py_False; + char *pk; + static char *kwList[] = { "pk", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", kwList, &pk)) { + return NULL; + } + if (BRPrivKeyIsValid(pk)) result = Py_True; + return result; +} + +static int b_KeySetSecret(b_Key *self, PyObject *value, void *closure) { + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "Cannot delete the secret attribute"); + return -1; + } + if (!PyObject_IsInstance(value, (PyObject *)&b_UInt256Type)) { + PyErr_SetString(PyExc_TypeError, "The secret object must be a UInt256"); + return -1; + } + b_UInt256 *sec = (b_UInt256 *)value; + if (self->ob_fval == NULL) { + self->ob_fval = calloc(1, sizeof(BRKey)); + } + if (!BRKeySetSecret(self->ob_fval, &sec->ob_fval, 1)) { + if (!BRKeySetSecret(self->ob_fval, &sec->ob_fval, 0)) { + PyErr_SetString(PyExc_ValueError, "Could not parse the secret (tried both compressed and uncompressed)"); + return -1; + } + } + return 0; +} + +static PyObject *b_KeyGetSecret(b_Key *self, void *closure) { + if (self->ob_fval == NULL) { + return Py_BuildValue(""); + } + b_UInt256 *ret = (b_UInt256 *)PyObject_New(b_UInt256, &b_UInt256Type); + ret->ob_fval = self->ob_fval->secret; + return (PyObject *)ret; +} + +static int b_KeySetPrivKey(b_Key *self, PyObject *value, void *closure) { + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "Cannot delete the privkey attribute"); + return -1; + } + if (!PyUnicode_Check(value)) { + PyErr_SetString(PyExc_TypeError, "Priv key must be a string"); + return -1; + } + PyObject *buf = PyUnicode_AsEncodedString(value, "utf-8", "strict"); + if (buf == NULL) { + PyErr_SetString(PyExc_ValueError, "Error decoding unicode value as utf8"); + return -1; + } + const char *privkey = PyBytes_AsString(buf); + if (self->ob_fval == NULL) { + self->ob_fval = calloc(1, sizeof(BRKey)); + } + if (!BRKeySetPrivKey(self->ob_fval, privkey)) { + PyErr_SetString(PyExc_ValueError, "Unable to set private key (was it correctly serialized?)"); + return -1; + } + return 0; +} + +static PyObject *b_KeyGetPrivKey(b_Key *self, void *closure) { + PyObject *ret; + if (self->ob_fval == NULL) { + ret = Py_BuildValue(""); + } else { + char privKey[BRKeyPrivKey(self->ob_fval, NULL, 0)]; + BRKeyPrivKey(self->ob_fval, privKey, sizeof(privKey)); + ret = PyUnicode_FromString(privKey); + if (ret == NULL) { + printf("ERROR: could not export private key\n"); + ret = Py_BuildValue(""); + } + } + return ret; +} + +static int b_KeySetPubKey(b_Key *self, PyObject *value, void *closure) { + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "Cannot delete the pubkey attribute"); + return -1; + } + if (value == Py_None || !PyBytes_Check(value)) { + PyErr_SetString(PyExc_TypeError, "Pub key must be a bytes object"); + return -1; + } + const char *pubkey = PyBytes_AsString(value); + if (self->ob_fval == NULL) { + self->ob_fval = calloc(1, sizeof(BRKey)); + } + // just try both options (same as we do for secret key) + if (!BRKeySetPubKey(self->ob_fval, (uint8_t *)pubkey, 33)) { + if (!BRKeySetPubKey(self->ob_fval, (uint8_t *)pubkey, 65)) { + PyErr_SetString(PyExc_ValueError, "Unable to set public key (was it correctly serialized?) " + "it should be either 33 or 65 bytes long"); + return -1; + } + } + return 0; +} + +static PyObject *b_KeyGetPubKey(b_Key *self, void *closure) { + PyObject *ret; + if (self->ob_fval == NULL) { + ret = Py_BuildValue(""); + } else { + uint8_t pubkey[BRKeyPubKey(self->ob_fval, NULL, 0)]; + size_t keyLen = BRKeyPubKey(self->ob_fval, pubkey, sizeof(pubkey)); + ret = PyBytes_FromStringAndSize((const char *)pubkey, keyLen); + if (ret == NULL) { + printf("ERROR: could not export public key\n"); + ret = Py_BuildValue(""); + } + } + return ret; +} + +static PyObject *b_KeyGetAddress(b_Key *self, void *closure) { + if (self->ob_fval == NULL) { + return Py_BuildValue(""); + } + BRAddress address; + BRKeyAddress(self->ob_fval, address.s, sizeof(address)); + b_Address *ret = (b_Address *)PyObject_New(b_Address, &b_AddressType); + ret->ob_fval = address; + return (PyObject *)ret; +} + +static PyGetSetDef b_KeyGetSetters[] = { + {"secret", + (getter)b_KeyGetSecret, (setter)b_KeySetSecret, + "get or set the secret. will reset the key", + NULL}, + {"privkey", + (getter)b_KeyGetPrivKey, (setter)b_KeySetPrivKey, + "get or set the private key. throws a ValueError when key can't be parsed", + NULL}, + {"pubkey", + (getter)b_KeyGetPubKey, (setter)b_KeySetPubKey, + "get or set the public key. throws a ValueError when the key can't be parsed", + NULL}, + {"address", + (getter)b_KeyGetAddress, (setter)NULL, + "get the p2sh address of the key", + NULL}, + {NULL} +}; + +static PyMethodDef b_KeyMethods[] = { + /* Class Methods */ + {"from_bitid", (PyCFunction)b_KeyFromBitID, (METH_VARARGS | METH_KEYWORDS | METH_CLASS), + "generate a bitid Key from a seed and some bitid parameters"}, + {"recover_pubkey", (PyCFunction)b_KeyRecoverPubKey, (METH_VARARGS | METH_KEYWORDS | METH_CLASS), + "recover a public key from a compact signature"}, + {"privkey_is_valid", (PyCFunction)b_KeyPrivKeyIsValid, (METH_VARARGS | METH_KEYWORDS | METH_CLASS), + "determine whether or not a serialized private key is valid"}, + /* Instance Methods */ + {"sign", (PyCFunction)b_KeySign, (METH_VARARGS | METH_KEYWORDS), + "sign a bytes or an object with a digest() method"}, + {"sign_compact", (PyCFunction)b_KeyCompactSign, (METH_VARARGS | METH_KEYWORDS), + "sign some bytes (or an object with the digest() method) using the compact signature format"}, + {"verify", (PyCFunction)b_KeyVerify, (METH_VARARGS | METH_KEYWORDS), + "verify the message signature was made by this key"}, + {NULL} +}; + +static PyTypeObject b_KeyType = { + PyVarObject_HEAD_INIT(NULL, 0) + "breadwallet.Key", /* tp_name */ + sizeof(b_Key), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "Key Object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + b_KeyMethods, /* tp_methods */ + 0, /* tp_members */ + b_KeyGetSetters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + b_KeyNew, /* tp_new */ +}; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Wallet + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +typedef struct { + PyObject_HEAD + b_MasterPubKey *mpk; + BRWallet *ob_fval; + PyObject *onBalanceChanged; + PyObject *onTxAdded; + PyObject *onTxUpdated; + PyObject *onTxDeleted; +} b_Wallet; + +static void b_WalletDealloc(b_Wallet *self) { + if (self->mpk != NULL) { + Py_XDECREF(self->mpk); + self->mpk = NULL; + } + if (self->ob_fval != NULL) { + BRWalletFree(self->ob_fval); + self->ob_fval = NULL; + } + // TODO: figure this out. currently causes a dealloc error when callbacks are not set + // if (self->onBalanceChanged != NULL) { + // Py_XDECREF(self->onBalanceChanged); + // self->onBalanceChanged = NULL; + // } + // if (self->onTxAdded != NULL) { + // Py_XDECREF(self->onTxAdded); + // self->onTxAdded = NULL; + // } + // if (self->onTxUpdated != NULL) { + // Py_XDECREF(self->onTxUpdated); + // self->onTxUpdated = NULL; + // } + // if (self->onTxDeleted != NULL) { + // Py_XDECREF(self->onTxDeleted); + // self->onTxDeleted = NULL; + // } + Py_TYPE(self)->tp_free((PyObject*)self); +} + +static PyObject *b_WalletNew(PyTypeObject *type, PyObject *args, PyObject *kwds) { + b_Wallet *self = (b_Wallet *)type->tp_alloc(type, 0); + if (self != NULL) { + self->mpk = NULL; + self->ob_fval = NULL; + self->onBalanceChanged = NULL; + self->onTxAdded = NULL; + self->onTxUpdated = NULL; + self->onTxDeleted = NULL; + } + return (PyObject *)self; +} + +void b_WalletCallbackBalanceChanged(void *ctx, uint64_t newBalance) { + printf("balance changed new=%lld", newBalance); + b_Wallet *self = (b_Wallet *)ctx; + if (self->onBalanceChanged != NULL && self->onBalanceChanged != Py_None) { + PyObject *balObj = PyLong_FromUnsignedLongLong(newBalance); + PyObject_CallFunctionObjArgs(self->onBalanceChanged, balObj, NULL); + } +} + +void b_WalletCallbackTxAdded(void *ctx, BRTransaction *tx) { + printf("tx added tx=%s", u256_hex_encode(tx->txHash)); + b_Wallet *self = (b_Wallet *)ctx; + if (self->onTxAdded != NULL && self->onTxAdded != Py_None) { + b_Transaction *txObj = (b_Transaction *)PyObject_New(b_Transaction, &b_TransactionType); + txObj->ob_fval = tx; + PyObject_CallFunctionObjArgs(self->onTxAdded, txObj, NULL); + } +} + +void b_WalletCallbackTxUpdated(void *ctx, const UInt256 txHashes[], size_t count, + uint32_t blockHeight, uint32_t timestamp) { + printf("tx updated count=%ld blockheight=%d ts=%d", count, blockHeight, timestamp); + b_Wallet *self = (b_Wallet *)ctx; + if (self->onTxUpdated != NULL && self->onTxUpdated != Py_None) { + PyObject *hashList = PyList_New(count); + for (size_t i = 0; i < count; i++) { + b_UInt256 *hashObj = (b_UInt256 *)PyObject_New(b_UInt256, &b_UInt256Type); + hashObj->ob_fval = txHashes[i]; + PyList_SET_ITEM(hashList, i, (PyObject *)hashObj); + } + PyObject *blockHeightObj = PyLong_FromUnsignedLong(blockHeight); + PyObject *timestampObj = PyLong_FromUnsignedLong(timestamp); + PyObject_CallFunctionObjArgs( + self->onTxUpdated, hashList, blockHeightObj, timestampObj, NULL + ); + } +} + +void b_WalletCallbackTxDeleted(void *ctx, UInt256 txHash, int notifyUser, int recommendRescan) { + printf("tx deleted txhash=%s notify=%d recommend=%d", u256_hex_encode(txHash), notifyUser, recommendRescan); + b_Wallet *self = (b_Wallet *)ctx; + if (self->onTxDeleted != NULL && self->onTxUpdated != Py_None) { + b_UInt256 *hashObj = (b_UInt256 *)PyObject_New(b_UInt256, &b_UInt256Type); + hashObj->ob_fval = txHash; + PyObject_CallFunctionObjArgs( + self->onTxDeleted, hashObj, notifyUser ? Py_True : Py_False, + recommendRescan ? Py_True : Py_False, NULL + ); + } +} + +static int b_WalletInit(b_Wallet *self, PyObject *args, PyObject *kwds) { + PyObject *mpk = NULL, *txList = NULL; + static char *kwlist[] = {"master_pubkey", "tx_list", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &mpk, &txList)) return -1; + + if (!mpk) { + // TODO: set correct error + return -1; + } + + if (!PyObject_IsInstance(mpk, (PyObject *)&b_MasterPubKeyType)) { + // TODO: set correct error + return -1; + } + + // build instance data + self->mpk = (b_MasterPubKey *)mpk; + Py_INCREF(self->mpk); + self->ob_fval = BRWalletNew(NULL, 0, self->mpk->ob_fval); + BRWalletSetCallbacks( + self->ob_fval, (void *)self, + b_WalletCallbackBalanceChanged, + b_WalletCallbackTxAdded, + b_WalletCallbackTxUpdated, + b_WalletCallbackTxDeleted + ); + + // TODO: parse transaction list + + return 0; +} + +PyObject *b_WalletGetBalanceChanged(b_Wallet *self, void *closure){ + if (self->onBalanceChanged != NULL) { + Py_INCREF(self->onBalanceChanged); + return self->onBalanceChanged; + } + Py_INCREF(Py_None); + return Py_None; +} + +static int b_WalletSetBalanceChanged(b_Wallet *self, PyObject *value, void *closure) { + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "Cannot delete the on_balance_changed attribute"); + return -1; + } + + if (value != Py_None && !PyFunction_Check(value)) { + PyErr_SetString(PyExc_TypeError, "The on_balance_changed object must be a function"); + return -1; + } + + if (self->onBalanceChanged != NULL) { + Py_DECREF(self->onBalanceChanged); + } + Py_INCREF(value); + self->onBalanceChanged = value; + + return 0; +} + +PyObject *b_WalletGetTxAdded(b_Wallet *self, void *closure){ + if (self->onTxAdded) { + Py_INCREF(self->onTxAdded); + return self->onTxAdded; + } + Py_INCREF(Py_None); + return Py_None; +} + +static int b_WalletSetTxAdded(b_Wallet *self, PyObject *value, void *closure) { + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "Cannot delete the on_tx_added attribute"); + return -1; + } + + if (value != Py_None && !PyFunction_Check(value)) { + PyErr_SetString(PyExc_TypeError, "The on_tx_added object must be a function"); + return -1; + } + + if (self->onTxAdded != NULL) { + Py_DECREF(self->onTxAdded); + } + Py_INCREF(value); + self->onTxAdded = value; + + return 0; +} + +PyObject *b_WalletGetTxUpdated(b_Wallet *self, void *closure){ + if (self->onTxUpdated != NULL) { + Py_INCREF(self->onTxUpdated); + return self->onTxUpdated; + } + Py_INCREF(Py_None); + return Py_None; +} + +static int b_WalletSetTxUpdated(b_Wallet *self, PyObject *value, void *closure) { + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "Cannot delete the on_tx_updated attribute"); + return -1; + } + + if (value != Py_None && !PyFunction_Check(value)) { + PyErr_SetString(PyExc_TypeError, "The on_tx_updated object must be a function"); + return -1; + } + + if (self->onTxUpdated != NULL) { + Py_DECREF(self->onTxUpdated); + } + Py_INCREF(value); + self->onTxUpdated = value; + + return 0; +} + +PyObject *b_WalletGetTxDeleted(b_Wallet *self, void *closure){ + if (self->onTxDeleted != NULL) { + Py_INCREF(self->onTxDeleted); + return self->onTxDeleted; + } + Py_INCREF(Py_None); + return Py_None; +} + +static int b_WalletSetTxDeleted(b_Wallet *self, PyObject *value, void *closure) { + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "Cannot delete the on_tx_deleted attribute"); + return -1; + } + + if (value != Py_None && !PyFunction_Check(value)) { + PyErr_SetString(PyExc_TypeError, "The on_tx_deleted object must be a function"); + return -1; + } + + if (self->onTxDeleted != NULL) { + Py_DECREF(self->onTxDeleted); + } + Py_INCREF(value); + self->onTxDeleted = value; + + return 0; +} + +PyObject *b_WalletGetReceiveAddress(b_Wallet *self, void *closure) { + b_Address *addrObj = (b_Address *)PyObject_New(b_Address, &b_AddressType); + addrObj->ob_fval = BRWalletReceiveAddress(self->ob_fval); + return (PyObject *)addrObj; +} + +PyObject *b_WalletGetChangeAddress(b_Wallet *self, void *closure) { + b_Address *addrObj = (b_Address *)PyObject_New(b_Address, &b_AddressType); + addrObj->ob_fval = BRWalletChangeAddress(self->ob_fval); + return (PyObject *)addrObj; +} + +PyObject *b_WalletGetBalance(b_Wallet *self, void *closure) { + return Py_BuildValue("K", BRWalletBalance(self->ob_fval)); +} + +PyObject *b_WalletGetTotalSent(b_Wallet *self, void *closure) { + return Py_BuildValue("K", BRWalletTotalSent(self->ob_fval)); +} + +PyObject *b_WalletGetTotalReceived(b_Wallet *self, void *closure) { + return Py_BuildValue("K", BRWalletTotalReceived(self->ob_fval)); +} + +PyObject *b_WalletGetFeePerKB(b_Wallet *self, void *closure) { + return Py_BuildValue("K", BRWalletFeePerKb(self->ob_fval)); +} + +int b_WalletSetFeePerKB(b_Wallet *self, PyObject *value, void *closure) { + if (!PyLong_Check(value)) { + PyErr_SetString(PyExc_TypeError, "fee per kb must be a number"); + return -1;; + } + BRWalletSetFeePerKb(self->ob_fval, PyLong_AsUnsignedLongLong(value)); + return 0; +} + +static PyGetSetDef b_WalletGetSetters[] = { + // props + {"balance", (getter)b_WalletGetBalance, NULL, + "gets the total balance int he wallet, not including transactions known to be invalid", + NULL}, + {"total_sent", (getter)b_WalletGetTotalSent, NULL, + "gets the total amount sent not including change addresses", + NULL}, + {"total_received", (getter)b_WalletGetTotalReceived, NULL, + "gets the total amount received not including change addresses", + NULL}, + {"fee_per_kb", (getter)b_WalletGetFeePerKB, (setter)b_WalletSetFeePerKB, + "fee-per-kb size to use when creating a transaction, in satoshis", + NULL}, + // callbacks + {"on_balance_changed", + (getter)b_WalletGetBalanceChanged, (setter)b_WalletSetBalanceChanged, + "callback fired when sync is started", + NULL}, + {"on_tx_added", + (getter)b_WalletGetTxAdded, (setter)b_WalletSetTxAdded, + "callback fired when sync finishes successfully", + NULL}, + {"on_tx_updated", + (getter)b_WalletGetTxUpdated, (setter)b_WalletSetTxUpdated, + "callback fired when sync finishes with a failure", + NULL}, + {"on_tx_deleted", + (getter)b_WalletGetTxDeleted, (setter)b_WalletSetTxDeleted, + "callback fired when transaction status is updated", + NULL}, + {"receive_address", + (getter)b_WalletGetReceiveAddress, NULL, + "get the first unused receive address", + NULL}, + {"change_address", + (getter)b_WalletGetChangeAddress, NULL, + "get the first unused change address", + NULL}, + {NULL} /* Sentinel */ +}; + +static PyTypeObject b_WalletType = { + PyVarObject_HEAD_INIT(NULL, 0) + "breadwallet.Wallet", /* tp_name */ + sizeof(b_Wallet), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)b_WalletDealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "Wallet Object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + b_WalletGetSetters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)b_WalletInit, /* tp_init */ + 0, /* tp_alloc */ + b_WalletNew, /* tp_new */ +}; + +static PyMethodDef bmodulemethods[] = { + {"derive_key", b_DeriveKey, METH_VARARGS, "Derive a key from a seed phrase"}, + {NULL}, +}; + +static PyModuleDef bmodule = { + PyModuleDef_HEAD_INIT, +#if BITCOIN_TESTNET + "breadwallet_testnet", +#else + "breadwallet_mainnet", +#endif + "A simple, lightweight, performant SPV wallet", + -1, + bmodulemethods, NULL, NULL, NULL, NULL +}; + +#if BITCOIN_TESTNET +PyMODINIT_FUNC PyInit_breadwallet_testnet(void) { +#else +PyMODINIT_FUNC PyInit_breadwallet_mainnet(void) { +#endif + PyObject* m; + + if (PyType_Ready(&b_UInt256Type) < 0) return NULL; + if (PyType_Ready(&b_UInt512Type) < 0) return NULL; + if (PyType_Ready(&b_MasterPubKeyType) < 0) return NULL; + if (PyType_Ready(&b_KeyType) < 0) return NULL; + if (PyType_Ready(&b_AddressType) < 0) return NULL; + if (PyType_Ready(&b_TransactionType) < 0) return NULL; + if (PyType_Ready(&b_WalletType) < 0) return NULL; + + m = PyModule_Create(&bmodule); + if (m == NULL) return NULL; + + Py_INCREF(&b_UInt256Type); + Py_INCREF(&b_UInt512Type); + Py_INCREF(&b_MasterPubKeyType); + Py_INCREF(&b_KeyType); + Py_INCREF(&b_AddressType); + Py_INCREF(&b_TransactionType); + Py_INCREF(&b_WalletType); + PyModule_AddObject(m, "UInt256", (PyObject *)&b_UInt256Type); + PyModule_AddObject(m, "UInt512", (PyObject *)&b_UInt512Type); + PyModule_AddObject(m, "MasterPubKey", (PyObject *)&b_MasterPubKeyType); + PyModule_AddObject(m, "Key", (PyObject *)&b_KeyType); + PyModule_AddObject(m, "Address", (PyObject *)&b_AddressType); + PyModule_AddObject(m, "Transaction", (PyObject *)&b_TransactionType); + PyModule_AddObject(m, "Wallet", (PyObject *)&b_WalletType); + return m; +} diff --git a/python/breadwallet/__init__.py b/python/breadwallet/__init__.py new file mode 100644 index 000000000..7597eb9bc --- /dev/null +++ b/python/breadwallet/__init__.py @@ -0,0 +1,5 @@ +__version__ = '0.0.1' +__author__ = 'Samuel Sutch ' + +from breadwallet_mainnet import * +import breadwallet_testnet as testnet diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 000000000..1fe630283 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,52 @@ +import os +from setuptools import setup, Extension + +def here(*args): + return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), *args)) + +def fromroot(*args): + return here(os.pardir, *args) + +sources = [ + fromroot('BRAddress.c'), + fromroot('BRBase58.c'), + fromroot('BRBIP32Sequence.c'), + fromroot('BRBIP38Key.c'), + fromroot('BRBIP39Mnemonic.c'), + fromroot('BRBloomFilter.c'), + fromroot('BRCrypto.c'), + fromroot('BRKey.c'), + fromroot('BRMerkleBlock.c'), + fromroot('BRPaymentProtocol.c'), + fromroot('BRPeer.c'), + fromroot('BRPeerManager.c'), + fromroot('BRSet.c'), + fromroot('BRTransaction.c'), + fromroot('BRWallet.c'), + here('bindings.c') +] + +includes = [fromroot(), fromroot('secp256k1')] + +breadwallet_mainnet = Extension( + 'breadwallet_mainnet', + sources=sources, + include_dirs=includes +) + +breadwallet_testnet = Extension( + 'breadwallet_testnet', + sources=sources, + include_dirs=includes, + define_macros=[('BITCOIN_TESTNET', '1')] +) + +setup( + name='breadwallet-core', + version='0.1', + description='A simple, easy to use SPV wallet.', + packages=['breadwallet'], + ext_modules=[breadwallet_mainnet, breadwallet_testnet], + test_suite='nose.collector', + tests_require=['nose'] +) diff --git a/python/tests.py b/python/tests.py new file mode 100644 index 000000000..c2d8a976c --- /dev/null +++ b/python/tests.py @@ -0,0 +1,252 @@ +import binascii +import hashlib +import os +import unittest + +import breadwallet + + +class IntTests(unittest.TestCase): + def test_allocation_256(self): + u256 = breadwallet.UInt256() + self.assertNotEqual(u256, None) + + def test_from_hex_256(self): + u256 = breadwallet.UInt256.from_hex('0000000000000000000000000000000000000000000000000000000000000001') + self.assertNotEqual(u256, None) + + def test_from_hex_and_back_256(self): + h = '0000000000000000000000000000000000000000000000000000000000000001' + u256 = breadwallet.UInt256.from_hex(h) + self.assertEqual(h, u256.hex) + + def test_from_hash_and_back_256(self): + h = hashlib.sha256() + h.update('test123'.encode('utf8')) + u256 = breadwallet.UInt256.from_hash(h) + self.assertEqual(h.hexdigest(), u256.hex) + + def test_allocation(self): + u512 = breadwallet.UInt512() + self.assertNotEqual(u512, None) + + +class AddressTests(unittest.TestCase): + def test_allocation(self): + addy = breadwallet.Address("1J34vj4wowwPYafbeibZGht3zy3qERoUM1") + self.assertNotEqual(addy, None) + + with self.assertRaises(TypeError): + addy2 = breadwallet.Address(2304203942340) + + def test_equality(self): + addy1_s = "1J34vj4wowwPYafbeibZGht3zy3qERoUM1" + addy1_o = breadwallet.Address(addy1_s) + addy2_s = "1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX" + addy2_o = breadwallet.Address(addy2_s) + self.assertEqual(addy1_o, addy1_o) + self.assertEqual(addy1_o, addy1_s) + self.assertNotEqual(addy1_o, addy2_o) + self.assertNotEqual(addy1_o, addy2_s) + self.assertNotEqual(addy1_o, None) # allow none comparison + with self.assertRaises(TypeError): + addy1_o == 234234 # raises when trying to compare against anything other than a string/address/none + + def test_to_str(self): + addy1_s = "1J34vj4wowwPYafbeibZGht3zy3qERoUM1" + self.assertEqual(str(breadwallet.Address(addy1_s)), addy1_s) + + +class KeyTests(unittest.TestCase): + def test_allocation(self): + phrase = "axis husband project any sea patch drip tip spirit tide bring belt" + mpk = breadwallet.MasterPubKey.from_phrase(phrase) + self.assertNotEqual(mpk, None) + + def test_derive_key_allocate(self): + phrase = "axis husband project any sea patch drip tip spirit tide bring belt" + seed = breadwallet.derive_key(phrase) + self.assertNotEqual(seed, None) + + def test_bitid_allocate(self): + phrase = "inhale praise target steak garlic cricket paper better evil almost sadness crawl city banner amused fringe fox insect roast aunt prefer hollow basic ladder" + seed = breadwallet.derive_key(phrase) + key = breadwallet.Key.from_bitid(seed, 0, "http://bitid.bitcoin.blue/callback") + self.assertNotEqual(key, None) + + def test_bitid_address(self): + phrase = "inhale praise target steak garlic cricket paper better evil almost sadness crawl city banner amused fringe fox insect roast aunt prefer hollow basic ladder" + seed = breadwallet.derive_key(phrase) + key = breadwallet.Key.from_bitid(seed, 0, "http://bitid.bitcoin.blue/callback") + self.assertNotEqual(key.address, None) + self.assertEqual(str(key.address), "1J34vj4wowwPYafbeibZGht3zy3qERoUM1") + + def test_privkeykey_is_valid(self): + self.assertFalse(breadwallet.Key.privkey_is_valid("S6c56bnXQiBjk9mqSYE7ykVQ7NzrRz")) + self.assertTrue(breadwallet.Key.privkey_is_valid("S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy")) + + def test_set_privkey(self): + k = breadwallet.Key() + k.privkey = 'S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy' + self.assertEqual(k.address, '1CciesT23BNionJeXrbxmjc7ywfiyM4oLW') + + def test_set_seckey_random(self): + i = os.urandom(32) + h = binascii.hexlify(i).decode('utf8') + u256 = breadwallet.UInt256.from_hex(h) + k = breadwallet.Key() + k.secret = u256 + self.assertEqual(k.secret.hex, h) + + def test_sign(self): + k = breadwallet.Key() + k.secret = breadwallet.UInt256.from_hex('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140') + message = "Equations are more important to me, because politics is for the present, but an equation is something for eternity." + h = hashlib.sha256() + h.update(message.encode('utf8')) + + sig = k.sign(h.digest()) # can take either a hash or a bytes object + sig2 = k.sign(h) + sig3 = b'\x30\x44\x02\x20\x54\xc4\xa3\x3c\x64\x23\xd6\x89\x37\x8f\x16\x0a\x7f\xf8\xb6\x13\x30\x44\x4a\xbb\x58\xfb\x47\x0f\x96\xea\x16\xd9\x9d\x4a\x2f\xed\x02\x20\x07\x08\x23\x04\x41\x0e\xfa\x6b\x29\x43\x11\x1b\x6a\x4e\x0a\xaa\x7b\x7d\xb5\x5a\x07\xe9\x86\x1d\x1f\xb3\xcb\x1f\x42\x10\x44\xa5' + self.assertEqual(len(sig), len(sig3)) + self.assertEqual(sig, sig3) + self.assertEqual(sig, sig2) + + def test_sign_compact_and_recover_pubkey(self): + k = breadwallet.Key() + k.secret = breadwallet.UInt256.from_hex('0000000000000000000000000000000000000000000000000000000000000001') + message = "foo" + h = hashlib.sha256() + h.update(message.encode('utf8')) + sig = k.sign_compact(h) + k2 = breadwallet.Key.recover_pubkey(h, sig) + self.assertEqual(k.pubkey, k2.pubkey) + + def test_verify(self): + k = breadwallet.Key() + k.secret = breadwallet.UInt256.from_hex('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140') + message = "Equations are more important to me, because politics is for the present, but an equation is something for eternity." + h = hashlib.sha256() + h.update(message.encode('utf8')) + + sig = k.sign(h) + self.assertTrue(k.verify(h, sig)) + self.assertTrue(k.verify(h.digest(), sig)) # works with both hash objects or bytes objects + + +class TransactionTests(unittest.TestCase): + def test_allocation(self): + t = breadwallet.Transaction() + self.assertNotEqual(t, None) + + +class WalletTests(unittest.TestCase): + def _get_wallet(self): + phrase = "axis husband project any sea patch drip tip spirit tide bring belt" + mpk = breadwallet.MasterPubKey.from_phrase(phrase) + return breadwallet.Wallet(mpk) + + def test_allocation(self): + phrase = "axis husband project any sea patch drip tip spirit tide bring belt" + mpk = breadwallet.MasterPubKey.from_phrase(phrase) + wallet = breadwallet.Wallet(mpk) + self.assertNotEqual(wallet, None) + + def test_callback_on_balance_changed_setters_and_getters(self): + wallet = self._get_wallet() + + def cb_a(): pass + def cb_b(): pass + + self.assertEqual(wallet.on_balance_changed, None) + wallet.on_balance_changed = None + self.assertEqual(wallet.on_balance_changed, None) + wallet.on_balance_changed = cb_a + self.assertEqual(wallet.on_balance_changed, cb_a) + wallet.on_balance_changed = None + self.assertEqual(wallet.on_balance_changed, None) + wallet.on_balance_changed = cb_a + wallet.on_balance_changed = cb_b + self.assertEqual(wallet.on_balance_changed, cb_b) + + def test_callback_on_tx_added_setters_and_getters(self): + wallet = self._get_wallet() + + def cb_a(): pass + def cb_b(): pass + + self.assertEqual(wallet.on_tx_added, None) + wallet.on_tx_added = None + self.assertEqual(wallet.on_tx_added, None) + wallet.on_tx_added = cb_a + self.assertEqual(wallet.on_tx_added, cb_a) + wallet.on_tx_added = None + self.assertEqual(wallet.on_tx_added, None) + wallet.on_tx_added = cb_a + wallet.on_tx_added = cb_b + self.assertEqual(wallet.on_tx_added, cb_b) + + def test_callback_on_tx_updated_setters_and_getters(self): + wallet = self._get_wallet() + + def cb_a(): pass + def cb_b(): pass + + self.assertEqual(wallet.on_tx_updated, None) + wallet.on_tx_updated = None + self.assertEqual(wallet.on_tx_updated, None) + wallet.on_tx_updated = cb_a + self.assertEqual(wallet.on_tx_updated, cb_a) + wallet.on_tx_updated = None + self.assertEqual(wallet.on_tx_updated, None) + wallet.on_tx_updated = cb_a + wallet.on_tx_updated = cb_b + self.assertEqual(wallet.on_tx_updated, cb_b) + + def test_callback_on_tx_deleted_setters_and_getters(self): + wallet = self._get_wallet() + + def cb_a(): pass + def cb_b(): pass + + self.assertEqual(wallet.on_tx_deleted, None) + wallet.on_tx_deleted = None + self.assertEqual(wallet.on_tx_deleted, None) + wallet.on_tx_deleted = cb_a + self.assertEqual(wallet.on_tx_deleted, cb_a) + wallet.on_tx_deleted = None + self.assertEqual(wallet.on_tx_deleted, None) + wallet.on_tx_deleted = cb_a + wallet.on_tx_deleted = cb_b + self.assertEqual(wallet.on_tx_deleted, cb_b) + + def test_get_receive_address(self): + wallet = self._get_wallet() + self.assertNotEqual(wallet.receive_address, None) + self.assertEqual(wallet.receive_address, wallet.receive_address) + + def test_get_change_address(self): + wallet = self._get_wallet() + self.assertNotEqual(wallet.change_address, None) + self.assertEqual(wallet.change_address, wallet.change_address) + + def test_get_balance(self): + wallet = self._get_wallet() + self.assertEqual(wallet.balance, 0) + + def test_get_total_sent(self): + wallet = self._get_wallet() + self.assertEqual(wallet.total_sent, 0) + + def test_get_total_received(self): + wallet = self._get_wallet() + self.assertEqual(wallet.total_received, 0) + + def test_get_fee(self): + wallet = self._get_wallet() + self.assertGreater(wallet.fee_per_kb, 1) + + def test_set_fee(self): + wallet = self._get_wallet() + wallet.fee_per_kb = 123 + self.assertEqual(wallet.fee_per_kb, 123)