monero-c
C ABI for using Monero
Loading...
Searching...
No Matches
Monero C ABI

Build Tests Codacy Badge Codacy Badge

‍[!WARNING]

monero-c is currently under maintenance and unfunded, expect bugs and breaking changes.

A C ABI for creating Monero applications using RPC and FFI bindings to monero v0.18.5.3 'Fluorine Fermi'.

  • Supports wallet and daemon RPC clients.
  • Supports client-side wallets using native bindings.
  • Supports multisig, view-only, and offline wallets.
  • Wallet types are interchangeable behind a common interface.
  • Uses a clearly defined data model and API specification intended to be intuitive and robust.
  • Query wallet transactions, transfers, and outputs by their properties.
  • Fetch and process binary data from the daemon (e.g. raw blocks).
  • Receive notifications when blocks are added to the chain or when wallets sync, send, or receive.

Architecture


Build applications using FFI bindings to monero-project/monero. Wallet implementations are interchangeable by conforming to a common interface, monero_wallet.h.

Sample code

#include <stdio.h>
#include "monero_c.h"
static void on_sync_progress(void* user_data, uint64_t height, uint64_t start_height, uint64_t end_height, double percent_done, const char* message) {
// feed a progress bar?
}
static void on_output_received(void* user_data, const char* output_json) {
// JSON-serialized monero_output_wallet, with the amount and the transaction
}
// connect to daemon
monero_daemon* daemon = NULL;
monero_daemon_connect("http://localhost:38081", "superuser", "abctesting123", "", 0, &daemon);
uint64_t height = 0;
monero_daemon_get_height(daemon, &height); // 1523651
char* txs_in_pool = NULL;
monero_daemon_get_tx_pool(daemon, &txs_in_pool); // JSON object with the monero_tx in a block
monero_utils_free(txs_in_pool);
// create wallet from mnemonic phrase using native bindings to monero-project
monero_wallet* wallet_full = NULL;
monero_wallet_create_from_seed("sample_wallet_full", "supersecretpassword123", MONERO_UTILS_NETWORK_STAGENET,
"hefty value scenic...", "", 573936, NULL, &wallet_full);
monero_wallet_set_daemon_connection(wallet_full, "http://localhost:38081", "superuser", "abctesting123", "", NULL, true); // NULL trusts a local daemon
// synchronize the wallet and receive progress notifications
monero_wallet_listener_callbacks sync_callbacks = {NULL, on_sync_progress, NULL, NULL, NULL, NULL};
monero_wallet_listener* sync_listener = NULL;
monero_wallet_listener_create(&sync_callbacks, &sync_listener);
char* sync_result = NULL;
monero_wallet_sync(wallet_full, NULL, sync_listener, &sync_result);
monero_utils_free(sync_result);
// synchronize in the background every 5 seconds
monero_wallet_start_syncing(wallet_full, 5000);
// receive notifications when funds are received, confirmed, and unlocked
monero_wallet_listener_callbacks callbacks = {NULL, NULL, NULL, NULL, on_output_received, NULL};
monero_wallet_listener* listener = NULL;
monero_wallet_listener_create(&callbacks, &listener);
monero_wallet_add_listener(wallet_full, listener);
// connect to wallet RPC and open wallet
monero_wallet* wallet_rpc = NULL;
monero_wallet_rpc_open("http://localhost:38083", "rpc_user", "abc123", "sample_wallet_rpc", "supersecretpassword123", &wallet_rpc);
char* primary_address = NULL;
monero_wallet_get_primary_address(wallet_rpc, &primary_address); // 555zgduFhmKd2o8rPUz...
uint64_t balance = 0;
monero_wallet_get_balance(wallet_rpc, &balance); // 533648366742
char* txs = NULL;
monero_wallet_get_txs(wallet_rpc, NULL, &txs); // JSON object with the blocks of the monero_tx_wallet
// send funds from RPC wallet to full wallet
char* address = NULL;
monero_wallet_get_primary_address(wallet_full, &address);
char tx_config[256];
snprintf(tx_config, sizeof(tx_config),
"{\"accountIndex\":0,\"destinations\":[{\"address\":\"%s\",\"amount\":250000000000}],\"relay\":false}", address);
char* created_tx = NULL;
monero_wallet_create_tx(wallet_rpc, tx_config, &created_tx); // "Are you sure you want to send... ?"
char* tx_hash = NULL;
monero_wallet_relay_tx_json(wallet_rpc, created_tx, &tx_hash); // relay the transaction
monero_utils_free(created_tx);
monero_utils_free(primary_address);
// save and close wallet
monero_wallet_close(wallet_full, true);
monero_wallet_free(wallet_full);
monero_wallet_free(wallet_rpc);
MONERO_EXPORT void monero_daemon_free(monero_daemon *daemon)
Release a handle returned by monero_daemon_connect().
MONERO_EXPORT monero_result monero_daemon_connect(const char *uri, const char *username, const char *password, const char *proxy_uri, uint32_t timeout_ms, monero_daemon **out_daemon)
Create a daemon RPC client and checks the connection, which can take a while if the daemon is slow to...
MONERO_EXPORT monero_result monero_daemon_get_height(monero_daemon *daemon, uint64_t *out_height)
Get the number of blocks in the longest chain known to the node.
MONERO_EXPORT monero_result monero_daemon_get_tx_pool(monero_daemon *daemon, char **out_json)
Get the valid transactions seen by the node but not yet mined into a block.
struct monero_daemon monero_daemon
Copyright (c) Libmonero.
Definition monero_c_daemon.h:53
MONERO_EXPORT void monero_utils_free(void *ptr)
Frees a string or byte buffer returned by any monero_utils_* function.
@ MONERO_UTILS_NETWORK_STAGENET
Definition monero_c_utils.h:93
MONERO_EXPORT void monero_wallet_listener_free(monero_wallet_listener *listener)
Release a listener.
MONERO_EXPORT monero_result monero_wallet_listener_create(const monero_wallet_listener_callbacks *callbacks, monero_wallet_listener **out_listener)
Create a listener from its callbacks.
struct monero_wallet_listener monero_wallet_listener
Opaque listener that receives the wallet's notifications.
Definition monero_c_wallet.h:66
MONERO_EXPORT monero_result monero_wallet_start_syncing(monero_wallet *wallet, uint64_t sync_period_ms)
Start a background thread that syncs the wallet with the daemon.
MONERO_EXPORT void monero_wallet_free(monero_wallet *wallet)
Release a wallet handle.
MONERO_EXPORT monero_result monero_wallet_rpc_open(const char *uri, const char *username, const char *password, const char *name, const char *wallet_password, monero_wallet **out_wallet)
Open a wallet on a monero-wallet-rpc server.
MONERO_EXPORT monero_result monero_wallet_add_listener(monero_wallet *wallet, monero_wallet_listener *listener)
Register a listener with a wallet.
MONERO_EXPORT monero_result monero_wallet_set_daemon_connection(monero_wallet *wallet, const char *uri, const char *username, const char *password, const char *proxy_uri, const bool *is_trusted, bool ssl_verify)
Set the daemon connection of the wallet.
MONERO_EXPORT monero_result monero_wallet_get_primary_address(monero_wallet *wallet, char **out_address)
Get the primary address of the wallet.
MONERO_EXPORT monero_result monero_wallet_get_txs(monero_wallet *wallet, const char *query_json, char **out_json)
Get the transactions of the wallet.
MONERO_EXPORT monero_result monero_wallet_close(monero_wallet *wallet, bool save)
Close the wallet.
struct monero_wallet monero_wallet
Copyright (c) Libmonero.
Definition monero_c_wallet.h:56
MONERO_EXPORT monero_result monero_wallet_relay_tx_json(monero_wallet *wallet, const char *tx_json, char **out_hash)
Relay a transaction created without relaying, from its JSON.
MONERO_EXPORT monero_result monero_wallet_get_balance(monero_wallet *wallet, uint64_t *out_balance)
Get the balance of the wallet, including funds that are still locked.
MONERO_EXPORT monero_result monero_wallet_create_tx(monero_wallet *wallet, const char *config_json, char **out_json)
Create a transaction that transfers funds from the wallet.
MONERO_EXPORT monero_result monero_wallet_sync(monero_wallet *wallet, const uint64_t *start_height, monero_wallet_listener *listener, char **out_json)
Synchronize the wallet with the daemon in the calling thread.
MONERO_EXPORT monero_result monero_wallet_create_from_seed(const char *path, const char *password, int32_t network_type, const char *seed, const char *seed_offset, uint64_t restore_height, const char *language, monero_wallet **out_wallet)
Restore a wallet from its mnemonic seed and save it to disk.
Callbacks of a wallet listener.
Definition monero_c_wallet.h:123

