#include "lez_ffi_marshalling.h" #include #include #include #include #include #include namespace marshalling { namespace { // Single hex nibble -> 0..15, or -1 if not a hex digit. int hexNibble(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } // Plain Bitcoin Base58 alphabet (no checksum, no version byte). Must stay // byte-for-byte identical to the `base58` crate (lee::AccountId) and the // wallet UI's Base58.js so account-id strings round-trip across the stack. constexpr char kBase58Alphabet[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; // Position of `c` in the Base58 alphabet, or -1 if not a Base58 char. int base58Index(char c) { for (int i = 0; i < 58; ++i) { if (kBase58Alphabet[i] == c) return i; } return -1; } // Decode a Base58 string (whitespace-trimmed) into raw bytes. Returns // false on any character outside the alphabet. Big-integer base 58 -> 256, // leading '1's map to leading zero bytes. Mirrors Base58.js::decode. bool base58Decode(const std::string& s, std::vector* out) { size_t begin = 0; size_t end = s.size(); while (begin < end && std::isspace(static_cast(s[begin]))) ++begin; while (end > begin && std::isspace(static_cast(s[end - 1]))) --end; if (begin == end) { return false; } size_t leadingZeros = 0; for (size_t i = begin; i < end && s[i] == '1'; ++i) { ++leadingZeros; } std::vector digits; // base-256, least-significant first for (size_t i = begin; i < end; ++i) { const int idx = base58Index(s[i]); if (idx < 0) { return false; } int carry = idx; for (size_t j = 0; j < digits.size(); ++j) { carry += digits[j] * 58; digits[j] = static_cast(carry & 0xFF); carry >>= 8; } while (carry > 0) { digits.push_back(static_cast(carry & 0xFF)); carry >>= 8; } } out->assign(leadingZeros, 0); for (size_t i = digits.size(); i-- > 0;) { out->push_back(digits[i]); } return true; } } // namespace // Lower-case hex of `length` raw bytes (used for 32-byte hashes/ids/keys and // 64-byte signatures). std::string bytesToHex(const uint8_t* data, const size_t length) { static const char* digits = "0123456789abcdef"; std::string out; out.resize(length * 2); for (size_t i = 0; i < length; ++i) { out[2 * i] = digits[(data[i] >> 4) & 0xF]; out[2 * i + 1] = digits[data[i] & 0xF]; } return out; } // FfiU128 is a 16-byte little-endian integer (balances, nonces). C++ has no // native u128, so build the decimal string via __uint128_t (GCC/Clang, 64-bit). std::string u128LeToDecimal(const uint8_t data[16]) { #if defined(__SIZEOF_INT128__) && __SIZEOF_INT128__ >= 16 __uint128_t v = 0; for (int i = 0; i < 16; ++i) { v |= static_cast<__uint128_t>(data[i]) << (i * 8); } if (v == 0) { return "0"; } char buf[40]; int n = 0; while (v) { buf[n++] = static_cast('0' + static_cast(v % 10)); v /= 10; } std::reverse(buf, buf + n); return std::string(buf, n); #else #error "u128LeToDecimal requires __uint128_t; build with GCC or Clang on 64-bit" #endif } // 64-bit values are emitted as decimal STRINGS, not JSON numbers: JSON // numbers are doubles in many parsers and silently lose precision above 2^53. std::string u64ToString(uint64_t v) { return std::to_string(v); } // Parse a hex string (optionally 0x-prefixed) into a fixed 32-byte FfiBytes32. // Returns false unless it decodes to exactly 32 bytes. bool hexToBytes32(const std::string& hex, FfiBytes32* out) { size_t begin = 0; size_t end = hex.size(); while (begin < end && std::isspace(static_cast(hex[begin]))) ++begin; while (end > begin && std::isspace(static_cast(hex[end - 1]))) --end; if (end - begin >= 2 && hex[begin] == '0' && (hex[begin + 1] == 'x' || hex[begin + 1] == 'X')) { begin += 2; } if (end - begin != 64) { return false; } for (size_t i = 0; i < 32; ++i) { const int hi = hexNibble(hex[begin + 2 * i]); const int lo = hexNibble(hex[begin + 2 * i + 1]); if (hi < 0 || lo < 0) { return false; } out->data[i] = static_cast((hi << 4) | lo); } return true; } // Base58-encode `length` raw bytes (plain Bitcoin alphabet). Big-integer // base 256 -> 58, leading zero bytes map to leading '1's. Mirrors // Base58.js::encode so account ids match the wallet UI and canonical LEZ. std::string bytes32ToBase58(const uint8_t* data, const size_t length) { size_t leadingZeros = 0; while (leadingZeros < length && data[leadingZeros] == 0) { ++leadingZeros; } std::vector digits; // base-58, least-significant first for (size_t i = 0; i < length; ++i) { int carry = data[i]; for (size_t j = 0; j < digits.size(); ++j) { carry += digits[j] * 256; digits[j] = static_cast(carry % 58); carry /= 58; } while (carry > 0) { digits.push_back(static_cast(carry % 58)); carry /= 58; } } std::string out(leadingZeros, '1'); for (size_t i = digits.size(); i-- > 0;) { out += kBase58Alphabet[digits[i]]; } return out; } // Accept an account id as Base58 (canonical) or 64-char hex. Base58 first: // a real 32-byte account is ~44 Base58 chars, so a 64-hex string either hits a // non-Base58 char (e.g. '0') or decodes to the wrong length and falls through. bool accountStrToBytes32(const std::string& account_id, FfiBytes32* out) { std::vector bytes; if (base58Decode(account_id, &bytes) && bytes.size() == 32) { std::memcpy(out->data, bytes.data(), 32); return true; } return hexToBytes32(account_id, out); } nlohmann::json ffiAccountToJson(const FfiAccount& account) { nlohmann::json obj; obj["program_owner"] = bytesToHex(reinterpret_cast(account.program_owner.data), 32); obj["balance"] = u128LeToDecimal(account.balance.data); obj["nonce"] = u128LeToDecimal(account.nonce.data); obj["data_size"] = static_cast(account.data_len); return obj; } nlohmann::json ffiTransactionToJson(const FfiTransaction& tx) { nlohmann::json obj; switch (tx.kind) { case Public: { const FfiPublicTransactionBody* body = tx.body.public_body; if (!body) { break; } obj["type"] = "Public"; obj["hash"] = bytesToHex(body->hash.data, 32); obj["program_id"] = bytesToHex(reinterpret_cast(body->message.program_id.data), 32); nlohmann::json accounts = nlohmann::json::array(); const FfiAccountIdList& ids = body->message.account_ids; const FfiNonceList& nonces = body->message.nonces; for (uintptr_t i = 0; i < ids.len; ++i) { nlohmann::json ref; ref["account_id"] = bytes32ToBase58(ids.entries[i].data, 32); ref["nonce"] = i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : std::string("0"); accounts.push_back(ref); } obj["accounts"] = accounts; nlohmann::json instructionData = nlohmann::json::array(); const FfiInstructionDataList& instr = body->message.instruction_data; for (uintptr_t i = 0; i < instr.len; ++i) { instructionData.push_back(static_cast(instr.entries[i])); } obj["instruction_data"] = instructionData; obj["signature_count"] = static_cast(body->witness_set.len); break; } case Private: { const FfiPrivateTransactionBody* body = tx.body.private_body; if (!body) { break; } obj["type"] = "PrivacyPreserving"; obj["hash"] = bytesToHex(body->hash.data, 32); nlohmann::json accounts = nlohmann::json::array(); const FfiAccountIdList& ids = body->message.public_account_ids; const FfiNonceList& nonces = body->message.nonces; for (uintptr_t i = 0; i < ids.len; ++i) { nlohmann::json ref; ref["account_id"] = bytes32ToBase58(ids.entries[i].data, 32); ref["nonce"] = i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : std::string("0"); accounts.push_back(ref); } obj["accounts"] = accounts; obj["new_commitments_count"] = static_cast(body->message.new_commitments.len); obj["nullifiers_count"] = static_cast(body->message.new_nullifiers.len); obj["encrypted_states_count"] = static_cast(body->message.encrypted_private_post_states.len); obj["validity_window_start"] = u64ToString(body->message.block_validity_window[0]); obj["validity_window_end"] = u64ToString(body->message.block_validity_window[1]); obj["signature_count"] = static_cast(body->witness_set.len); obj["proof_size"] = static_cast(body->proof.len); break; } case ProgramDeploy: { const FfiProgramDeploymentTransactionBody* body = tx.body.program_deployment_body; if (!body) { break; } obj["type"] = "ProgramDeployment"; obj["hash"] = bytesToHex(body->hash.data, 32); obj["bytecode_size"] = static_cast(body->message.len); break; } } return obj; } namespace { std::string bedrockStatusToString(FfiBedrockStatus status) { switch (status) { case Safe: return "Safe"; case Finalized: return "Finalized"; case Pending: default: return "Pending"; } } } // namespace nlohmann::json ffiBlockToJson(const FfiBlock& block) { nlohmann::json obj; obj["block_id"] = u64ToString(block.header.block_id); obj["hash"] = bytesToHex(block.header.hash.data, 32); obj["prev_block_hash"] = bytesToHex(block.header.prev_block_hash.data, 32); obj["timestamp"] = u64ToString(block.header.timestamp); obj["signature"] = bytesToHex(block.header.signature.data, 64); obj["bedrock_status"] = bedrockStatusToString(block.bedrock_status); nlohmann::json transactions = nlohmann::json::array(); for (uintptr_t i = 0; i < block.body.len; ++i) { transactions.push_back(ffiTransactionToJson(block.body.entries[i])); } obj["transactions"] = transactions; return obj; } std::string jsonToCompactString(const nlohmann::json& j) { return j.dump(); } } // namespace marshalling