mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-26 06:31:19 +00:00
feat(token): add Logos token API module
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
#include "token_instruction_words.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace token_module::detail {
|
||||
|
||||
std::vector<std::uint8_t> jsonInstructionLeBytes(const nlohmann::json& input) {
|
||||
std::vector<std::uint8_t> result;
|
||||
if (!input.is_array()) return result;
|
||||
result.reserve(input.size() * sizeof(std::uint32_t));
|
||||
for (const auto& item : input) {
|
||||
if (!item.is_number_unsigned() && !item.is_number_integer()) return {};
|
||||
std::uint64_t raw = 0;
|
||||
if (item.is_number_unsigned()) {
|
||||
raw = item.get<std::uint64_t>();
|
||||
} else {
|
||||
const std::int64_t signed_raw = item.get<std::int64_t>();
|
||||
if (signed_raw < 0) return {};
|
||||
raw = static_cast<std::uint64_t>(signed_raw);
|
||||
}
|
||||
if (raw > std::numeric_limits<std::uint32_t>::max()) return {};
|
||||
const auto word = static_cast<std::uint32_t>(raw);
|
||||
result.push_back(static_cast<std::uint8_t>(word & 0xff));
|
||||
result.push_back(static_cast<std::uint8_t>((word >> 8) & 0xff));
|
||||
result.push_back(static_cast<std::uint8_t>((word >> 16) & 0xff));
|
||||
result.push_back(static_cast<std::uint8_t>((word >> 24) & 0xff));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace token_module::detail
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
namespace token_module::detail {
|
||||
|
||||
// Decodes a plan's `instruction` word array (u32 values) into the little-endian
|
||||
// byte string the wallet module's send_generic_public_transaction expects (its
|
||||
// `instruction` param is a byte-string IPC type). Returns {} on any non-array
|
||||
// input or a word that is negative, fractional, or exceeds u32.
|
||||
std::vector<std::uint8_t> jsonInstructionLeBytes(const nlohmann::json& input);
|
||||
|
||||
} // namespace token_module::detail
|
||||
@@ -0,0 +1,808 @@
|
||||
#include "token_module_impl.h"
|
||||
#include "token_instruction_words.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// Generated by logos-module-builder. Kept out of the universal API header.
|
||||
#include "logos_sdk.h"
|
||||
|
||||
extern "C" {
|
||||
#include "token_ffi.h"
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
constexpr char TOKEN_PROGRAM_BIN_ENV[] = "TOKEN_PROGRAM_BIN";
|
||||
constexpr char TOKEN_PROGRAM_ID_ENV[] = "TOKEN_PROGRAM_ID";
|
||||
std::mutex programInfoMutex;
|
||||
|
||||
bool tokenDebug() {
|
||||
static const bool enabled = std::getenv("TOKEN_DEBUG") != nullptr;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
#define TOKEN_TRACE(message) \
|
||||
do { \
|
||||
if (tokenDebug()) std::cerr << "[token-debug] " << message << '\n'; \
|
||||
} while (false)
|
||||
|
||||
int hexValue(char value) {
|
||||
if (value >= '0' && value <= '9') return value - '0';
|
||||
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
|
||||
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool isHexLength(const std::string& value, std::size_t length) {
|
||||
if (value.size() != length) return false;
|
||||
return std::all_of(value.begin(), value.end(), [](char c) { return hexValue(c) >= 0; });
|
||||
}
|
||||
|
||||
bool isEvenHex(const std::string& value) {
|
||||
return value.size() % 2 == 0
|
||||
&& std::all_of(value.begin(), value.end(), [](char c) { return hexValue(c) >= 0; });
|
||||
}
|
||||
|
||||
std::string lowercase(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string trim(const std::string& value) {
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = value.size();
|
||||
while (begin < end && std::isspace(static_cast<unsigned char>(value[begin]))) ++begin;
|
||||
while (end > begin && std::isspace(static_cast<unsigned char>(value[end - 1]))) --end;
|
||||
return value.substr(begin, end - begin);
|
||||
}
|
||||
|
||||
bool isZeroId(const std::string& value) {
|
||||
return value.size() == 64
|
||||
&& std::all_of(value.begin(), value.end(), [](char c) { return c == '0'; });
|
||||
}
|
||||
|
||||
bool isAllZero(const std::string& value) {
|
||||
return !value.empty()
|
||||
&& std::all_of(value.begin(), value.end(), [](char c) { return c == '0'; });
|
||||
}
|
||||
|
||||
bool isDefaultPublicAccount(const std::string& owner,
|
||||
const std::string& balance,
|
||||
const std::string& nonce,
|
||||
const std::string& data) {
|
||||
// The execution-zone read API represents an absent public account with
|
||||
// Account::default(): zero owner, balance, and nonce, with no data.
|
||||
return isZeroId(owner) && isAllZero(balance) && isAllZero(nonce) && data.empty();
|
||||
}
|
||||
|
||||
std::string jsonString(const json& object, const char* key) {
|
||||
const auto it = object.find(key);
|
||||
return it != object.end() && it->is_string() ? it->get<std::string>() : std::string();
|
||||
}
|
||||
|
||||
std::string bytesToHex(const std::uint8_t* bytes, std::size_t length) {
|
||||
static constexpr char digits[] = "0123456789abcdef";
|
||||
std::string result;
|
||||
result.reserve(length * 2);
|
||||
for (std::size_t i = 0; i < length; ++i) {
|
||||
result.push_back(digits[bytes[i] >> 4]);
|
||||
result.push_back(digits[bytes[i] & 0x0f]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
LogosMap publicOk() {
|
||||
return {{"status", "ok"}, {"error", ""}};
|
||||
}
|
||||
|
||||
LogosMap publicError(const std::string& error) {
|
||||
return {{"status", "error"}, {"error", error}};
|
||||
}
|
||||
|
||||
template <typename Operation>
|
||||
LogosMap guarded(Operation&& operation) noexcept {
|
||||
try {
|
||||
return operation();
|
||||
} catch (const std::exception& error) {
|
||||
TOKEN_TRACE("caught exception: " << error.what());
|
||||
} catch (...) {
|
||||
TOKEN_TRACE("caught non-standard exception");
|
||||
}
|
||||
return publicError("backend_error");
|
||||
}
|
||||
|
||||
struct FfiResult {
|
||||
bool ok = false;
|
||||
json value;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
FfiResult callToken(char* (*operation)(const char*), const json& request) {
|
||||
const std::string payload = request.dump();
|
||||
std::unique_ptr<char, decltype(&token_free)> response(
|
||||
operation(payload.c_str()),
|
||||
&token_free);
|
||||
if (!response) {
|
||||
TOKEN_TRACE("token_ffi returned null");
|
||||
return {};
|
||||
}
|
||||
|
||||
const json document = json::parse(response.get(), nullptr, false);
|
||||
if (!document.is_object()) {
|
||||
TOKEN_TRACE("token_ffi returned malformed JSON");
|
||||
return {};
|
||||
}
|
||||
const auto ok = document.find("ok");
|
||||
if (ok == document.end() || !ok->is_boolean()) return {};
|
||||
if (!ok->get<bool>()) {
|
||||
const std::string error = jsonString(document, "error");
|
||||
TOKEN_TRACE("token_ffi failure: " << error);
|
||||
return {false, json(), error};
|
||||
}
|
||||
const auto value = document.find("value");
|
||||
if (value == document.end() || !value->is_object()) return {};
|
||||
return {true, *value, {}};
|
||||
}
|
||||
|
||||
bool isStableError(const std::string& error) {
|
||||
static const std::set<std::string> stable = {
|
||||
"bad_request",
|
||||
"bad_amount",
|
||||
"invalid_account_id",
|
||||
"invalid_authority",
|
||||
"invalid_metadata_standard",
|
||||
"account_read_failed",
|
||||
"token_program_mismatch",
|
||||
"invalid_definition_data",
|
||||
"invalid_holding_data",
|
||||
"invalid_metadata_data",
|
||||
"backend_error",
|
||||
};
|
||||
return stable.find(error) != stable.end();
|
||||
}
|
||||
|
||||
std::string ffiError(const FfiResult& result, const std::string& fallback = "backend_error") {
|
||||
return isStableError(result.error) ? result.error : fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint8_t> TokenModuleImpl::loadTokenBinary() const {
|
||||
const char* path = std::getenv(TOKEN_PROGRAM_BIN_ENV);
|
||||
if (path == nullptr || *path == '\0') return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) return {};
|
||||
return std::vector<std::uint8_t>(std::istreambuf_iterator<char>(file),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
std::string TokenModuleImpl::loadTokenProgramId() const {
|
||||
const char* value = std::getenv(TOKEN_PROGRAM_ID_ENV);
|
||||
return value == nullptr ? std::string() : trim(value);
|
||||
}
|
||||
|
||||
nlohmann::json TokenModuleImpl::tokenProgramInfo() {
|
||||
const std::lock_guard<std::mutex> lock(programInfoMutex);
|
||||
if (programInfoResolved_) {
|
||||
return {{"programId", programId_}, {"programIdHex", programIdHex_}};
|
||||
}
|
||||
|
||||
const std::string configured_program = loadTokenProgramId();
|
||||
const bool program_id_configured = !configured_program.empty();
|
||||
const char* binary_path = std::getenv(TOKEN_PROGRAM_BIN_ENV);
|
||||
const bool binary_configured = binary_path != nullptr && *binary_path != '\0';
|
||||
const std::string configured_program_hex = program_id_configured
|
||||
? normalizeAccountId(configured_program)
|
||||
: std::string();
|
||||
if (program_id_configured && configured_program_hex.empty()) {
|
||||
TOKEN_TRACE("TOKEN_PROGRAM_ID is not a valid account ID");
|
||||
return json();
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t> binary = loadTokenBinary();
|
||||
if (binary_configured && binary.empty()) {
|
||||
TOKEN_TRACE("TOKEN_PROGRAM_BIN is configured but unreadable");
|
||||
return json();
|
||||
}
|
||||
|
||||
std::string derived_program;
|
||||
std::string derived_program_hex;
|
||||
if (!binary.empty()) {
|
||||
const FfiResult result = callToken(
|
||||
token_program_id,
|
||||
{{"elf", bytesToHex(binary.data(), binary.size())}});
|
||||
if (!result.ok) return json();
|
||||
|
||||
derived_program_hex = lowercase(jsonString(result.value, "programId"));
|
||||
derived_program = jsonString(result.value, "programIdBase58");
|
||||
if (derived_program.empty() || !isHexLength(derived_program_hex, 64)
|
||||
|| isZeroId(derived_program_hex)) {
|
||||
return json();
|
||||
}
|
||||
}
|
||||
|
||||
if (!program_id_configured && !binary_configured) return json();
|
||||
if (program_id_configured && binary_configured
|
||||
&& configured_program_hex != derived_program_hex) {
|
||||
TOKEN_TRACE("TOKEN_PROGRAM_ID does not match TOKEN_PROGRAM_BIN");
|
||||
return json();
|
||||
}
|
||||
|
||||
const std::string program_id_hex = program_id_configured
|
||||
? configured_program_hex
|
||||
: derived_program_hex;
|
||||
const std::string program_id = binary_configured
|
||||
? derived_program
|
||||
: modules().logos_execution_zone.account_id_to_base58(program_id_hex);
|
||||
if (program_id.empty() || !isHexLength(program_id_hex, 64) || isZeroId(program_id_hex)) {
|
||||
return json();
|
||||
}
|
||||
|
||||
// Cache successful resolution only: an early missing/unreadable
|
||||
// configuration may become available later during process startup.
|
||||
programId_ = program_id;
|
||||
programIdHex_ = program_id_hex;
|
||||
programInfoResolved_ = true;
|
||||
return {{"programId", programId_}, {"programIdHex", programIdHex_}};
|
||||
}
|
||||
|
||||
std::string TokenModuleImpl::normalizeAccountId(const std::string& id) {
|
||||
std::string normalized = trim(id);
|
||||
if (isHexLength(normalized, 64)) {
|
||||
normalized = lowercase(std::move(normalized));
|
||||
return isZeroId(normalized) ? std::string() : normalized;
|
||||
}
|
||||
if (normalized.empty()) return {};
|
||||
|
||||
normalized = lowercase(
|
||||
modules().logos_execution_zone.account_id_from_base58(normalized));
|
||||
return isHexLength(normalized, 64) && !isZeroId(normalized)
|
||||
? normalized
|
||||
: std::string();
|
||||
}
|
||||
|
||||
nlohmann::json TokenModuleImpl::readPublicAccount(const std::string& account_id) {
|
||||
json read = {{"id", account_id}, {"status", "not_found"}};
|
||||
logos::CallError call_error;
|
||||
const std::string raw =
|
||||
modules().logos_execution_zone.get_account_public(account_id, &call_error);
|
||||
if (!call_error.ok()) {
|
||||
TOKEN_TRACE("logos_execution_zone account read transport failure: " << call_error.code);
|
||||
read["status"] = "backend_error";
|
||||
return read;
|
||||
}
|
||||
if (raw.empty()) return read;
|
||||
|
||||
const json account = json::parse(raw, nullptr, false);
|
||||
if (!account.is_object()) {
|
||||
read["status"] = "backend_error";
|
||||
return read;
|
||||
}
|
||||
|
||||
std::string owner = lowercase(jsonString(account, "program_owner"));
|
||||
std::string balance = lowercase(jsonString(account, "balance"));
|
||||
std::string nonce = lowercase(jsonString(account, "nonce"));
|
||||
std::string data = lowercase(jsonString(account, "data"));
|
||||
if (!isHexLength(owner, 64) || !isHexLength(balance, 32)
|
||||
|| !isHexLength(nonce, 32) || !isEvenHex(data)) {
|
||||
read["status"] = "backend_error";
|
||||
return read;
|
||||
}
|
||||
|
||||
if (isDefaultPublicAccount(owner, balance, nonce, data)) return read;
|
||||
|
||||
read["status"] = "ok";
|
||||
read["account"] = {
|
||||
{"program_owner", std::move(owner)},
|
||||
{"balance", std::move(balance)},
|
||||
{"nonce", std::move(nonce)},
|
||||
{"data", std::move(data)},
|
||||
};
|
||||
return read;
|
||||
}
|
||||
|
||||
nlohmann::json TokenModuleImpl::walletAccountIds() {
|
||||
logos::CallError call_error;
|
||||
const json accounts = modules().logos_execution_zone.list_accounts(&call_error);
|
||||
if (!call_error.ok()) {
|
||||
TOKEN_TRACE("logos_execution_zone account list transport failure: " << call_error.code);
|
||||
return json();
|
||||
}
|
||||
if (!accounts.is_array()) return json();
|
||||
|
||||
std::set<std::string> unique;
|
||||
for (const auto& account : accounts) {
|
||||
if (!account.is_object()) continue;
|
||||
const auto is_public = account.find("is_public");
|
||||
if (is_public == account.end() || !is_public->is_boolean() || !is_public->get<bool>()) {
|
||||
continue;
|
||||
}
|
||||
const std::string id = normalizeAccountId(jsonString(account, "account_id"));
|
||||
if (!id.empty()) unique.insert(id);
|
||||
}
|
||||
return json(unique);
|
||||
}
|
||||
|
||||
bool TokenModuleImpl::isFreshOwnedAccount(const std::string& account_id,
|
||||
bool& fresh,
|
||||
std::string& error) {
|
||||
const json read = readPublicAccount(account_id);
|
||||
if (jsonString(read, "status") == "ok") {
|
||||
const json info = tokenProgramInfo();
|
||||
if (!info.is_object()) {
|
||||
error = "config_missing";
|
||||
return false;
|
||||
}
|
||||
const FfiResult decoded = callToken(token_decode_holding, {
|
||||
{"tokenProgramId", info["programIdHex"]},
|
||||
{"holding", read},
|
||||
});
|
||||
if (!decoded.ok) {
|
||||
error = ffiError(decoded);
|
||||
return false;
|
||||
}
|
||||
fresh = false;
|
||||
return true;
|
||||
}
|
||||
if (jsonString(read, "status") != "not_found") {
|
||||
error = "backend_error";
|
||||
return false;
|
||||
}
|
||||
|
||||
const json wallet_ids = walletAccountIds();
|
||||
if (!wallet_ids.is_array()) {
|
||||
error = "backend_error";
|
||||
return false;
|
||||
}
|
||||
if (std::find(wallet_ids.begin(), wallet_ids.end(), json(account_id)) == wallet_ids.end()) {
|
||||
error = "account_read_failed";
|
||||
return false;
|
||||
}
|
||||
fresh = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TokenModuleImpl::requireFreshOwnedAccount(const std::string& account_id,
|
||||
std::string& error) {
|
||||
bool fresh = false;
|
||||
if (!isFreshOwnedAccount(account_id, fresh, error)) return false;
|
||||
if (!fresh) {
|
||||
error = "account_read_failed";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::programInfo() {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const json info = tokenProgramInfo();
|
||||
if (!info.is_object()) return publicError("config_missing");
|
||||
LogosMap result = publicOk();
|
||||
result["programId"] = info["programId"];
|
||||
result["programIdHex"] = info["programIdHex"];
|
||||
result["networkFingerprint"] = info["programIdHex"];
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::inspectAccount(const std::string& account_id,
|
||||
TokenOperation operation,
|
||||
const char* payload_key) {
|
||||
const json info = tokenProgramInfo();
|
||||
if (!info.is_object()) return publicError("config_missing");
|
||||
|
||||
const std::string normalized = normalizeAccountId(account_id);
|
||||
if (normalized.empty()) return publicError("invalid_account_id");
|
||||
const json read = readPublicAccount(normalized);
|
||||
const std::string read_status = jsonString(read, "status");
|
||||
if (read_status == "backend_error") return publicError("backend_error");
|
||||
if (read_status != "ok") return publicError("account_read_failed");
|
||||
|
||||
json request = {{"tokenProgramId", info["programIdHex"]}};
|
||||
request[payload_key] = read;
|
||||
const FfiResult decoded = callToken(operation, request);
|
||||
if (!decoded.ok) return publicError(ffiError(decoded));
|
||||
|
||||
LogosMap result = publicOk();
|
||||
result[payload_key] = decoded.value;
|
||||
return result;
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::inspectDefinition(const std::string& definition_id) {
|
||||
return guarded([&]() {
|
||||
return inspectAccount(definition_id, token_decode_definition, "definition");
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::inspectHolding(const std::string& holding_id) {
|
||||
return guarded([&]() {
|
||||
return inspectAccount(holding_id, token_decode_holding, "holding");
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::inspectMetadata(const std::string& metadata_id) {
|
||||
return guarded([&]() {
|
||||
return inspectAccount(metadata_id, token_decode_metadata, "metadata");
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::walletTokenAccounts() {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const json info = tokenProgramInfo();
|
||||
if (!info.is_object()) return publicError("config_missing");
|
||||
const std::string program_id = jsonString(info, "programIdHex");
|
||||
|
||||
const json ids = walletAccountIds();
|
||||
if (!ids.is_array()) return publicError("backend_error");
|
||||
|
||||
json accounts = json::array();
|
||||
for (const auto& id_value : ids) {
|
||||
if (!id_value.is_string()) continue;
|
||||
const std::string id = id_value.get<std::string>();
|
||||
const json read = readPublicAccount(id);
|
||||
if (jsonString(read, "status") != "ok") continue;
|
||||
const auto account = read.find("account");
|
||||
if (account == read.end() || !account->is_object()
|
||||
|| lowercase(jsonString(*account, "program_owner")) != program_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const FfiResult decoded = callToken(token_decode_account, {
|
||||
{"tokenProgramId", program_id},
|
||||
{"account", read},
|
||||
});
|
||||
if (!decoded.ok || !isHexLength(jsonString(decoded.value, "accountIdHex"), 64)) {
|
||||
continue;
|
||||
}
|
||||
accounts.push_back(decoded.value);
|
||||
}
|
||||
|
||||
std::sort(accounts.begin(), accounts.end(), [](const json& left, const json& right) {
|
||||
return lowercase(jsonString(left, "accountIdHex"))
|
||||
< lowercase(jsonString(right, "accountIdHex"));
|
||||
});
|
||||
LogosMap result = publicOk();
|
||||
result["accounts"] = std::move(accounts);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::submitPlan(const nlohmann::json& plan) {
|
||||
const auto account_ids_it = plan.find("accountIds");
|
||||
const auto signers_it = plan.find("signingRequirements");
|
||||
const auto instruction_it = plan.find("instruction");
|
||||
const std::string program_id = lowercase(jsonString(plan, "programId"));
|
||||
if (account_ids_it == plan.end() || signers_it == plan.end()
|
||||
|| instruction_it == plan.end() || !isHexLength(program_id, 64)) {
|
||||
return publicError("backend_error");
|
||||
}
|
||||
|
||||
const std::vector<std::string> account_ids =
|
||||
account_ids_it->get<std::vector<std::string>>();
|
||||
const std::vector<bool> signers = signers_it->get<std::vector<bool>>();
|
||||
const std::vector<std::uint8_t> instruction =
|
||||
token_module::detail::jsonInstructionLeBytes(*instruction_it);
|
||||
if (account_ids.empty() || account_ids.size() != signers.size()
|
||||
|| instruction.empty()
|
||||
|| std::any_of(account_ids.begin(), account_ids.end(), [](const std::string& id) {
|
||||
return !isHexLength(id, 64) || isZeroId(id);
|
||||
})) {
|
||||
return publicError("backend_error");
|
||||
}
|
||||
|
||||
logos::CallError call_error;
|
||||
const std::string raw = modules().logos_execution_zone.send_generic_public_transaction(
|
||||
account_ids,
|
||||
signers,
|
||||
instruction,
|
||||
program_id,
|
||||
&call_error);
|
||||
if (!call_error.ok()) {
|
||||
TOKEN_TRACE("logos_execution_zone submission transport failure: " << call_error.code);
|
||||
return publicError("wallet_submission_failed");
|
||||
}
|
||||
const json reply = json::parse(raw, nullptr, false);
|
||||
if (!reply.is_object()) return publicError("wallet_submission_failed");
|
||||
const auto success = reply.find("success");
|
||||
const std::string transaction_id = jsonString(reply, "tx_hash");
|
||||
if (success == reply.end() || !success->is_boolean() || !success->get<bool>()
|
||||
|| transaction_id.empty()) {
|
||||
return publicError("wallet_submission_failed");
|
||||
}
|
||||
|
||||
LogosMap result = publicOk();
|
||||
result["transactionId"] = transaction_id;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool TokenModuleImpl::normalizeAuthority(const std::string& authority,
|
||||
std::string& normalized) {
|
||||
const std::string sentinel = lowercase(trim(authority));
|
||||
if (sentinel == "none" || sentinel == "self") {
|
||||
normalized = sentinel;
|
||||
return true;
|
||||
}
|
||||
normalized = normalizeAccountId(authority);
|
||||
return !normalized.empty();
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::planAndSubmit(TokenOperation planner, nlohmann::json request) {
|
||||
const json info = tokenProgramInfo();
|
||||
if (!info.is_object()) return publicError("config_missing");
|
||||
const std::string configured_program = jsonString(info, "programIdHex");
|
||||
request["tokenProgramId"] = configured_program;
|
||||
|
||||
const FfiResult result = callToken(planner, request);
|
||||
if (!result.ok) return publicError(ffiError(result));
|
||||
if (lowercase(jsonString(result.value, "programId")) != configured_program) {
|
||||
return publicError("backend_error");
|
||||
}
|
||||
return submitPlan(result.value);
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::createFungible(const std::string& definition_target_id,
|
||||
const std::string& holding_target_id,
|
||||
const std::string& name,
|
||||
const nlohmann::json& total_supply_raw,
|
||||
const std::string& mint_authority) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_target_id);
|
||||
const std::string holding = normalizeAccountId(holding_target_id);
|
||||
if (definition.empty() || holding.empty()) return publicError("invalid_account_id");
|
||||
std::string error;
|
||||
if (!requireFreshOwnedAccount(definition, error)
|
||||
|| !requireFreshOwnedAccount(holding, error)) {
|
||||
return publicError(error);
|
||||
}
|
||||
std::string authority;
|
||||
if (!normalizeAuthority(mint_authority, authority)) {
|
||||
return publicError("invalid_authority");
|
||||
}
|
||||
return planAndSubmit(token_create_fungible_plan, {
|
||||
{"definitionTargetId", definition},
|
||||
{"holdingTargetId", holding},
|
||||
{"name", name},
|
||||
{"totalSupplyRaw", total_supply_raw},
|
||||
{"mintAuthority", authority},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::createFungibleWithMetadata(
|
||||
const std::string& definition_target_id,
|
||||
const std::string& holding_target_id,
|
||||
const std::string& metadata_target_id,
|
||||
const std::string& name,
|
||||
const nlohmann::json& total_supply_raw,
|
||||
const std::string& mint_authority,
|
||||
const std::string& metadata_standard,
|
||||
const std::string& uri,
|
||||
const std::string& creators) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_target_id);
|
||||
const std::string holding = normalizeAccountId(holding_target_id);
|
||||
const std::string metadata = normalizeAccountId(metadata_target_id);
|
||||
if (definition.empty() || holding.empty() || metadata.empty()) {
|
||||
return publicError("invalid_account_id");
|
||||
}
|
||||
std::string error;
|
||||
if (!requireFreshOwnedAccount(definition, error)
|
||||
|| !requireFreshOwnedAccount(holding, error)
|
||||
|| !requireFreshOwnedAccount(metadata, error)) {
|
||||
return publicError(error);
|
||||
}
|
||||
std::string authority;
|
||||
if (!normalizeAuthority(mint_authority, authority)) {
|
||||
return publicError("invalid_authority");
|
||||
}
|
||||
return planAndSubmit(token_create_fungible_with_metadata_plan, {
|
||||
{"definitionTargetId", definition},
|
||||
{"holdingTargetId", holding},
|
||||
{"metadataTargetId", metadata},
|
||||
{"name", name},
|
||||
{"totalSupplyRaw", total_supply_raw},
|
||||
{"mintAuthority", authority},
|
||||
{"metadataStandard", metadata_standard},
|
||||
{"uri", uri},
|
||||
{"creators", creators},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::createNonFungible(
|
||||
const std::string& definition_target_id,
|
||||
const std::string& master_holding_target_id,
|
||||
const std::string& metadata_target_id,
|
||||
const std::string& name,
|
||||
const nlohmann::json& printable_supply_raw,
|
||||
const std::string& metadata_standard,
|
||||
const std::string& uri,
|
||||
const std::string& creators) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_target_id);
|
||||
const std::string master = normalizeAccountId(master_holding_target_id);
|
||||
const std::string metadata = normalizeAccountId(metadata_target_id);
|
||||
if (definition.empty() || master.empty() || metadata.empty()) {
|
||||
return publicError("invalid_account_id");
|
||||
}
|
||||
std::string error;
|
||||
if (!requireFreshOwnedAccount(definition, error)
|
||||
|| !requireFreshOwnedAccount(master, error)
|
||||
|| !requireFreshOwnedAccount(metadata, error)) {
|
||||
return publicError(error);
|
||||
}
|
||||
return planAndSubmit(token_create_non_fungible_plan, {
|
||||
{"definitionTargetId", definition},
|
||||
{"masterHoldingTargetId", master},
|
||||
{"metadataTargetId", metadata},
|
||||
{"name", name},
|
||||
{"printableSupplyRaw", printable_supply_raw},
|
||||
{"metadataStandard", metadata_standard},
|
||||
{"uri", uri},
|
||||
{"creators", creators},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::initializeHolding(const std::string& definition_id,
|
||||
const std::string& holding_target_id) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_id);
|
||||
const std::string holding = normalizeAccountId(holding_target_id);
|
||||
if (definition.empty() || holding.empty()) return publicError("invalid_account_id");
|
||||
std::string error;
|
||||
if (!requireFreshOwnedAccount(holding, error)) return publicError(error);
|
||||
return planAndSubmit(token_initialize_holding_plan, {
|
||||
{"definitionId", definition},
|
||||
{"holdingTargetId", holding},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::transfer(const std::string& sender_holding_id,
|
||||
const std::string& recipient_holding_id,
|
||||
const nlohmann::json& amount_raw) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string sender = normalizeAccountId(sender_holding_id);
|
||||
const std::string recipient = normalizeAccountId(recipient_holding_id);
|
||||
if (sender.empty() || recipient.empty()) return publicError("invalid_account_id");
|
||||
bool recipient_fresh = false;
|
||||
std::string error;
|
||||
if (!isFreshOwnedAccount(recipient, recipient_fresh, error)) {
|
||||
return publicError(error);
|
||||
}
|
||||
return planAndSubmit(token_transfer_plan, {
|
||||
{"senderHoldingId", sender},
|
||||
{"recipientHoldingId", recipient},
|
||||
{"amountRaw", amount_raw},
|
||||
{"recipientIsFresh", recipient_fresh},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::burn(const std::string& definition_id,
|
||||
const std::string& holding_id,
|
||||
const nlohmann::json& amount_raw) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_id);
|
||||
const std::string holding = normalizeAccountId(holding_id);
|
||||
if (definition.empty() || holding.empty()) return publicError("invalid_account_id");
|
||||
return planAndSubmit(token_burn_plan, {
|
||||
{"definitionId", definition},
|
||||
{"holdingId", holding},
|
||||
{"amountRaw", amount_raw},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::mint(const std::string& definition_id,
|
||||
const std::string& holding_id,
|
||||
const nlohmann::json& amount_raw) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_id);
|
||||
const std::string holding = normalizeAccountId(holding_id);
|
||||
if (definition.empty() || holding.empty()) return publicError("invalid_account_id");
|
||||
bool holding_fresh = false;
|
||||
std::string error;
|
||||
if (!isFreshOwnedAccount(holding, holding_fresh, error)) return publicError(error);
|
||||
return planAndSubmit(token_mint_plan, {
|
||||
{"definitionId", definition},
|
||||
{"holdingId", holding},
|
||||
{"amountRaw", amount_raw},
|
||||
{"holdingIsFresh", holding_fresh},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::mintWithAuthority(const std::string& definition_id,
|
||||
const std::string& holding_id,
|
||||
const std::string& authority_id,
|
||||
const nlohmann::json& amount_raw) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_id);
|
||||
const std::string holding = normalizeAccountId(holding_id);
|
||||
const std::string authority = normalizeAccountId(authority_id);
|
||||
if (definition.empty() || holding.empty()) return publicError("invalid_account_id");
|
||||
if (authority.empty()) return publicError("invalid_authority");
|
||||
bool holding_fresh = false;
|
||||
std::string error;
|
||||
if (!isFreshOwnedAccount(holding, holding_fresh, error)) return publicError(error);
|
||||
return planAndSubmit(token_mint_with_authority_plan, {
|
||||
{"definitionId", definition},
|
||||
{"holdingId", holding},
|
||||
{"authorityId", authority},
|
||||
{"amountRaw", amount_raw},
|
||||
{"holdingIsFresh", holding_fresh},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::setAuthority(const std::string& definition_id,
|
||||
const std::string& new_authority) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_id);
|
||||
if (definition.empty()) return publicError("invalid_account_id");
|
||||
std::string authority;
|
||||
if (!normalizeAuthority(new_authority, authority)) {
|
||||
return publicError("invalid_authority");
|
||||
}
|
||||
return planAndSubmit(token_set_authority_plan, {
|
||||
{"definitionId", definition},
|
||||
{"newAuthority", authority},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::setAuthorityWithAuthority(
|
||||
const std::string& definition_id,
|
||||
const std::string& authority_id,
|
||||
const std::string& new_authority) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string definition = normalizeAccountId(definition_id);
|
||||
const std::string current_authority = normalizeAccountId(authority_id);
|
||||
if (definition.empty()) return publicError("invalid_account_id");
|
||||
if (current_authority.empty()) return publicError("invalid_authority");
|
||||
std::string next_authority;
|
||||
if (!normalizeAuthority(new_authority, next_authority)) {
|
||||
return publicError("invalid_authority");
|
||||
}
|
||||
return planAndSubmit(token_set_authority_with_authority_plan, {
|
||||
{"definitionId", definition},
|
||||
{"authorityId", current_authority},
|
||||
{"newAuthority", next_authority},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LogosMap TokenModuleImpl::printNft(const std::string& master_holding_id,
|
||||
const std::string& printed_holding_target_id) {
|
||||
return guarded([&]() -> LogosMap {
|
||||
const std::string master = normalizeAccountId(master_holding_id);
|
||||
const std::string printed = normalizeAccountId(printed_holding_target_id);
|
||||
if (master.empty() || printed.empty()) return publicError("invalid_account_id");
|
||||
std::string error;
|
||||
if (!requireFreshOwnedAccount(printed, error)) return publicError(error);
|
||||
return planAndSubmit(token_print_nft_plan, {
|
||||
{"masterHoldingId", master},
|
||||
{"printedHoldingTargetId", printed},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <logos_json.h>
|
||||
#include <logos_module_context.h>
|
||||
|
||||
// Universal Logos core module for the LEZ Token program. Public methods form
|
||||
// the generated API, so this header deliberately uses only standard C++ and
|
||||
// Logos JSON types. Rust token_ffi owns token codecs and instruction encoding;
|
||||
// logos_execution_zone owns wallet/account I/O and transaction submission.
|
||||
class TokenModuleImpl : public LogosModuleContext {
|
||||
public:
|
||||
TokenModuleImpl() = default;
|
||||
~TokenModuleImpl() = default;
|
||||
|
||||
/// Returns `{status,error,programId,programIdHex,networkFingerprint}`.
|
||||
/// Configure TOKEN_PROGRAM_ID or TOKEN_PROGRAM_BIN. If both are set, they
|
||||
/// must identify the same program.
|
||||
LogosMap programInfo();
|
||||
|
||||
/// Reads and decodes any public token definition account. `definition_id`
|
||||
/// accepts 32-byte hex or base58. Success adds `definition`; failures use
|
||||
/// invalid_account_id, account_read_failed, token_program_mismatch,
|
||||
/// invalid_definition_data, config_missing, or backend_error.
|
||||
LogosMap inspectDefinition(const std::string& definition_id);
|
||||
|
||||
/// Reads and decodes any public token holding account. Success adds
|
||||
/// `holding`; failures follow the same stable read envelope.
|
||||
LogosMap inspectHolding(const std::string& holding_id);
|
||||
|
||||
/// Reads and decodes any public token metadata account. Success adds
|
||||
/// `metadata`; failures follow the same stable read envelope.
|
||||
LogosMap inspectMetadata(const std::string& metadata_id);
|
||||
|
||||
/// Discovers wallet-owned public token accounts from live reads. Invalid,
|
||||
/// foreign, unreadable, and ambiguously decoded rows are skipped. Success
|
||||
/// adds `accounts`, sorted by lowercase accountIdHex.
|
||||
LogosMap walletTokenAccounts();
|
||||
|
||||
/// Creates a fungible definition and initial holding. Both target IDs must
|
||||
/// be fresh public wallet accounts and sign. `total_supply_raw` is an exact
|
||||
/// non-negative u128 decimal. `mint_authority`: none, self, or account ID.
|
||||
LogosMap createFungible(const std::string& definition_target_id,
|
||||
const std::string& holding_target_id,
|
||||
const std::string& name,
|
||||
const nlohmann::json& total_supply_raw,
|
||||
const std::string& mint_authority);
|
||||
|
||||
/// Creates a metadata-backed fungible definition. All three target IDs are
|
||||
/// fresh public wallet accounts and sign. Standard is simple or expanded.
|
||||
LogosMap createFungibleWithMetadata(const std::string& definition_target_id,
|
||||
const std::string& holding_target_id,
|
||||
const std::string& metadata_target_id,
|
||||
const std::string& name,
|
||||
const nlohmann::json& total_supply_raw,
|
||||
const std::string& mint_authority,
|
||||
const std::string& metadata_standard,
|
||||
const std::string& uri,
|
||||
const std::string& creators);
|
||||
|
||||
/// Creates a non-fungible definition, master holding, and metadata. All
|
||||
/// targets must be fresh public wallet accounts and sign.
|
||||
LogosMap createNonFungible(const std::string& definition_target_id,
|
||||
const std::string& master_holding_target_id,
|
||||
const std::string& metadata_target_id,
|
||||
const std::string& name,
|
||||
const nlohmann::json& printable_supply_raw,
|
||||
const std::string& metadata_standard,
|
||||
const std::string& uri,
|
||||
const std::string& creators);
|
||||
|
||||
/// Initializes a fresh wallet-owned holding; holding target signs.
|
||||
LogosMap initializeHolding(const std::string& definition_id,
|
||||
const std::string& holding_target_id);
|
||||
|
||||
/// Transfers exact raw amount. Sender signs. Recipient signs only when its
|
||||
/// live account is empty and wallet ownership proves it is fresh.
|
||||
LogosMap transfer(const std::string& sender_holding_id,
|
||||
const std::string& recipient_holding_id,
|
||||
const nlohmann::json& amount_raw);
|
||||
|
||||
/// Burns exact raw amount. Holding signs.
|
||||
LogosMap burn(const std::string& definition_id,
|
||||
const std::string& holding_id,
|
||||
const nlohmann::json& amount_raw);
|
||||
|
||||
/// Mints through self-authority. Definition signs; holding signs only when
|
||||
/// live state and wallet ownership prove it is fresh.
|
||||
LogosMap mint(const std::string& definition_id,
|
||||
const std::string& holding_id,
|
||||
const nlohmann::json& amount_raw);
|
||||
|
||||
/// Mints through explicit external authority. Authority signs; holding
|
||||
/// signs only when live state and wallet ownership prove it is fresh.
|
||||
LogosMap mintWithAuthority(const std::string& definition_id,
|
||||
const std::string& holding_id,
|
||||
const std::string& authority_id,
|
||||
const nlohmann::json& amount_raw);
|
||||
|
||||
/// Rotates/revokes self authority. `new_authority`: none, self, or account
|
||||
/// ID. Definition signs.
|
||||
LogosMap setAuthority(const std::string& definition_id,
|
||||
const std::string& new_authority);
|
||||
|
||||
/// Rotates/revokes explicit external authority. Current authority signs.
|
||||
LogosMap setAuthorityWithAuthority(const std::string& definition_id,
|
||||
const std::string& authority_id,
|
||||
const std::string& new_authority);
|
||||
|
||||
/// Prints an NFT copy. Master and fully fresh printed target both sign.
|
||||
LogosMap printNft(const std::string& master_holding_id,
|
||||
const std::string& printed_holding_target_id);
|
||||
|
||||
private:
|
||||
using TokenOperation = char* (*)(const char*);
|
||||
|
||||
std::vector<std::uint8_t> loadTokenBinary() const;
|
||||
std::string loadTokenProgramId() const;
|
||||
nlohmann::json tokenProgramInfo();
|
||||
std::string normalizeAccountId(const std::string& id);
|
||||
nlohmann::json readPublicAccount(const std::string& account_id);
|
||||
nlohmann::json walletAccountIds();
|
||||
bool isFreshOwnedAccount(const std::string& account_id,
|
||||
bool& fresh,
|
||||
std::string& error);
|
||||
bool requireFreshOwnedAccount(const std::string& account_id,
|
||||
std::string& error);
|
||||
LogosMap inspectAccount(const std::string& account_id,
|
||||
TokenOperation operation,
|
||||
const char* payload_key);
|
||||
bool normalizeAuthority(const std::string& authority,
|
||||
std::string& normalized);
|
||||
LogosMap planAndSubmit(TokenOperation planner, nlohmann::json request);
|
||||
LogosMap submitPlan(const nlohmann::json& plan);
|
||||
|
||||
bool programInfoResolved_ = false;
|
||||
std::string programId_;
|
||||
std::string programIdHex_;
|
||||
};
|
||||
Reference in New Issue
Block a user