Every call returns MONERO_OK or MONERO_ERROR, and monero_last_error() gives the message. The sample leaves the checks out.

Documentation

Building monero-c from source

Linux and macOS

  1. Clone the project repository:
    git clone --recurse-submodules https://github.com/libmonero/monero-c.git
  2. Build monero-project, monero-cpp and monero-c. Extra arguments are passed to cmake, e.g. -DBUILD_TESTS=ON:
    cd monero-c
    ./bin/build_libmonero_c.sh -DBUILD_TESTS=ON

From Rider or Visual Studio, open monero-c.sln, right-click the project Monero.Native and choose Build: it runs the same script (see native/Monero.Native.csproj for the arguments and the MSYS2 path on Windows). Building the solution builds it too, except where CI or GITHUB_ACTIONS is true (pass -p:MoneroCSkipNative=true to skip it elsewhere), so the C# tests of CI don't start a native build. The next builds are incremental: make only rebuilds what changed.

The project Monero.Native.CTests lists the tests of CTest in the test runner of the IDE, one test for each, and runs them from the build directory (MONERO_C_BUILD_DIR changes it), so build Monero.Native first (Build Solution does, in this order). The tests are skipped where CI or GITHUB_ACTIONS is true, because the CI jobs run the C tests on their own.

Flags are cached in ./build/CMakeCache.txt, so pass them on every build to change them. To sync submodules after pulling new changes, run ./bin/update_submodules.sh.

