Compare commits

..
Author SHA1 Message Date
such-gitea 4050d51760 ci: keep GitHub source checks separate from private release builds 2026-10-17 05:15:39 -04:00
such-gitea 8dacc3ba5a ci: gate Wowlet documentation changes 2026-10-16 14:43:04 -04:00
such-gitea 66c16d152d fix(docs): route Wowlet support to Wownero contacts 2026-10-16 14:27:59 -04:00
jwinterm f4bbfd49ad fix(ci): publish the signed Windows zips to the tag's release
The Linux and macOS jobs already attach their tag-build output to the release,
because this Gitea reports zero artifacts for every run through its REST API and
its web download route answers "Not found." under every auth style. The Windows
job was left out, so even a green signed build produced an exe nobody could
fetch back.

The step runs after the Authenticode, RFC 3161 timestamp and signer checks, so
an unsigned or untimestamped binary can never reach a release. Both archives go
up: the signed one users should download, and the unsigned one that stays
byte-reproducible for verifiers.

curl.exe rather than Invoke-WebRequest, because `shell: powershell` is Windows
PowerShell 5.1 and its Invoke-WebRequest has no -Form for multipart uploads.
Fail-safe like the others: no token, no resolvable release or a non-201 upload
reports itself and exits 0 rather than reddening a build that produced a
correctly signed binary.
2026-09-13 18:09:17 -04:00
jwinterm 142b79ee27 fix(depends): tell the consuming build where the host Qt is
A cross Qt carrying qtdeclarative makes find_package(Qt6) demand a host Qt,
because QML needs host-side tools: qmlcachegen, qmlimportscanner and
qmltyperegistrar. depends already builds one. native_qt installs it to
native/qt-host, and qt.mk passes that path to the cross Qt's own configure.

What nobody passed it to was wowlet. Qt6Config re-runs the host-path setup when
our project calls find_package, finds nothing, and stops with "To use a
cross-compiled Qt, please set the QT_HOST_PATH cache variable". The Windows
cross build has failed that way on every tag since qtdeclarative landed in
June, which is why 5.1.4 shipped macOS and Linux but no exe.

Stating it in the toolchain rather than in the workflow fixes every cross
target and anyone building locally through depends, and the built-cache key
already hashes this file, so the stale mingw tree is rebuilt rather than
restored without the host Qt.
2026-09-13 18:06:55 -04:00
jwinterm a43e9fe122 fix(ci): publish tag builds to the release, where they can be fetched back
Run artifacts upload fine, but this Gitea reports zero artifacts for every run
through its REST API, and the web download route answers "Not found." under
token, bearer and basic auth alike. A signed build was therefore unreachable
from anywhere except the browser session that produced it.

Release assets do work: create, upload, list and download all round-trip with
an ordinary token, so tag builds now attach their output to the tag's release
as well as to the run. Retrieval must use browser_download_url; the asset-id
API path returns the asset's JSON metadata even when asked for octet-stream.

The step is fail-safe on purpose. With no token, no resolvable release, or a
non-201 upload it explains itself and exits 0 rather than turning a green
build red. The macOS upload also gains if-no-files-found, without which an
empty publish passes silently.
2026-09-13 13:18:22 -04:00
jwinterm ce84b1d062 fix(ci): scope the embed extracts to the run, not to a shared /tmp
ubuntu-latest and windows-latest both resolve to the same host in host mode,
and both jobs began with `rm -rf /tmp/wownerod-embed`. Dispatched together for
5.1.4, the Windows fetch deleted the Linux job's binary in the eight seconds
between its own `ls` showing the file present and cmake configuring, so the
Linux build died on "WOWNEROD_DIR was specified but the wownerod binary could
not be found" while the identical workflow had passed minutes earlier when it
ran alone.

Both jobs now extract under /tmp/wowlet-$GITHUB_RUN_ID, so concurrent runs stop
sharing state. macOS is left alone: it runs on its own host, it never rm -rf's
the directory, and it is the one platform currently building green.
2026-09-13 13:13:35 -04:00
jwinterm 0e6113729d chore(release): 5.1.4
Carries the binary rename: the shipped executable, the macOS bundle and the
Linux desktop entry are all wowlet now, where 5.1.3 still shipped feather.
2026-09-13 13:03:50 -04:00
such-gitea 27d7b346c0 fix(build): ship the binary as wowlet, not feather 2026-09-13 10:57:32 -04:00
jwinterm ea9c73826b fix(build): ship the binary as wowlet, not feather
The CMake target keeps its upstream name so the twenty-odd target_* calls stay
put, but OUTPUT_NAME now produces build/bin/wowlet, and on macOS a wowlet.app
whose CFBundleExecutable matches it.

Linux and Windows already renamed the artifact as they copied it, so their cp
lines just move to the new path. macOS copied the whole bundle instead, so it
shipped Contents/MacOS/feather with a Feather name in the Info.plist, which is
what anyone installing it actually saw. The Linux install rules had the same
problem one level down: they installed feather.desktop, which sets Exec=feather
and Name=Feather Wallet, while the correct wowlet.desktop sat unused beside it.

CFBundleIdentifier moves to org.wownero.wowlet. Nothing in signing or
notarization reads it, but macOS keys per-app state by bundle id, so an existing
install starts with fresh window state.
2026-09-13 10:49:00 -04:00
jwinterm 5a9605c87b release: wowlet 5.1.3
Carries the macOS QML bundling fix, so the mining tab loads on mac instead of
painting grey, and the mining offer restricted to a node that is ours.
2026-09-12 20:57:13 -04:00
jwinterm db151c2920 fix(ci/macos): bundle the QML modules the app actually imports
5.1.2 shipped a grey mining tab on macOS while Linux was fine. The app said
why: 'qrc:/qml/mining.qml:2:1: module "QtQuick.Layouts" is not installed',
and Contents/Resources held only appicon.icns and qt.conf, with no qml
directory at all. macdeployqt cannot scan .qrc resources, and it was called
without -qmldir, so the bundle shipped no QML modules; scene.qml survived only
because it imports nothing beyond QtQuick.

Pass -qmldir, then blanket-copy the QtQuick and QtQml trees as the AppImage
build already does, because qmlimportscanner does not follow a module's own
qmldir depends chain. Both happen before the dylib passes so new plugin deps
resolve, and before signing, which already sweeps every non-framework Mach-O.
A guard fails the build when a module is missing, because an absent QML module
is invisible until someone opens the tab.
2026-09-12 20:49:23 -04:00
jwinterm c7fca31653 fix(mining): only offer to mine on a node that is ours
Start Mining was live regardless of which daemon the wallet was talking
to. It is a `start_mining` RPC to the connected node, so on a remote one
it is either refused — the button appearing to do nothing — or, if the
operator left the RPC open, it sets someone else's machine mining to our
address. Neither is a thing a wallet should offer.

The axe now greys out unless `Nodes::connection().isLocal()`, hovering it
explains why, and clicking anyway flashes the same notice rather than
silently doing nothing. `onStartMining` refuses too: the QML disables the
control, but a signal is not a promise.

isLocal() counts localhost, .local, 127/8 and the RFC1918 ranges, so the
embedded node and a daemon on the LAN both qualify.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-04 05:36:45 -04:00
jwinterm 081c01ada9 release: wowlet 5.1.2
Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-03 15:12:27 -04:00
jwinterm 613d4e7f61 Merge scene work for 5.1.2
The ambient scene grows up: the hidden-tab bug that rained +1s out of an
empty sky is fixed at both ends, the wipeout is a skid rather than a
stunt, clearing a field says WIN, and each tab gets its own amount of
scene — History runs, Receive sits, Send is money going past.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-03 15:12:18 -04:00
jwinterm 79daae6796 fix(send): the scene sits in the middle of the space below the form
It was butted straight up against the Clear/Send row, so a band of night
sky touched a button and read as part of the form. Now there is a fixed
14px gap under the buttons and a stretch either side of the scene, which
centres it in whatever space the form leaves.

