diff --git a/src/lez_ffi_marshalling.cpp b/src/lez_ffi_marshalling.cpp index 8256600..6f8f3b2 100644 --- a/src/lez_ffi_marshalling.cpp +++ b/src/lez_ffi_marshalling.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace marshalling { @@ -19,6 +20,64 @@ namespace marshalling { 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 @@ -93,6 +152,48 @@ namespace marshalling { 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); @@ -120,7 +221,7 @@ namespace marshalling { const FfiNonceList& nonces = body->message.nonces; for (uintptr_t i = 0; i < ids.len; ++i) { nlohmann::json ref; - ref["account_id"] = bytesToHex(ids.entries[i].data, 32); + 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); } @@ -148,7 +249,7 @@ namespace marshalling { const FfiNonceList& nonces = body->message.nonces; for (uintptr_t i = 0; i < ids.len; ++i) { nlohmann::json ref; - ref["account_id"] = bytesToHex(ids.entries[i].data, 32); + 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); } diff --git a/src/lez_ffi_marshalling.h b/src/lez_ffi_marshalling.h index 0a52f01..0eae49b 100644 --- a/src/lez_ffi_marshalling.h +++ b/src/lez_ffi_marshalling.h @@ -40,6 +40,19 @@ namespace marshalling { // Returns false unless it decodes to exactly 32 bytes. bool hexToBytes32(const std::string& hex, FfiBytes32* out); + // Base58 (plain Bitcoin alphabet, no checksum) of `length` raw bytes. This is + // the canonical string form of an LEZ *account id* — it matches lee::AccountId + // Display, wallet_ffi's account_id_to_base58, and the wallet UI's Base58.js. + // Hashes/tx ids/program ids stay hex; only account ids are Base58. + std::string bytes32ToBase58(const uint8_t* data, size_t length); + + // Parse an account id string into a fixed 32-byte buffer, accepting EITHER + // Base58 (canonical) or 64-char hex (optionally 0x-prefixed). Base58 is tried + // first; since a 32-byte value's Base58 is ~44 chars, a 64-hex string fails the + // 32-byte check and falls through to hex. Returns false unless it resolves to + // exactly 32 bytes. + bool accountStrToBytes32(const std::string& account_id, FfiBytes32* out); + nlohmann::json ffiAccountToJson(const FfiAccount& account); nlohmann::json ffiTransactionToJson(const FfiTransaction& tx); nlohmann::json ffiBlockToJson(const FfiBlock& block); diff --git a/src/lez_indexer_module_impl.cpp b/src/lez_indexer_module_impl.cpp index 2b312ba..f600b92 100644 --- a/src/lez_indexer_module_impl.cpp +++ b/src/lez_indexer_module_impl.cpp @@ -79,8 +79,8 @@ std::string LezIndexerModuleImpl::getAccount(const std::string& account_id) { } FfiAccountId id; - if (!hexToBytes32(account_id, &id)) { - warn("getAccount", "invalid account id (need 32-byte hex)"); + if (!accountStrToBytes32(account_id, &id)) { + warn("getAccount", "invalid account id (need Base58 or 32-byte hex)"); return {}; } @@ -267,8 +267,8 @@ std::string LezIndexerModuleImpl::getTransactionsByAccount( } FfiAccountId id; - if (!hexToBytes32(account_id, &id)) { - warn("getTransactionsByAccount", "invalid account id (need 32-byte hex)"); + if (!accountStrToBytes32(account_id, &id)) { + warn("getTransactionsByAccount", "invalid account id (need Base58 or 32-byte hex)"); return {}; } diff --git a/src/lez_indexer_module_impl.h b/src/lez_indexer_module_impl.h index 1a2df06..55d2340 100644 --- a/src/lez_indexer_module_impl.h +++ b/src/lez_indexer_module_impl.h @@ -41,8 +41,8 @@ public: /// own). Returns 0 on success, else the FFI OperationStatus code. int64_t stop_indexer(); - /// Account by 32-byte hex id. The returned JSON omits the id; callers inject - /// the queried id themselves. + /// Account by id, accepting Base58 (canonical) or 32-byte hex. The returned + /// JSON omits the id; callers inject the queried id themselves. std::string getAccount(const std::string& account_id); /// Block by decimal block id. std::string getBlockById(const std::string& block_id); @@ -59,8 +59,8 @@ public: /// "catching up" from "failed": { state (starting/syncing/caught_up/error), /// indexedBlockId, lastError }. Empty string if the indexer isn't running. std::string getStatus(); - /// Transactions touching `account_id` (32-byte hex), paginated by decimal - /// `offset`/`limit`. + /// Transactions touching `account_id` (Base58 or 32-byte hex), paginated by + /// decimal `offset`/`limit`. // // MUST stay on a single line: the universal `--header-to-lidl` parser drops // methods whose parameter list spans multiple lines, so a wrapped signature