Windows

  1. Download and install MSYS2.
  2. Press the Windows button and launch MSYS2 MINGW64.
  3. Update packages: pacman -Syu and confirm at prompts.
  4. Relaunch MSYS2 (if necessary) and install dependencies:
    pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-openssl mingw-w64-x86_64-zeromq mingw-w64-x86_64-libsodium mingw-w64-x86_64-hidapi mingw-w64-x86_64-unbound mingw-w64-x86_64-protobuf mingw-w64-x86_64-libusb mingw-w64-x86_64-expat mingw-w64-x86_64-ntldd git make gettext base-devel wget
    wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-icu-76.1-1-any.pkg.tar.zst
    pacman -U mingw-w64-x86_64-icu-76.1-1-any.pkg.tar.zst
    wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-boost-1.87.0-3-any.pkg.tar.zst
    pacman -U mingw-w64-x86_64-boost-1.87.0-3-any.pkg.tar.zst
  5. Clone the repo: git clone --recurse-submodules https://github.com/libmonero/monero-c.git
  6. Build monero-project, monero-cpp and monero-c. Extra arguments are passed to cmake, e.g. -DBUILD_TESTS=ON:
    cd monero-c
    ./bin/build_libmonero_c.sh -DBUILD_TESTS=ON

Running tests

ctest --test-dir build --output-on-failure

The native unit tests in tests/c/unit need nothing else. The native integration tests in tests/c/integration call a regtest monerod and a monero-wallet-rpc server, so they are skipped unless MONERO_C_TEST_DAEMON_URI and MONERO_C_TEST_WALLET_RPC_URI point at them. The compose file starts both, with the same image that monero-python uses:

docker compose -f tests/c/integration/docker-compose.yml up -d
MONERO_C_TEST_DAEMON_URI=http://127.0.0.1:18081 MONERO_C_TEST_WALLET_RPC_URI=http://127.0.0.1:18082 ctest --test-dir build --output-on-failure
docker compose -f tests/c/integration/docker-compose.yml down -v

The wallet tests stop the wallet RPC server when they end, and the wallets they create stay in its volume, so run down -v and up -d before the next run.

All the tests include only the public header and call only exported functions, the same way a real FFI consumer would.

