Overview
What it is
NFTMixer (.NET) is the original generative-NFT builder and the predecessor that
nftmixer-go replaces: a C#/.NET 6 Blazor Server application over
PostgreSQL via EF Core 6 (lazy-loading proxies), with MudBlazor for UI,
Z.Blazor.Diagrams for the node canvas, Nethereum for wallet connect,
SixLabors.ImageSharp for compositing, and AWS S3 + local disk for storage. A
three-project solution (NftMixer, plus a dead NftGenerator library and a
ConsoleTests harness), it reached a real, feature-rich product — sources →
assets → variants → traits → rarities → payloads → node graph → generate →
curate → render → zip — with ~40 EF migrations spanning 2022–2026. In this
workflow it is a read-only reference, kept because the interesting engineering
story is the delta between a mature-but-unshippable app and its disciplined
rewrite.
Blazor Server is the fork in the road
The whole UI is .razor components rendered server-side and diffed to the browser
over a SignalR websocket (AddServerSideBlazor, MapBlazorHub,
MapFallbackToPage("/_Host")). That model is convenient — it gave generation
progress dialogs "for free" over the socket — but it is precisely the thing with
no Go equivalent, which is the honest reason the rewrite was ~70% frontend:
the entire interactive surface had to be rebuilt from scratch regardless of how
clean the engine port was. Storage compounded it: the app persisted to local disk
under {userId}/{mixName}/{n}.png|.json|.png.sha256 on a volume that "must never
be lost," which is exactly what blocked safe containerization and drove the Go
app's stateless S3-for-everything design. The Dockerfile even runs a three-stage
build whose runtime installs postgresql-client because the app shells out to
pg_dump.
The live engine, notably, is not in the NftGenerator project at all — that
library is dead code. Generation, the graph model and rendering live in
Components/ProjectV3.razor (866 lines) and Components/Curation.razor,
alongside whole superseded generations of components (Project.razor,
Project2.razor, ProjectOLD.razor, an AssetSelectorDialog v1/v2, a
... - Copy.razor) that are referenced but unreachable.
The honest defects that motivated the rewrite
All are real and citable, and each maps to a specific Go-side fix:
- Authentication that does not authenticate.
MainLayout.razorreadsGetProviderSelectedAccountAsync()and callsInitUserData(SelectedAccount), whichFindAsynces the user or silently creates one — no signature challenge anywhere. The server trusts whatever address the browser names, so anyone can log in as anyone, including an admin:
var enableProvider = await _ethereumHostProvider.EnableProviderAsync();
SelectedAccount = await _ethereumHostProvider.GetProviderSelectedAccountAsync();
if (SelectedAccount != null) {
await InitUserData(SelectedAccount); // server trusts the address as-is
}
- An unauthenticated database-dump endpoint.
GET /api/Download/export/dbshells out topg_dumpand streams back the full data dump, with no[Authorize]on the controller or action:
[HttpGet("export/db")]
public IActionResult ExportDatabase() {
string connectionString = _configuration.GetConnectionString("DefaultConnection");
string arguments = $"pg_dump -h {dbHost} -p {dbPort} -U {dbUser} --data-only --column-inserts -Fp {dbName}";
var commandResult = BashExecutorService.Execute(arguments, backupFilePath, dbPwd);
if (commandResult.Success) return File(commandResult.file, "application/octet-stream", backupFileName);
}
-
Ownership taken from the URL.
GET /api/Download/zipmixer/{userId}/{mixName}builds the output path straight from the URLuserIdwith no membership check, so anyone can download anyone's output. -
Sessions that never expire. Web3 tokens live in a process-local
ConcurrentDictionarywhoseCleanse()method is literally empty; the map is an unbounded, never-swept in-memory store:
public static void RegisterToken(string token, Web3User user) {
_activeTokens.TryAdd(token, (DateTime.UtcNow, user));
Cleanse();
}
static void Cleanse() {
}
-
A committed live Pinata API key and secret, hardcoded in
UploadDataToPinataand committed to git — confirmed present; the values are treated as burned and are never carried forward or reproduced. -
Unfinished IPFS publish —
PublishToIpfsreturns before uploading, so IPFS never actually worked in the C# app.
The two-generators problem
The clearest illustration of why a rewrite (not a patch) was the right call: the
app literally shipped a "Process Paths" button and a "Process Paths
(Accurate)" button that produced different collections. The "accurate" path
(ProjectV3.razor:607) is unique-but-uniform-random with an unbounded
while (generatedCount < stat.NumberToGenerate) that hangs when the graph cannot
produce enough distinct combinations, and it does nftNumber++ on every attempt,
leaving gaps in the numbering. The Go rewrite folds both into a single
weighted-and-unique generator with a bounded retry and gap-free numbering.
Why keep it in the constellation
It was not a toy — ~40 migrations, V3 generation, masters, rarity tiers, real rendered output. That maturity is the point: the story worth telling is not "an app was rewritten" but "a working, feature-complete product had foundations — statefulness, spoofable auth, an open dump endpoint, a committed key — that could not be kept," and what a disciplined rewrite does with that inheritance.