From 38515bd560ffd29ae678025e683bd2dadfdc2008 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 17 Jun 2026 01:43:49 -0300 Subject: [PATCH] feat: add identifiers, withdraw methods and vault claim functions --- flake.lock | 4 +- flake.nix | 2 +- src/logos_execution_zone_wallet_module.cpp | 153 ++++++++++++++- src/logos_execution_zone_wallet_module.h | 10 +- tests/mocks/mock_wallet_ffi.cpp | 56 +++++- tests/mocks/mock_wallet_ffi_capture.h | 17 ++ tests/stubs/wallet_ffi.h | 30 ++- tests/test_execution_zone.cpp | 216 +++++++++++++++++++++ 8 files changed, 464 insertions(+), 24 deletions(-) create mode 100644 tests/mocks/mock_wallet_ffi_capture.h diff --git a/flake.lock b/flake.lock index 768c5a7..376e99b 100644 --- a/flake.lock +++ b/flake.lock @@ -1585,13 +1585,13 @@ "lastModified": 1782310268, "narHash": "sha256-tIIIO+V+w/C/KLtKnLIv8O8/GoJ4PzgJSWrlQCXJ2P4=", "owner": "logos-blockchain", - "repo": "lssa", + "repo": "logos-execution-zone", "rev": "fb8cbac40e0bda4f152415ff4f181cdc6bca6d4a", "type": "github" }, "original": { "owner": "logos-blockchain", - "repo": "lssa", + "repo": "logos-execution-zone", "rev": "fb8cbac40e0bda4f152415ff4f181cdc6bca6d4a", "type": "github" } diff --git a/flake.nix b/flake.nix index a3ed1b8..1b1679b 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,7 @@ inputs = { logos-module-builder.url = "github:logos-co/logos-module-builder"; nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx"; - logos-execution-zone.url = "github:logos-blockchain/lssa?rev=fb8cbac40e0bda4f152415ff4f181cdc6bca6d4a"; # lez-core-v0.2.0 + logos-execution-zone.url = "github:logos-blockchain/logos-execution-zone?rev=fb8cbac40e0bda4f152415ff4f181cdc6bca6d4a"; # lez-core-v0.2.0 }; outputs = inputs@{ logos-module-builder, ... }: diff --git a/src/logos_execution_zone_wallet_module.cpp b/src/logos_execution_zone_wallet_module.cpp index 85fd061..9ca42d6 100644 --- a/src/logos_execution_zone_wallet_module.cpp +++ b/src/logos_execution_zone_wallet_module.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -53,6 +54,7 @@ constexpr auto Nonce = "nonce"; constexpr auto Data = "data"; constexpr auto NullifierPublicKey = "nullifier_public_key"; constexpr auto ViewingPublicKey = "viewing_public_key"; +constexpr auto Identifier = "identifier"; constexpr auto AccountId = "account_id"; constexpr auto IsPublic = "is_public"; constexpr auto Secrets = "secrets"; @@ -176,6 +178,35 @@ std::string ffiPrivateAccountKeysToJson(const FfiPrivateAccountKeys& keys) { return obj.dump(); } +// Nothing in this codebase currently emits an "identifier" field in to_keys_json — NPK/VPK +// identify a key group, not one specific account in it, so get_private_account_keys never +// attaches one. Kept for forward compatibility (e.g. a hand-crafted or future payload that +// targets one specific account within a group) and to make the fallback below explicit. +bool jsonExtractIdentifier(const std::string& json, FfiU128* out_identifier) { + nlohmann::json doc = nlohmann::json::parse(json, nullptr, false); + if (doc.is_discarded() || !doc.is_object()) + return false; + if (!doc.contains(JsonKeys::Identifier) || !doc[JsonKeys::Identifier].is_string()) + return false; + std::vector buffer; + if (!hexToBytes(doc[JsonKeys::Identifier].get(), buffer, 16)) + return false; + memcpy(out_identifier->data, buffer.data(), 16); + return true; +} + +// A foreign recipient's identifier isn't known to the sender; the recipient's wallet +// recovers it from the encrypted transfer payload the next time it runs sync-private. +FfiU128 randomFfiU128() { + static std::mt19937_64 rng(std::random_device{}()); + FfiU128 value{}; + for (int i = 0; i < 16; i += 8) { + uint64_t chunk = rng(); + memcpy(value.data + i, &chunk, sizeof(chunk)); + } + return value; +} + bool jsonToFfiPrivateAccountKeys(const std::string& json, FfiPrivateAccountKeys* output_keys) { nlohmann::json doc = nlohmann::json::parse(json, nullptr, false); if (doc.is_discarded() || !doc.is_object()) @@ -366,6 +397,11 @@ std::string LogosExecutionZoneWalletModule::get_private_account_keys(const std:: fprintf(stderr, "get_private_account_keys: wallet FFI error %d\n", error); return {}; } + + // NPK/VPK identify the key group a private account belongs to, not one specific + // account within it — there's no real identifier to attach here. (wallet_ffi_resolve_private_account + // can't supply one either: for a regular owned account it returns AccountIdentity::PrivateOwned, + // which carries no identifier and defaults to zero — not a real value.) std::string result = ffiPrivateAccountKeysToJson(keys); wallet_ffi_free_private_account_keys(&keys); return result; @@ -591,13 +627,17 @@ std::string LogosExecutionZoneWalletModule::transfer_shielded( return transferResultToJson(nullptr, "transfer_shielded: amount_le16_hex must be 32 hex characters (16 bytes)"); } - // ToDo: Bandaid, I am not sure, how exactly identifiers should be used. - FfiU128 identifier {}; + // to_keys_json never carries an identifier in this codebase (NPK/VPK name a key group, + // not one account in it) — pick a random one, which the recipient's wallet will recover + // from the encrypted transfer payload on its next sync-private. See jsonExtractIdentifier. + FfiU128 toIdentifier{}; + if (!jsonExtractIdentifier(to_keys_json, &toIdentifier)) + toIdentifier = randomFfiU128(); // ToDo: Add keycard support const char *key_path = nullptr; FfiTransferResult result{}; - const WalletFfiError error = wallet_ffi_transfer_shielded(walletHandle, &fromId, &toKeys, &identifier, &amount, key_path, &result); + const WalletFfiError error = wallet_ffi_transfer_shielded(walletHandle, &fromId, &toKeys, &toIdentifier, &amount, key_path, &result); free(const_cast(toKeys.viewing_public_key)); if (error != SUCCESS) { fprintf(stderr, "transfer_shielded: wallet FFI error %d\n", error); @@ -660,11 +700,13 @@ std::string LogosExecutionZoneWalletModule::transfer_private( return transferResultToJson(nullptr, "transfer_private: amount_le16_hex must be 32 hex characters (16 bytes)"); } - // ToDo: Bandaid, I am not sure, how exactly identifiers should be used. - FfiU128 identifier {}; - + // See transfer_shielded above: to_keys_json never carries an identifier, so always pick + // a random one for the recipient's wallet to recover via sync-private. + FfiU128 toIdentifier{}; + if (!jsonExtractIdentifier(to_keys_json, &toIdentifier)) + toIdentifier = randomFfiU128(); FfiTransferResult result{}; - const WalletFfiError error = wallet_ffi_transfer_private(walletHandle, &fromId, &toKeys, &identifier, &amount, &result); + const WalletFfiError error = wallet_ffi_transfer_private(walletHandle, &fromId, &toKeys, &toIdentifier, &amount, &result); free(const_cast(toKeys.viewing_public_key)); if (error != SUCCESS) { fprintf(stderr, "transfer_private: wallet FFI error %d\n", error); @@ -751,6 +793,103 @@ std::string LogosExecutionZoneWalletModule::register_public_account(const std::s return resultJson; } +// === Bridge (L1 Bedrock <-> L2) === + +std::string LogosExecutionZoneWalletModule::bridge_withdraw( + const std::string& from_hex, + const std::string& bedrock_account_pk_hex, + const uint64_t amount +) { + FfiBytes32 fromId{}, bedrockAccountPk{}; + if (!hexToBytes32(from_hex, &fromId) || !hexToBytes32(bedrock_account_pk_hex, &bedrockAccountPk)) { + fprintf(stderr, "bridge_withdraw: invalid account id or bedrock account pk hex\n"); + return transferResultToJson(nullptr, "bridge_withdraw: invalid account id or bedrock account pk hex"); + } + + FfiTransferResult result{}; + const WalletFfiError error = wallet_ffi_bridge_withdraw( + walletHandle, &fromId, amount, &bedrockAccountPk, &result); + if (error != SUCCESS) { + fprintf(stderr, "bridge_withdraw: wallet FFI error %d\n", error); + return transferResultToJson(nullptr, "bridge_withdraw: wallet FFI error " + std::to_string(error)); + } + std::string resultJson = transferResultToJson(&result, std::string()); + wallet_ffi_free_transfer_result(&result); + return resultJson; +} + +// === Vault claiming === + +std::string LogosExecutionZoneWalletModule::get_vault_balance(const std::string& owner_account_id_hex) { + FfiBytes32 ownerId{}; + if (!hexToBytes32(owner_account_id_hex, &ownerId)) { + fprintf(stderr, "get_vault_balance: invalid owner_account_id_hex\n"); + return {}; + } + + uint8_t balance[16] = {0}; + const WalletFfiError error = wallet_ffi_get_vault_balance(walletHandle, &ownerId, &balance); + if (error != SUCCESS) { + fprintf(stderr, "get_vault_balance: wallet FFI error %d\n", error); + return {}; + } + return balanceLe16ToDecimalString(balance); +} + +std::string LogosExecutionZoneWalletModule::vault_claim( + const std::string& owner_account_id_hex, + const std::string& amount_le16_hex +) { + FfiBytes32 ownerId{}; + if (!hexToBytes32(owner_account_id_hex, &ownerId)) { + fprintf(stderr, "vault_claim: invalid owner_account_id_hex\n"); + return transferResultToJson(nullptr, "vault_claim: invalid owner_account_id_hex"); + } + + uint8_t amount[16]; + if (!hexToU128(amount_le16_hex, &amount)) { + fprintf(stderr, "vault_claim: amount_le16_hex must be 32 hex characters (16 bytes)\n"); + return transferResultToJson(nullptr, "vault_claim: amount_le16_hex must be 32 hex characters (16 bytes)"); + } + + FfiTransferResult result{}; + const WalletFfiError error = wallet_ffi_vault_claim(walletHandle, &ownerId, &amount, &result); + if (error != SUCCESS) { + fprintf(stderr, "vault_claim: wallet FFI error %d\n", error); + return transferResultToJson(nullptr, "vault_claim: wallet FFI error " + std::to_string(error)); + } + std::string resultJson = transferResultToJson(&result, std::string()); + wallet_ffi_free_transfer_result(&result); + return resultJson; +} + +std::string LogosExecutionZoneWalletModule::vault_claim_private( + const std::string& owner_account_id_hex, + const std::string& amount_le16_hex +) { + FfiBytes32 ownerId{}; + if (!hexToBytes32(owner_account_id_hex, &ownerId)) { + fprintf(stderr, "vault_claim_private: invalid owner_account_id_hex\n"); + return transferResultToJson(nullptr, "vault_claim_private: invalid owner_account_id_hex"); + } + + uint8_t amount[16]; + if (!hexToU128(amount_le16_hex, &amount)) { + fprintf(stderr, "vault_claim_private: amount_le16_hex must be 32 hex characters (16 bytes)\n"); + return transferResultToJson(nullptr, "vault_claim_private: amount_le16_hex must be 32 hex characters (16 bytes)"); + } + + FfiTransferResult result{}; + const WalletFfiError error = wallet_ffi_vault_claim_private(walletHandle, &ownerId, &amount, &result); + if (error != SUCCESS) { + fprintf(stderr, "vault_claim_private: wallet FFI error %d\n", error); + return transferResultToJson(nullptr, "vault_claim_private: wallet FFI error " + std::to_string(error)); + } + std::string resultJson = transferResultToJson(&result, std::string()); + wallet_ffi_free_transfer_result(&result); + return resultJson; +} + std::string LogosExecutionZoneWalletModule::register_private_account(const std::string& account_id_hex) { FfiBytes32 id{}; if (!hexToBytes32(account_id_hex, &id)) { diff --git a/src/logos_execution_zone_wallet_module.h b/src/logos_execution_zone_wallet_module.h index 7d2feaa..9b2273d 100644 --- a/src/logos_execution_zone_wallet_module.h +++ b/src/logos_execution_zone_wallet_module.h @@ -79,7 +79,7 @@ public: std::string send_generic_public_transaction( const std::vector& account_ids, - const std::vector& signing_requirements, + const std::vector& signing_requirements, const std::vector& instruction, const std::vector& program_elf, const std::vector>& program_dependencies @@ -94,6 +94,14 @@ public: const std::vector& program_elf ); + // === Bridge (L1 Bedrock <-> L2) === + std::string bridge_withdraw(const std::string& from_hex, const std::string& bedrock_account_pk_hex, uint64_t amount); + + // === Vault claiming (L1 deposits credited to an owner's vault account) === + std::string get_vault_balance(const std::string& owner_account_id_hex); + std::string vault_claim(const std::string& owner_account_id_hex, const std::string& amount_le16_hex); + std::string vault_claim_private(const std::string& owner_account_id_hex, const std::string& amount_le16_hex); + // === Configuration === std::string get_sequencer_addr(); diff --git a/tests/mocks/mock_wallet_ffi.cpp b/tests/mocks/mock_wallet_ffi.cpp index 56fd37b..ab66149 100644 --- a/tests/mocks/mock_wallet_ffi.cpp +++ b/tests/mocks/mock_wallet_ffi.cpp @@ -14,9 +14,16 @@ extern "C" { #include } +#include "mock_wallet_ffi_capture.h" + #include #include +namespace MockWalletFfiCapture { +uint8_t lastTransferShieldedIdentifier[16] = {0}; +uint8_t lastTransferPrivateIdentifier[16] = {0}; +} // namespace MockWalletFfiCapture + namespace { // A single non-null sentinel handle handed back by create_new / open. @@ -342,11 +349,14 @@ WalletFfiError wallet_ffi_transfer_public( } WalletFfiError wallet_ffi_transfer_shielded( - WalletHandle*, const FfiBytes32*, const FfiPrivateAccountKeys*, const FfiU128*, - const uint8_t (*)[16], + WalletHandle*, const FfiBytes32*, const FfiPrivateAccountKeys*, const FfiU128* identifier, + const uint8_t (*)[16], const char*, FfiTransferResult* out_result) { LOGOS_CMOCK_RECORD("wallet_ffi_transfer_shielded"); + if (identifier) { + memcpy(MockWalletFfiCapture::lastTransferShieldedIdentifier, identifier->data, 16); + } return fillTransferResult("wallet_ffi_transfer_shielded", out_result); } @@ -357,14 +367,17 @@ WalletFfiError wallet_ffi_transfer_deshielded( } WalletFfiError wallet_ffi_transfer_private( - WalletHandle*, const FfiBytes32*, const FfiPrivateAccountKeys*, const FfiU128*, + WalletHandle*, const FfiBytes32*, const FfiPrivateAccountKeys*, const FfiU128* identifier, const uint8_t (*)[16], FfiTransferResult* out_result) { LOGOS_CMOCK_RECORD("wallet_ffi_transfer_private"); + if (identifier) { + memcpy(MockWalletFfiCapture::lastTransferPrivateIdentifier, identifier->data, 16); + } return fillTransferResult("wallet_ffi_transfer_private", out_result); } WalletFfiError wallet_ffi_transfer_shielded_owned( - WalletHandle*, const FfiBytes32*, const FfiBytes32*, const uint8_t (*)[16], + WalletHandle*, const FfiBytes32*, const FfiBytes32*, const uint8_t (*)[16], const char *, FfiTransferResult* out_result) { LOGOS_CMOCK_RECORD("wallet_ffi_transfer_shielded_owned"); @@ -477,6 +490,41 @@ FfiTransactionResult *out_result) { return fillTransactionResult("wallet_ffi_program_deployment", out_result); } +// === Bridge (L1 Bedrock <-> L2) === + +WalletFfiError wallet_ffi_bridge_withdraw( + WalletHandle*, const FfiBytes32*, uint64_t, const FfiBytes32*, FfiTransferResult* out_result) { + LOGOS_CMOCK_RECORD("wallet_ffi_bridge_withdraw"); + return fillTransferResult("wallet_ffi_bridge_withdraw", out_result); +} + +// === Vault claiming === + +WalletFfiError wallet_ffi_get_vault_balance(WalletHandle*, const FfiBytes32*, uint8_t (*out_balance)[16]) { + LOGOS_CMOCK_RECORD("wallet_ffi_get_vault_balance"); + const int err = LOGOS_CMOCK_RETURN(int, "wallet_ffi_get_vault_balance"); + if (err == 0 && out_balance) { + const uint64_t value = static_cast(LOGOS_CMOCK_RETURN(int, "get_vault_balance_value")); + memset(*out_balance, 0, 16); + for (int i = 0; i < 8; ++i) { + (*out_balance)[i] = static_cast((value >> (i * 8)) & 0xFF); + } + } + return static_cast(err); +} + +WalletFfiError wallet_ffi_vault_claim( + WalletHandle*, const FfiBytes32*, const uint8_t (*)[16], FfiTransferResult* out_result) { + LOGOS_CMOCK_RECORD("wallet_ffi_vault_claim"); + return fillTransferResult("wallet_ffi_vault_claim", out_result); +} + +WalletFfiError wallet_ffi_vault_claim_private( + WalletHandle*, const FfiBytes32*, const uint8_t (*)[16], FfiTransferResult* out_result) { + LOGOS_CMOCK_RECORD("wallet_ffi_vault_claim_private"); + return fillTransferResult("wallet_ffi_vault_claim_private", out_result); +} + // === Configuration === char* wallet_ffi_get_sequencer_addr(WalletHandle*) { diff --git a/tests/mocks/mock_wallet_ffi_capture.h b/tests/mocks/mock_wallet_ffi_capture.h new file mode 100644 index 0000000..46225a7 --- /dev/null +++ b/tests/mocks/mock_wallet_ffi_capture.h @@ -0,0 +1,17 @@ +#ifndef MOCK_WALLET_FFI_CAPTURE_H +#define MOCK_WALLET_FFI_CAPTURE_H + +// LogosCMockStore (logos_clib_mock.h) only lets tests control return values, not +// inspect call arguments. The transfer_shielded/transfer_private identifier logic +// needs the latter, so capture the relevant args here in plain static storage. + +#include + +namespace MockWalletFfiCapture { + +extern uint8_t lastTransferShieldedIdentifier[16]; +extern uint8_t lastTransferPrivateIdentifier[16]; + +} // namespace MockWalletFfiCapture + +#endif // MOCK_WALLET_FFI_CAPTURE_H diff --git a/tests/stubs/wallet_ffi.h b/tests/stubs/wallet_ffi.h index 46ff0d6..c1e5ca0 100644 --- a/tests/stubs/wallet_ffi.h +++ b/tests/stubs/wallet_ffi.h @@ -36,6 +36,11 @@ typedef struct FfiBytes16 { uint8_t data[16]; } FfiBytes16; +// 16-byte little-endian u128 (e.g. a private account identifier). +typedef struct FfiU128 { + uint8_t data[16]; +} FfiU128; + // Full account record. typedef struct FfiAccount { FfiBytes32 program_owner; @@ -90,13 +95,6 @@ typedef enum FfiAccountIdentityKind { PRIVATE_PDA_SHARED = 8, } FfiAccountIdentityKind; -/** - * U128 - 16 bytes little endian. - */ -typedef struct FfiU128 { - uint8_t data[16]; -} FfiU128; - typedef struct FfiInstructionWords { uint32_t *instruction_words; uintptr_t instruction_words_size; @@ -236,7 +234,7 @@ WalletFfiError wallet_ffi_transfer_public( WalletFfiError wallet_ffi_transfer_shielded( WalletHandle* handle, const FfiBytes32* from, const FfiPrivateAccountKeys* to_keys, const FfiU128 *to_identifier, - const uint8_t (*amount)[16], + const uint8_t (*amount)[16], const char *key_path, FfiTransferResult* out_result); WalletFfiError wallet_ffi_transfer_deshielded( @@ -248,7 +246,7 @@ WalletFfiError wallet_ffi_transfer_private( const uint8_t (*amount)[16], FfiTransferResult* out_result); WalletFfiError wallet_ffi_transfer_shielded_owned( WalletHandle* handle, const FfiBytes32* from, const FfiBytes32* to, - const uint8_t (*amount)[16], + const uint8_t (*amount)[16], const char *key_path, FfiTransferResult* out_result); WalletFfiError wallet_ffi_transfer_private_owned( @@ -296,6 +294,20 @@ void wallet_ffi_free_transfer_result(FfiTransferResult* result); void wallet_ffi_free_transaction_result(FfiTransactionResult *result); void wallet_ffi_free_ffi_program(FfiProgram *ffi_program); +// === Bridge (L1 Bedrock <-> L2) === + +WalletFfiError wallet_ffi_bridge_withdraw( + WalletHandle* handle, const FfiBytes32* from, uint64_t amount, + const FfiBytes32* bedrock_account_pk, FfiTransferResult* out_result); + +// === Vault claiming === + +WalletFfiError wallet_ffi_get_vault_balance(WalletHandle* handle, const FfiBytes32* owner, uint8_t (*out_balance)[16]); +WalletFfiError wallet_ffi_vault_claim( + WalletHandle* handle, const FfiBytes32* owner, const uint8_t (*amount)[16], FfiTransferResult* out_result); +WalletFfiError wallet_ffi_vault_claim_private( + WalletHandle* handle, const FfiBytes32* owner, const uint8_t (*amount)[16], FfiTransferResult* out_result); + // === Configuration === char* wallet_ffi_get_sequencer_addr(WalletHandle* handle); diff --git a/tests/test_execution_zone.cpp b/tests/test_execution_zone.cpp index 53968dc..c8e816b 100644 --- a/tests/test_execution_zone.cpp +++ b/tests/test_execution_zone.cpp @@ -3,7 +3,9 @@ #include #include "logos_execution_zone_wallet_module.h" +#include "mocks/mock_wallet_ffi_capture.h" +#include #include #include @@ -296,6 +298,78 @@ LOGOS_TEST(transfer_shielded_success_json) { LOGOS_ASSERT_TRUE(obj["success"].get()); } +// to_keys_json carrying an explicit "identifier" must be forwarded to the FFI call as-is. +LOGOS_TEST(transfer_shielded_with_identifier_forwards_it_unchanged) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const std::string identifierHex(32, '7'); // 16 bytes of 0x77 + const std::string keysJson = std::string("{\"nullifier_public_key\":\"") + std::string(64, 'a') + + "\",\"identifier\":\"" + identifierHex + "\"}"; + + const nlohmann::json obj = parseObject(module.transfer_shielded(VALID_ID, keysJson, VALID_U128)); + LOGOS_ASSERT_TRUE(obj["success"].get()); + + uint8_t expected[16]; + memset(expected, 0x77, sizeof(expected)); + LOGOS_ASSERT(memcmp(MockWalletFfiCapture::lastTransferShieldedIdentifier, expected, sizeof(expected)) == 0); +} + +// to_keys_json never carries an identifier in practice -> a random, non-zero one must be +// generated each call (regression guard against reintroducing the old hardcoded-zero identifier). +LOGOS_TEST(transfer_shielded_without_identifier_uses_random_nonzero_identifier) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const std::string keysJson = std::string("{\"nullifier_public_key\":\"") + std::string(64, 'a') + "\"}"; + + module.transfer_shielded(VALID_ID, keysJson, VALID_U128); + uint8_t first[16]; + memcpy(first, MockWalletFfiCapture::lastTransferShieldedIdentifier, sizeof(first)); + + module.transfer_shielded(VALID_ID, keysJson, VALID_U128); + const uint8_t* second = MockWalletFfiCapture::lastTransferShieldedIdentifier; + + uint8_t zero[16] = {0}; + LOGOS_ASSERT_FALSE(memcmp(first, zero, sizeof(first)) == 0); + LOGOS_ASSERT_FALSE(memcmp(first, second, sizeof(first)) == 0); +} + +// transfer_private mirrors transfer_shielded's identifier handling exactly (see above). +LOGOS_TEST(transfer_private_with_identifier_forwards_it_unchanged) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const std::string identifierHex(32, '7'); // 16 bytes of 0x77 + const std::string keysJson = std::string("{\"nullifier_public_key\":\"") + std::string(64, 'a') + + "\",\"identifier\":\"" + identifierHex + "\"}"; + + const nlohmann::json obj = parseObject(module.transfer_private(VALID_ID, keysJson, VALID_U128)); + LOGOS_ASSERT_TRUE(obj["success"].get()); + + uint8_t expected[16]; + memset(expected, 0x77, sizeof(expected)); + LOGOS_ASSERT(memcmp(MockWalletFfiCapture::lastTransferPrivateIdentifier, expected, sizeof(expected)) == 0); +} + +LOGOS_TEST(transfer_private_without_identifier_uses_random_nonzero_identifier) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const std::string keysJson = std::string("{\"nullifier_public_key\":\"") + std::string(64, 'a') + "\"}"; + + module.transfer_private(VALID_ID, keysJson, VALID_U128); + uint8_t first[16]; + memcpy(first, MockWalletFfiCapture::lastTransferPrivateIdentifier, sizeof(first)); + + module.transfer_private(VALID_ID, keysJson, VALID_U128); + const uint8_t* second = MockWalletFfiCapture::lastTransferPrivateIdentifier; + + uint8_t zero[16] = {0}; + LOGOS_ASSERT_FALSE(memcmp(first, zero, sizeof(first)) == 0); + LOGOS_ASSERT_FALSE(memcmp(first, second, sizeof(first)) == 0); +} + LOGOS_TEST(register_public_account_invalid_hex_error_json) { auto t = LogosTestContext("logos_execution_zone"); LogosExecutionZoneWalletModule module; @@ -314,6 +388,148 @@ LOGOS_TEST(register_private_account_success_json) { LOGOS_ASSERT_TRUE(obj["success"].get()); } +// ============================================================================ +// Bridge (L1 Bedrock <-> L2) +// ============================================================================ + +LOGOS_TEST(bridge_withdraw_success_json) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.bridge_withdraw(VALID_ID, VALID_ID_2, 100)); + LOGOS_ASSERT(t.cFunctionCalled("wallet_ffi_bridge_withdraw")); + LOGOS_ASSERT_TRUE(obj["success"].get()); + LOGOS_ASSERT_EQ(obj["tx_hash"].get(), std::string("0xmocktxhash")); + LOGOS_ASSERT_TRUE(obj["error"].get().empty()); +} + +LOGOS_TEST(bridge_withdraw_invalid_hex_error_json) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.bridge_withdraw("bad", VALID_ID_2, 100)); + LOGOS_ASSERT_FALSE(obj["success"].get()); + LOGOS_ASSERT_FALSE(obj["error"].get().empty()); + LOGOS_ASSERT_FALSE(t.cFunctionCalled("wallet_ffi_bridge_withdraw")); +} + +LOGOS_TEST(bridge_withdraw_ffi_error_json) { + auto t = LogosTestContext("logos_execution_zone"); + t.mockCFunction("wallet_ffi_bridge_withdraw").returns(static_cast(INTERNAL_ERROR)); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.bridge_withdraw(VALID_ID, VALID_ID_2, 100)); + LOGOS_ASSERT_FALSE(obj["success"].get()); + LOGOS_ASSERT_FALSE(obj["error"].get().empty()); +} + +// ============================================================================ +// Vault claiming +// ============================================================================ + +LOGOS_TEST(get_vault_balance_invalid_hex_returns_empty) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + LOGOS_ASSERT_TRUE(module.get_vault_balance("not-hex").empty()); + LOGOS_ASSERT_FALSE(t.cFunctionCalled("wallet_ffi_get_vault_balance")); +} + +LOGOS_TEST(get_vault_balance_returns_decimal_string) { + auto t = LogosTestContext("logos_execution_zone"); + t.mockCFunction("get_vault_balance_value").returns(42); + LogosExecutionZoneWalletModule module; + + const std::string balance = module.get_vault_balance(VALID_ID); + LOGOS_ASSERT(t.cFunctionCalled("wallet_ffi_get_vault_balance")); + LOGOS_ASSERT_EQ(balance, std::string("42")); +} + +LOGOS_TEST(get_vault_balance_ffi_error_returns_empty) { + auto t = LogosTestContext("logos_execution_zone"); + t.mockCFunction("wallet_ffi_get_vault_balance").returns(static_cast(INTERNAL_ERROR)); + LogosExecutionZoneWalletModule module; + + LOGOS_ASSERT_TRUE(module.get_vault_balance(VALID_ID).empty()); +} + +LOGOS_TEST(vault_claim_success_json) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.vault_claim(VALID_ID, VALID_U128)); + LOGOS_ASSERT(t.cFunctionCalled("wallet_ffi_vault_claim")); + LOGOS_ASSERT_TRUE(obj["success"].get()); + LOGOS_ASSERT_EQ(obj["tx_hash"].get(), std::string("0xmocktxhash")); + LOGOS_ASSERT_TRUE(obj["error"].get().empty()); +} + +LOGOS_TEST(vault_claim_invalid_hex_error_json) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.vault_claim("bad", VALID_U128)); + LOGOS_ASSERT_FALSE(obj["success"].get()); + LOGOS_ASSERT_FALSE(obj["error"].get().empty()); + LOGOS_ASSERT_FALSE(t.cFunctionCalled("wallet_ffi_vault_claim")); +} + +LOGOS_TEST(vault_claim_invalid_amount_error_json) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.vault_claim(VALID_ID, "ff")); + LOGOS_ASSERT_FALSE(obj["success"].get()); + LOGOS_ASSERT_CONTAINS(obj["error"].get(), std::string("amount")); +} + +LOGOS_TEST(vault_claim_ffi_error_json) { + auto t = LogosTestContext("logos_execution_zone"); + t.mockCFunction("wallet_ffi_vault_claim").returns(static_cast(INTERNAL_ERROR)); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.vault_claim(VALID_ID, VALID_U128)); + LOGOS_ASSERT_FALSE(obj["success"].get()); + LOGOS_ASSERT_FALSE(obj["error"].get().empty()); +} + +LOGOS_TEST(vault_claim_private_success_json) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.vault_claim_private(VALID_ID, VALID_U128)); + LOGOS_ASSERT(t.cFunctionCalled("wallet_ffi_vault_claim_private")); + LOGOS_ASSERT_TRUE(obj["success"].get()); +} + +LOGOS_TEST(vault_claim_private_invalid_hex_error_json) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.vault_claim_private("bad", VALID_U128)); + LOGOS_ASSERT_FALSE(obj["success"].get()); + LOGOS_ASSERT_FALSE(t.cFunctionCalled("wallet_ffi_vault_claim_private")); +} + +LOGOS_TEST(vault_claim_private_invalid_amount_error_json) { + auto t = LogosTestContext("logos_execution_zone"); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.vault_claim_private(VALID_ID, "ff")); + LOGOS_ASSERT_FALSE(obj["success"].get()); + LOGOS_ASSERT_CONTAINS(obj["error"].get(), std::string("amount")); +} + +LOGOS_TEST(vault_claim_private_ffi_error_json) { + auto t = LogosTestContext("logos_execution_zone"); + t.mockCFunction("wallet_ffi_vault_claim_private").returns(static_cast(INTERNAL_ERROR)); + LogosExecutionZoneWalletModule module; + + const nlohmann::json obj = parseObject(module.vault_claim_private(VALID_ID, VALID_U128)); + LOGOS_ASSERT_FALSE(obj["success"].get()); + LOGOS_ASSERT_FALSE(obj["error"].get().empty()); +} + // ============================================================================ // Pinata claiming // ============================================================================