Some of the unit tests look for crashes and leaks instead of checking a feature, and CI runs all of them under AddressSanitizer, UndefinedBehaviorSanitizer and LeakSanitizer:

  • monero_c_arguments_unit_tests calls every exported function with a NULL in each pointer, one at a time, and then once with plausible arguments, once with the largest numbers and once with long texts, on a full wallet, a keys wallet, a closed wallet, an RPC wallet, a daemon and a connection. It checks that the NULL of a required pointer is refused with <name> must not be null and that a failed call resets its outputs. Its list of functions, tests/c/unit/monero_c_arguments_table.h, comes from the headers: python3 tests/c/generator/generate_argument_tests.py writes it, and CI fails when it is out of date, so a new function can't miss the test.
  • monero_c_fuzz_unit_tests gives each function that reads JSON, hex or binary a few hundred mutations of a valid input: a number at the edge of its type, a value of the wrong type, text that stops in the middle, a size that is too large. The generator has a fixed seed, so a failure happens again. MONERO_C_FUZZ_SEED and MONERO_C_FUZZ_ITERATIONS change the run, and MONERO_C_FUZZ_VERBOSE prints each input before its call, so the last line is the input of a crash.
  • monero_c_threads_unit_tests calls the functions from several threads, with wallets of their own and with the connection and the daemon that the headers say can be shared.

Memory Ownership

Any out_* parameter that receives a string or byte buffer (char**, uint8_t**) is heap-allocated by monero_c and must be freed by the caller with monero_utils_free(), as shown above – the sample's monero_utils_free(json) call is not optional. Fixed-size out-params (e.g. uint8_t out_payment_id[32]) write into a buffer the caller already owns, so there's nothing to free. See src/utils/monero_c_utils.h for the one exception (monero_last_error()). Handles are released with their matching *_free() function: monero_daemon_free(), monero_wallet_free(), monero_rpc_connection_free() and the listener functions. A daemon or RPC wallet created from a monero_rpc_connection keeps the connection, so the connection handle can be freed right after. To save a full wallet, close it with monero_wallet_close(wallet, true) before freeing it. After a failed call, the string, buffer, array and handle outputs are NULL and the counts are 0, even when the failure was a NULL argument, so there is nothing to free. A thread that makes or opens a wallet, or gives it a new password, gets a scratchpad of 2 MB for the password hash, as monero-project allocates it, and monero_c frees it when the thread ends, so a thread pool costs less than a new thread for each call.

Results with context

The results come with the parents that their models have, so a transfer is in its tx and a tx in its block, as in monero-java. The shapes are in docs/json-shapes.md, and the docs of each function in src/wallet/monero_c_wallet.h and src/daemon/monero_c_daemon.h say which one it returns.

{"blocks":[
{"height":2,"timestamp":1791675257,
"txs":[{"hash":"62ded941...","fee":0,"numConfirmations":99,"unlockTime":62,"isMinerTx":true,"isLocked":false,
"incomingTransfers":[{"amount":35184271425600,"accountIndex":0,"subaddressIndex":0}]}]},
{"txs":[{"hash":"ffa2a44c...","inTxPool":true,"outgoingTransfer":{"amount":1000000000,"accountIndex":0}}]}
]}

The lists of values that have no parent, such as the accounts, the subaddresses, the address book and the key images, are plain JSON arrays.

Limits

A JSON argument can't nest deeper than 64 levels, and a mnemonic can't be longer than 4096 bytes. monero_daemon_get_blocks_by_range() takes at most 100000 blocks per call, and monero_daemon_get_blocks_by_range_chunked() has no limit. A call over a limit fails with MONERO_ERROR before any parser or request runs.

TLS

