wallet3 cli

This introduces wallet3 cli to the codebase, build using python and
wraps the c++ wallet3 core using pybind.
This commit is contained in:
Sean Darcy 2022-10-24 07:08:52 +11:00
parent f60d722e4d
commit 172093fbbf
36 changed files with 619 additions and 11 deletions

4
.gitmodules vendored
View File

@ -43,3 +43,7 @@
[submodule "external/oxen-mq"]
path = external/oxen-mq
url = https://github.com/oxen-io/oxen-mq.git
[submodule "external/pybind11"]
path = external/pybind11
url = https://github.com/pybind/pybind11
branch = stable

View File

@ -299,6 +299,8 @@ if(NOT MANUAL_SUBMODULES)
endif()
check_submodule(external/uWebSockets uSockets)
check_submodule(external/ghc-filesystem)
check_submodule(external/SQLiteCpp)
check_submodule(external/pybind11)
endif()
endif()
@ -967,3 +969,5 @@ add_custom_target(create_zip
DEPENDS ${oxen_exec_tgts})
add_custom_target(create_archive DEPENDS ${default_archive})
add_subdirectory(pybind)

View File

@ -67,5 +67,4 @@ target_link_libraries(epee
PRIVATE
filesystem
Boost::thread
date::date
extra)

View File

