Add channel deposit calls and notes retrieval

This commit is contained in:
Daniel 2026-06-17 15:09:57 +02:00
parent fc1439cf16
commit 04ac9d1bd5
7 changed files with 660 additions and 6 deletions

8
flake.lock generated
View File

@ -24,16 +24,16 @@
"rust-rapidsnark": "rust-rapidsnark"
},
"locked": {
"lastModified": 1781525456,
"narHash": "sha256-SOH9dY1Jg8JmfqMWMZ9YzbGQgWL+ifJ4z+8tHW/4zWs=",
"lastModified": 1781698521,
"narHash": "sha256-cnk7CmRskPL5CjH3Q0vZEoLbYVkwUaQrq86xdJiLje8=",
"owner": "logos-blockchain",
"repo": "logos-blockchain",
"rev": "4efdf816e2a3d1447096c3f64c9e230769eb4ecb",
"rev": "0222706b14010fbdfbe9a94d3617ebf46a77fdd1",
"type": "github"
},
"original": {
"owner": "logos-blockchain",
"ref": "4efdf816e2a3d1447096c3f64c9e230769eb4ecb",
"ref": "0222706b14010fbdfbe9a94d3617ebf46a77fdd1",
"repo": "logos-blockchain",
"type": "github"
}

View File

@ -3,8 +3,8 @@
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
# v0.1.3-rc.10-compatible + rust-rapidsnark nix fixes + cli commands + leader_claim C binding
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?ref=4efdf816e2a3d1447096c3f64c9e230769eb4ecb";
# v0.1.3-rc.10-compatible + rust-rapidsnark nix fixes + cli commands + leader_claim + channel deposit / wallet notes C bindings
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?ref=0222706b14010fbdfbe9a94d3617ebf46a77fdd1";
};
outputs = inputs@{ logos-module-builder, ... }:

View File