A connection to an https server checks the certificate against the CA certificates of the system, unless its sslVerify is false. The library carries its own OpenSSL, which looks for the certificates in the directory of the machine that built it, so on Linux and macOS the library sets SSL_CERT_FILE and SSL_CERT_DIR when it is loaded, to the bundle and the directory of the system (the Debian, Fedora, openSUSE, Alpine and macOS ones, and Android's directory). It leaves the environment alone if you set one of the two, or if OpenSSL finds the certificates by itself, as it does on Debian and Ubuntu. Windows reads the root store of the system, and nothing is set there.

Proxy and Tor

A connection can go through a SOCKS proxy, such as the one of a Tor node, which has to run already and listens on 127.0.0.1:9050 by default. The proxy is the proxy_uri argument of monero_daemon_connect() and monero_wallet_set_daemon_connection(), and the proxyUri of a connection JSON (monero_rpc_connection_create(), monero_wallet_rpc_set_daemon_connection()) and of the server in the config of monero_wallet_create(). Its form is [scheme://]ip:port, where the scheme is socks4a (the default), socks4 or socks5, and the host is an IP address, not a name such as localhost. Use the default or socks5, so that the proxy resolves the host name of an .onion address: socks4 resolves it on your machine and the connection fails. The proxy is not checked when it is set, so a mistake shows as a network error at the first call. monero_rpc_connection_is_onion() tells if the URI of a connection is an onion address.

// a daemon on Tor, through the proxy of the local node. Tor is slower, so wait up to a minute
monero_daemon* onion_daemon = NULL;
monero_daemon_connect("http://kyaklhqp4yyza4fmtbvs6z4lrzr5cljxwa7o2d42sfelfhczsmbwzfad.onion:18081", "superuser", "abctesting123",
"127.0.0.1:9050", 60000, &onion_daemon);
// a full wallet through the same proxy. NULL would trust only a daemon on this machine, so say that this one is trusted
bool trusted = true;
monero_wallet_set_daemon_connection(wallet_full, "http://kyaklhqp4yyza4fmtbvs6z4lrzr5cljxwa7o2d42sfelfhczsmbwzfad.onion:18081",
"superuser", "abctesting123", "127.0.0.1:9050", &trusted, true);
// monero-wallet-rpc reaches its daemon through the proxy of the connection
"{\"uri\":\"http://kyaklhqp4yyza4fmtbvs6z4lrzr5cljxwa7o2d42sfelfhczsmbwzfad.onion:18081\",\"username\":\"superuser\",\"password\":\"abctesting123\",\"proxyUri\":\"127.0.0.1:9050\"}",
true, NULL);
MONERO_EXPORT monero_result monero_wallet_rpc_set_daemon_connection(monero_wallet *wallet, const char *connection_json, bool is_trusted, const char *ssl_options_json)
Set the daemon connection of an RPC wallet, with the SSL options of the connection.

Thread Safety

‍[!WARNING]

Thread safety is incomplete. Some concurrent uses are still unsafe.

What holds today:

  • monero-cpp serializes calls on one monero_daemon handle, so they don't run in parallel.
  • monero-cpp locks a monero_rpc_connection, so one handle can be shared by several threads and by the daemons and RPC wallets created from it.
  • Don't use one monero_wallet handle from two threads at once. monero_wallet_start_syncing() syncs on a thread owned by monero-cpp, next to the calls on the handle. The exception is monero_wallet_request_shutdown(), which makes the calls in flight return, so that a thread can wait for them and then close and free the wallet.
  • Listener callbacks run on a thread owned by monero-cpp. Inside a callback, monero_daemon_remove_listener() and monero_daemon_remove_listeners() are safe. Don't free a wallet, a daemon or a listener from a callback.
  • A wallet callback runs while the sync holds the lock of the wallet, so monero_wallet_get_balance(), monero_wallet_get_unlocked_balance(), monero_wallet_get_txs(), monero_wallet_get_outputs() and monero_wallet_get_accounts() called on that wallet from it never return. Copy what the callback gets and make those calls from another thread. A daemon callback can call the daemon.
  • Different listeners on one daemon can be managed from different threads, and monero_daemon_get_listeners() can run meanwhile. The handles that it returns are the ones that were added at that moment, and a thread must not free a handle of another.
  • One listener can be on one wallet at a time. Adding it to a second wallet fails.
  • A binding avoids the races of adding, removing and freeing a wallet listener by keeping one native listener per wallet and fanning out to its own listeners. It adds the native listener once, when the first of its own arrives, and doesn't remove it. It frees it after monero_wallet_close() has returned, which waits for the callbacks in flight, and then frees the wallet. monero-java works this way.
  • monero_daemon_wait_for_next_block_header() blocks only its calling thread, and other calls on the handle run meanwhile.
  • monero_last_error() is per thread: it returns the last error of the calling thread.

Related projects

License

This project is licensed under MIT.

Donations

If this library has been valuable to you, please consider donating to support its continued development. 🙏

XMR

852punAC8Ub7tLqrA2L72p3VVL9ZdjSAxUgryqcoiFSmLZnE7UJiHENCRYZboz6bETEmznh9tBSkGGWiPLWdigVm8AYgFcJ