a Monero wallet in pure managed C#, from the curve up
No P/Invoke, no native binaries, no NuGet packages in the parts that hold keys — one artifact that runs wherever the runtime does, and every byte of it readable in this repository.
Status: early. Crypto, serialization, the daemon client, RingCT and a scanning wallet are in. The TUI syncs a wallet to the mainnet tip — pruned blocks, so the proofs a scan never reads are not fetched — and finds real payments to its own addresses. The CLSAG signatures and the Bulletproof+ range proof of a real transaction verify against this code, and it reads that transaction's change amount back. All of it is also a linkable library with a C interface, so a user interface in any language can drive the same wallet. What is missing is the other half of spending: decoy selection, fees and the transaction builder. Nothing here spends money yet.
Nothing to install: every archive holds moonlight, moonlight-tui, the shared library libmoonlight.* with its header, and the licences. Unpack it and run it. From the latest release:
| platform | archive | also built by bflat |
|---|---|---|
| Linux x64 | moonlight-<version>-linux-x64.tar.gz |
yes |
| Linux arm64 | moonlight-<version>-linux-arm64.tar.gz |
yes |
| Windows x64 | moonlight-<version>-win-x64.zip |
yes |
| Windows arm64 | moonlight-<version>-win-arm64.zip |
no — it builds, and it does not start: docs/build-modes.md |
| macOS, Apple silicon | moonlight-<version>-osx-arm64.tar.gz |
no |
Intel Macs are not built. The bflat archives are the same wallet from a different compiler and a much smaller one — -bflat in the name; either is the wallet, and the plain one is the answer if you have no reason to prefer the other.
Four more things travel with a release. moonlight-gui-demo-<version>-<platform> is the interface on made-up data — a demonstration, not a wallet, and it is the one artifact carrying somebody else's binaries. moonlight-<version>-managed.zip is one build for every platform, needing a .NET 8 runtime and starting dotnet app/moonlight.dll. …-symbols are the debug symbols, apart because most people downloading a wallet do not want them. moonlight-<version>-src.tar.gz is the tag's tree.
Check what you downloaded against SHA256SUMS, which covers every file on the page:
sha256sum --ignore-missing -c SHA256SUMS # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS # macOS(Get-FileHash moonlight-<version>-win-x64.zip).Hash # compare with its line in SHA256SUMSNothing is code-signed yet, so macOS holds a downloaded archive in quarantine until told otherwise — xattr -dr com.apple.quarantine moonlight-<version>-osx-arm64 on the unpacked directory.
Nine packages, and referencing the top one brings the rest:
dotnet add package Moonlight.Core.Http --version <version>Moonlight.Core.Http is the wallet with a socket attached. Take Moonlight.Core instead to drive the sync engine over your own networking, and neither pulls in a package of anyone else's — the dependency graph below Moonlight.Core.Http is the nine and the framework.
moonlight-<version>-managed.zip also carries single/Moonlight.dll: all nine layers in one assembly, to reference directly where a package feed is not wanted. Do not mix the two. One application gets the nine or gets the single assembly; the types are distinct even where the names match, and a Scalar from one is not a Scalar from the other.
nix run github:daniilvaino/moonlight/v<version> -- version
nix run github:daniilvaino/moonlight/v<version>#moonlight-tuidotnet build Moonlight.slnx
dotnet test Moonlight.slnx
dotnet publish apps/Cli -c Release # NativeAOT
dotnet publish apps/Tui -c Release # NativeAOT
dotnet publish src/Core.Abi -c Release -r linux-x64 # a shared library, and it
# needs the identifier given
pwsh tools/purity-gate.ps1
bflat build ... # no SDK, no MSBuild, smaller — docs/build-modes.mdTargets net8.0, compiled as C# 14 — SDK 10 to build, nothing newer than .NET 8 to run. Everything is AOT-compatible with the trim and AOT analyzers on.
Three modes build the same code — managed, NativeAOT, and bflat, which gets the shared library down to 1900 KB. Sizes, flags, and how each artifact was run rather than only built: docs/build-modes.md.
With Nix it is hermetic — pinned SDKs, no network during the build:
nix build .#moonlight # apps/Cli -> result/bin/moonlight
nix build .#moonlight-tui # apps/Tui -> result/bin/moonlight-tui
nix flake check # both apps, the test corpus, the purity gatesrc/ sterile: 0 packages, 0 P/Invoke
Crypto.Ed25519 vendored ref10 field/group/scalar arithmetic
Diagnostics the log
Crypto Scalar, Point; Keccak (legacy pad), VarInt, view tags,
derivations, key images, CryptoNote signatures
Serialization transaction and block parsers, ids, Merkle root, Epee
RingCT Pedersen, ECDH, CLSAG, Bulletproofs+, MultiExp
Node monerod JSON-RPC; /getblocks.bin over Epee
Wallet keys, accounts, addresses, subaddresses, scanner,
restore heights, encrypted storage
Core the sync engine, the open wallet, amounts
Core.Http the socket: the engine driven over HttpClient
Core.Abi the C interface, exports only
apps/
Cli sterile
Tui sterile; vendored Terminal.Gui v1
Gui.Demo outside the gate — the interface, on made-up data so far
tests/ outside the gate
vectors/ the corpus (8.3 MB)
media/ brand assets
flowchart TD
Tui["apps/Tui"]:::sterile --> TG["Vendor Terminal.Gui"]:::sterile
Tui --> CoreHttp
Cli["apps/Cli"]:::sterile --> CoreHttp
Gui["apps/Gui.Demo"]:::outside --> CoreHttp
CoreHttp["Core.Http<br/>the socket"]:::sterile --> Core
Abi["Core.Abi<br/>the C interface"]:::bridge --> Core
%% Abi is a consumer of Core like the three above it, not a layer under them.
%% The invisible link only lifts it into their row; it draws nothing.
Abi ~~~ CoreHttp
Core["Core<br/>sync engine, open wallet"]:::sterile --> Wallet["Wallet"]:::sterile
Wallet --> RingCT["RingCT"]:::sterile
Wallet --> Node["Node"]:::sterile
RingCT --> Ser["Serialization"]:::sterile
Node --> Ser
Ser --> Crypto["Crypto"]:::sterile
Crypto --> Ed["Crypto.Ed25519"]:::sterile
Node --> Diag["Diagnostics"]:::sterile
classDef sterile fill:#2b1d4d,stroke:#7c3aed,stroke-width:1px,color:#e9e4f7
classDef outside fill:#4a2a10,stroke:#f2640a,stroke-width:1px,color:#f7ece4
classDef bridge fill:#2b1d4d,stroke:#f2640a,stroke-width:2px,color:#e9e4f7
Applications reference Core.Http and nothing else; Core.Abi references Core, and the two know nothing about each other. That is what keeps an HTTP stack out of a library somebody links into their own application. Following the chain is a state machine in Core that does no I/O — it says what to send, the host sends it, the answer goes back in — so a user interface written in anything can link the library and keep its own networking. Details in docs/architecture.md.
Layers depend downward only. Key material never travels as byte[]: constructing a Scalar or a Point is the only place bytes are checked, so holding one means holding something the curve will accept.
src/, apps/Cli and apps/Tui allow 0 PackageReference, 0 DllImport, and no native assets in the restore graph. The sterile tree restores fully offline.
Enforced twice, because a rule nothing checks is a preference:
Directory.Build.targetsfails the build of any sterile project that declares a package.tools/purity-gate.ps1scans the sources and the restore graph in CI, and refuses aVendor/directory with noVENDORED.md.
Core.Abi is sterile too — just the one project allowed to export a symbol out of managed code, hence the orange edge in the diagram. tests/ and apps/Gui.Demo are outside the gate on purpose. Nothing sterile references the demo. Details in docs/purity-policy.md.
Caught up to the mainnet tip, with a payment found — every byte of that scan done by the code in this repository.
apps/Tui vendors Terminal.Gui v1 from the SharpOS fork — the compile-in module: no ConsoleDrivers, no P/Invoke, which is how a whole UI toolkit lives inside the purity gate. That fork, because the wallet is meant to run on SharpOS eventually.
Vectors before code. The harness for monero's tests/crypto/tests.txt was the first thing committed, so every crypto file since is born under the whole corpus: 5945 lines, 20 operations, 5539 replayed line by line — including monero's deterministic test generator, reproduced byte for byte, draw pattern included. Corpus tests assert the vector files themselves are intact, because a truncated vector file is the one failure that makes every later test pass for free. Inventory in docs/test-corpus.md.
Point MOONLIGHT_DAEMON at a node and the chain tests run too: every block must parse to the last byte, every transaction must hash to the id the daemon listed.
Steal what is stealable; port what is not. Every Vendor/ tree carries a VENDORED.md with the upstream commit and a full list of local changes.
| donor | licence | use |
|---|---|---|
| MoneroRing | MIT | crypto layer — copied |
| ref10, through MoneroRing | public domain (Chaos.NaCl, djb) | field, group and scalar arithmetic — copied |
| MoneroSharp | MIT | the English word list — copied, nothing else |
| Terminal.Gui (SharpOS fork), XtermSharp, QRCoder | MIT | the TUI — copied |
| monero-oxide | MIT (per crate, all checked) | Rust reference; BP+ transcript intermediates |
| monero | BSD-3 | vectors copied; src/ringct ported by hand |
| skunkworks, CantiLib, Determ | GPL-3 / AGPL / undecided | read only — never copied |
work/ holds the clones; pwsh tools/clone-donors.ps1 fetches them all.
architecture · build modes · purity policy · test corpus · hard forks · upstream bugs · security · notices
ref10 and the crypto layer, againsttests.txt.Serialization: transactions, blocks, ids, Merkle root, Epee.Node: monerod JSON-RPC and/getblocks.bin.RingCT: Pedersen, ECDH, CLSAG, Bulletproofs+ both ways — verification checked against proofs monero made.Pippenger for the multiexp remains.Wallet: seeds, addresses, subaddresses, scanner, balance, encrypted storage.Decoys, fees and the transaction builder remain.One library the applications share, following the chain without owning the socket, offered to C as a linkable artifact — with CI building all three modes and checking the exported symbols against the header.Errors still cross the C boundary as a bare code with no message.- Spending: decoy selection, fee and weight, the transaction builder.
- Integration: stagenet end to end, plus our own verify-chain over real blocks.
MIT licensed.
