fix: accept base58 for accounts

This commit is contained in:
erhant 2026-06-29 18:32:18 +03:00
parent 78d33cccf7
commit b650152a18
4 changed files with 124 additions and 10 deletions

View File

@ -5,6 +5,7 @@
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
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<uint8_t>* out) {
size_t begin = 0;
size_t end = s.size();
while (begin < end && std::isspace(static_cast<unsigned char>(s[begin])))
++begin;
while (end > begin && std::isspace(static_cast<unsigned char>(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<uint8_t> 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<uint8_t>(carry & 0xFF);
carry >>= 8;
}
while (carry > 0) {
digits.push_back(static_cast<uint8_t>(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<uint8_t> 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<uint8_t>(carry % 58);
carry /= 58;
}
while (carry > 0) {
digits.push_back(static_cast<uint8_t>(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<uint8_t> 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<const uint8_t*>(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);
}

View File

@ -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);

View File

@ -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 {};
}

View File

@ -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