mirror of
https://github.com/wownero/wowlet.git
synced 2026-10-30 20:58:34 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c65c12d328 | ||
|
|
38c63088b5 | ||
|
|
3d38fdd135 | ||
|
|
f2c3ce45a0 | ||
|
|
87d5977243 | ||
|
|
9e6d85c246 | ||
|
|
49b3264b0e | ||
|
|
3cf58f0313 | ||
|
|
0f6892c123 | ||
|
|
1fedbf0faa | ||
|
|
410df0c8ff | ||
|
|
1853ebf558 | ||
|
|
a2b34f5ebc | ||
|
|
27f694c4bd | ||
|
|
c7333a775c | ||
|
|
8b846a8dc0 | ||
|
|
dc5f940801 | ||
|
|
36d878e7bf | ||
|
|
a526061d97 | ||
|
|
f4c5ec3c25 | ||
|
|
1217ce174c | ||
|
|
914fa1ed32 | ||
|
|
a3509a6c3b | ||
|
|
93d13a6568 | ||
|
|
64326119eb |
@@ -0,0 +1,218 @@
|
|||||||
|
name: macos build (gitea self-hosted)
|
||||||
|
|
||||||
|
# Runs ONLY on the Gitea mac runner (GitHub ignores .gitea/workflows).
|
||||||
|
# Dispatch-only: the mac runner has capacity 1 and also serves the Hash Bags
|
||||||
|
# iOS lane, so macOS wowlet builds are an explicit button, not a push hook.
|
||||||
|
# All build state lands under the runner's work dir, which lives on
|
||||||
|
# /Volumes/DataSSD (never backed up: the fleet backup pulls an allowlist of
|
||||||
|
# gitea data only).
|
||||||
|
#
|
||||||
|
# Signing/notarization uses the SAME seven secrets as the smirk-monorepo
|
||||||
|
# desktop lane, set by ~/keys/suchsoftware/set-gitea-desktop-secrets.sh:
|
||||||
|
# APPLE_CERTIFICATE (b64 Developer ID Application .p12),
|
||||||
|
# APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_TEAM_ID,
|
||||||
|
# APPLE_API_ISSUER, APPLE_API_KEY, APPLE_API_KEY_P8_BASE64 (notarytool).
|
||||||
|
# If APPLE_CERTIFICATE is unset the build ships unsigned instead of failing.
|
||||||
|
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-macos:
|
||||||
|
name: "macOS (brew, native)"
|
||||||
|
runs-on: macos-latest
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||||
|
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: install dependencies (brew)
|
||||||
|
run: |
|
||||||
|
export PATH="/opt/homebrew/bin:$PATH"
|
||||||
|
set +o pipefail # yes(1) takes SIGPIPE when brew has nothing to ask
|
||||||
|
export NONINTERACTIVE=1 HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_INSTALL_UPGRADE=1
|
||||||
|
yes | brew install --quiet qt libsodium libzip qrencode unbound cmake boost hidapi openssl expat protobuf pkgconfig zxing-cpp
|
||||||
|
|
||||||
|
- name: fetch wownerod to embed
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
arm64) WOWTAR=wownero-aarch64-apple-darwin11-v0.11.4.0.tar.bz2 ;;
|
||||||
|
*) WOWTAR=wownero-x86_64-apple-darwin11-v0.11.4.0.tar.bz2 ;;
|
||||||
|
esac
|
||||||
|
mkdir -p /tmp/wownerod-embed /tmp/wow-extract
|
||||||
|
curl -L -o /tmp/wownero.tar.bz2 \
|
||||||
|
"https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/$WOWTAR"
|
||||||
|
# /tmp is a symlink on macOS — find(1) won't descend it as a bare argument
|
||||||
|
tar xjf /tmp/wownero.tar.bz2 -C /tmp/wow-extract
|
||||||
|
find /tmp/wow-extract -name wownerod -type f -exec cp {} /tmp/wownerod-embed/ \;
|
||||||
|
chmod +x /tmp/wownerod-embed/wownerod
|
||||||
|
ls -la /tmp/wownerod-embed
|
||||||
|
|
||||||
|
- name: fetch tor to embed
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
arm64) TORTAR=tor-expert-bundle-macos-aarch64-15.0.tar.gz ;;
|
||||||
|
*) TORTAR=tor-expert-bundle-macos-x86_64-15.0.tar.gz ;;
|
||||||
|
esac
|
||||||
|
mkdir -p /tmp/teb /tmp/tor-embed
|
||||||
|
curl -L -o /tmp/teb.tgz "https://archive.torproject.org/tor-package-archive/torbrowser/15.0/$TORTAR"
|
||||||
|
tar xzf /tmp/teb.tgz -C /tmp/teb
|
||||||
|
cp /tmp/teb/tor/tor /tmp/teb/tor/libevent-*.dylib /tmp/tor-embed/
|
||||||
|
# tor ships UNSIGNED; arm64 macOS SIGKILLs unsigned binaries. Ad-hoc sign
|
||||||
|
# BEFORE embedding — the signature travels inside the file, so the copy
|
||||||
|
# the app unpacks at runtime on user machines is runnable too.
|
||||||
|
codesign -s - -f /tmp/tor-embed/tor /tmp/tor-embed/libevent-*.dylib
|
||||||
|
chmod +x /tmp/tor-embed/tor
|
||||||
|
# expert-bundle RELEASE number (15.0) is NOT the tor version — derive it
|
||||||
|
TORVER=$(/tmp/tor-embed/tor --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1)
|
||||||
|
echo "TOR_VERSION=$TORVER" >> "$GITHUB_ENV"
|
||||||
|
echo "embedded tor version: $TORVER"
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
run: |
|
||||||
|
export PATH="/opt/homebrew/bin:$PATH"
|
||||||
|
# clang does not search /opt/homebrew/include by default; the feather
|
||||||
|
# target pulls monero headers that include boost, and modern Boost's
|
||||||
|
# CMake config leaves Boost_INCLUDE_DIRS empty. CPATH/LIBRARY_PATH make
|
||||||
|
# every linked brew header/lib findable without per-dep flag surgery.
|
||||||
|
export CPATH="$(brew --prefix)/include"
|
||||||
|
export LIBRARY_PATH="$(brew --prefix)/lib"
|
||||||
|
mkdir -p build && cd build
|
||||||
|
cmake -DWITH_SCANNER=OFF -DUSE_DEVICE_TREZOR=OFF -DBoost_USE_STATIC_LIBS=ON -DWOWNEROD_DIR=/tmp/wownerod-embed -DTOR_DIR=/tmp/tor-embed -DTOR_VERSION="$TOR_VERSION" -DCMAKE_PREFIX_PATH="$(brew --prefix qt)" ..
|
||||||
|
cmake --build . -j "$(sysctl -n hw.ncpu)"
|
||||||
|
|
||||||
|
- name: package app bundle
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd build
|
||||||
|
APP=$(find . -maxdepth 3 -name "*.app" -type d | head -1)
|
||||||
|
[ -n "$APP" ] || { echo "no .app bundle produced"; ls -la bin/; exit 1; }
|
||||||
|
mkdir -p dist
|
||||||
|
cp -R "$APP" dist/Wowlet.app
|
||||||
|
# cmake never places the icon in the bundle (upstream only copies it
|
||||||
|
# for the dormant `installed/` layout)
|
||||||
|
mkdir -p dist/Wowlet.app/Contents/Resources
|
||||||
|
cp ../src/assets/images/appicons/appicon.icns dist/Wowlet.app/Contents/Resources/
|
||||||
|
# bundle Qt frameworks/plugins so the app runs off-machine
|
||||||
|
/opt/homebrew/opt/qt/bin/macdeployqt dist/Wowlet.app
|
||||||
|
|
||||||
|
# macdeployqt bundles Qt frameworks but not the plain-dylib brew deps
|
||||||
|
# of Qt's image plugins (libwebp/libsharpyuv/libbrotli...), which are
|
||||||
|
# referenced as @rpath/lib*.dylib. Without them a clean (no-Homebrew)
|
||||||
|
# Mac can't load those image formats. Qt frameworks use a different
|
||||||
|
# ref form (@rpath/Qt*.framework/...), so keying strictly on
|
||||||
|
# `@rpath/*.dylib` bundles the stragglers WITHOUT touching Qt.
|
||||||
|
# Several passes converge (a bundled lib pulls in further deps).
|
||||||
|
APP=dist/Wowlet.app
|
||||||
|
FW="$APP/Contents/Frameworks"
|
||||||
|
mkdir -p "$FW"
|
||||||
|
for pass in 1 2 3 4; do
|
||||||
|
while IFS= read -r macho; do
|
||||||
|
file "$macho" | grep -q Mach-O || continue
|
||||||
|
chmod u+w "$macho"
|
||||||
|
otool -L "$macho" | awk 'NR>1{print $1}' | while IFS= read -r dep; do
|
||||||
|
case "$dep" in
|
||||||
|
@rpath/*.dylib)
|
||||||
|
name=$(basename "$dep")
|
||||||
|
if [ -f "$FW/$name" ]; then
|
||||||
|
install_name_tool -change "$dep" "@executable_path/../Frameworks/$name" "$macho" 2>/dev/null || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
real=$(find /opt/homebrew/lib /opt/homebrew/opt/*/lib -maxdepth 1 -name "$name" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$real" ]; then
|
||||||
|
cp -n "$real" "$FW/$name"
|
||||||
|
chmod u+w "$FW/$name"
|
||||||
|
install_name_tool -id "@executable_path/../Frameworks/$name" "$FW/$name" 2>/dev/null || true
|
||||||
|
install_name_tool -change "$dep" "@executable_path/../Frameworks/$name" "$macho" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
done < <(find "$APP" -type f)
|
||||||
|
done
|
||||||
|
|
||||||
|
# Fail if any @rpath/*.dylib (non-Qt) remains unresolved in the bundle
|
||||||
|
leftover=$(find "$APP" -type f | while IFS= read -r m; do
|
||||||
|
file "$m" | grep -q Mach-O || continue
|
||||||
|
otool -L "$m" | awk 'NR>1{print $1}' | grep -E '^@rpath/[^/]+\.dylib$' || true
|
||||||
|
done | sort -u)
|
||||||
|
if [ -n "$leftover" ]; then
|
||||||
|
echo "UNBUNDLED @rpath dylibs remain:"; echo "$leftover"; exit 1
|
||||||
|
fi
|
||||||
|
echo "all plain-dylib deps bundled"
|
||||||
|
ls -la "$FW" | grep -iE "webp|sharpyuv|brotli" || true
|
||||||
|
ls -la dist/
|
||||||
|
|
||||||
|
- name: sign and notarize (skipped when secrets absent)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd build
|
||||||
|
if [ -z "$APPLE_CERTIFICATE" ]; then
|
||||||
|
echo "APPLE_CERTIFICATE not set — shipping UNSIGNED build"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
KC=/tmp/wowlet-ci.keychain-db
|
||||||
|
KCPW=$(/usr/bin/uuidgen)
|
||||||
|
echo "$KCPW" > /tmp/wowlet-ci-kcpw # for the cleanup step only
|
||||||
|
security create-keychain -p "$KCPW" "$KC"
|
||||||
|
security set-keychain-settings -lut 3600 "$KC"
|
||||||
|
security unlock-keychain -p "$KCPW" "$KC"
|
||||||
|
printf '%s' "$APPLE_CERTIFICATE" | base64 -d > /tmp/dev_id.p12
|
||||||
|
security import /tmp/dev_id.p12 -k "$KC" -P "$APPLE_CERTIFICATE_PASSWORD" \
|
||||||
|
-T /usr/bin/codesign
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -s -k "$KCPW" "$KC" >/dev/null
|
||||||
|
security list-keychains -d user -s "$KC" login.keychain-db
|
||||||
|
|
||||||
|
# Sign inside-out: every Mach-O NOT inside a .framework (embedded
|
||||||
|
# wownerod/tor, the bundled webp/sharpyuv/brotli dylibs, and the
|
||||||
|
# image-format plugins whose signatures install_name_tool just
|
||||||
|
# invalidated — keyed on `file`, not exec bit, since plugins/dylibs
|
||||||
|
# aren't +x), then each Qt framework as a unit, then the bundle.
|
||||||
|
SIGN() { codesign --force --options runtime --timestamp --keychain "$KC" \
|
||||||
|
--sign "$APPLE_SIGNING_IDENTITY" "$@"; }
|
||||||
|
find dist/Wowlet.app -type f ! -path "*/*.framework/*" \
|
||||||
|
| while read -r f; do file "$f" | grep -q Mach-O && SIGN "$f" || true; done
|
||||||
|
find dist/Wowlet.app/Contents/Frameworks -maxdepth 1 -name "*.framework" 2>/dev/null \
|
||||||
|
| while read -r fw; do SIGN "$fw"; done
|
||||||
|
SIGN dist/Wowlet.app
|
||||||
|
codesign --verify --deep --strict dist/Wowlet.app
|
||||||
|
|
||||||
|
printf '%s' "$APPLE_API_KEY_P8_BASE64" | base64 -d > /tmp/notary.p8
|
||||||
|
ditto -c -k --keepParent dist/Wowlet.app /tmp/wowlet-notarize.zip
|
||||||
|
xcrun notarytool submit /tmp/wowlet-notarize.zip \
|
||||||
|
--key /tmp/notary.p8 --key-id "$APPLE_API_KEY" --issuer "$APPLE_API_ISSUER" \
|
||||||
|
--wait --timeout 30m
|
||||||
|
xcrun stapler staple dist/Wowlet.app
|
||||||
|
echo "signed + notarized + stapled"
|
||||||
|
|
||||||
|
- name: zip artifact
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd build/dist
|
||||||
|
ditto -c -k --keepParent Wowlet.app ../wowlet-macos-$(uname -m).zip
|
||||||
|
ls -la ../wowlet-macos-*.zip
|
||||||
|
|
||||||
|
- name: cleanup signing material
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f /tmp/wowlet-ci-kcpw ]; then
|
||||||
|
security delete-keychain /tmp/wowlet-ci.keychain-db || true
|
||||||
|
fi
|
||||||
|
rm -f /tmp/dev_id.p12 /tmp/notary.p8 /tmp/wowlet-ci-kcpw /tmp/wowlet-notarize.zip
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wowlet-macos
|
||||||
|
path: build/wowlet-macos-*.zip
|
||||||
|
retention-days: 14
|
||||||
+9
-5
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.18)
|
cmake_minimum_required(VERSION 3.18)
|
||||||
|
|
||||||
project(feather
|
project(feather
|
||||||
VERSION "5.0.2"
|
VERSION "5.1.0"
|
||||||
DESCRIPTION "A free Monero desktop wallet"
|
DESCRIPTION "A free Monero desktop wallet"
|
||||||
LANGUAGES CXX C ASM
|
LANGUAGES CXX C ASM
|
||||||
)
|
)
|
||||||
@@ -148,15 +148,19 @@ if(TOR_DIR)
|
|||||||
|
|
||||||
message(STATUS "Embedded Tor version: ${TOR_VERSION}")
|
message(STATUS "Embedded Tor version: ${TOR_VERSION}")
|
||||||
|
|
||||||
|
# wowlet: the target dir is not tracked in git — create it (BSD cp's usage
|
||||||
|
# error exits 64, which the ret check below does not catch)
|
||||||
|
execute_process(COMMAND bash -c "mkdir -p ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor")
|
||||||
|
|
||||||
# Always copy Tor when doing a reproducible build to prevent old versions from getting included
|
# Always copy Tor when doing a reproducible build to prevent old versions from getting included
|
||||||
if (REPRODUCIBLE)
|
if (REPRODUCIBLE)
|
||||||
set(TOR_COPY_CMD "cp -a ${TOR_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor")
|
set(TOR_COPY_CMD "cp -a ${TOR_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor")
|
||||||
else()
|
else()
|
||||||
set(TOR_COPY_CMD "cp -au ${TOR_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor")
|
set(TOR_COPY_CMD "cp -a ${TOR_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/tor") # wowlet: BSD cp has no -u (macOS: illegal option, exit 64, silent no-op)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
execute_process(COMMAND bash -c "${TOR_COPY_CMD}" RESULT_VARIABLE ret)
|
execute_process(COMMAND bash -c "${TOR_COPY_CMD}" RESULT_VARIABLE ret)
|
||||||
if(ret EQUAL "1")
|
if(NOT ret EQUAL "0")
|
||||||
message(FATAL_ERROR "Tor copy failure: ${TOR_COPY_CMD}")
|
message(FATAL_ERROR "Tor copy failure: ${TOR_COPY_CMD}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -172,11 +176,11 @@ if(WOWNEROD_DIR)
|
|||||||
if (REPRODUCIBLE)
|
if (REPRODUCIBLE)
|
||||||
set(WOWNEROD_COPY_CMD "cp -a ${WOWNEROD_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/wownerod")
|
set(WOWNEROD_COPY_CMD "cp -a ${WOWNEROD_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/wownerod")
|
||||||
else()
|
else()
|
||||||
set(WOWNEROD_COPY_CMD "cp -au ${WOWNEROD_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/wownerod")
|
set(WOWNEROD_COPY_CMD "cp -a ${WOWNEROD_DIR}/* ${CMAKE_CURRENT_SOURCE_DIR}/src/assets/wownerod") # wowlet: BSD cp has no -u
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
execute_process(COMMAND bash -c "${WOWNEROD_COPY_CMD}" RESULT_VARIABLE ret)
|
execute_process(COMMAND bash -c "${WOWNEROD_COPY_CMD}" RESULT_VARIABLE ret)
|
||||||
if(ret EQUAL "1")
|
if(NOT ret EQUAL "0")
|
||||||
message(FATAL_ERROR "wownerod copy failure: ${WOWNEROD_COPY_CMD}")
|
message(FATAL_ERROR "wownerod copy failure: ${WOWNEROD_COPY_CMD}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -542,6 +542,24 @@ void MainWindow::initWalletContext() {
|
|||||||
connect(m_wallet, &Wallet::deviceError, this, &MainWindow::onDeviceError);
|
connect(m_wallet, &Wallet::deviceError, this, &MainWindow::onDeviceError);
|
||||||
|
|
||||||
connect(m_wallet, &Wallet::multiBroadcast, this, &MainWindow::onMultiBroadcast);
|
connect(m_wallet, &Wallet::multiBroadcast, this, &MainWindow::onMultiBroadcast);
|
||||||
|
|
||||||
|
// wowlet: count multi-broadcast relay results so a total failure (Tor not
|
||||||
|
// bootstrapped, every node unreachable) is surfaced instead of silent.
|
||||||
|
connect(m_rpc, &DaemonRpc::ApiResponse, this, [this](const DaemonRpc::DaemonResponse &resp){
|
||||||
|
if (resp.endpoint == DaemonRpc::Endpoint::SEND_RAW_TRANSACTION && resp.ok) {
|
||||||
|
m_broadcastBatchSuccesses++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// wowlet: when the embedded daemon reports synced after a (re)start,
|
||||||
|
// re-announce any still-pending outgoing txs; a restarted pool has
|
||||||
|
// forgotten them.
|
||||||
|
connect(daemonManager(), &DaemonManager::synced, this, &MainWindow::relayPendingTransactions);
|
||||||
|
|
||||||
|
// wowlet: periodic local check for outgoing txs stuck unconfirmed.
|
||||||
|
m_stuckTxTimer = new QTimer(this);
|
||||||
|
connect(m_stuckTxTimer, &QTimer::timeout, this, &MainWindow::checkStuckTransactions);
|
||||||
|
m_stuckTxTimer->start(2 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::menuToggleTabVisible(const QString &key){
|
void MainWindow::menuToggleTabVisible(const QString &key){
|
||||||
@@ -760,6 +778,7 @@ void MainWindow::onSubtractFeeFromAmountEnabled(bool enabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onMultiBroadcast(const QMap<QString, QString> &txHexMap) {
|
void MainWindow::onMultiBroadcast(const QMap<QString, QString> &txHexMap) {
|
||||||
|
m_broadcastBatchSuccesses = 0;
|
||||||
QMapIterator<QString, QString> i(txHexMap);
|
QMapIterator<QString, QString> i(txHexMap);
|
||||||
while (i.hasNext()) {
|
while (i.hasNext()) {
|
||||||
i.next();
|
i.next();
|
||||||
@@ -770,6 +789,99 @@ void MainWindow::onMultiBroadcast(const QMap<QString, QString> &txHexMap) {
|
|||||||
m_rpc->sendRawTransaction(i.value());
|
m_rpc->sendRawTransaction(i.value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wowlet: if not a single node accepted the relay, say so. The commit to
|
||||||
|
// the local daemon may still confirm on its own, but when Tor is down and
|
||||||
|
// the daemon runs with an exclusive --tx-proxy, nothing will ever leave
|
||||||
|
// this machine; the user should know within a minute, not after two days.
|
||||||
|
const QStringList txids = txHexMap.keys();
|
||||||
|
QTimer::singleShot(45 * 1000, this, [this, txids]{
|
||||||
|
if (m_broadcastBatchSuccesses > 0 || txids.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QMessageBox box(this);
|
||||||
|
box.setWindowTitle("Transaction relay may have failed");
|
||||||
|
box.setText("No node accepted the transaction broadcast. If Tor is "
|
||||||
|
"still starting (or unavailable), the transaction may not "
|
||||||
|
"reach the network at all.");
|
||||||
|
box.setInformativeText("Rebroadcasting later is always safe: a "
|
||||||
|
"transaction can never be spent twice.");
|
||||||
|
QPushButton *retry = box.addButton("Rebroadcast now", QMessageBox::YesRole);
|
||||||
|
box.addButton("Later", QMessageBox::RejectRole);
|
||||||
|
box.exec();
|
||||||
|
if (box.clickedButton() == retry) {
|
||||||
|
this->onResendTransaction(txids[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::checkStuckTransactions() {
|
||||||
|
// wowlet: a pending outgoing tx unconfirmed for this long has, in
|
||||||
|
// practice, not reached the network (Tor down at send time, embedded
|
||||||
|
// node restarted mid-relay). Local history scan only; no network calls,
|
||||||
|
// and no txid is ever looked up against any external service. The remedy
|
||||||
|
// reuses the existing rebroadcast path, which reconnects to a fresh node
|
||||||
|
// first.
|
||||||
|
constexpr qint64 kStuckSecs = 25 * 60;
|
||||||
|
if (m_wallet == nullptr || m_wallet->history() == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TransactionHistory *history = m_wallet->history();
|
||||||
|
for (quint64 i = 0; i < history->count(); i++) {
|
||||||
|
const TransactionRow &row = history->transaction(static_cast<int>(i));
|
||||||
|
if (!row.pending || row.direction == TransactionRow::Direction_In) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (row.timestamp.secsTo(QDateTime::currentDateTime()) < kStuckSecs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (m_stuckTxWarned.contains(row.hash)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_stuckTxWarned.insert(row.hash);
|
||||||
|
QMessageBox box(this);
|
||||||
|
box.setWindowTitle("Transaction appears stuck");
|
||||||
|
box.setText(QString("An outgoing transaction has been unconfirmed for "
|
||||||
|
"more than %1 minutes:\n\n%2")
|
||||||
|
.arg(kStuckSecs / 60).arg(row.hash));
|
||||||
|
box.setInformativeText("It may never have reached the network. "
|
||||||
|
"Rebroadcasting is safe: a transaction can "
|
||||||
|
"never be spent twice, and your change stays "
|
||||||
|
"locked until this either confirms or fails.");
|
||||||
|
QPushButton *retry = box.addButton("Rebroadcast now", QMessageBox::YesRole);
|
||||||
|
box.addButton("Later", QMessageBox::RejectRole);
|
||||||
|
box.exec();
|
||||||
|
if (box.clickedButton() == retry) {
|
||||||
|
this->onResendTransaction(row.hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::relayPendingTransactions() {
|
||||||
|
// wowlet: rebuild the multi-broadcast map from the wallet's own tx cache
|
||||||
|
// for every pending outgoing tx and re-announce it. Strictly local
|
||||||
|
// sources; the announcement itself goes through the normal (Tor-routed)
|
||||||
|
// broadcast path.
|
||||||
|
if (m_wallet == nullptr || m_wallet->history() == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TransactionHistory *history = m_wallet->history();
|
||||||
|
QMap<QString, QString> txHexMap;
|
||||||
|
for (quint64 i = 0; i < history->count(); i++) {
|
||||||
|
const TransactionRow &row = history->transaction(static_cast<int>(i));
|
||||||
|
if (!row.pending || row.direction == TransactionRow::Direction_In) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString txHex = m_wallet->getCacheTransaction(row.hash);
|
||||||
|
if (!txHex.isEmpty()) {
|
||||||
|
txHexMap[row.hash] = txHex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (txHexMap.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qDebug() << QString("Re-relaying %1 pending transaction(s) after daemon sync").arg(txHexMap.size());
|
||||||
|
this->onMultiBroadcast(txHexMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onSyncStatus(quint64 height, quint64 target, bool daemonSync) {
|
void MainWindow::onSyncStatus(quint64 height, quint64 target, bool daemonSync) {
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ namespace Ui {
|
|||||||
class MainWindow;
|
class MainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include <QSet>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
class ToggleTab : QObject {
|
class ToggleTab : QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -214,12 +217,23 @@ private:
|
|||||||
QIcon hardwareDevicePairedIcon();
|
QIcon hardwareDevicePairedIcon();
|
||||||
QIcon hardwareDeviceUnpairedIcon();
|
QIcon hardwareDeviceUnpairedIcon();
|
||||||
|
|
||||||
|
// wowlet: stuck-transaction detection and pending re-relay. Both are
|
||||||
|
// strictly local: history rows and the wallet's own tx cache; no txid is
|
||||||
|
// ever queried against any external service.
|
||||||
|
void checkStuckTransactions();
|
||||||
|
void relayPendingTransactions();
|
||||||
|
|
||||||
QScopedPointer<Ui::MainWindow> ui;
|
QScopedPointer<Ui::MainWindow> ui;
|
||||||
WindowManager *m_windowManager;
|
WindowManager *m_windowManager;
|
||||||
Wallet *m_wallet = nullptr;
|
Wallet *m_wallet = nullptr;
|
||||||
Nodes *m_nodes;
|
Nodes *m_nodes;
|
||||||
DaemonRpc *m_rpc;
|
DaemonRpc *m_rpc;
|
||||||
|
|
||||||
|
// wowlet: see checkStuckTransactions / relayPendingTransactions
|
||||||
|
QTimer *m_stuckTxTimer = nullptr;
|
||||||
|
QSet<QString> m_stuckTxWarned;
|
||||||
|
int m_broadcastBatchSuccesses = 0;
|
||||||
|
|
||||||
SplashDialog *m_splashDialog = nullptr;
|
SplashDialog *m_splashDialog = nullptr;
|
||||||
AccountSwitcherDialog *m_accountSwitcherDialog = nullptr;
|
AccountSwitcherDialog *m_accountSwitcherDialog = nullptr;
|
||||||
TxPoolViewerDialog *m_txPoolViewerDialog = nullptr;
|
TxPoolViewerDialog *m_txPoolViewerDialog = nullptr;
|
||||||
|
|||||||
+21
-15
@@ -134,10 +134,12 @@ Rectangle {
|
|||||||
Text {
|
Text {
|
||||||
id: hashRateText
|
id: hashRateText
|
||||||
text: "-"
|
text: "-"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.fill: parent
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 4
|
anchors.leftMargin: 4
|
||||||
font.pointSize: 16
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
fontSizeMode: Text.HorizontalFit
|
||||||
|
minimumPointSize: 8
|
||||||
|
font.pointSize: 14
|
||||||
font.family: comicMono.name;
|
font.family: comicMono.name;
|
||||||
antialiasing: false
|
antialiasing: false
|
||||||
color: "#41FF00"
|
color: "#41FF00"
|
||||||
@@ -160,7 +162,7 @@ Rectangle {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "uptime"
|
text: "Uptime"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 4
|
anchors.leftMargin: 4
|
||||||
@@ -179,10 +181,12 @@ Rectangle {
|
|||||||
Text {
|
Text {
|
||||||
id: miningUptime
|
id: miningUptime
|
||||||
text: "-"
|
text: "-"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.fill: parent
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 4
|
anchors.leftMargin: 4
|
||||||
font.pointSize: 12
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
fontSizeMode: Text.HorizontalFit
|
||||||
|
minimumPointSize: 8
|
||||||
|
font.pointSize: 14
|
||||||
font.family: comicMono.name;
|
font.family: comicMono.name;
|
||||||
antialiasing: false
|
antialiasing: false
|
||||||
color: "#41FF00"
|
color: "#41FF00"
|
||||||
@@ -270,7 +274,7 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 8
|
anchors.leftMargin: 8
|
||||||
font.pointSize: 20
|
font.pointSize: 14
|
||||||
font.family: comicMonoBold.name;
|
font.family: comicMonoBold.name;
|
||||||
color: "#41FF00"
|
color: "#41FF00"
|
||||||
antialiasing: false
|
antialiasing: false
|
||||||
@@ -285,10 +289,12 @@ Rectangle {
|
|||||||
Text {
|
Text {
|
||||||
id: heightText
|
id: heightText
|
||||||
text: "-"
|
text: "-"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.fill: parent
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 8
|
anchors.leftMargin: 8
|
||||||
font.pointSize: 18
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
fontSizeMode: Text.HorizontalFit
|
||||||
|
minimumPointSize: 9
|
||||||
|
font.pointSize: 14
|
||||||
font.family: comicMonoBold.name;
|
font.family: comicMonoBold.name;
|
||||||
color: "#41FF00"
|
color: "#41FF00"
|
||||||
antialiasing: false
|
antialiasing: false
|
||||||
@@ -311,7 +317,7 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 8
|
anchors.leftMargin: 8
|
||||||
font.pointSize: 20
|
font.pointSize: 14
|
||||||
font.family: comicMonoBold.name;
|
font.family: comicMonoBold.name;
|
||||||
color: "#41FF00"
|
color: "#41FF00"
|
||||||
antialiasing: false
|
antialiasing: false
|
||||||
@@ -327,9 +333,9 @@ Rectangle {
|
|||||||
id: syncPctText
|
id: syncPctText
|
||||||
text: "-"
|
text: "-"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 8
|
||||||
font.pointSize: 18
|
font.pointSize: 14
|
||||||
font.family: comicMonoBold.name;
|
font.family: comicMonoBold.name;
|
||||||
color: "#41FF00"
|
color: "#41FF00"
|
||||||
antialiasing: false
|
antialiasing: false
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "utils/DaemonManager.h"
|
#include "utils/DaemonManager.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
@@ -62,6 +63,11 @@ void DaemonManager::start() {
|
|||||||
if (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting)
|
if (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// wowlet: a deliberate (re)start clears the stop latch, so a later UNEXPECTED exit can auto-restart
|
||||||
|
// again. Without this, m_stopping stays true after the first node toggle-off and the crash
|
||||||
|
// auto-restart is silently dead for the rest of the session.
|
||||||
|
m_stopping = false;
|
||||||
|
|
||||||
if (!this->unpackBins()) {
|
if (!this->unpackBins()) {
|
||||||
this->setErrorMessage("Unable to unpack the embedded wownerod binary.");
|
this->setErrorMessage("Unable to unpack the embedded wownerod binary.");
|
||||||
return;
|
return;
|
||||||
@@ -85,6 +91,14 @@ void DaemonManager::start() {
|
|||||||
// and surfaces a clear error via handleProcessError().
|
// and surfaces a clear error via handleProcessError().
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wowlet: the retry cap guards against a rapid crash-loop, not legit restarts. If the node had been
|
||||||
|
// up a while before this (re)start, clear the budget — otherwise a few node toggles over a session
|
||||||
|
// trip "maximum retries exceeded" and it won't start again until wowlet is relaunched.
|
||||||
|
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
if (m_lastSpawnMs != 0 && (nowMs - m_lastSpawnMs) > 60000)
|
||||||
|
m_restarts = 0;
|
||||||
|
m_lastSpawnMs = nowMs;
|
||||||
|
|
||||||
m_restarts += 1;
|
m_restarts += 1;
|
||||||
if (m_restarts > 4) {
|
if (m_restarts > 4) {
|
||||||
this->setErrorMessage("wownerod failed to start: maximum retries exceeded");
|
this->setErrorMessage("wownerod failed to start: maximum retries exceeded");
|
||||||
@@ -108,6 +122,14 @@ void DaemonManager::start() {
|
|||||||
arguments << "--no-igd"; // no UPnP port mapping
|
arguments << "--no-igd"; // no UPnP port mapping
|
||||||
arguments << "--out-peers" << "16";
|
arguments << "--out-peers" << "16";
|
||||||
arguments << "--log-file" << QDir(dataDir).filePath("wownerod.log");
|
arguments << "--log-file" << QDir(dataDir).filePath("wownerod.log");
|
||||||
|
// wowlet: disable LMDB batch write mode so every block is committed in its own transaction.
|
||||||
|
// Without this, wownero accumulates blocks in a single large write transaction that is ABORTED
|
||||||
|
// (not committed) when the daemon shuts down mid-sync — causing all progress since the last
|
||||||
|
// batch boundary to be silently lost. This is visible in wownerod.log: sessions consistently
|
||||||
|
// revert to a stale height despite a full graceful shutdown sequence. Using "safe" mode is
|
||||||
|
// slower for initial sync but is the only correct choice for an embedded node that starts and
|
||||||
|
// stops with the wallet; the wownero chain is small enough that the overhead is acceptable.
|
||||||
|
arguments << "--db-sync-mode" << "safe";
|
||||||
|
|
||||||
// Full archival by default (wownero's chain is small); prune is an explicit opt-in.
|
// Full archival by default (wownero's chain is small); prune is an explicit opt-in.
|
||||||
if (conf()->get(Config::pruneBlockchain).toBool())
|
if (conf()->get(Config::pruneBlockchain).toBool())
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ private:
|
|||||||
bool m_synced = false;
|
bool m_synced = false;
|
||||||
bool m_stopping = false;
|
bool m_stopping = false;
|
||||||
int m_restarts = 0;
|
int m_restarts = 0;
|
||||||
|
qint64 m_lastSpawnMs = 0; // wowlet: wall-clock of last spawn — ages out the crash-retry budget
|
||||||
|
|
||||||
static QPointer<DaemonManager> m_instance;
|
static QPointer<DaemonManager> m_instance;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user