mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-26 22:51:14 +00:00
809 lines
30 KiB
C++
809 lines
30 KiB
C++
#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},
|
|
});
|
|
});
|
|
}
|