And the coins come in handfuls. One at a time reads as a metronome, so
Send now has three of them: usually one goes past, sometimes two, and
about one time in seven all three, staggered by a beat so they arrive
together rather than in a rank. Each rolls its own height, size, duration
and spin direction.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-03 15:10:48 -04:00
jwinterm b1e751a6b7 fix(scene): the sitting doge belongs to Receive only
He was filling History's empty beat as well, left over from before
Receive had a scene of its own. Two tabs showing the same sitting doge
reads as one tab's scene leaking into the other, and History is meant to
be the tab where he runs.

History's opening beat is now sky, grass, clouds and the sun — the scene
breathing before the coins come out — and the sitting doge appears on
Receive and nowhere else. The per-lap idle roll goes with him.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-03 15:04:41 -04:00
jwinterm 068e4c2aa5 fix(scene): money no longer vanishes mid-flight, and the quiet tabs have something to watch
Send's bills were being cut off halfway across. Money mode reused the
run's lap machinery, so every 9-14 seconds the cycle restarted and reset
every bill to the start of its flight — and a roll whose delay plus
duration ran past the cycle simply disappeared wherever it had got to.

Bills now keep their own clock: cross, re-roll, wait, cross again, with
the re-roll deciding afresh whether that bill flies at all. Nothing
resets them from outside, so nothing can cut them short, and the sky
thins and fills instead of carrying identical traffic for ever. 26
seconds of frames now contain no mid-screen disappearance.

Both quiet tabs get an ambient clock, because a form is a long time to
look at a dog breathing:

  * Send: a gold coin arcs across on its own trajectory now and then,
    bills tumble in proportion to their speed, and at night the odd
    shooting star.
  * Receive: a coin drops out of the sky, bounces, and settles in the
    grass beside the doge with a small sparkle burst — a coin arriving is
    the entire point of that tab — and he perks up when it lands. Birds
    fly by in daylight. It lands to one side of him and clear of the sun,
    which is where the first version put it.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-03 14:17:54 -04:00
jwinterm f1144c483e fix(scene): the wipeout is a skid, the win says so, and each tab gets its own amount of scene
The crash launched him. Rotating 66 degrees about a pivot inside the
sprite, shearing him through the middle and scaling him about the same
point left him diagonal in mid-air, elongated, nowhere near the ground —
a stunt, not a trip.

It is now a face-plant and a skid: 26 degrees, a 21px drop so the snout
reaches the grass, and 26px of forward slide along the ground with the
dust coming up. The shear is gone entirely. The pivot is the snout at the
grass line rather than a point inside the body.

The other half of looking airborne was the frozen frame: stopping a run
cycle leaves whatever mid-stride pose the gif was on, legs extended. He
now lands on frame 4, the one with the legs tucked under him.

Clearing the field and getting across now says WIN, with the tally, the
way eating it says GAME OVER. Before, a won lap just ended and the next
one opened with a doge sitting down, which reads as nothing having
happened.

And the scene comes in three amounts, because a form should not have to
compete with a dog: History runs the whole thing, Receive is a doge
sitting in the field, Send is money blowing past and nothing else. One
QML file, gated by mode; the beat machine only runs where there is a run.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-03 13:07:04 -04:00
jwinterm e5b8ef6a00 fix(scene): park hidden tabs, and put some weight behind the doge
The +1s raining out of an empty sky came from a scene running on a tab
that had never been shown. An unshown QQuickWidget is 0px wide, and at
0px every coin sits at x=0, so the doge "collected" the whole field in
one step; the pickup pops from that phantom lap were still mid-flight
when the tab was finally selected and the real width arrived. Their
`SequentialAnimation on opacity { running: <bound> }` was cut off by the
width change, which leaves the property wherever it stopped — so a +1
hung in the air for the rest of the lap, next to a coin nobody had
taken.

Both halves are fixed:

  * SceneWidget parks its scene on hide and starts a fresh lap on show,
    and the QML refuses to run at all until it has been laid out. A
    background tab now costs nothing, too.
  * Every one-shot flourish (+1, bark, dust, GAME OVER) is started and
    reset from an explicit handler, so an interrupted lap cannot strand
    one on screen.

The rest is the ambient scene earning its keep:

  * The face-plant has weight. He tips over his front paws — pivoting on
    the paw at the grass line, not the middle of the sprite, which used
    to swing his snout through the ground — squashes at the nose, bends
    through the body, throws dust off the impact, and lies there
    breathing. GAME OVER now lands a beat later so the crash reads on
    its own, and the lap ends shortly after instead of running out the
    clock on a wreck.
  * He has opinions: HRNMMROOF, BONK, SNOOT.EXE and friends on a
    wipeout; MUCH COIN, AMAZE, VERY WIN when he clears the field, with a
    victory hop to go with it.
  * More variety per lap: zero to three bills at their own speeds and
    heights, a 1-in-3 fat +5 coin with a halo, a doge sitting in the
    field during the empty beat, drifting clouds, a soft corona on the
    sun, blinking stars and the odd shooting star at night, birds by
    day, and a run speed that is no longer always eight seconds.