@ -48,6 +48,27 @@ namespace {
}
}
// Parse arbitrary-length hex (optional 0x prefix) into bytes. Unlike
// parse_address_hex this does not enforce a fixed length; used for the
// variable-length channel deposit metadata. Returns false on odd length or
// non-hex input.
bool parse_hex_bytes(const std::string& hex_in, std::vector<uint8_t>& out) {
std::string hex = hex_in;
boost::algorithm::trim(hex);
if (hex.size() >= 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X'))
hex = hex.substr(2);
if (hex.size() % 2 != 0)
return false;
try {
std::string decoded;
boost::algorithm::unhex(hex.begin(), hex.end(), std::back_inserter(decoded));
out.assign(decoded.begin(), decoded.end());
return true;
} catch (const boost::algorithm::non_hex_input&) {
return false;
}
}
std::string bytes_to_hex(const uint8_t* data, size_t len) {
std::string out;
out.reserve(len * 2);
@ -613,6 +634,54 @@ std::vector<std::string> LogosBlockchainModule::wallet_get_known_addresses() {
return out;
}
std::string LogosBlockchainModule::wallet_get_notes(
const std::string& wallet_address_hex,
const std::string& optional_tip_hex
) {
if (!node) {
return "Error: The node is not running.";
}
const std::vector<uint8_t> address_bytes = parse_address_hex(wallet_address_hex);
if (address_bytes.empty() || static_cast<int>(address_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid wallet address (64 hex characters required).";
}
std::vector<uint8_t> tip_bytes;
const HeaderId* optional_tip = nullptr;
if (!optional_tip_hex.empty()) {
tip_bytes = parse_address_hex(optional_tip_hex);
if (tip_bytes.empty() || static_cast<int>(tip_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid optional tip (64 hex characters or empty).";
}
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.data());
}
auto [value, error] = get_wallet_notes(node, address_bytes.data(), optional_tip);
if (!is_ok(&error)) {
return "Error: Failed to get wallet notes: " + std::to_string(error);
}
json obj;
obj["tip"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value.tip), ADDRESS_BYTES);
json notes = json::array();
for (size_t i = 0; i < value.len; ++i) {
const WalletNote& note = value.notes[i];
json n;
n["id"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(note.id), ADDRESS_BYTES);
// Value is u64; serialized as a string to avoid JSON number precision loss.
n["value"] = std::to_string(note.value);
notes.push_back(std::move(n));
}
obj["notes"] = std::move(notes);
const OperationStatus free_status = free_wallet_notes(value);
if (!is_ok(&free_status)) {
fprintf(stderr, "Failed to free wallet notes. Error: %d\n", free_status);
}
return obj.dump();
}
std::string LogosBlockchainModule::leader_claim() {
if (!node) {
return "Error: The node is not running.";
@ -626,6 +695,166 @@ std::string LogosBlockchainModule::leader_claim() {
return bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), TX_HASH_BYTES);
}
// Channel
std::string LogosBlockchainModule::channel_deposit(
const std::string& channel_id_hex,
const std::string& funding_public_key_hex,
const std::string& amount,
const std::string& metadata_hex,
const std::string& optional_tip_hex
) {
if (!node) {
return "Error: The node is not running.";
}
std::string amount_trimmed = amount;
boost::algorithm::trim(amount_trimmed);
uint64_t amount_val = 0;
auto [ptr, ec] = std::from_chars(amount_trimmed.data(), amount_trimmed.data() + amount_trimmed.size(), amount_val);
if (ec != std::errc{} || ptr != amount_trimmed.data() + amount_trimmed.size() || amount_trimmed.empty()) {
return "Error: Invalid amount (positive integer required).";
}
if (amount_val == 0) {
return "Error: Invalid amount (must be greater than zero).";
}
const std::vector<uint8_t> channel_bytes = parse_address_hex(channel_id_hex);
if (channel_bytes.empty() || static_cast<int>(channel_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid channel_id (64 hex characters required).";
}
const std::vector<uint8_t> funding_bytes = parse_address_hex(funding_public_key_hex);
if (funding_bytes.empty() || static_cast<int>(funding_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid funding_public_key (64 hex characters required).";
}
std::vector<uint8_t> metadata_bytes;
if (!metadata_hex.empty() && !parse_hex_bytes(metadata_hex, metadata_bytes)) {
return "Error: Invalid metadata (even-length hex string required).";
}
std::vector<uint8_t> tip_bytes;
const HeaderId* optional_tip = nullptr;
if (!optional_tip_hex.empty()) {
tip_bytes = parse_address_hex(optional_tip_hex);
if (tip_bytes.empty() || static_cast<int>(tip_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid optional tip (64 hex characters or empty).";
}
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.data());
}
ChannelDepositArguments args{};
args.optional_tip = optional_tip;
args.channel_id = channel_bytes.data();
args.funding_public_key = funding_bytes.data();
args.amount = amount_val;
args.metadata = metadata_bytes.empty() ? nullptr : metadata_bytes.data();
args.metadata_len = metadata_bytes.size();
auto [value, error] = ::channel_deposit(node, &args);
if (!is_ok(&error)) {
return "Error: Failed to deposit into channel: " + std::to_string(error);
}
return bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES);
}
std::string LogosBlockchainModule::channel_deposit_with_notes(
const std::string& channel_id_hex,
const std::vector<std::string>& input_note_id_hexes,
const std::string& metadata_hex,
const std::string& change_public_key_hex,
const std::vector<std::string>& funding_public_key_hexes,
const std::string& max_tx_fee,
const std::string& optional_tip_hex
) {
if (!node) {
return "Error: The node is not running.";
}
const std::vector<uint8_t> channel_bytes = parse_address_hex(channel_id_hex);
if (channel_bytes.empty() || static_cast<int>(channel_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid channel_id (64 hex characters required).";
}
if (input_note_id_hexes.empty()) {
return "Error: At least one input note required.";
}
// Note IDs are 32-byte values stored contiguously so the buffer can be passed
// as a `NoteId` (uint8_t[32]) array.
std::vector<uint8_t> note_ids_flat;
note_ids_flat.reserve(input_note_id_hexes.size() * ADDRESS_BYTES);
for (const std::string& hex : input_note_id_hexes) {
const std::vector<uint8_t> b = parse_address_hex(hex);
if (b.empty() || static_cast<int>(b.size()) != ADDRESS_BYTES) {
return "Error: Invalid input note id (64 hex characters required).";
}
note_ids_flat.insert(note_ids_flat.end(), b.begin(), b.end());
}
const std::vector<uint8_t> change_bytes = parse_address_hex(change_public_key_hex);
if (change_bytes.empty() || static_cast<int>(change_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid change_public_key (64 hex characters required).";
}
if (funding_public_key_hexes.empty()) {
return "Error: At least one funding public key required.";
}
std::vector<std::vector<uint8_t>> funding_bytes;
for (const std::string& hex : funding_public_key_hexes) {
std::vector<uint8_t> b = parse_address_hex(hex);
if (b.empty() || static_cast<int>(b.size()) != ADDRESS_BYTES) {
return "Error: Invalid funding public key (64 hex characters required).";
}
funding_bytes.push_back(std::move(b));
}
std::vector<const uint8_t*> funding_ptrs;
funding_ptrs.reserve(funding_bytes.size());
for (const auto& b : funding_bytes)
funding_ptrs.push_back(b.data());
std::string fee_trimmed = max_tx_fee;
boost::algorithm::trim(fee_trimmed);
uint64_t max_tx_fee_val = 0;
auto [ptr, ec] = std::from_chars(fee_trimmed.data(), fee_trimmed.data() + fee_trimmed.size(), max_tx_fee_val);
if (ec != std::errc{} || ptr != fee_trimmed.data() + fee_trimmed.size() || fee_trimmed.empty()) {
return "Error: Invalid max_tx_fee (non-negative integer required).";
}
std::vector<uint8_t> metadata_bytes;
if (!metadata_hex.empty() && !parse_hex_bytes(metadata_hex, metadata_bytes)) {
return "Error: Invalid metadata (even-length hex string required).";
}
std::vector<uint8_t> tip_bytes;
const HeaderId* optional_tip = nullptr;
if (!optional_tip_hex.empty()) {
tip_bytes = parse_address_hex(optional_tip_hex);
if (tip_bytes.empty() || static_cast<int>(tip_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid optional tip (64 hex characters or empty).";
}
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.data());
}
ChannelDepositWithNotesArguments args{};
args.optional_tip = optional_tip;
args.channel_id = channel_bytes.data();
args.input_note_ids = reinterpret_cast<const NoteId*>(note_ids_flat.data());
args.input_note_ids_len = input_note_id_hexes.size();
args.metadata = metadata_bytes.empty() ? nullptr : metadata_bytes.data();
args.metadata_len = metadata_bytes.size();
args.change_public_key = change_bytes.data();
args.funding_public_keys = funding_ptrs.data();
args.funding_public_keys_len = funding_ptrs.size();
args.max_tx_fee = max_tx_fee_val;
auto [value, error] = ::channel_deposit_with_notes(node, &args);
if (!is_ok(&error)) {
return "Error: Failed to deposit into channel: " + std::to_string(error);
}
return bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES);
}
// Blend
std::string LogosBlockchainModule::blend_join_as_core_node(

View File

@ -75,8 +75,44 @@ public:
const std::string& optional_tip_hex
);
std::vector<std::string> wallet_get_known_addresses();
// Spendable notes (UTXOs) of a wallet address, as a JSON string:
// { "tip": "<hex>", "notes": [ { "id": "<hex>", "value": "<u64>" }, ... ] }
// optional_tip_hex may be empty to query at the current tip. Note IDs round-trip
// into channel_deposit_with_notes.
std::string wallet_get_notes(
const std::string& wallet_address_hex,
const std::string& optional_tip_hex
);
std::string leader_claim();
// Channel
// Amount-based deposit: the binding selects funding notes itself (splitting a
// note via a transfer when no exact-value note exists) so the channel receives
// exactly `amount`. funding_public_key owns the funding notes, the deposit note
// and any change. metadata_hex may be empty; optional_tip_hex may be empty to
// build against the current tip. Returns the transaction hash hex on success.
std::string channel_deposit(
const std::string& channel_id_hex,
const std::string& funding_public_key_hex,
const std::string& amount,
const std::string& metadata_hex,
const std::string& optional_tip_hex
);
// Note-based deposit: the caller supplies the exact notes to consume (their
// whole value enters the channel), so amount = sum of the notes' values. Use
// wallet_get_notes to obtain note IDs. The gas fee is funded from
// funding_public_keys (change to change_public_key), capped at max_tx_fee.
// metadata_hex / optional_tip_hex may be empty. Returns the tx hash hex.
std::string channel_deposit_with_notes(
const std::string& channel_id_hex,
const std::vector<std::string>& input_note_id_hexes,
const std::string& metadata_hex,
const std::string& change_public_key_hex,
const std::vector<std::string>& funding_public_key_hexes,
const std::string& max_tx_fee,
const std::string& optional_tip_hex
);
// Blend
std::string blend_join_as_core_node(
const std::string& provider_id_hex,

View File

@ -169,6 +169,58 @@ OperationStatus free_known_addresses(KnownAddresses addrs) {
return 0;
}
// Wallet-notes mock storage (up to 4 notes)
static WalletNote s_mockNotes[4];
FfiWalletNotesResult get_wallet_notes(
LogosBlockchainNode* node,
const uint8_t* wallet_address,
const HeaderId* optional_tip)
{
LOGOS_CMOCK_RECORD("get_wallet_notes");
FfiWalletNotesResult result;
memset(&result.value, 0, sizeof(WalletNotes));
int err = LOGOS_CMOCK_RETURN(int, "get_wallet_notes_error");
result.error = err;
if (err == 0) {
int count = LOGOS_CMOCK_RETURN(int, "get_wallet_notes_count");
if (count > 4) count = 4;
if (count < 0) count = 0;
for (int i = 0; i < count; ++i) {
memset(s_mockNotes[i].id, 0x10 + i, sizeof(NoteId));
s_mockNotes[i].value = static_cast<uint64_t>(100 * (i + 1));
}
memset(result.value.tip, 0xFF, sizeof(HeaderId));
result.value.notes = count > 0 ? s_mockNotes : nullptr;
result.value.len = static_cast<size_t>(count);
}
return result;
}
OperationStatus free_wallet_notes(WalletNotes notes) {
LOGOS_CMOCK_RECORD("free_wallet_notes");
return 0;
}
FfiChannelDepositResult channel_deposit(LogosBlockchainNode* node, const ChannelDepositArguments* arguments) {
LOGOS_CMOCK_RECORD("channel_deposit");
FfiChannelDepositResult result;
memset(result.value, 0xBC, sizeof(Hash));
result.error = LOGOS_CMOCK_RETURN(int, "channel_deposit_error");
return result;
}
FfiChannelDepositResult channel_deposit_with_notes(
LogosBlockchainNode* node,
const ChannelDepositWithNotesArguments* arguments)
{
LOGOS_CMOCK_RECORD("channel_deposit_with_notes");
FfiChannelDepositResult result;
memset(result.value, 0xDE, sizeof(Hash));
result.error = LOGOS_CMOCK_RETURN(int, "channel_deposit_with_notes_error");
return result;
}
BlendHashResult blend_join_as_core_node(
LogosBlockchainNode* node,
const uint8_t* provider_id,

View File

@ -16,6 +16,7 @@ extern "C" {
typedef uint8_t Hash[32];
typedef uint8_t HeaderId[32];
typedef uint8_t TxHash[32];
typedef Hash NoteId;
// Opaque node handle
typedef struct LogosBlockchainNode LogosBlockchainNode;
@ -64,12 +65,49 @@ typedef struct {
uint64_t amount;
} TransferFundsArguments;
// Arguments for channel_deposit (amount-based)
typedef struct {
const HeaderId* optional_tip;
const uint8_t* channel_id;
const uint8_t* funding_public_key;
uint64_t amount;
const uint8_t* metadata;
size_t metadata_len;
} ChannelDepositArguments;
// Arguments for channel_deposit_with_notes (note-based)
typedef struct {
const HeaderId* optional_tip;
const uint8_t* channel_id;
const NoteId* input_note_ids;
size_t input_note_ids_len;
const uint8_t* metadata;
size_t metadata_len;
const uint8_t* change_public_key;
const uint8_t* const* funding_public_keys;
size_t funding_public_keys_len;
uint64_t max_tx_fee;
} ChannelDepositWithNotesArguments;
// Known addresses result container
typedef struct {
uint8_t** addresses;
size_t len;
} KnownAddresses;
// A single spendable wallet note (UTXO): its note ID and value.
typedef struct {
NoteId id;
uint64_t value;
} WalletNote;
// The set of spendable notes for a wallet address at a given tip.
typedef struct {
HeaderId tip;
WalletNote* notes;
size_t len;
} WalletNotes;
// Cryptarchia consensus info
typedef struct {
uint8_t lib[32];
@ -84,7 +122,9 @@ typedef struct { LogosBlockchainNode* value; OperationStatus error; } NodeResult
typedef struct { uint64_t value; OperationStatus error; } BalanceResult;
typedef struct { Hash value; OperationStatus error; } TransferHashResult;
typedef struct { TxHash value; OperationStatus error; } FfiLeaderClaimResult;
typedef struct { Hash value; OperationStatus error; } FfiChannelDepositResult;
typedef struct { KnownAddresses value; OperationStatus error; } KnownAddressesResult;
typedef struct { WalletNotes value; OperationStatus error; } FfiWalletNotesResult;
typedef struct { Hash value; OperationStatus error; } BlendHashResult;
typedef struct { char* value; OperationStatus error; } StringResult;
typedef struct { CryptarchiaInfo* value; OperationStatus error; } CryptarchiaInfoResult;
@ -140,6 +180,17 @@ TransferHashResult transfer_funds(LogosBlockchainNode* node, const TransferFunds
FfiLeaderClaimResult leader_claim(LogosBlockchainNode* node);
KnownAddressesResult get_known_addresses(LogosBlockchainNode* node);
OperationStatus free_known_addresses(KnownAddresses addrs);
FfiWalletNotesResult get_wallet_notes(
LogosBlockchainNode* node,
const uint8_t* wallet_address,
const HeaderId* optional_tip);
OperationStatus free_wallet_notes(WalletNotes notes);
// Channel
FfiChannelDepositResult channel_deposit(LogosBlockchainNode* node, const ChannelDepositArguments* arguments);
FfiChannelDepositResult channel_deposit_with_notes(
LogosBlockchainNode* node,
const ChannelDepositWithNotesArguments* arguments);
// Blend
BlendHashResult blend_join_as_core_node(

View File

@ -155,6 +155,31 @@ LOGOS_TEST(leader_claim_without_node_returns_error) {
LOGOS_ASSERT_TRUE(contains(result, "not running"));
}
LOGOS_TEST(channel_deposit_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
std::string result = module.channel_deposit(VALID_HEX, VALID_HEX, "100", "", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "not running"));
}
LOGOS_TEST(channel_deposit_with_notes_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
std::string result = module.channel_deposit_with_notes(
VALID_HEX, {VALID_HEX}, "", VALID_HEX, {VALID_HEX}, "0", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "not running"));
}
LOGOS_TEST(wallet_get_notes_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
std::string result = module.wallet_get_notes(VALID_HEX, "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "not running"));
}
LOGOS_TEST(wallet_get_known_addresses_without_node_returns_empty) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
@ -534,6 +559,267 @@ LOGOS_TEST(leader_claim_returns_error_on_ffi_failure) {
delete module;
}
LOGOS_TEST(channel_deposit_returns_tx_hash) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("channel_deposit_error").returns(0);
std::string result = module->channel_deposit(VALID_HEX, VALID_HEX, "500", "", "");
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT_EQ(static_cast<int>(result.length()), 64);
LOGOS_ASSERT_TRUE(starts_with(result, "bc"));
LOGOS_ASSERT(t.cFunctionCalled("channel_deposit"));
delete module;
}
LOGOS_TEST(channel_deposit_with_metadata_and_tip) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("channel_deposit_error").returns(0);
std::string result = module->channel_deposit(VALID_HEX, VALID_HEX, "100", "deadbeef", VALID_HEX);
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT_EQ(static_cast<int>(result.length()), 64);
delete module;
}
LOGOS_TEST(channel_deposit_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("channel_deposit_error").returns(1);
std::string result = module->channel_deposit(VALID_HEX, VALID_HEX, "100", "", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "Failed to deposit into channel"));
delete module;
}
LOGOS_TEST(channel_deposit_rejects_invalid_amount) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit(VALID_HEX, VALID_HEX, "not_a_number", "", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "amount"));
delete module;
}
LOGOS_TEST(channel_deposit_rejects_zero_amount) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit(VALID_HEX, VALID_HEX, "0", "", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "amount"));
delete module;
}
LOGOS_TEST(channel_deposit_rejects_invalid_channel_id) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit("bad", VALID_HEX, "100", "", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "channel_id"));
delete module;
}
LOGOS_TEST(channel_deposit_rejects_invalid_funding_key) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit(VALID_HEX, "short", "100", "", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "funding_public_key"));
delete module;
}
LOGOS_TEST(channel_deposit_rejects_invalid_metadata) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit(VALID_HEX, VALID_HEX, "100", "xyz", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "metadata"));
delete module;
}
LOGOS_TEST(channel_deposit_rejects_invalid_optional_tip) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit(VALID_HEX, VALID_HEX, "100", "", "bad_tip");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "tip"));
delete module;
}
LOGOS_TEST(wallet_get_notes_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_wallet_notes_error").returns(0);
t.mockCFunction("get_wallet_notes_count").returns(2);
std::string result = module->wallet_get_notes(VALID_HEX, "");
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "\"tip\""));
LOGOS_ASSERT_TRUE(contains(result, "\"notes\""));
LOGOS_ASSERT_TRUE(contains(result, "\"value\":\"100\""));
LOGOS_ASSERT_TRUE(contains(result, "\"value\":\"200\""));
LOGOS_ASSERT(t.cFunctionCalled("get_wallet_notes"));
LOGOS_ASSERT(t.cFunctionCalled("free_wallet_notes"));
delete module;
}
LOGOS_TEST(wallet_get_notes_returns_empty_notes_array) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_wallet_notes_error").returns(0);
t.mockCFunction("get_wallet_notes_count").returns(0);
std::string result = module->wallet_get_notes(VALID_HEX, "");
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "\"notes\":[]"));
delete module;
}
LOGOS_TEST(wallet_get_notes_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_wallet_notes_error").returns(1);
std::string result = module->wallet_get_notes(VALID_HEX, "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "Failed to get wallet notes"));
delete module;
}
LOGOS_TEST(wallet_get_notes_rejects_invalid_address) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_get_notes("bad", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "wallet address"));
delete module;
}
LOGOS_TEST(channel_deposit_with_notes_returns_tx_hash) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("channel_deposit_with_notes_error").returns(0);
std::string result = module->channel_deposit_with_notes(
VALID_HEX, {VALID_HEX}, "deadbeef", VALID_HEX, {VALID_HEX}, "1000", "");
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT_EQ(static_cast<int>(result.length()), 64);
LOGOS_ASSERT_TRUE(starts_with(result, "de"));
LOGOS_ASSERT(t.cFunctionCalled("channel_deposit_with_notes"));
delete module;
}
LOGOS_TEST(channel_deposit_with_notes_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("channel_deposit_with_notes_error").returns(1);
std::string result = module->channel_deposit_with_notes(
VALID_HEX, {VALID_HEX}, "", VALID_HEX, {VALID_HEX}, "0", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "Failed to deposit into channel"));
delete module;
}
LOGOS_TEST(channel_deposit_with_notes_rejects_empty_notes) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit_with_notes(
VALID_HEX, {}, "", VALID_HEX, {VALID_HEX}, "0", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "input note"));
delete module;
}
LOGOS_TEST(channel_deposit_with_notes_rejects_invalid_note_id) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit_with_notes(
VALID_HEX, {"bad"}, "", VALID_HEX, {VALID_HEX}, "0", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "input note id"));
delete module;
}
LOGOS_TEST(channel_deposit_with_notes_rejects_empty_funding_keys) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit_with_notes(
VALID_HEX, {VALID_HEX}, "", VALID_HEX, {}, "0", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "funding public key"));
delete module;
}
LOGOS_TEST(channel_deposit_with_notes_rejects_invalid_max_tx_fee) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->channel_deposit_with_notes(
VALID_HEX, {VALID_HEX}, "", VALID_HEX, {VALID_HEX}, "not_a_number", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "max_tx_fee"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_single_sender_via_vector) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;