@ -169,3 +169,6 @@ set(SQLITECPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(SQLiteCpp)
add_subdirectory(Catch2)
add_subdirectory(pybind11 EXCLUDE_FROM_ALL)

1
external/pybind11 vendored Submodule

@ -0,0 +1 @@
Subproject commit 0ba639d6177659c5dc2955ac06ad7b5b0d22e05c

6
pybind/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*~
*\#*
build/
__pycache__
dist/
*.egg-info

16
pybind/CMakeLists.txt Normal file
View File

@ -0,0 +1,16 @@
pybind11_add_module(pywallet3 MODULE
module.cpp
wallet/daemon_comms_config.cpp
wallet/rpc_config.cpp
wallet/keyring.cpp
wallet/keyring_manager.cpp
wallet/wallet.cpp
wallet/wallet_config.cpp
)
target_link_libraries(pywallet3 PUBLIC wallet3)
target_include_directories(pywallet3 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
file(GENERATE
OUTPUT setup.py
INPUT setup.py.in)
configure_file(pyproject.toml pyproject.toml COPYONLY)

16
pybind/README.md Normal file
View File

@ -0,0 +1,16 @@
# PyWallet3
Python interface to oxen wallet3
## building
First build the static oxen core
```
mkdir build
cd build
cmake -DBUILD_STATIC_DEPS=ON ..
make wallet3_merged -j16
```
Then, still in the build directory, install via pip using:
```
pip3 install ./pybind
```

27
pybind/common.hpp Normal file
View File

@ -0,0 +1,27 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/functional.h>
namespace py = pybind11;
namespace wallet
{
void
Wallet_Init(py::module& mod);
void
Keyring_Init(py::module& mod);
void
KeyringManager_Init(py::module& mod);
void
DaemonCommsConfig_Init(py::module& mod);
void
RPCConfig_Init(py::module& mod);
void
WalletConfig_Init(py::module& mod);
} // namespace wallet

11
pybind/module.cpp Normal file
View File

@ -0,0 +1,11 @@
#include "common.hpp"
PYBIND11_MODULE(pywallet3, m)
{
wallet::Wallet_Init(m);
wallet::Keyring_Init(m);
wallet::KeyringManager_Init(m);
wallet::DaemonCommsConfig_Init(m);
wallet::RPCConfig_Init(m);
wallet::WalletConfig_Init(m);
}

8
pybind/pyproject.toml Normal file
View File

@ -0,0 +1,8 @@
[build-system]
requires = [
"setuptools>=42",
"wheel",
"pybind11>=2.6.0",
]
build-backend = "setuptools.build_meta"

38
pybind/setup.py.in Normal file
View File

@ -0,0 +1,38 @@
from setuptools import setup
# Available at setup time due to pyproject.toml
from pybind11.setup_helpers import Pybind11Extension, build_ext
__version__ = "0.0.1.dev0"
pywallet3_sources = ['$<TARGET_PROPERTY:pywallet3,SOURCE_DIR>/' + src for src in (
'$<JOIN:$<TARGET_PROPERTY:pywallet3,SOURCES>,',
'>')]
pywallet3_include_dirs = [
'$<JOIN:$<TARGET_PROPERTY:wallet3,INCLUDE_DIRECTORIES>,',
'>']
# Note:
# Sort input source files if you glob sources to ensure bit-for-bit
# reproducible builds (https://github.com/pybind/python_example/pull/53)
ext_modules = [Pybind11Extension(
"pywallet3",
pywallet3_sources,
cxx_std=17,
include_dirs=pywallet3_include_dirs,
extra_objects=['$<TARGET_PROPERTY:wallet3,BINARY_DIR>/libwallet3_merged.a'],
),
]
setup(
name="pywallet3",
version=__version__,
author="Sean Darcy",
author_email="sean@oxen.io",
url="https://github.com/oxen-io/oxen-core",
description="Python wrapper for oxen wallet3 library",
long_description="",
ext_modules=ext_modules,
zip_safe=False,
)

View File

@ -0,0 +1,14 @@
#include "../common.hpp"
#include "wallet3/config/config.hpp"
namespace wallet
{
void
DaemonCommsConfig_Init(py::module& mod)
{
py::class_<DaemonCommsConfig, std::shared_ptr<DaemonCommsConfig>>(mod, "DaemonCommsConfig")
.def(py::init<>())
.def_readwrite("address", &DaemonCommsConfig::address);
}
} // namespace wallet

31
pybind/wallet/keyring.cpp Normal file
View File

@ -0,0 +1,31 @@
#include "../common.hpp"
#include "wallet3/keyring.hpp"
#include <crypto/crypto.h>
#include <common/hex.h>
#include <cryptonote_basic/cryptonote_basic.h>
namespace wallet
{
void
Keyring_Init(py::module& mod)
{
py::class_<Keyring, std::shared_ptr<Keyring>>(mod, "Keyring")
.def(py::init([]( std::string ssk, std::string spk, std::string vsk, std::string vpk, std::string nettype) {
auto type = cryptonote::network_type::MAINNET;
if (nettype == "testnet") type = cryptonote::network_type::TESTNET;
else if (nettype == "devnet") type = cryptonote::network_type::DEVNET;
crypto::secret_key spend_priv;
crypto::public_key spend_pub;
crypto::secret_key view_priv;
crypto::public_key view_pub;
tools::hex_to_type<crypto::secret_key>(ssk, spend_priv);
tools::hex_to_type<crypto::public_key>(spk, spend_pub);
tools::hex_to_type<crypto::secret_key>(vsk, view_priv);
tools::hex_to_type<crypto::public_key>(vpk, view_pub);
return Keyring(spend_priv, spend_pub, view_priv, view_pub, std::move(type)); }))
.def("get_main_address", &Keyring::get_main_address);
}
} // namespace wallet

View File

@ -0,0 +1,22 @@
#include "../common.hpp"
#include "wallet3/keyring_manager.hpp"
#include <crypto/crypto.h>
#include <cryptonote_basic/cryptonote_basic.h>
namespace wallet
{
void
KeyringManager_Init(py::module& mod)
{
py::class_<KeyringManager, std::shared_ptr<KeyringManager>>(mod, "KeyringManager")
.def(py::init([](std::string nettype) {
auto type = cryptonote::network_type::MAINNET;
if (nettype == "testnet") type = cryptonote::network_type::TESTNET;
else if (nettype == "devnet") type = cryptonote::network_type::DEVNET;
return KeyringManager(std::move(type)); }))
.def("generate_keyring_from_electrum_seed", &KeyringManager::generate_keyring_from_electrum_seed);
}
} // namespace wallet

View File

@ -0,0 +1,14 @@
#include "../common.hpp"
#include "wallet3/config/config.hpp"
namespace wallet
{
void
RPCConfig_Init(py::module& mod)
{
py::class_<rpc::Config, std::shared_ptr<rpc::Config>>(mod, "RPCConfig")
.def(py::init<>())
.def_readwrite("sockname", &rpc::Config::sockname);
}
} // namespace wallet

27
pybind/wallet/wallet.cpp Normal file
View File

@ -0,0 +1,27 @@
#include "../common.hpp"
#include <wallet3/wallet.hpp>
#include <wallet3/default_daemon_comms.hpp>
#include <wallet3/keyring.hpp>
#include <wallet3/config/config.hpp>
#include <oxenmq/oxenmq.h>
namespace wallet
{
void
Wallet_Init(py::module& mod)
{
py::class_<Wallet, std::shared_ptr<Wallet>>(mod, "Wallet")
.def(py::init([](const std::string& wallet_name, std::shared_ptr<Keyring> keyring, Config config) {
auto& comms_config = config.daemon;
auto& omq_rpc_config = config.omq_rpc;
auto oxenmq = std::make_shared<oxenmq::OxenMQ>();
auto comms = std::make_shared<DefaultDaemonComms>(std::move(oxenmq), comms_config);
return Wallet::create(oxenmq, std::move(keyring), nullptr, std::move(comms), wallet_name + ".sqlite", "", std::move(config));
}))
.def("get_balance", &Wallet::get_balance)
.def("get_unlocked_balance", &Wallet::get_unlocked_balance)
.def("deregister", &Wallet::deregister);
}
} // namespace wallet

View File

@ -0,0 +1,15 @@
#include "../common.hpp"
#include "wallet3/config/config.hpp"
namespace wallet
{
void
WalletConfig_Init(py::module& mod)
{
py::class_<Config, std::shared_ptr<Config>>(mod, "WalletConfig")
.def(py::init<>())
.def_readwrite("daemon", &Config::daemon)
.def_readwrite("omq_rpc", &Config::omq_rpc);
}
} // namespace wallet

View File

@ -125,3 +125,8 @@ else()
endif()
add_library(version "${CMAKE_CURRENT_BINARY_DIR}/version.cpp")
install(
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/"
DESTINATION "include${OXEN_INSTALL_INCLUDEDIR_SUFFIX}"
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp")

View File

@ -66,7 +66,6 @@ target_link_libraries(common
filesystem
oxen::logging
fmt::fmt
date::date
PRIVATE
sodium
logging

View File

@ -33,10 +33,9 @@
#include <string>
#include <iomanip>
#include <thread>
#include <fmt/chrono.h>
#include <fmt/color.h>
#include <date/date.h>
#include "epee/string_tools.h"
#include "epee/wipeable_string.h"
#include "crypto/crypto.h"
@ -197,7 +196,7 @@ namespace tools
{
if (t < 1234567890)
return "<unknown>";
return date::format("%Y-%m-%d %H:%M:%S UTC", std::chrono::system_clock::from_time_t(t));
return "{:%Y-%m-%d %H:%M:%S} UTC"_format(fmt::gmtime(t));
}
std::string get_human_readable_timespan(std::chrono::seconds seconds)

View File

@ -66,7 +66,6 @@ extern "C" {
#include "service_node_rules.h"
#include "service_node_swarm.h"
#include "version.h"
#include <date/date.h>
using cryptonote::hf;

View File

@ -49,8 +49,7 @@
#include <oxenc/base32z.h>
#include <oxenc/variant.h>
#include <fmt/core.h>
#include <date/date.h>
#include <fmt/core.h>
#include <fmt/chrono.h>
#include <fstream>
#include <ctime>
@ -1544,7 +1543,7 @@ static void append_printable_service_node_list_entry(cryptonote::network_type ne
stream << expiry_height << " (in " << delta_height << ") blocks\n";
stream << indent2 << "Expiry Date (estimated): " <<
date::format("%Y-%m-%d %I:%M:%S %p UTC", std::chrono::system_clock::from_time_t(expiry_epoch_time)) <<
"{:%Y-%m-%d %I:%M:%S %p} UTC"_format(fmt::gmtime(expiry_epoch_time)) <<
" (" << get_human_time_ago(expiry_epoch_time, now) << ")\n";
}
}

View File

@ -75,7 +75,6 @@
namespace cryptonote::rpc {
using nlohmann::json;
using oxen::json_to_bt;
static auto logcat = log::Cat("daemon.rpc");

View File

@ -2,6 +2,7 @@ add_library(wallet3
db_schema.cpp
default_daemon_comms.cpp
keyring.cpp
keyring_manager.cpp
transaction_constructor.cpp
transaction_scanner.cpp
pending_transaction.cpp
@ -27,3 +28,90 @@ target_link_libraries(wallet3
extra
mnemonics
SQLiteCpp)
function(combine_archives output_archive)
set(FULL_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/lib${output_archive}.a)
if(NOT APPLE)
set(mri_file ${CMAKE_CURRENT_BINARY_DIR}/${output_archive}.mri)
set(mri_content "create ${FULL_OUTPUT_PATH}\n")
foreach(in_archive ${ARGN})
string(APPEND mri_content "addlib $<TARGET_FILE:${in_archive}>\n")
endforeach()
string(APPEND mri_content "save\nend\n")
file(GENERATE OUTPUT ${mri_file} CONTENT "${mri_content}")
add_custom_command(
OUTPUT ${FULL_OUTPUT_PATH}
DEPENDS ${mri_file} ${ARGN}
COMMAND ar -M < ${mri_file})
else()
set(merge_libs)
foreach(in_archive ${ARGN})
list(APPEND merge_libs $<TARGET_FILE:${in_archive}>)
endforeach()
add_custom_command(
OUTPUT ${FULL_OUTPUT_PATH}
DEPENDS ${mri_file} ${ARGN}
COMMAND /usr/bin/libtool -static -o ${FULL_OUTPUT_PATH} ${merge_libs})
endif()
add_custom_target(wallet3_merged DEPENDS ${FULL_OUTPUT_PATH})
endfunction(combine_archives)
if (STATIC AND BUILD_STATIC_DEPS)
set(optional_targets)
foreach(maybe_target IN ITEMS libusb_vendor hidapi_libusb)
if(TARGET ${maybe_target})
list(APPEND optional_targets ${maybe_target})
endif()
endforeach()
combine_archives(wallet3_merged
multisig
cryptonote_core
cryptonote_basic
cryptonote_protocol
sqlitedb
logging
wallet3
mnemonics
common
cncrypto
device
ringct
ringct_basic
checkpoints
version
net
epee
blockchain_db
rpc_common
rpc_http_client
rpc_commands
# Static deps:
Boost::program_options Boost::serialization Boost::system Boost::thread
zlib
SQLite::SQLite3
SQLiteCpp
sodium
libzmq
CURL::libcurl
oxenmq::oxenmq
lmdb
randomx
uSockets
cpr
oxen::logging
spdlog::spdlog
fmt::fmt
${optional_targets}
)
if(IOS)
set(lib_folder lib-${ARCH})
else()
set(lib_folder lib)
endif()
endif()

6
src/wallet3/cli-wallet/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
__pycache__
*.egg-info
venv/
build/
release/
dist/

View File

@ -0,0 +1,23 @@
PYTHON_MAJOR_VERSION=3
PYTHON_MINOR_VERSION=8
PYTHON_VERSION=$(PYTHON_MAJOR_VERSION).$(PYTHON_MINOR_VERSION)
PYTHON_WITH_VERSION=python$(PYTHON_VERSION)
PIP_WITH_VERSION=pip$(PYTHON_VERSION)
all: build
system_dependencies:
$(PIP_WITH_VERSION) install --upgrade setuptools
$(PIP_WITH_VERSION) install --upgrade build
build:
$(PYTHON_WITH_VERSION) -m build
$(PIP_WITH_VERSION) install --editable .
run:
oxen_wallet_cli
clean:
find . -type d -name "__pycache__" -exec rm -r {} +
find . -type d -name "*.egg-info" -exec rm -r {} +
rm -rf dist/ build/

View File

@ -0,0 +1,13 @@
# Oxen Wallet CLI
## Development Notes
### Click stuff
https://click.palletsprojects.com/en/8.1.x/
https://openbase.com/python/click-repl
### Example Python wallets
https://github.com/AndreMiras/PyWallet
https://github.com/Blockstream/green_cli

View File

@ -0,0 +1 @@
version = '0.0.1'

View File

@ -0,0 +1,5 @@
"""Entry point for oxen-wallet-cli."""
from oxen_wallet_cli.walletcli import main
if __name__ == "__main__":
main()

View File

@ -0,0 +1,27 @@
import atexit
import sys
import pywallet3
from oxen_wallet_cli import version
class Context:
"""Holds global context related to the invocation of the tool"""
def __init__(self):
self.options = None
self.logged_in = False
self.configured = False
self.wallet = None
self.wallet_core_config = None
self.keyring_manager = None
def configure(self, options):
self.options = options
self.__dict__.update(options)
self.wallet_core_config = pywallet3.WalletConfig()
self.wallet_core_config.daemon.address = self.options["oxend_url"]
self.keyring_manager = pywallet3.KeyringManager(self.options["network"])
self.configured = True
sys.modules[__name__] = Context()

View File

@ -0,0 +1,109 @@
import logging
import os
import click
from click_repl import repl
from oxen_wallet_cli import context
import pywallet3
def _get_config_dir(options):
"""Return the default config dir for network"""
return os.path.expanduser(os.path.join('~', '.oxen-wallet', options['network']))
@click.group(invoke_without_command=True)
@click.option('--log-level', type=click.Choice(['error', 'warn', 'info', 'debug']))
@click.option('--network', default='testnet', help='Network: mainnet|testnet|devnet.')
@click.option('--config-dir', '-C', default=None, help='Override config directory.')
@click.option('--oxend-url', default="ipc:///home/sean/.oxen/testnet/oxend.sock", type=str, help='Use the given daemon')
@click.option('--datadir', help='A directory which the wallet will save data')
@click.pass_context
def walletcli(click_ctx, **options):
"""Command line interface for Oxen Wallet CLI."""
if context.configured:
# In repl mode run configuration once only
return
if options['log_level']:
py_log_level = {
'error': logging.ERROR,
'warn': logging.WARNING,
'info': logging.INFO,
'debug': logging.DEBUG,
}[options['log_level']]
logging.basicConfig(level=py_log_level)
if options['config_dir'] is None:
options['config_dir'] = _get_config_dir(options)
os.makedirs(options['config_dir'], exist_ok=True)
if options['datadir'] is None:
options['datadir'] = os.path.join(options['config_dir'], 'oxen_datadir')
os.makedirs(options['datadir'], exist_ok=True)
context.configure(options)
if click_ctx.invoked_subcommand is None:
click.echo("Run ':help' for help information, or ':quit' to quit.")
repl(click_ctx)
@walletcli.command()
def load_test_wallet():
click.echo("Loading test wallet")
if context.wallet is not None:
click.echo("Wallet already loaded")
return
spend_priv = "e6c9165356c619a64a0d26fafd99891acccccf8717a8067859d972ecd8bcfc0a"
spend_pub = "b76f2d7c8a036ff65c564dcb27081c04fe3f2157942e23b0496ca797ba728e4f"
view_priv = "961d67bb5b3ed1af8678bbfcf621f9c15c2b7bff080892890020bdfd47fe4f0a"
view_pub = "8a0ebacd613e0b03b8f27bc64bd961ea2ebf4c671c6e7f3268651acf0823fed5"
keyring = pywallet3.Keyring(spend_priv, spend_pub, view_priv, view_pub, context.options["network"])
click.echo("Wallet address {} loaded".format(keyring.get_main_address()))
name = click.prompt("Wallet Name", default="{}-oxen-wallet".format(context.options["network"])).strip()
context.wallet_core_config.omq_rpc.sockname = name + ".sock";
context.wallet = pywallet3.Wallet(name, keyring, context.wallet_core_config)
@walletcli.command()
@click.argument('seed_phrase', nargs=25)
@click.argument('seed_phrase_passphrase', default="")
def load_from_seed(seed_phrase, seed_phrase_passphrase):
click.echo("Loading wallet from seed")
if context.wallet is not None:
click.echo("Wallet already loaded")
return
seed_phrase_str = ' '.join(seed_phrase)
keyring = context.keyring_manager.generate_keyring_from_electrum_seed(seed_phrase_str, seed_phrase_passphrase)
click.echo("Wallet address {} loaded".format(keyring.get_main_address()))
name = click.prompt("Wallet Name", default="{}-oxen-wallet".format(context.options["network"])).strip()
context.wallet_core_config.omq_rpc.sockname = name + ".sock";
context.wallet = pywallet3.Wallet(name, keyring, context.wallet_core_config)
@walletcli.command()
def register_service_node():
click.echo("Registering Service Node")
if click.confirm("Would you like to register a service node now"):
click.echo("")
name = click.prompt("Enter the wallet address of the operator", default="").strip()
click.echo("The wallet address to be used is: {}".format(name))
click.echo("TODO: This function is not yet implemented")
@walletcli.command()
def address():
# click.echo("Address: {}".format(context.keyring.get_main_address()))
click.echo("Address: {}".format("TODO sean get the address here"))
@walletcli.command()
def get_balance():
click.echo("Balance: {}".format(context.wallet.get_balance()))
@walletcli.command()
def get_unlocked_balance():
click.echo("Unlocked Balance: {}".format(context.wallet.get_unlocked_balance()))
def main():
walletcli()

View File

@ -0,0 +1,30 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = 'oxen_wallet_cli'
authors = [
{name = "Sean Darcy", email = "sean@oxen.io"},
]
description = "CLI wallet for Oxen"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: GPL-3.0-or-later",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
]
dependencies = [
"Click",
"click-repl",
"pywallet3",
]
dynamic = ["version"]
[project.scripts]
oxen_wallet_cli = "oxen_wallet_cli.__main__:main"

View File

@ -0,0 +1,32 @@
#include "keyring_manager.hpp"
#include "mnemonics/electrum-words.h"
namespace wallet
{
std::shared_ptr<Keyring> KeyringManager::generate_keyring_from_electrum_seed(std::string& seed_phrase, std::string& seed_phrase_passphrase)
{
std::string old_language;
crypto::secret_key recovery_key;
if (!crypto::ElectrumWords::words_to_bytes(seed_phrase, recovery_key, old_language))
throw std::runtime_error("Electrum-style word list failed verification");
if (!seed_phrase_passphrase.empty())
recovery_key = cryptonote::decrypt_key(recovery_key, seed_phrase_passphrase);
cryptonote::account_base account;
// Generate the account keys using the recovery key
// param recovery_param If it is a restore, the recovery key
// param recover Whether it is a restore
// param two_random Whether it is a non-deterministic wallet
account.generate(recovery_key, true, false);
cryptonote::account_keys account_keys = account.get_keys();
return std::make_shared<wallet::Keyring>(
account_keys.m_spend_secret_key,
account_keys.m_account_address.m_spend_public_key,
account_keys.m_view_secret_key,
account_keys.m_account_address.m_view_public_key,
nettype);
}
} // namespace wallet

View File

@ -0,0 +1,18 @@
#pragma once
#include "keyring.hpp"
namespace wallet
{
class KeyringManager
{
public:
KeyringManager() = default;
KeyringManager(const cryptonote::network_type& type): nettype(type) {};
std::shared_ptr<Keyring> generate_keyring_from_electrum_seed(std::string& seed_phrase, std::string& seed_phrase_passphrase);
private:
cryptonote::network_type nettype = cryptonote::network_type::MAINNET;
};
} // namespace wallet

View File

@ -58,7 +58,7 @@ namespace wallet
{
request_handler.set_wallet(weak_from_this());
omq->start();
daemon_comms->set_remote("ipc://./oxend.sock");
daemon_comms->set_remote(config.daemon.address);
daemon_comms->register_wallet(*this, last_scan_height + 1 /*next needed block*/,
true /* update sync height */,
true /* new wallet */);