Verified by rendering scene.qml headless (qml6 + xvfb, frame grabs) at
the real widget sizes, and by driving two scenes through a QTabWidget to
watch the hidden-tab case before and after.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
2026-09-02 22:34:23 -04:00
27 changed files with 1357 additions and 239 deletions
+64 -19
View File
@@ -1,7 +1,7 @@
name: linux appimage (gitea self-hosted)
# Runs ONLY on Gitea (GitHub ignores .gitea/workflows), in the private Builds
# repository. Gitea is master; GitHub is a mirror and a release surface.
# repository. GitHub wownero/wowlet owns source and release publication.
#
# Unlike the Windows lane, which builds a static toolchain of its own through
# contrib/depends, the x86_64 AppImage links against the host's Qt6. Those
@@ -42,30 +42,36 @@ jobs:
- name: fetch wownerod to embed
run: |
rm -rf /tmp/wownerod-embed && mkdir -p /tmp/wownerod-embed
curl -fL -o /tmp/wownero.tar.bz2 \
# ubuntu-latest and windows-latest both resolve to the same host in
# host mode, so a fixed /tmp path lets a concurrently dispatched job's
# `rm -rf` delete this job's binary between fetch and configure. Scope
# every extract to the run.
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
rm -rf "$EMBED/wownerod-embed" && mkdir -p "$EMBED/wownerod-embed"
curl -fL -o "$EMBED/wownero.tar.bz2" \
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-linux-gnu-v0.11.4.0.tar.bz2
rm -rf /tmp/wow-x && mkdir -p /tmp/wow-x
tar xjf /tmp/wownero.tar.bz2 -C /tmp/wow-x
find /tmp/wow-x -name wownerod -type f -exec cp {} /tmp/wownerod-embed/ \;
chmod +x /tmp/wownerod-embed/wownerod
ls -la /tmp/wownerod-embed
rm -rf "$EMBED/wow-x" && mkdir -p "$EMBED/wow-x"
tar xjf "$EMBED/wownero.tar.bz2" -C "$EMBED/wow-x"
find "$EMBED/wow-x" -name wownerod -type f -exec cp {} "$EMBED/wownerod-embed/" \;
chmod +x "$EMBED/wownerod-embed/wownerod"
ls -la "$EMBED/wownerod-embed"
- name: fetch tor to embed
run: |
rm -rf /tmp/tor-embed && mkdir -p /tmp/tor-embed
curl -fL -o /tmp/teb.tgz \
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
rm -rf "$EMBED/tor-embed" && mkdir -p "$EMBED/tor-embed"
curl -fL -o "$EMBED/teb.tgz" \
https://archive.torproject.org/tor-package-archive/torbrowser/15.0/tor-expert-bundle-linux-x86_64-15.0.tar.gz
rm -rf /tmp/teb && mkdir -p /tmp/teb && tar xzf /tmp/teb.tgz -C /tmp/teb
cp /tmp/teb/tor/tor /tmp/teb/tor/libssl.so.3 /tmp/teb/tor/libevent-2.1.so.7 \
/tmp/teb/tor/libcrypto.so.3 /tmp/tor-embed/
rm -rf "$EMBED/teb" && mkdir -p "$EMBED/teb" && tar xzf "$EMBED/teb.tgz" -C "$EMBED/teb"
cp "$EMBED/teb/tor/tor" "$EMBED/teb/tor/libssl.so.3" "$EMBED/teb/tor/libevent-2.1.so.7" \
"$EMBED/teb/tor/libcrypto.so.3" "$EMBED/tor-embed/"
# The expert bundle's tor carries no rpath; point it and its libs at
# $ORIGIN so the embedded copy resolves them from the extract dir on
# any distro.
for f in tor libssl.so.3 libevent-2.1.so.7 libcrypto.so.3; do
patchelf --set-rpath '$ORIGIN' /tmp/tor-embed/$f
patchelf --set-rpath '$ORIGIN' "$EMBED/tor-embed/$f"
done
chmod +x /tmp/tor-embed/tor
chmod +x "$EMBED/tor-embed/tor"
- name: build
run: |
@@ -73,15 +79,16 @@ jobs:
# binary, or TorManager::unpackBins() sees embedded > unpacked and
# rewrites Tor on every launch. The bundle's release number (15.0) is
# not its tor version, so derive it.
TORVER=$(/tmp/tor-embed/tor --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1)
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
TORVER=$("$EMBED/tor-embed/tor" --version | grep -oE '[0-9]+(\.[0-9]+){3}' | head -1)
echo "embedded tor version: $TORVER"
# ARCH=x86-64, never native: the build host's CPU is newer than many
# users' and native bakes in unsupported instructions -> SIGILL.
cmake -S . -B build \
-DWITH_SCANNER=OFF \
-DARCH=x86-64 \
-DWOWNEROD_DIR=/tmp/wownerod-embed \
-DTOR_DIR=/tmp/tor-embed \
-DWOWNEROD_DIR="$EMBED/wownerod-embed" \
-DTOR_DIR="$EMBED/tor-embed" \
-DTOR_VERSION="$TORVER"
cmake --build build -j6
ls -la build/bin/
@@ -91,7 +98,7 @@ jobs:
rm -rf AppDir
mkdir -p AppDir/usr/bin AppDir/usr/share/applications \
AppDir/usr/share/icons/hicolor/256x256/apps
cp build/bin/feather AppDir/usr/bin/wowlet
cp build/bin/wowlet AppDir/usr/bin/wowlet
chmod +x AppDir/usr/bin/wowlet
cp src/assets/wowlet.desktop AppDir/usr/share/applications/wowlet.desktop
cp src/assets/images/appicons/256x256.png \
@@ -147,3 +154,41 @@ jobs:
name: wowlet-x86_64-linux-AppImage
path: wowlet-x86_64.AppImage
if-no-files-found: error
- name: publish the AppImage to the tag's release
# The artifact above uploads fine but this Gitea's REST listing reports
# zero artifacts for every run, so a tag build's output cannot be fetched
# back through the API. Release assets can, so publish there as well.
# Fail-safe by design: with no usable token this says why and exits 0,
# it never turns a green build red.
if: startsWith(github.ref, 'refs/tags/')
env:
RELEASE_TOKEN: ${{ github.token }}
run: |
set -eu
FILE=wowlet-x86_64.AppImage
TAG="${GITHUB_REF#refs/tags/}"
API="${GITHUB_SERVER_URL:-https://git.such.software}/api/v1/repos/$GITHUB_REPOSITORY"
if [ -z "${RELEASE_TOKEN:-}" ]; then
echo "no token available to this runner, skipping the release upload" >&2
exit 0
fi
H="Authorization: token $RELEASE_TOKEN"
# Single-line python on purpose: a continuation line at column 0 would
# end this block scalar and break the workflow's YAML. `|| true` keeps
# a malformed response from aborting the step under `set -e`.
rid=$(curl -sS -H "$H" "$API/releases/tags/$TAG" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$rid" ]; then
rid=$(curl -sS -H "$H" -H "Content-Type: application/json" -X POST \
-d "{\"tag_name\":\"$TAG\",\"name\":\"wowlet $TAG\"}" "$API/releases" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
fi
if [ -z "$rid" ]; then
echo "could not resolve or create a release for $TAG, skipping the upload" >&2
exit 0
fi
code=$(curl -sS -H "$H" -o /dev/null -w '%{http_code}' -X POST \
-F "attachment=@$FILE" "$API/releases/$rid/assets?name=$(basename "$FILE")")
echo "release asset upload returned HTTP $code"
if [ "$code" != "201" ]; then
echo "the release upload did not return 201; the run artifact still holds the build" >&2
fi
+77 -2
View File
@@ -103,8 +103,39 @@ jobs:
# 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
# bundle Qt frameworks/plugins so the app runs off-machine.
# -qmldir is not optional: our QML lives in .qrc resources, which
# macdeployqt cannot scan, so without it the bundle ships no QML
# modules at all and every page importing anything beyond QtQuick
# fails to load. That is how 5.1.2 shipped a grey mining tab on mac
# while Linux was fine: Contents/Resources held no qml directory, and
# the app logged 'module "QtQuick.Layouts" is not installed'.
/opt/homebrew/opt/qt/bin/macdeployqt dist/Wowlet.app -qmldir=../src/assets/qml
# qmlimportscanner reads only the `import` lines in our QML and does
# not follow a module's own qmldir depends chain, so transitive
# modules (QtQuick -> QtQml.WorkerScript/Models) are silently skipped.
# The AppImage build hit this twice and blanket-copies the trees; do
# the same here, before the dylib passes below so their deps resolve.
# Ask Qt where its QML modules live rather than assuming a brew
# layout; the AppImage build asks qmake the same question.
QTPREFIX="$(brew --prefix qt)"
QMLSRC="$("$QTPREFIX/bin/qmake6" -query QT_INSTALL_QML 2>/dev/null || echo "$QTPREFIX/share/qt/qml")"
[ -d "$QMLSRC" ] || { echo "ERROR: Qt QML modules not found at $QMLSRC" >&2; exit 1; }
mkdir -p dist/Wowlet.app/Contents/Resources/qml
for m in QtQuick QtQml; do
cp -Rn "$QMLSRC/$m" dist/Wowlet.app/Contents/Resources/qml/ 2>/dev/null || true
done
# Guard: the module the mining tab needs has to be in the bundle. A
# missing QML module is invisible until someone opens the tab, so it
# fails the build instead of shipping a page that paints grey.
for m in QtQuick QtQuick/Layouts; do
[ -d "dist/Wowlet.app/Contents/Resources/qml/$m" ] || {
echo "ERROR: $m missing from the bundle; QML pages will not load" >&2
exit 1
}
done
# macdeployqt bundles Qt frameworks but not the plain-dylib brew deps
# of Qt's image plugins (libwebp/libsharpyuv/libbrotli...), which are
@@ -216,3 +247,47 @@ jobs:
name: wowlet-macos
path: build/wowlet-macos-*.zip
retention-days: 14
# Without this an empty upload passes silently, which is how a signed
# build can appear to publish while shipping nothing.
if-no-files-found: error
- name: publish the signed zip to the tag's release
# Same reason as the Linux job: run artifacts upload but this Gitea's
# REST listing returns zero for every run, so the notarized zip cannot
# be fetched back through the API. Release assets can be. Fail-safe:
# with no usable token this says why and exits 0.
if: startsWith(github.ref, 'refs/tags/')
env:
RELEASE_TOKEN: ${{ github.token }}
run: |
set -eu
FILE=$(ls build/wowlet-macos-*.zip 2>/dev/null | head -1)
if [ -z "$FILE" ]; then
echo "no macOS zip to publish; the packaging step should have failed first" >&2
exit 0
fi
TAG="${GITHUB_REF#refs/tags/}"
API="${GITHUB_SERVER_URL:-https://git.such.software}/api/v1/repos/$GITHUB_REPOSITORY"
if [ -z "${RELEASE_TOKEN:-}" ]; then
echo "no token available to this runner, skipping the release upload" >&2
exit 0
fi
H="Authorization: token $RELEASE_TOKEN"
# Single-line python on purpose: a continuation line at column 0 would
# end this block scalar and break the workflow's YAML. `|| true` keeps
# a malformed response from aborting the step under `set -e`.
rid=$(curl -sS -H "$H" "$API/releases/tags/$TAG" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$rid" ]; then
rid=$(curl -sS -H "$H" -H "Content-Type: application/json" -X POST \
-d "{\"tag_name\":\"$TAG\",\"name\":\"wowlet $TAG\"}" "$API/releases" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
fi
if [ -z "$rid" ]; then
echo "could not resolve or create a release for $TAG, skipping the upload" >&2
exit 0
fi
code=$(curl -sS -H "$H" -o /dev/null -w '%{http_code}' -X POST \
-F "attachment=@$FILE" "$API/releases/$rid/assets?name=$(basename "$FILE")")
echo "release asset upload returned HTTP $code"
if [ "$code" != "201" ]; then
echo "the release upload did not return 201; the run artifact still holds the build" >&2
fi
+55 -7
View File
@@ -1,7 +1,7 @@
name: windows build + sign (gitea self-hosted)
# Runs ONLY on Gitea (GitHub ignores .gitea/workflows), in the private Builds
# repository. Gitea is master; GitHub is a mirror and a release surface.
# repository. GitHub wownero/wowlet owns source and release publication.
#
# Two jobs on two different machines, and that split is the whole point:
#
@@ -98,13 +98,17 @@ jobs:
- name: fetch wownerod.exe to embed
run: |
rm -rf /tmp/wownerod-embed && mkdir -p /tmp/wownerod-embed
curl -fL -o /tmp/wownero-win.zip \
# This job and the Linux one land on the same host in host mode, and a
# fixed /tmp path let each wipe the other's binary mid-build. Scope the
# extract to the run.
EMBED="/tmp/wowlet-${GITHUB_RUN_ID:-local}"
rm -rf "$EMBED/wownerod-embed" && mkdir -p "$EMBED/wownerod-embed"
curl -fL -o "$EMBED/wownero-win.zip" \
https://github.com/wownero/wownero-codeberg-mirror/releases/download/v0.11.4.0/wownero-x86_64-w64-mingw32-v0.11.4.0.zip
# -j flattens the versioned subdir so wownerod.exe is a DIRECT child
# (WownerodQrcGenerator globs ${WOWNEROD_DIR}/* non-recursively).
unzip -o -j /tmp/wownero-win.zip 'wownero-x86_64-w64-mingw32-v0.11.4.0/wownerod.exe' -d /tmp/wownerod-embed
ls -la /tmp/wownerod-embed
unzip -o -j "$EMBED/wownero-win.zip" 'wownero-x86_64-w64-mingw32-v0.11.4.0/wownerod.exe' -d "$EMBED/wownerod-embed"
ls -la "$EMBED/wownerod-embed"
- name: build wowlet.exe
run: |
@@ -118,7 +122,7 @@ jobs:
-DCHECK_UPDATES=OFF \
-DTOR_INSTALLED=On \
-DFEATHER_TARGET_TRIPLET="$HOST" \
-DWOWNEROD_DIR=/tmp/wownerod-embed \
-DWOWNEROD_DIR="/tmp/wowlet-${GITHUB_RUN_ID:-local}/wownerod-embed" \
..
cmake --build . -j6
ls -la bin/
@@ -129,7 +133,7 @@ jobs:
# archive contains the signed binary.
run: |
mkdir -p stage/tor
cp build/bin/feather.exe stage/wowlet.exe
cp build/bin/wowlet.exe stage/wowlet.exe
cp "contrib/depends/$HOST/Tor/tor.exe" stage/tor/tor.exe
(cp LICENSE stage/LICENSE.txt 2>/dev/null || cp COPYING stage/LICENSE.txt 2>/dev/null) || true
ls -la stage stage/tor
@@ -266,3 +270,47 @@ jobs:
wowlet-x86_64-windows.zip
wowlet-x86_64-windows-unsigned.zip
if-no-files-found: error
- name: publish the Windows zips to the tag's release
# Same reason as the Linux and macOS jobs: artifacts upload fine, but
# this Gitea's REST listing reports zero for every run, so the signed exe
# cannot be fetched back through the API. Release assets can be.
#
# This runs after the Authenticode, timestamp and signer checks above, so
# an unsigned or untimestamped binary never reaches a release. curl.exe,
# not Invoke-WebRequest: `shell: powershell` is Windows PowerShell 5.1,
# whose Invoke-WebRequest has no -Form for multipart uploads.
#
# Fail-safe: a build that produced a correctly signed binary must not go
# red because publishing failed, so every bad path reports and exits 0.
if: startsWith(github.ref, 'refs/tags/')
shell: powershell
env:
RELEASE_TOKEN: ${{ github.token }}
run: |
$ErrorActionPreference = 'Continue'
if (-not $env:RELEASE_TOKEN) {
Write-Host "no token available to this runner, skipping the release upload"
exit 0
}
$tag = $env:GITHUB_REF -replace '^refs/tags/', ''
$server = if ($env:GITHUB_SERVER_URL) { $env:GITHUB_SERVER_URL } else { 'https://git.such.software' }
$api = "$server/api/v1/repos/$env:GITHUB_REPOSITORY"
$auth = "Authorization: token $env:RELEASE_TOKEN"
$rid = ''
try { $rid = ((curl.exe -sS -H $auth "$api/releases/tags/$tag") | ConvertFrom-Json).id } catch { $rid = '' }
if (-not $rid) {
try {
$body = "{""tag_name"":""$tag"",""name"":""wowlet $tag""}"
$rid = ((curl.exe -sS -H $auth -H "Content-Type: application/json" -X POST -d $body "$api/releases") | ConvertFrom-Json).id
} catch { $rid = '' }
}
if (-not $rid) {
Write-Host "could not resolve or create a release for $tag, skipping the upload"
exit 0
}
foreach ($f in @('wowlet-x86_64-windows.zip', 'wowlet-x86_64-windows-unsigned.zip')) {
if (-not (Test-Path $f)) { Write-Host "missing $f, nothing to publish for it"; continue }
$code = curl.exe -sS -o NUL -w '%{http_code}' -H $auth -F "attachment=@$f" "$api/releases/$rid/assets?name=$f"
Write-Host "release asset upload for $f returned HTTP $code"
}
+2 -1
View File
@@ -1,6 +1,7 @@
name: ci/gh-actions/build
on: [push, pull_request, workflow_dispatch]
# Legacy builds are manual only. Release artifacts are built in Builds/wowlet.
on: [workflow_dispatch]
# NOTE (wownero-rebase branch): the Arch + macOS jobs from upstream Feather's build.yml are
# temporarily disabled to get a fast, cheap iteration loop while bringing up the build against the
+16
View File
@@ -0,0 +1,16 @@
name: source checks
on: [push, pull_request]
permissions:
contents: read
jobs:
docs:
name: Wowlet documentation gate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Check generated documentation and support contacts
run: make check
+2 -1
View File
@@ -1,6 +1,7 @@
name: ci/gh-actions/guix
on: [push, pull_request]
# Legacy builds are manual only. Release artifacts are built in Builds/wowlet.
on: [workflow_dispatch]
jobs:
cache-sources:
+1 -1
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.18)
project(feather
VERSION "5.1.1"
VERSION "5.1.4"
DESCRIPTION "A free Monero desktop wallet"
LANGUAGES CXX C ASM
)
+4 -1
View File
@@ -13,4 +13,7 @@ clean:
DEFAULT_GOAL := default
default: build
.PHONY: default build attest verify clean
check:
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s contrib/docs/tests -v
.PHONY: default build attest verify clean check
+28 -44
View File
@@ -1,60 +1,44 @@
# Feather Wallet
# Wowlet
Feather is a free Monero desktop wallet for Linux, Tails, macOS and Windows. It is written in C++ with the Qt framework.
- **easy-to-use**, **small** and **fast** - Feather runs well on any modern hardware, including virtual machines and live operating systems.
- **beginner friendly**, but also caters to advanced Monero users by providing a [feature set](https://docs.featherwallet.org/guides/features) that is on par with the official CLI.
- ships with **sane defaults** that suit most users, but can also be configured for high or uncommon threat models.
- serves as a testing grounds for **experimental features** that may later be adopted in the reference wallets.
Wowlet is a Wownero desktop wallet for Linux, macOS and Windows, built with C++ and
Qt. It is based on [Feather Wallet](https://github.com/feather-wallet/feather).
## Download
You can download Feather from **[featherwallet.org](https://featherwallet.org/download/)** or **[GitHub](https://github.com/feather-wallet/feather/releases)**.
Download Wowlet and its signed release hashes from
[Wowlet releases](https://github.com/wownero/wowlet/releases).
If you need help installing Feather, check the [installation documentation](https://docs.featherwallet.org/).
## Support
We recommend that you verify downloads with GPG. Releases are signed with our [release signing key](https://docs.featherwallet.org/guides/release-signing-key). The fingerprint is:
- Matrix: [#wownero-gen:wowne.ro](https://matrix.to/#/#wownero-gen:wowne.ro)
- IRC: `#wownero` on [OFTC](https://www.oftc.net/), or use
[webchat](https://webchat.oftc.net/?randomnick=1&channels=wownero)
- Email: [wownero@wownero.org](mailto:wownero@wownero.org)
- Bug reports: [Wowlet issues](https://github.com/wownero/wowlet/issues)
```
8185 E158 A333 30C7 FD61 BC0D 1F76 E155 CEFB A71C
```
If nobody replies immediately, stay in the room and give volunteers time to respond.
Include your Wowlet version, operating system, reproduction steps and any error
message. Never share your wallet seed, private keys, password or wallet files.
Windows releases are code-signed for free by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
## Resources
* [Official Site](https://featherwallet.org)
* [Documentation](https://docs.featherwallet.org)
* [Git Repository](https://github.com/feather-wallet/feather)
* [Matrix](https://matrix.to/#/#feather:monero.social)
* IRC: `#feather` on [OFTC](https://www.oftc.net/)
* Mail: dev@featherwallet.org
If you need help with your wallet, please contact us via Matrix or IRC.
If you don't have an IRC client, you can join the room via [webchat](https://webchat.oftc.net/?randomnick=1&channels=feather).
If you don’t receive a response immediately please idle in the room.
## Release Builds
To learn how to run a bootstrappable release build, see: [contrib/guix/README.md](https://github.com/feather-wallet/feather/blob/master/contrib/guix/README.md)
For release attestations, see the [feather-sigs](http://github.com/feather-wallet/feather-sigs) repo.
For release policy, see: [RELEASE.md](https://github.com/feather-wallet/feather/blob/master/RELEASE.md)
Report vulnerabilities privately using [SECURITY.md](SECURITY.md).
## Development
If you are looking to set up a development environment for Feather, see [HACKING.md](https://github.com/feather-wallet/feather/blob/master/HACKING.md).
[GitHub](https://github.com/wownero/wowlet) is the canonical source repository.
Release builds run in our private Builds system. Signed release artifacts and
hashes are published on GitHub Releases. GitHub runs lightweight source checks;
its legacy build workflows require an explicit manual dispatch.
It is highly recommended that you join our Matrix or IRC channel if you are hacking on Feather.
Idling in this channel is the best way to stay updated on best practices and new developments.
The application includes an offline documentation browser under Help. Its guides
are generated by `contrib/docs/generate.py` from the Feather documentation submodule.
Wowlet-specific guides in `contrib/docs/overrides` take precedence over upstream
guides with the same filename. Edit those sources, then regenerate; files under
`src/assets/docs` are generated and must not be edited directly.
For information on how Feather is maintained, see: [MAINTENANCE.md](https://github.com/feather-wallet/feather/blob/master/MAINTENANCE.md)
To report a security vulnerability, see: [SECURITY.md](https://github.com/feather-wallet/feather/blob/master/SECURITY.md)
Run the documentation regression checks with `make check`. The inherited build
instructions are in [HACKING.md](HACKING.md).
## License
Feather is free and open-source software, [licensed under BSD-3](https://raw.githubusercontent.com/feather-wallet/feather/master/LICENSE).
Copyright (c) 2020-2025, The Monero Project
Wowlet is free and open-source software under [BSD-3-Clause](LICENSE).
Upstream copyright notices are retained in the source.
+7 -39
View File
@@ -2,43 +2,11 @@
## Reporting a Vulnerability
Please do not open an issue to report security issues.
Report Wowlet security vulnerabilities privately to
[wownero@wownero.org](mailto:wownero@wownero.org).
Please do not disclose them in public issues or community chat before the
maintainers have had time to investigate.
To report a vulnerability email to dev@featherwallet.org
The following keys may be used to communicate sensitive information to developers:
| Name | Fingerprint |
|---------|---------------------------------------------------|
| tobtoht | E87B D921 CDD8 85C9 D78A 38C5 E45B 10DD 027D 2472 |
Public keys can be found in [`utils/pubkeys`](utils/pubkeys).
## Bug Bounty Program
A bounty may be rewarded to a vulnerability report **if and only if** the issue can result in **a loss of funds**.
You must describe a **plausible scenario** in which a loss of funds can occur (or has occurred) that **isn't solely attributable to user error**.
Only **the code** of the **latest tagged release** of **[this repository](https://github.com/feather-wallet/feather/)** is in scope.
**The bounty can only be rewarded in XMR**. The bounty amount for your report is determined by the maintainers and ranges from USD 150 to USD 3000 (in terms of XMR) and depends on the severity of the issue and other factors.
Clarifications on scope:
- The issue **must be present in a [signed](https://docs.featherwallet.org/guides/release-signing-key) [release build](https://github.com/feather-wallet/feather/blob/master/contrib/guix/README.md)**. Custom builds, including distribution packages, are out of scope.
- The developers **must be able to reproduce and fix the issue**. If the issue cannot be fixed **in our code** for any reason, it is out of scope.
- The live [websites](https://github.com/feather-wallet/feather-site/blob/master/mirrors.txt) and their repositories are out of scope.
- Loss of funds due to malware on the user's machine is out of scope.
- Memory imaging, including cold boot attacks, is out of scope.
- Social engineering against users is out of scope. This includes messages received from the [websocket server](https://docs.featherwallet.org/guides/websocket).
- Any form of coercion, physical or psychological, is out of scope.
- Vulnerabilities that are attributable to hardware are out of scope.
- If the issue was fixed in the `master` branch before we receive your report, it is invalid and not eligible for a bounty from this program.
- If the vulnerability involves binary exploitation, we may ask you to provide a proof of concept of secret key exfiltration.
- Vulnerabilities that are present in the monero submodule but were not introduced in patches made by the Feather developers must
be reported [upstream](https://github.com/monero-project/meta/blob/master/VULNERABILITY_RESPONSE_PROCESS.md) and are not eligible for a bounty from this program.
- Vulnerabilities that are present in any of our third-party dependencies must be reported upstream and are not eligible for a bounty from this program.
- Vulnerabilities that are present in [supported hardware wallets](https://docs.featherwallet.org/guides/hardware-wallet-support) must be reported upstream and are not eligible for a bounty from this program.
- A bounty will not be awarded if the reported vulnerability was already known. We may make an exception if you demonstrate that the severity of the issue was underestimated and no immediate fix was planned.
- If, during your research, you disrupt Feather's release infrastructure or services, or attempt to coerce its developers, you will not be awarded a bounty.
Include the affected Wowlet version, operating system, expected impact and steps
to reproduce the issue. Use a disposable wallet for any reproduction and never
send a real wallet seed, private keys, password or wallet files.
+1 -1
View File
@@ -5,4 +5,4 @@ echo $HERE
# Tails 5.8 sets QT_QPA_PLATFORM to wayland which breaks our app
export QT_QPA_PLATFORM=xcb
${APPDIR}/usr/bin/feather $*
${APPDIR}/usr/bin/wowlet $*
+13 -13
View File
@@ -5,29 +5,29 @@ unset SOURCE_DATE_EPOCH
# Manually create the AppImage (reproducibly) since linuxdeployqt is not able to create cross-compiled AppImages
APPDIR="$PWD/feather.AppDir"
APPDIR="$PWD/wowlet.AppDir"
mkdir -p "$APPDIR"
mkdir -p "$APPDIR/usr/share/applications/"
mkdir -p "$APPDIR/usr/bin"
cp "src/assets/feather.desktop" "$APPDIR/usr/share/applications/feather.desktop"
cp "src/assets/feather.desktop" "$APPDIR/feather.desktop"
cp "src/assets/images/appicons/64x64.png" "$APPDIR/feather.png"
cp "build/bin/feather" "$APPDIR/usr/bin/feather"
chmod +x "$APPDIR/usr/bin/feather"
cp "src/assets/wowlet.desktop" "$APPDIR/usr/share/applications/wowlet.desktop"
cp "src/assets/wowlet.desktop" "$APPDIR/wowlet.desktop"
cp "src/assets/images/appicons/64x64.png" "$APPDIR/wowlet.png"
cp "build/bin/wowlet" "$APPDIR/usr/bin/wowlet"
chmod +x "$APPDIR/usr/bin/wowlet"
cp "contrib/AppImage/AppRun" "$APPDIR/"
chmod +x "$APPDIR/AppRun"
find feather.AppDir/ -exec touch -h -a -m -t 202101010100.00 {} \;
find wowlet.AppDir/ -exec touch -h -a -m -t 202101010100.00 {} \;
mksquashfs feather.AppDir feather.squashfs -comp zstd -info -root-owned -no-xattrs -noappend -fstime 0
mksquashfs wowlet.AppDir wowlet.squashfs -comp zstd -info -root-owned -no-xattrs -noappend -fstime 0
# mksquashfs writes a timestamp to the header
printf '\x00\x00\x00\x00' | dd conv=notrunc of=feather.squashfs bs=1 seek=$((0x8))
printf '\x00\x00\x00\x00' | dd conv=notrunc of=wowlet.squashfs bs=1 seek=$((0x8))
rm -f feather.AppImage
rm -f wowlet.AppImage
cat "${GITHUB_WORKSPACE:-/feather}/contrib/depends/${HOST}/runtime" >> feather.AppImage
cat feather.squashfs >> feather.AppImage
chmod a+x feather.AppImage
cat "${GITHUB_WORKSPACE:-/feather}/contrib/depends/${HOST}/runtime" >> wowlet.AppImage
cat wowlet.squashfs >> wowlet.AppImage
chmod a+x wowlet.AppImage
+9
View File
@@ -43,6 +43,15 @@ SET(absl_ROOT @prefix@)
SET(NATIVE_BIN_PATH @prefix@/native/bin CACHE FILEPATH "path to native binaries" FORCE)
SET(LRELEASE_PATH @prefix@/native/bin CACHE FILEPATH "path to lrelease" FORCE)
# A cross Qt that carries qtdeclarative makes find_package(Qt6) demand a HOST Qt,
# because QML needs host-side tools (qmlcachegen, qmlimportscanner,
# qmltyperegistrar). depends already builds one: native_qt installs to
# native/qt-host and qt.mk hands that path to the cross Qt's own configure. Our
# build never heard about it, so Qt6Config refused with "please set the
# QT_HOST_PATH cache variable" and the Windows cross build has failed since
# qtdeclarative landed. Same path, stated once more for the consuming project.
SET(QT_HOST_PATH @prefix@/native/qt-host CACHE PATH "host Qt for cross QML tooling")
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Android")
SET(LIBUNWIND_INCLUDE_DIR @prefix@/include)
SET(LIBUNWIND_LIBRARIES @prefix@/lib/libunwind.a)
+6 -2
View File
@@ -2,6 +2,7 @@ import os
import glob
DOCS_DIR = "external/feather-docs/content/_guides"
OVERRIDES_DIR = "contrib/docs/overrides"
OUT_DIR = "src/assets/docs"
CATEGORY_MAP = {
@@ -21,9 +22,12 @@ outfiles = glob.glob(f"{OUT_DIR}/*.md")
for file in outfiles:
os.remove(file)
files = glob.glob(f"{DOCS_DIR}/*.md")
# Wowlet owns its support contacts. Local sources take precedence so regenerating
# the bundled docs cannot restore Feather's contact details.
files = {os.path.basename(path): path for path in glob.glob(f"{DOCS_DIR}/*.md")}
files.update({os.path.basename(path): path for path in glob.glob(f"{OVERRIDES_DIR}/*.md")})
for file in files:
for file in sorted(files.values()):
with open(file) as f:
doc = f.read()
+29
View File
@@ -0,0 +1,29 @@
---
title: "How to report an issue"
nav_title: "Report an issue"
category: "help"
---
For help with Wowlet, contact the Wownero community:
- Matrix: [#wownero-gen:wowne.ro](https://matrix.to/#/#wownero-gen:wowne.ro)
- IRC: `#wownero` on [OFTC](https://www.oftc.net/)
- Email: [wownero@wownero.org](mailto:wownero@wownero.org)
- Bug reports: [Wowlet issues](https://github.com/wownero/wowlet/issues)
For live support, use Matrix or IRC. If you do not have an IRC client, join
[#wownero in your browser](https://webchat.oftc.net/?randomnick=1&channels=wownero).
If nobody replies immediately, stay in the room and give volunteers time to respond.
Please include your Wowlet version, operating system, the steps that caused the
problem, and any error message. Review **Help → Debug Info** before sharing it and
remove any personal information. Never share your wallet seed, private keys or
password, and do not upload your wallet files.
Report security vulnerabilities privately to
[wownero@wownero.org](mailto:wownero@wownero.org), rather than in a public issue or
chat. See the [Wowlet security policy](https://github.com/wownero/wowlet/blob/master/SECURITY.md).
Downloads and signed release hashes are available from
[Wowlet releases](https://github.com/wownero/wowlet/releases), also linked from
[wownero.org](https://wownero.org/).
+85
View File
@@ -0,0 +1,85 @@
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
ROOT = Path(__file__).resolve().parents[3]
class GenerateDocsTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
self.upstream = self.root / "external/feather-docs/content/_guides"
self.upstream.mkdir(parents=True)
self.output = self.root / "src/assets/docs"
self.output.mkdir(parents=True)
shutil.copytree(ROOT / "contrib/docs/overrides", self.root / "contrib/docs/overrides")
def write_upstream(self, name, body):
(self.upstream / name).write_text(
'---\ntitle: "Upstream guide"\nnav_title: "Upstream"\ncategory: "help"\n---\n'
+ body
)
def generate(self):
result = subprocess.run(
[sys.executable, "-B", str(ROOT / "contrib/docs/generate.py")],
cwd=self.root, capture_output=True, text=True,
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def assert_wownero_contacts(self, document):
urls = [urlsplit(url) for url in re.findall(r"\]\(([^)]+)\)", document)]
self.assertTrue(any(
url.hostname == "matrix.to" and url.fragment == "/#wownero-gen:wowne.ro"
for url in urls
), "Matrix must lead to Wownero's published room")
self.assertTrue(any(
url.hostname == "webchat.oftc.net"
and "wownero" in parse_qs(url.query).get("channels", [])
for url in urls
), "Webchat must join Wownero's IRC channel")
self.assertTrue(any(
url.hostname == "github.com" and url.path == "/wownero/wowlet/issues"
for url in urls
), "Bug reports must go to the Wowlet tracker")
self.assertTrue(any(
url.scheme == "mailto" and url.path == "wownero@wownero.org"
for url in urls
), "Email must reach the published Wownero contact")
for url in urls:
self.assertNotIn("feather", url.geturl().lower(), "Support must not route to Feather")
def test_support_override_survives_regeneration(self):
self.write_upstream("report_an_issue.md", "Upstream support contacts")
self.write_upstream("accounts.md", "An upstream guide without an override")
(self.output / "stale.md").write_text("Obsolete generated guide")
for body in ("Upstream support contacts", "Changed upstream support contacts"):
self.write_upstream("report_an_issue.md", body)
self.generate()
support = (self.output / "report_an_issue.md").read_text()
self.assert_wownero_contacts(support)
self.assertNotIn(body, support)
self.assertRegex(support, r"(?m)^\[nav_title\]: # \(.+\)$")
self.assertRegex(support, r"(?m)^\[category\]: # \(.+\)$")
self.assertIn("An upstream guide without an override", (self.output / "accounts.md").read_text())
self.assertFalse((self.output / "stale.md").exists())
def test_readme_and_security_contacts_agree_with_bundled_help(self):
readme = (ROOT / "README.md").read_text()
support = readme.split("## Support\n", 1)[1].split("\n## ", 1)[0]
self.assert_wownero_contacts(support)
policy = (ROOT / "SECURITY.md").read_text()
self.assertIn("mailto:wownero@wownero.org", policy)
self.assertNotIn("featherwallet.org", policy)
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -34,13 +34,13 @@
<string>????</string>
<key>CFBundleExecutable</key>
<string>feather</string>
<string>wowlet</string>
<key>CFBundleName</key>
<string>Feather</string>
<string>Wowlet</string>
<key>CFBundleIdentifier</key>
<string>org.monero-project.feather</string>
<string>org.wownero.wowlet</string>
<key>NSCameraUsageDescription</key>
<string>Camera is used to scan QR codes on demand.</string>
+13 -9
View File
@@ -148,7 +148,10 @@ add_executable(feather ${EXECUTABLE_FLAG} main.cpp
${ASSETS_TOR}
)
# The CMake target keeps its upstream name so the target_* calls below stay put;
# only what we ship is renamed.
set_target_properties(feather PROPERTIES
OUTPUT_NAME wowlet
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
LINK_FLAGS_RELEASE -s
)
@@ -370,14 +373,14 @@ else()
)
if (UNIX AND NOT APPLE)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/feather.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/32x32.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/32x32/apps RENAME "feather.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/48x48.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/48x48/apps RENAME "feather.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/64x64.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/64x64/apps RENAME "feather.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/96x96.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/96x96/apps RENAME "feather.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/128x128.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/128x128/apps RENAME "feather.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/256x256.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps RENAME "feather.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/512x512.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/512x512/apps RENAME "feather.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/wowlet.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/32x32.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/32x32/apps RENAME "wowlet.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/48x48.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/48x48/apps RENAME "wowlet.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/64x64.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/64x64/apps RENAME "wowlet.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/96x96.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/96x96/apps RENAME "wowlet.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/128x128.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/128x128/apps RENAME "wowlet.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/256x256.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps RENAME "wowlet.png")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/images/appicons/512x512.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/512x512/apps RENAME "wowlet.png")
endif()
endif()
@@ -385,13 +388,14 @@ if(APPLE)
configure_file(${CMAKE_SOURCE_DIR}/contrib/macdeploy/Info.plist.in ${CMAKE_SOURCE_DIR}/contrib/macdeploy/Info.plist @ONLY)
set_target_properties(feather PROPERTIES
OUTPUT_NAME wowlet
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/contrib/macdeploy/Info.plist"
LINK_FLAGS_RELEASE -s
)
file(COPY "${CMAKE_SOURCE_DIR}/src/assets/images/appicons/appicon.icns" DESTINATION "${CMAKE_SOURCE_DIR}/installed/feather.app/Contents/Resources/" )
file(COPY "${CMAKE_SOURCE_DIR}/src/assets/images/appicons/appicon.icns" DESTINATION "${CMAKE_SOURCE_DIR}/installed/wowlet.app/Contents/Resources/" )
endif()
qt_finalize_executable(feather)
+1 -1
View File
@@ -252,7 +252,7 @@ void MainWindow::initWidgets() {
});
// [Mining] — wowlet: the pixel-art mining tab (mining.qml + embedded-node solo miner)
m_miningWidget = new MiningWidget(m_wallet, this);
m_miningWidget = new MiningWidget(m_wallet, m_nodes, this);
ui->tabWidget->addTab(m_miningWidget, "Mining");
// [Notes]
+1 -1
View File
@@ -23,7 +23,7 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
// wowlet: ambient scene as a short banner under the address table. A layout
// item, never an overlay over the table.
auto *scene = new SceneWidget(this, SceneWidget::Dawn);
auto *scene = new SceneWidget(this, SceneWidget::Dawn, SceneWidget::Sit);
scene->setMinimumHeight(110);
scene->setMaximumHeight(130);
ui->verticalLayout->addWidget(scene);
+10 -3
View File
@@ -29,10 +29,17 @@ SendWidget::SendWidget(Wallet *wallet, QWidget *parent)
// wowlet: the form only needs the top of the tab — give the rest of it to
// the ambient scene. A layout item under the form, never an overlay: the
// send form itself stays untouched.
auto *scene = new SceneWidget(this, SceneWidget::Night);
//
// Centred in the space below the form rather than stacked straight under
// it: butted up against the Clear/Send row the scene reads as part of the
// form, and a band of night sky touching a button looks like a mistake.
auto *scene = new SceneWidget(this, SceneWidget::Night, SceneWidget::Money);
scene->setMinimumHeight(160);
scene->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
ui->verticalLayoutOuter->addWidget(scene, 1);
scene->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
ui->verticalLayoutOuter->addSpacing(14); // never touches the Send row
ui->verticalLayoutOuter->addStretch(1);
ui->verticalLayoutOuter->addWidget(scene);
ui->verticalLayoutOuter->addStretch(1);
QString amount_rx = R"(^\d{0,8}[\.,]\d{0,12}|(all)$)";
QRegularExpression rx;
+53 -3
View File
@@ -512,6 +512,10 @@ Rectangle {
Image {
id: imgAxe
visible: mining.daemonMiningState === 0
// Greyed out while the wallet is on someone else's node:
// solo mining is an RPC to the daemon we are connected
// to, and that daemon has to be ours.
opacity: mining.canMine ? 1.0 : 0.35
source: "qrc:/mining/axe.png"
width: 73
height: 75
@@ -551,7 +555,11 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: {
if(mining.daemonMiningState === 0) {
if (mining.daemonMiningState === 0) {
if (!mining.canMine) {
remoteNodeNotice.flash();
return;
}
root.startMining();
root.calcAvailableHeightConsoleLines();
}
@@ -559,14 +567,56 @@ Rectangle {
root.stopMining();
}
onEntered: {
imgAxe.height = 64
imgAxe.width = 64
if (mining.canMine) {
imgAxe.height = 64
imgAxe.width = 64
} else {
remoteNodeNotice.show();
}
}
onExited: {
imgAxe.height = 75
imgAxe.width = 73
remoteNodeNotice.hideSoon();
}
}
// Why the axe is greyed out. Shown on hover, and flashed if
// the axe is clicked anyway.
Rectangle {
id: remoteNodeNotice
z: parent.z + 5
visible: opacity > 0.01
opacity: 0.0
width: 250
height: noticeText.implicitHeight + 16
radius: 6
color: "#231a10"
border.color: "#e0b062"
border.width: 1
anchors.right: parent.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
Text {
id: noticeText
anchors.centerIn: parent
width: parent.width - 16
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
color: "#f2dcb4"
font.pointSize: 9
text: "Mining needs your own node.\nYou are connected to a remote one — " +
"start the built-in node or point the wallet at a local daemon " +
"under Settings to mine."
}
Behavior on opacity { NumberAnimation { duration: 160 } }
Timer { id: noticeHide; interval: 2600; onTriggered: remoteNodeNotice.opacity = 0.0 }
function show() { noticeHide.stop(); opacity = 1.0 }
function hideSoon() { noticeHide.restart() }
function flash() { show(); noticeHide.restart() }
}
}
}
}
+797 -84
View File
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -3,6 +3,8 @@
#include "MiningWidget.h"
#include "utils/nodes.h"
#include <algorithm>
#include <QVBoxLayout>
@@ -20,9 +22,10 @@ static QString formatUptime(qint64 secs) {
.arg(secs % 60, 2, 10, QChar('0'));
}
MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent)
MiningWidget::MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent)
: QWidget(parent)
, m_wallet(wallet)
, m_nodes(nodes)
, m_quickWidget(new QQuickWidget(this))
, m_backend(new MiningBackend(this))
, m_timer(new QTimer(this))
@@ -46,6 +49,10 @@ MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent)
}
void MiningWidget::onStartMining() {
// The QML disables the control, but the signal is not a promise: refuse
// here too rather than send start_mining to a stranger's daemon.
if (!m_backend->canMine())
return;
int threads = std::max(1, QThread::idealThreadCount() / 2);
if (m_wallet->startMining(threads))
m_backend->setState(3);
@@ -57,6 +64,8 @@ void MiningWidget::onStopMining() {
}
void MiningWidget::poll() {
m_backend->setCanMine(m_nodes && m_nodes->connection().isLocal());
bool mining = m_wallet->isMining();
m_backend->setState(mining ? 3 : 0);
+13 -1
View File
@@ -5,6 +5,8 @@
#define WOWLET_MININGWIDGET_H
#include <QWidget>
class Nodes;
#include <QTimer>
#include <QQuickWidget>
@@ -16,16 +18,24 @@ class MiningBackend : public QObject {
Q_OBJECT
Q_PROPERTY(int daemonMiningState READ daemonMiningState NOTIFY daemonMiningStateChanged)
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
Q_PROPERTY(bool canMine READ canMine NOTIFY canMineChanged)
public:
explicit MiningBackend(QObject *parent = nullptr) : QObject(parent) {}
int daemonMiningState() const { return m_state; }
void setState(int s) { if (m_state != s) { m_state = s; emit daemonMiningStateChanged(); } }
bool reducedMotion() const { return m_reducedMotion; }
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
// Solo mining is a start_mining RPC to whichever daemon the wallet is
// connected to. Against a remote node that is either refused outright or —
// worse, if the operator left the RPC open — it puts someone else's machine
// to work on our address. It is only ever ours to ask of our own node.
bool canMine() const { return m_canMine; }
void setCanMine(bool c) { if (m_canMine != c) { m_canMine = c; emit canMineChanged(); } }
signals:
void daemonMiningStateChanged();
void reducedMotionChanged();
void canMineChanged();
void daemonOutput(const QString &line);
void syncStatus(int from, int to, int pct);
void uptimeChanged(const QString &uptime);
@@ -34,6 +44,7 @@ signals:
private:
int m_state = 0; // 0:idle 1:startup 2:syncing 3:mining
bool m_reducedMotion = false;
bool m_canMine = false;
};
// wowlet: the pixel-art mining tab. Hosts mining.qml and wires its pick-axe to the wallet's solo
@@ -41,7 +52,7 @@ private:
class MiningWidget : public QWidget {
Q_OBJECT
public:
explicit MiningWidget(Wallet *wallet, QWidget *parent = nullptr);
explicit MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent = nullptr);
private slots:
void onStartMining();
@@ -50,6 +61,7 @@ private slots:
private:
Wallet *m_wallet;
Nodes *m_nodes;
QQuickWidget *m_quickWidget;
MiningBackend *m_backend;
QTimer *m_timer;
+27 -1
View File
@@ -4,18 +4,44 @@
#include "SceneWidget.h"
#include <QQmlContext>
#include <QQuickItem>
#include "utils/config.h"
SceneWidget::SceneWidget(QWidget *parent, Variant variant)
SceneWidget::SceneWidget(QWidget *parent, Variant variant, Mode mode)
: QQuickWidget(parent)
, m_backend(new SceneBackend(this))
{
m_backend->setReducedMotion(conf()->get(Config::reducedMotion).toBool());
m_backend->setVariant(static_cast<int>(variant));
m_backend->setMode(mode == Sit ? QStringLiteral("sit")
: mode == Money ? QStringLiteral("money")
: QStringLiteral("run"));
this->rootContext()->setContextProperty("sceneCtx", m_backend);
this->setResizeMode(QQuickWidget::SizeRootObjectToView);
this->setSource(QUrl("qrc:/qml/scene.qml"));
this->callScene("suspend");
}
void SceneWidget::showEvent(QShowEvent *event) {
QQuickWidget::showEvent(event);
this->callScene("restart");
}
void SceneWidget::hideEvent(QHideEvent *event) {
this->callScene("suspend");
QQuickWidget::hideEvent(event);
}
void SceneWidget::resizeEvent(QResizeEvent *event) {
QQuickWidget::resizeEvent(event);
if (this->isVisible())
this->callScene("restart");
}
void SceneWidget::callScene(const char *method) {
if (auto *root = this->rootObject())
QMetaObject::invokeMethod(root, method);
}
void SceneWidget::refreshReducedMotion() {
+30 -1
View File
@@ -5,6 +5,7 @@
#define WOWLET_SCENEWIDGET_H
#include <QObject>
#include <QString>
#include <QQuickWidget>
// Context object exposed to scene.qml as `sceneCtx`. Carries the reduced-motion
@@ -14,18 +15,23 @@ class SceneBackend : public QObject {
Q_OBJECT
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
Q_PROPERTY(int variant READ variant NOTIFY variantChanged)
Q_PROPERTY(QString mode READ mode NOTIFY modeChanged)
public:
explicit SceneBackend(QObject *parent = nullptr) : QObject(parent) {}
bool reducedMotion() const { return m_reducedMotion; }
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
int variant() const { return m_variant; }
void setVariant(int v) { if (m_variant != v) { m_variant = v; emit variantChanged(); } }
QString mode() const { return m_mode; }
void setMode(const QString &m) { if (m_mode != m) { m_mode = m; emit modeChanged(); } }
signals:
void reducedMotionChanged();
void variantChanged();
void modeChanged();
private:
bool m_reducedMotion = false;
int m_variant = 0;
QString m_mode = QStringLiteral("run");
};
// wowlet: reusable ambient-scene surface. Hosts scene.qml and reads the
@@ -38,9 +44,32 @@ public:
// same field. Must match the `skies` table in scene.qml.
enum Variant { Dusk = 0, Dawn = 1, Night = 2 };
explicit SceneWidget(QWidget *parent = nullptr, Variant variant = Dusk);
// How much scene a surface wants. History has room for the whole run;
// Receive gets a doge sitting in the field; Send gets money blowing past and
// nothing else, so the form it sits under is not competing with a dog.
enum Mode { Run, Sit, Money };
explicit SceneWidget(QWidget *parent = nullptr, Variant variant = Dusk,
Mode mode = Run);
void refreshReducedMotion();
protected:
// A scene on a hidden tab is parked rather than left running: an unshown
// QQuickWidget is 0px wide, and a lap run against a 0px field collects every
// coin at once and leaves its litter (+1s, barks) to surface on the real lap
// when the tab is finally shown. Parking also keeps a background tab from
// burning CPU on animation nobody is looking at.
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
// Belt and braces: a scene that is on screen and has just been given a real
// size is running, whether or not a show event was the thing that got it
// there. `restart` only sets a flag, so re-arming an already-running scene
// costs nothing and does not interrupt its lap.
void resizeEvent(QResizeEvent *event) override;
private:
void callScene(const char *method);
SceneBackend *m_backend;
};