Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 60 additions & 21 deletions scripts/libraries/lwsf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,26 +295,28 @@ export const lwsf = defineLib({
// Bump this whenever the rpc.cpp / config patch below changes, or the build
// silently reuses the cached (unpatched) library. The literal tag does not
// hash the patch content, so edits here are invisible to the cache otherwise.
cacheTag: '1-nym-timeout',
cacheTag: '2-da8e261-txcap',
libDeps: ['boost', 'libsodium', 'libunbound', 'libzmq', 'openssl'],
deps: ['monero.clone'],

url: 'https://github.com/vtnerd/lwsf.git',
hash: 'cedb2164f9ccd418b91a4e54ee8479c8d5c3cad0', // Nov 7, 2025
// Dec 13, 2025. Includes upstream da8e261 "Update get_address_txs tx array
// constraint": the old pin rejected any get_address_txs response with more
// than config::max_txes_in_rpc = 2048 transactions, permanently bricking
// sync for wallets whose (decoy-inflated) tx list crossed that size.
hash: 'da8e2617958312f10fe4406808c2a951c5cf0a09',

async build(build, platform, prefixPath) {
// Patch rpc.cpp to support api_key injection in HTTP requests
// and to optionally route requests through a JS-side fetch bridge
// (used for Nym mixnet support).
const rpcPath = join(build.cwd, 'src/rpc.cpp')
const rpcCpp = await readFile(rpcPath, 'utf8')
await writeFile(
rpcPath,
rpcCpp
// Add api_key storage + nym-fetch declarations after includes.
.replace(
'#include "wire/wrappers_impl.h"',
`#include "wire/wrappers_impl.h"
const patchedRpcCpp = rpcCpp
// Add api_key storage + nym-fetch declarations after includes.
.replace(
'#include "wire/wrappers_impl.h"',
`#include "wire/wrappers_impl.h"
#include <chrono>
#include <cstdint>
#include <limits>
Expand All @@ -341,25 +343,45 @@ namespace nymfetch {
const std::string& body,
std::uint64_t timeoutMs);
}`
)
// Modify invoke_payload to (1) inject api_key into JSON body and
// (2) redirect to the JS fetch bridge when Nym is enabled.
.replace(
`expect<std::string> invoke_payload(http_client& client, const boost::string_ref endpoint, const epee::byte_slice payload)
)
// Modify invoke_payload to (1) inject api_key into JSON body and
// (2) redirect to the JS fetch bridge when Nym is enabled.
.replace(
`expect<std::string> invoke_payload(http_client& client, const boost::string_ref prefix, boost::string_ref endpoint, const epee::byte_slice payload)
{
static const epee::net_utils::http::fields_list headers{
{"Content-Type", "application/json; charset=utf-8"}
};

if (!client.is_connected())
{
if (!client.connect(config::connect_timeout))
return {error::no_response};
}

std::string real;
if (!prefix.empty())
{
real = std::string{prefix} + std::string{endpoint};
endpoint = real;
}

const epee::net_utils::http::http_response_info* response = nullptr;
if (!client.invoke(endpoint, "POST", {reinterpret_cast<const char*>(payload.data()), payload.size()}, config::rpc_timeout, std::addressof(response), headers))
return {error::no_response};`,
`expect<std::string> invoke_payload(http_client& client, const boost::string_ref endpoint, epee::byte_slice payload)
`expect<std::string> invoke_payload(http_client& client, const boost::string_ref prefix, boost::string_ref endpoint, const epee::byte_slice payload)
{
static const epee::net_utils::http::fields_list headers{
{"Content-Type", "application/json; charset=utf-8"}
};

std::string real;
if (!prefix.empty())
{
real = std::string{prefix} + std::string{endpoint};
endpoint = real;
}

// Inject api_key if set (added by react-native build)
std::string body_str;
const std::string& key = config::api_key();
Expand All @@ -379,18 +401,20 @@ namespace nymfetch {

// Nym path: delegate to JS fetch bridge instead of hitting the network
// directly. The JS layer is responsible for actually executing the
// request through mixFetch.
// request through mixFetch. This must run before the socket connect
// below: a Nym wallet must never open a direct TCP connection to the
// daemon.
if (nymfetch::isEnabled()) {
const std::string base = nymfetch::getBaseUrl();
if (base.empty()) return {error::no_response};
std::string path(endpoint.data(), endpoint.size());
if (path.empty() || path.front() != '/') path.insert(path.begin(), '/');
const std::string url = base + path;
try {
// Nym mixnet round-trips routinely exceed the 5s config::rpc_timeout
// Nym mixnet round-trips routinely exceed the config::rpc_timeout
// used for the direct client below (the mixFetch client itself allows
// 300s). A single-shot spend RPC (get_random_outs, submit_raw_tx) has
// no retry, so a 5s budget makes LWS sends over Nym fail deterministically
// no retry, so a short budget makes LWS sends over Nym fail deterministically
// while the retrying sync loop merely limps. Give the Nym path a generous
// budget instead.
const std::uint64_t nymTimeoutMs = 120000;
Expand All @@ -408,12 +432,27 @@ namespace nymfetch {
}
}

if (!client.is_connected())
{
if (!client.connect(config::connect_timeout))
return {error::no_response};
}

const epee::net_utils::http::http_response_info* response = nullptr;
if (!client.invoke(endpoint, "POST", body_str, config::rpc_timeout, std::addressof(response), headers))
return {error::no_response};`
),
'utf8'
)
)
// Anchored replaces silently no-op when upstream drifts; fail the build
// instead of shipping an unpatched (no api_key, no Nym) client.
if (
!patchedRpcCpp.includes('void set_api_key') ||
!patchedRpcCpp.includes('client.invoke(endpoint, "POST", body_str')
) {
throw new Error(
'lwsf rpc.cpp patch anchors did not match the pinned source'
)
}
await writeFile(rpcPath, patchedRpcCpp, 'utf8')
build.log('Patched rpc.cpp for api_key and nym-fetch support')

build.exportEnv({
Expand Down