feat(docker): add dev.sh with auto-funding and ATM app setup
- Add dev.sh script for managing regtest development environment - Implement cmd_fund to fund ATM app owner via Lightning.Pub API - Add --fund flag to cmd_up for automatic funding on startup - Update setup_atm_app to write VITE_APP_ID to machine .env - Fix Electron IPC to pass appId and extensionApiUrl to renderer - Restructure repo from nested lamassu-next/ to root The dev.sh script now supports: - ./dev.sh up --fund # Start regtest and auto-fund ATM - ./dev.sh fund # Fund existing ATM app - ./dev.sh status # Show environment status - ./dev.sh reset # Clean restart Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
30a2eb2199
commit
c98f126ba7
180 changed files with 2695 additions and 9587 deletions
513
.devenv.flake.nix
Normal file
513
.devenv.flake.nix
Normal file
|
|
@ -0,0 +1,513 @@
|
||||||
|
{
|
||||||
|
inputs =
|
||||||
|
let
|
||||||
|
vars = {
|
||||||
|
version = "1.11.2";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
devenv_root = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next";
|
||||||
|
project_input_ref = "path:/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next";
|
||||||
|
devenv_dotfile = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next/.devenv";
|
||||||
|
devenv_dotfile_path = ./.devenv;
|
||||||
|
devenv_tmpdir = "/run/user/1000";
|
||||||
|
devenv_runtime = "/run/user/1000/devenv-f4ba770";
|
||||||
|
devenv_istesting = false;
|
||||||
|
devenv_direnvrc_latest_version = 1;
|
||||||
|
container_name = null;
|
||||||
|
active_profiles = [
|
||||||
|
];
|
||||||
|
hostname = "gizmo";
|
||||||
|
username = "padreug";
|
||||||
|
git_root = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages";
|
||||||
|
secretspec = null;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
git-hooks.url = "github:cachix/git-hooks.nix";
|
||||||
|
git-hooks.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
pre-commit-hooks.follows = "git-hooks";
|
||||||
|
nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
|
||||||
|
devenv.url = "github:cachix/devenv?dir=src/modules";
|
||||||
|
}
|
||||||
|
// (
|
||||||
|
if builtins.pathExists (vars.devenv_dotfile_path + "/flake.json") then
|
||||||
|
builtins.fromJSON (builtins.readFile (vars.devenv_dotfile_path + "/flake.json"))
|
||||||
|
else
|
||||||
|
{ }
|
||||||
|
);
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ nixpkgs, ... }@inputs:
|
||||||
|
let
|
||||||
|
vars = {
|
||||||
|
version = "1.11.2";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
devenv_root = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next";
|
||||||
|
project_input_ref = "path:/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next";
|
||||||
|
devenv_dotfile = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next/.devenv";
|
||||||
|
devenv_dotfile_path = ./.devenv;
|
||||||
|
devenv_tmpdir = "/run/user/1000";
|
||||||
|
devenv_runtime = "/run/user/1000/devenv-f4ba770";
|
||||||
|
devenv_istesting = false;
|
||||||
|
devenv_direnvrc_latest_version = 1;
|
||||||
|
container_name = null;
|
||||||
|
active_profiles = [
|
||||||
|
];
|
||||||
|
hostname = "gizmo";
|
||||||
|
username = "padreug";
|
||||||
|
git_root = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages";
|
||||||
|
secretspec = null;
|
||||||
|
};
|
||||||
|
devenv =
|
||||||
|
if builtins.pathExists (vars.devenv_dotfile_path + "/devenv.json") then
|
||||||
|
builtins.fromJSON (builtins.readFile (vars.devenv_dotfile_path + "/devenv.json"))
|
||||||
|
else
|
||||||
|
{ };
|
||||||
|
|
||||||
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Function to create devenv configuration for a specific system with profiles support
|
||||||
|
mkDevenvForSystem =
|
||||||
|
targetSystem:
|
||||||
|
let
|
||||||
|
getOverlays =
|
||||||
|
inputName: inputAttrs:
|
||||||
|
map (
|
||||||
|
overlay:
|
||||||
|
let
|
||||||
|
input =
|
||||||
|
inputs.${inputName} or (throw "No such input `${inputName}` while trying to configure overlays.");
|
||||||
|
in
|
||||||
|
input.overlays.${overlay}
|
||||||
|
or (throw "Input `${inputName}` has no overlay called `${overlay}`. Supported overlays: ${nixpkgs.lib.concatStringsSep ", " (builtins.attrNames input.overlays)}")
|
||||||
|
) inputAttrs.overlays or [ ];
|
||||||
|
overlays = nixpkgs.lib.flatten (nixpkgs.lib.mapAttrsToList getOverlays (devenv.inputs or { }));
|
||||||
|
permittedUnfreePackages =
|
||||||
|
devenv.nixpkgs.per-platform."${targetSystem}".permittedUnfreePackages
|
||||||
|
or devenv.nixpkgs.permittedUnfreePackages or [ ];
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
system = targetSystem;
|
||||||
|
config = {
|
||||||
|
allowUnfree =
|
||||||
|
devenv.nixpkgs.per-platform."${targetSystem}".allowUnfree or devenv.nixpkgs.allowUnfree
|
||||||
|
or devenv.allowUnfree or false;
|
||||||
|
allowBroken =
|
||||||
|
devenv.nixpkgs.per-platform."${targetSystem}".allowBroken or devenv.nixpkgs.allowBroken
|
||||||
|
or devenv.allowBroken or false;
|
||||||
|
cudaSupport =
|
||||||
|
devenv.nixpkgs.per-platform."${targetSystem}".cudaSupport or devenv.nixpkgs.cudaSupport or false;
|
||||||
|
cudaCapabilities =
|
||||||
|
devenv.nixpkgs.per-platform."${targetSystem}".cudaCapabilities or devenv.nixpkgs.cudaCapabilities
|
||||||
|
or [ ];
|
||||||
|
permittedInsecurePackages =
|
||||||
|
devenv.nixpkgs.per-platform."${targetSystem}".permittedInsecurePackages
|
||||||
|
or devenv.nixpkgs.permittedInsecurePackages or devenv.permittedInsecurePackages or [ ];
|
||||||
|
allowUnfreePredicate =
|
||||||
|
if (permittedUnfreePackages != [ ]) then
|
||||||
|
(pkg: builtins.elem (nixpkgs.lib.getName pkg) permittedUnfreePackages)
|
||||||
|
else
|
||||||
|
(_: false);
|
||||||
|
};
|
||||||
|
inherit overlays;
|
||||||
|
};
|
||||||
|
inherit (pkgs) lib;
|
||||||
|
importModule =
|
||||||
|
path:
|
||||||
|
if lib.hasPrefix "./" path then
|
||||||
|
if lib.hasSuffix ".nix" path then
|
||||||
|
./. + (builtins.substring 1 255 path)
|
||||||
|
else
|
||||||
|
./. + (builtins.substring 1 255 path) + "/devenv.nix"
|
||||||
|
else if lib.hasPrefix "../" path then
|
||||||
|
# For parent directory paths, concatenate with /.
|
||||||
|
# ./. refers to the directory containing this file (project root)
|
||||||
|
# So ./. + "/../shared" = <project-root>/../shared
|
||||||
|
if lib.hasSuffix ".nix" path then ./. + "/${path}" else ./. + "/${path}/devenv.nix"
|
||||||
|
else
|
||||||
|
let
|
||||||
|
paths = lib.splitString "/" path;
|
||||||
|
name = builtins.head paths;
|
||||||
|
input = inputs.${name} or (throw "Unknown input ${name}");
|
||||||
|
subpath = "/${lib.concatStringsSep "/" (builtins.tail paths)}";
|
||||||
|
devenvpath = "${input}" + subpath;
|
||||||
|
devenvdefaultpath = devenvpath + "/devenv.nix";
|
||||||
|
in
|
||||||
|
if lib.hasSuffix ".nix" devenvpath then
|
||||||
|
devenvpath
|
||||||
|
else if builtins.pathExists devenvdefaultpath then
|
||||||
|
devenvdefaultpath
|
||||||
|
else
|
||||||
|
throw (devenvdefaultpath + " file does not exist for input ${name}.");
|
||||||
|
|
||||||
|
# Phase 1: Base evaluation to extract profile definitions
|
||||||
|
baseProject = pkgs.lib.evalModules {
|
||||||
|
specialArgs = inputs // {
|
||||||
|
inherit inputs;
|
||||||
|
};
|
||||||
|
modules = [
|
||||||
|
(
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
_module.args.pkgs = pkgs.appendOverlays (config.overlays or [ ]);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(inputs.devenv.modules + /top-level.nix)
|
||||||
|
(
|
||||||
|
{ options, ... }:
|
||||||
|
{
|
||||||
|
config.devenv = lib.mkMerge [
|
||||||
|
{
|
||||||
|
cliVersion = vars.version;
|
||||||
|
root = vars.devenv_root;
|
||||||
|
dotfile = vars.devenv_dotfile;
|
||||||
|
}
|
||||||
|
(pkgs.lib.optionalAttrs (builtins.hasAttr "tmpdir" options.devenv) {
|
||||||
|
tmpdir = vars.devenv_tmpdir;
|
||||||
|
})
|
||||||
|
(pkgs.lib.optionalAttrs (builtins.hasAttr "isTesting" options.devenv) {
|
||||||
|
isTesting = vars.devenv_istesting;
|
||||||
|
})
|
||||||
|
(pkgs.lib.optionalAttrs (builtins.hasAttr "runtime" options.devenv) {
|
||||||
|
runtime = vars.devenv_runtime;
|
||||||
|
})
|
||||||
|
(pkgs.lib.optionalAttrs (builtins.hasAttr "direnvrcLatestVersion" options.devenv) {
|
||||||
|
direnvrcLatestVersion = vars.devenv_direnvrc_latest_version;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(
|
||||||
|
{ options, ... }:
|
||||||
|
{
|
||||||
|
config = lib.mkMerge [
|
||||||
|
(pkgs.lib.optionalAttrs (builtins.hasAttr "git" options) {
|
||||||
|
git.root = vars.git_root;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(pkgs.lib.optionalAttrs (vars.container_name != null) {
|
||||||
|
container.isBuilding = pkgs.lib.mkForce true;
|
||||||
|
containers.${vars.container_name}.isBuilding = true;
|
||||||
|
})
|
||||||
|
]
|
||||||
|
++ (map importModule (devenv.imports or [ ]))
|
||||||
|
++ [
|
||||||
|
(if builtins.pathExists ./devenv.nix then ./devenv.nix else { })
|
||||||
|
(devenv.devenv or { })
|
||||||
|
(if builtins.pathExists ./devenv.local.nix then ./devenv.local.nix else { })
|
||||||
|
(
|
||||||
|
if builtins.pathExists (vars.devenv_dotfile_path + "/cli-options.nix") then
|
||||||
|
import (vars.devenv_dotfile_path + "/cli-options.nix")
|
||||||
|
else
|
||||||
|
{ }
|
||||||
|
)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Phase 2: Extract and apply profiles using extendModules with priority overrides
|
||||||
|
project =
|
||||||
|
let
|
||||||
|
# Build ordered list of profile names: hostname -> user -> manual
|
||||||
|
manualProfiles = vars.active_profiles;
|
||||||
|
currentHostname = vars.hostname;
|
||||||
|
currentUsername = vars.username;
|
||||||
|
hostnameProfiles = lib.optional (
|
||||||
|
currentHostname != ""
|
||||||
|
&& builtins.hasAttr currentHostname (baseProject.config.profiles.hostname or { })
|
||||||
|
) "hostname.${currentHostname}";
|
||||||
|
userProfiles = lib.optional (
|
||||||
|
currentUsername != "" && builtins.hasAttr currentUsername (baseProject.config.profiles.user or { })
|
||||||
|
) "user.${currentUsername}";
|
||||||
|
|
||||||
|
# Ordered list of profiles to activate
|
||||||
|
orderedProfiles = hostnameProfiles ++ userProfiles ++ manualProfiles;
|
||||||
|
|
||||||
|
# Resolve profile extends with cycle detection
|
||||||
|
resolveProfileExtends =
|
||||||
|
profileName: visited:
|
||||||
|
if builtins.elem profileName visited then
|
||||||
|
throw "Circular dependency detected in profile extends: ${lib.concatStringsSep " -> " visited} -> ${profileName}"
|
||||||
|
else
|
||||||
|
let
|
||||||
|
profile = getProfileConfig profileName;
|
||||||
|
extends = profile.extends or [ ];
|
||||||
|
newVisited = visited ++ [ profileName ];
|
||||||
|
extendedProfiles = lib.flatten (map (name: resolveProfileExtends name newVisited) extends);
|
||||||
|
in
|
||||||
|
extendedProfiles ++ [ profileName ];
|
||||||
|
|
||||||
|
# Get profile configuration by name from baseProject
|
||||||
|
getProfileConfig =
|
||||||
|
profileName:
|
||||||
|
if lib.hasPrefix "hostname." profileName then
|
||||||
|
let
|
||||||
|
name = lib.removePrefix "hostname." profileName;
|
||||||
|
in
|
||||||
|
baseProject.config.profiles.hostname.${name}
|
||||||
|
else if lib.hasPrefix "user." profileName then
|
||||||
|
let
|
||||||
|
name = lib.removePrefix "user." profileName;
|
||||||
|
in
|
||||||
|
baseProject.config.profiles.user.${name}
|
||||||
|
else
|
||||||
|
let
|
||||||
|
availableProfiles = builtins.attrNames (baseProject.config.profiles or { });
|
||||||
|
hostnameProfiles = map (n: "hostname.${n}") (
|
||||||
|
builtins.attrNames (baseProject.config.profiles.hostname or { })
|
||||||
|
);
|
||||||
|
userProfiles = map (n: "user.${n}") (builtins.attrNames (baseProject.config.profiles.user or { }));
|
||||||
|
allAvailableProfiles = availableProfiles ++ hostnameProfiles ++ userProfiles;
|
||||||
|
in
|
||||||
|
baseProject.config.profiles.${profileName}
|
||||||
|
or (throw "Profile '${profileName}' not found. Available profiles: ${lib.concatStringsSep ", " allAvailableProfiles}");
|
||||||
|
|
||||||
|
# Fold over ordered profiles to build final list with extends
|
||||||
|
expandedProfiles = lib.foldl' (
|
||||||
|
acc: profileName:
|
||||||
|
let
|
||||||
|
allProfileNames = resolveProfileExtends profileName [ ];
|
||||||
|
in
|
||||||
|
acc ++ allProfileNames
|
||||||
|
) [ ] orderedProfiles;
|
||||||
|
|
||||||
|
# Map over expanded profiles and apply priorities
|
||||||
|
allPrioritizedModules = lib.imap0 (
|
||||||
|
index: profileName:
|
||||||
|
let
|
||||||
|
# Decrement priority for each profile (lower = higher precedence)
|
||||||
|
# Start with the next lowest priority after the default priority for values (100)
|
||||||
|
profilePriority = (lib.modules.defaultOverridePriority - 1) - index;
|
||||||
|
profileConfig = getProfileConfig profileName;
|
||||||
|
|
||||||
|
# Check if an option type needs explicit override to resolve conflicts
|
||||||
|
# Only apply overrides to LEAF values (scalars), not collection types that can merge
|
||||||
|
typeNeedsOverride =
|
||||||
|
type:
|
||||||
|
if type == null then
|
||||||
|
false
|
||||||
|
else
|
||||||
|
let
|
||||||
|
typeName = type.name or type._type or "";
|
||||||
|
|
||||||
|
# True leaf types that need priority resolution when they conflict
|
||||||
|
isLeafType = builtins.elem typeName [
|
||||||
|
"str"
|
||||||
|
"int"
|
||||||
|
"bool"
|
||||||
|
"enum"
|
||||||
|
"path"
|
||||||
|
"package"
|
||||||
|
"float"
|
||||||
|
"anything"
|
||||||
|
];
|
||||||
|
in
|
||||||
|
if isLeafType then
|
||||||
|
true
|
||||||
|
else if typeName == "nullOr" then
|
||||||
|
# For nullOr, check the wrapped type recursively
|
||||||
|
let
|
||||||
|
innerType =
|
||||||
|
type.elemType
|
||||||
|
or (if type ? nestedTypes && type.nestedTypes ? elemType then type.nestedTypes.elemType else null);
|
||||||
|
in
|
||||||
|
if innerType != null then typeNeedsOverride innerType else false
|
||||||
|
else
|
||||||
|
# Everything else (collections, submodules, etc.) should merge naturally
|
||||||
|
false;
|
||||||
|
|
||||||
|
# Check if a config path needs explicit override
|
||||||
|
pathNeedsOverride =
|
||||||
|
optionPath:
|
||||||
|
let
|
||||||
|
# Try direct option first
|
||||||
|
directOption = lib.attrByPath optionPath null baseProject.options;
|
||||||
|
in
|
||||||
|
if directOption != null && lib.isOption directOption then
|
||||||
|
typeNeedsOverride directOption.type
|
||||||
|
else if optionPath != [ ] then
|
||||||
|
# Check parent for freeform type
|
||||||
|
let
|
||||||
|
parentPath = lib.init optionPath;
|
||||||
|
parentOption = lib.attrByPath parentPath null baseProject.options;
|
||||||
|
in
|
||||||
|
if parentOption != null && lib.isOption parentOption then
|
||||||
|
let
|
||||||
|
# Look for freeform type:
|
||||||
|
# 1. Standard location: type.freeformType (primary)
|
||||||
|
# 2. Nested location: type.nestedTypes.freeformType (evaluated form)
|
||||||
|
freeformType = parentOption.type.freeformType or parentOption.type.nestedTypes.freeformType or null;
|
||||||
|
elementType =
|
||||||
|
if freeformType ? elemType then
|
||||||
|
freeformType.elemType
|
||||||
|
else if freeformType ? nestedTypes && freeformType.nestedTypes ? elemType then
|
||||||
|
freeformType.nestedTypes.elemType
|
||||||
|
else
|
||||||
|
freeformType;
|
||||||
|
in
|
||||||
|
typeNeedsOverride elementType
|
||||||
|
else
|
||||||
|
false
|
||||||
|
else
|
||||||
|
false;
|
||||||
|
|
||||||
|
# Support overriding both plain attrset modules and functions
|
||||||
|
applyModuleOverride =
|
||||||
|
config:
|
||||||
|
if builtins.isFunction config then
|
||||||
|
let
|
||||||
|
wrapper = args: applyOverrideRecursive (config args) [ ];
|
||||||
|
in
|
||||||
|
lib.mirrorFunctionArgs config wrapper
|
||||||
|
else
|
||||||
|
applyOverrideRecursive config [ ];
|
||||||
|
|
||||||
|
# Apply overrides recursively based on option types
|
||||||
|
applyOverrideRecursive =
|
||||||
|
config: optionPath:
|
||||||
|
if lib.isAttrs config && config ? _type then
|
||||||
|
config # Don't touch values with existing type metadata
|
||||||
|
else if lib.isAttrs config then
|
||||||
|
lib.mapAttrs (name: value: applyOverrideRecursive value (optionPath ++ [ name ])) config
|
||||||
|
else if pathNeedsOverride optionPath then
|
||||||
|
lib.mkOverride profilePriority config
|
||||||
|
else
|
||||||
|
config;
|
||||||
|
|
||||||
|
# Apply priority overrides recursively to the deferredModule imports structure
|
||||||
|
prioritizedConfig = (
|
||||||
|
profileConfig.module
|
||||||
|
// {
|
||||||
|
imports = lib.map (
|
||||||
|
importItem:
|
||||||
|
importItem
|
||||||
|
// {
|
||||||
|
imports = lib.map (nestedImport: applyModuleOverride nestedImport) (importItem.imports or [ ]);
|
||||||
|
}
|
||||||
|
) (profileConfig.module.imports or [ ]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
in
|
||||||
|
prioritizedConfig
|
||||||
|
) expandedProfiles;
|
||||||
|
in
|
||||||
|
if allPrioritizedModules == [ ] then
|
||||||
|
baseProject
|
||||||
|
else
|
||||||
|
baseProject.extendModules { modules = allPrioritizedModules; };
|
||||||
|
|
||||||
|
config = project.config;
|
||||||
|
|
||||||
|
options = pkgs.nixosOptionsDoc {
|
||||||
|
options = builtins.removeAttrs project.options [ "_module" ];
|
||||||
|
warningsAreErrors = false;
|
||||||
|
# Unpack Nix types, e.g. literalExpression, mDoc.
|
||||||
|
transformOptions =
|
||||||
|
let
|
||||||
|
isDocType =
|
||||||
|
v:
|
||||||
|
builtins.elem v [
|
||||||
|
"literalDocBook"
|
||||||
|
"literalExpression"
|
||||||
|
"literalMD"
|
||||||
|
"mdDoc"
|
||||||
|
];
|
||||||
|
in
|
||||||
|
lib.attrsets.mapAttrs (
|
||||||
|
_: v:
|
||||||
|
if v ? _type && isDocType v._type then
|
||||||
|
v.text
|
||||||
|
else if v ? _type && v._type == "derivation" then
|
||||||
|
v.name
|
||||||
|
else
|
||||||
|
v
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
# Recursively search for outputs in the config.
|
||||||
|
# This is used when not building a specific output by attrpath.
|
||||||
|
build =
|
||||||
|
options: config:
|
||||||
|
lib.concatMapAttrs (
|
||||||
|
name: option:
|
||||||
|
if lib.isOption option then
|
||||||
|
let
|
||||||
|
typeName = option.type.name or "";
|
||||||
|
in
|
||||||
|
if
|
||||||
|
builtins.elem typeName [
|
||||||
|
"output"
|
||||||
|
"outputOf"
|
||||||
|
]
|
||||||
|
then
|
||||||
|
{ ${name} = config.${name}; }
|
||||||
|
else
|
||||||
|
{ }
|
||||||
|
else if builtins.isAttrs option && !lib.isDerivation option then
|
||||||
|
let
|
||||||
|
v = build option config.${name};
|
||||||
|
in
|
||||||
|
if v != { } then
|
||||||
|
{
|
||||||
|
${name} = v;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ }
|
||||||
|
else
|
||||||
|
{ }
|
||||||
|
) options;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit
|
||||||
|
config
|
||||||
|
options
|
||||||
|
build
|
||||||
|
project
|
||||||
|
;
|
||||||
|
shell = config.shell;
|
||||||
|
packages = {
|
||||||
|
optionsJSON = options.optionsJSON;
|
||||||
|
# deprecated
|
||||||
|
inherit (config)
|
||||||
|
info
|
||||||
|
procfileScript
|
||||||
|
procfileEnv
|
||||||
|
procfile
|
||||||
|
;
|
||||||
|
ci = config.ciDerivation;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Generate per-system devenv configurations
|
||||||
|
perSystem = nixpkgs.lib.genAttrs systems mkDevenvForSystem;
|
||||||
|
|
||||||
|
# Default devenv for the current system
|
||||||
|
currentSystemDevenv = perSystem.${vars.system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShell = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.shell);
|
||||||
|
packages = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.packages);
|
||||||
|
|
||||||
|
# Per-system devenv configurations
|
||||||
|
devenv = {
|
||||||
|
# Default devenv for the current system
|
||||||
|
inherit (currentSystemDevenv)
|
||||||
|
config
|
||||||
|
options
|
||||||
|
build
|
||||||
|
shell
|
||||||
|
packages
|
||||||
|
project
|
||||||
|
;
|
||||||
|
# Per-system devenv configurations
|
||||||
|
inherit perSystem;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Legacy build output
|
||||||
|
build = currentSystemDevenv.build currentSystemDevenv.options currentSystemDevenv.config;
|
||||||
|
};
|
||||||
|
}
|
||||||
78
.gitignore
vendored
78
.gitignore
vendored
|
|
@ -4,13 +4,11 @@ node_modules/
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
build/
|
.next/
|
||||||
*.tsbuildinfo
|
.nuxt/
|
||||||
|
.output/
|
||||||
# Environment files
|
target/
|
||||||
.env
|
*.node
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|
@ -19,35 +17,51 @@ build/
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
*.nsec
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
.secrets.baseline
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
.turbo/
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.eslintcache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Nix
|
# Electron
|
||||||
result
|
apps/machine/dist-electron/
|
||||||
result-*
|
apps/machine/release/
|
||||||
.direnv/
|
|
||||||
|
# devenv
|
||||||
.devenv/
|
.devenv/
|
||||||
.devenv.flake.nix
|
.direnv/
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
logs/
|
|
||||||
|
|
||||||
# Test coverage
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# Lamassu specific
|
|
||||||
.lamassu/
|
|
||||||
*.pem
|
|
||||||
*.crt
|
|
||||||
*.key
|
|
||||||
|
|
||||||
# Claude Code
|
|
||||||
.claude/
|
|
||||||
|
|
||||||
# Pre-commit (auto-generated by devenv)
|
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
|
||||||
# External dependencies (cloned for docker)
|
# Docker
|
||||||
Lightning.Pub/
|
docker/**/data/
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
*.timestamp-*.mjs
|
||||||
|
|
|
||||||
12
.gitmodules
vendored
12
.gitmodules
vendored
|
|
@ -1,12 +0,0 @@
|
||||||
[submodule "lamassu-server"]
|
|
||||||
path = lamassu-server
|
|
||||||
url = https://github.com/lamassu/lamassu-server.git
|
|
||||||
[submodule "lamassu-machine"]
|
|
||||||
path = lamassu-machine
|
|
||||||
url = https://github.com/lamassu/lamassu-machine.git
|
|
||||||
[submodule "lamassu-install"]
|
|
||||||
path = lamassu-install
|
|
||||||
url = https://github.com/lamassu/lamassu-install.git
|
|
||||||
[submodule "lnbits"]
|
|
||||||
path = lnbits
|
|
||||||
url = https://github.com/lnbits/lnbits.git
|
|
||||||
404
CLAUDE.md
404
CLAUDE.md
|
|
@ -1,121 +1,339 @@
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code when working with the Lamassu Next codebase.
|
||||||
|
|
||||||
## Repository Overview
|
## Project Overview
|
||||||
|
|
||||||
This is the Lamassu Bitcoin ATM system, consisting of three components:
|
**Lamassu Next** is a Nostr-native Lightning ATM system. Key principles:
|
||||||
|
|
||||||
- **lamassu-server/** - Backend services and admin dashboard (pnpm monorepo with Turbo)
|
- **KYC-Free**: No identity collection, no compliance theater
|
||||||
- **lamassu-machine/** - ATM kiosk software that runs on the physical machines
|
- **Lightning-Native**: Security encapsulated in Lightning protocol
|
||||||
- **lamassu-install/** - Production installation and upgrade scripts
|
- **Nostr as Infrastructure**: Relay for communication, keypairs for identity
|
||||||
|
- **Open Source First**: Every component auditable and forkable
|
||||||
## Commands
|
|
||||||
|
|
||||||
### lamassu-server (monorepo)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd lamassu-server
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# Run all packages in development mode (server + admin-ui)
|
|
||||||
pnpm run dev
|
|
||||||
|
|
||||||
# Build all packages
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# Run tests across all packages
|
|
||||||
pnpm run test
|
|
||||||
|
|
||||||
# Run a single test file
|
|
||||||
cd packages/admin-ui && pnpm vitest run path/to/file.test.js
|
|
||||||
|
|
||||||
# Database migrations
|
|
||||||
node packages/server/bin/lamassu-migrate
|
|
||||||
|
|
||||||
# Generate SSL certificates (first-time setup)
|
|
||||||
bash packages/server/tools/cert-gen.sh
|
|
||||||
|
|
||||||
# Create admin user
|
|
||||||
node packages/server/bin/lamassu-register admin@example.com superuser
|
|
||||||
|
|
||||||
# Regenerate database types (requires running postgres)
|
|
||||||
cd packages/typesafe-db && pnpm run generate-types
|
|
||||||
```
|
|
||||||
|
|
||||||
### lamassu-machine
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd lamassu-machine
|
|
||||||
|
|
||||||
# Install and build
|
|
||||||
npm install
|
|
||||||
bash ./setup.sh
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
npm test
|
|
||||||
|
|
||||||
# Development with mock hardware
|
|
||||||
node bin/fake-bills.js # In one terminal
|
|
||||||
node bin/lamassu-machine --mockBillValidator --mockBillDispenser --mockCam --mockPair '<totem>'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### lamassu-server Monorepo Structure
|
|
||||||
|
|
||||||
```
|
```
|
||||||
packages/
|
lamassu-next/
|
||||||
├── server/ # Express + Apollo GraphQL backend (CommonJS)
|
├── apps/
|
||||||
├── admin-ui/ # React 18 + Vite + MUI admin dashboard (ESM)
|
│ ├── machine/ # Electron + Vue 3 ATM kiosk application ✅
|
||||||
├── coins/ # @lamassu/coins - cryptocurrency constants (TypeScript)
|
│ ├── dashboard/ # Vue 3 operator dashboard (planned)
|
||||||
└── typesafe-db/ # @lamassu/typesafe-db - Kysely database layer (TypeScript)
|
│ └── relay/ # strfry relay configuration (planned)
|
||||||
|
├── packages/
|
||||||
|
│ ├── hal/ # TypeScript Hardware Abstraction Layer ✅
|
||||||
|
│ ├── nostr-client/ # Nostr client library ✅
|
||||||
|
│ ├── clink/ # CLINK protocol implementation ✅
|
||||||
|
│ ├── state-machine/ # XState v5 ATM state machine ✅
|
||||||
|
│ ├── lightning/ # Lightning.Pub RPC client ✅
|
||||||
|
│ ├── cashu/ # Cashu ecash (placeholder)
|
||||||
|
│ └── ui-shared/ # Shared Vue components (placeholder)
|
||||||
|
└── docker/ # Development infrastructure ✅
|
||||||
```
|
```
|
||||||
|
|
||||||
**Dependency flow**: `server` and `admin-ui` depend on `coins` and `typesafe-db`
|
## Implementation Status
|
||||||
|
|
||||||
**Key server entry points**:
|
### Completed Packages
|
||||||
- `bin/lamassu-server` - Main HTTPS server (port 3000, client cert auth)
|
|
||||||
- `bin/lamassu-admin-server` - Admin API server
|
|
||||||
- 20+ CLI utilities in `bin/` for operations tasks
|
|
||||||
|
|
||||||
**GraphQL**: Two implementations exist:
|
| Package | Description | Tests |
|
||||||
- `lib/graphql/` - Machine-facing API
|
| ------------------------ | ------------------------------------------------------ | ----- |
|
||||||
- `lib/new-admin/graphql/` - Admin dashboard API
|
| `@lamassu/nostr-client` | Nostr relay client with NIP-42 auth, NIP-44 encryption | 13 |
|
||||||
|
| `@lamassu/clink` | CLINK protocol (kinds 21001-21003), noffer encoding | 7 |
|
||||||
|
| `@lamassu/lightning` | Lightning.Pub RPC client (kind 21000) | 10 |
|
||||||
|
| `@lamassu/state-machine` | XState v5 ATM state machine (idle, cashIn, cashOut) | 14 |
|
||||||
|
| `@lamassu/hal` | Hardware drivers (ID003 validator, F56 dispenser) | - |
|
||||||
|
|
||||||
### lamassu-machine
|
### Placeholder Packages
|
||||||
|
|
||||||
The ATM kiosk uses a state machine architecture (`machina.js`) in `lib/brain.js` (134KB). The UI is vanilla JavaScript with Babel transpilation.
|
| Package | Description |
|
||||||
|
| -------------------- | --------------------------------- |
|
||||||
|
| `@lamassu/cashu` | Cashu ecash for offline operation |
|
||||||
|
| `@lamassu/ui-shared` | Shared Vue 3 components |
|
||||||
|
|
||||||
**Hardware drivers** in `lib/`: id003, mei, puloon, ccnet (bill validators), printer, leds, camera
|
### Completed Applications
|
||||||
|
|
||||||
|
- **apps/machine** - Electron ATM kiosk with Vue 3 UI (HAL integrated, ready for hardware testing)
|
||||||
|
|
||||||
|
### Planned Components
|
||||||
|
|
||||||
|
- **apps/dashboard** - Operator dashboard for fleet management
|
||||||
|
|
||||||
|
### Critical Documentation
|
||||||
|
|
||||||
|
- **`packages/lightning/TROUBLESHOOTING.md`** - Lightning.Pub integration gotchas. Read this BEFORE debugging payment issues. Contains solutions to 9 non-obvious issues that took 5+ hours to diagnose.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enter development environment
|
||||||
|
devenv shell
|
||||||
|
|
||||||
|
# Start development
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# Build all packages
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Infrastructure management
|
||||||
|
infra-up # Start all Docker services
|
||||||
|
infra-down # Stop all Docker services
|
||||||
|
infra-status # Show service status
|
||||||
|
infra-logs # Follow service logs
|
||||||
|
|
||||||
|
# Bitcoin/Lightning (regtest)
|
||||||
|
btccli # Bitcoin CLI
|
||||||
|
lncli # LND CLI (Lightning.Pub's node)
|
||||||
|
lncli-alice # LND CLI (Alice's node for testing payments)
|
||||||
|
mine-blocks # Mine regtest blocks (default: 1)
|
||||||
|
setup-channel # Setup channel between Alice and LND
|
||||||
|
alice-pay # Pay invoice from Alice's node
|
||||||
|
relay-test # Test Nostr relay connection
|
||||||
|
|
||||||
|
# Testing (E2E)
|
||||||
|
test-setup # Validate test environment (services, channels, payments)
|
||||||
|
test-payment # Quick e2e payment test (ATM → customer)
|
||||||
|
fund-atm # Fund ATM account (default: 100k sats)
|
||||||
|
alice-invoice # Create invoice on Alice's node
|
||||||
|
node-info # Show node pubkeys and channel info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Infrastructure
|
||||||
|
|
||||||
|
The `docker/` directory contains a complete development environment:
|
||||||
|
|
||||||
|
| Service | Container | Port(s) | Description |
|
||||||
|
| ------------- | --------------------- | ----------- | ------------------------------------- |
|
||||||
|
| strfry | lamassu-relay | 7777 | Private Nostr relay |
|
||||||
|
| bitcoind | lamassu-bitcoind | 18443 | Bitcoin Core (regtest) |
|
||||||
|
| LND | lamassu-lnd | 10009, 8080 | Lightning node (Lightning.Pub's node) |
|
||||||
|
| LND Alice | lamassu-lnd-alice | 10010, 8081 | Second LND for payment testing |
|
||||||
|
| Lightning.Pub | lamassu-lightning-pub | 1776 | Nostr-native account system |
|
||||||
|
| PostgreSQL | lamassu-postgres | 5432 | Database for server-side state |
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devenv shell # Enter dev environment
|
||||||
|
infra-up # Start all services (30-60s first run)
|
||||||
|
mine-blocks 101 # Fund the regtest wallet
|
||||||
|
setup-channel # Open channel between Alice and LND
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Payments
|
||||||
|
|
||||||
|
The development setup includes two LND nodes to enable proper payment testing:
|
||||||
|
|
||||||
|
1. **LND** (`lamassu-lnd`) - Used by Lightning.Pub to create invoices
|
||||||
|
2. **Alice** (`lamassu-lnd-alice`) - Used to pay invoices (simulates external payers)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get Lightning.Pub admin token
|
||||||
|
curl -X POST "http://localhost:1776/api/admin/app/auth" \
|
||||||
|
-H "Authorization: Bearer lamassu-dev-admin-token" \
|
||||||
|
-d '{"name": "wallet"}'
|
||||||
|
|
||||||
|
# Create user and invoice
|
||||||
|
curl -X POST "http://localhost:1776/api/app/user/add" -H "Authorization: Bearer $APP_TOKEN" \
|
||||||
|
-d '{"identifier": "test-user", "balance": 0}'
|
||||||
|
|
||||||
|
curl -X POST "http://localhost:1776/api/app/user/add/invoice" -H "Authorization: Bearer $APP_TOKEN" \
|
||||||
|
-d '{"receiver_identifier": "test-user", "payer_identifier": "external", "http_callback_url": "", "invoice_req": {"amountSats": 1000, "memo": "Test"}}'
|
||||||
|
|
||||||
|
# Pay from Alice
|
||||||
|
alice-pay <invoice>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comprehensive Regtest Integration
|
||||||
|
|
||||||
|
For advanced testing with multiple Lightning implementations, use the regtest environment at `~/dev/local/docker/regtest`. This provides:
|
||||||
|
|
||||||
|
| Service | Description |
|
||||||
|
| ----------------- | ------------------------------------- |
|
||||||
|
| 4 LND nodes | lnd-1 (hub), lnd-2 (Boltz), lnd-3 (LNbits), lnd-4 (standalone) |
|
||||||
|
| 3 CLN nodes | Core Lightning with REST/gRPC |
|
||||||
|
| 1 Eclair node | ACINQ Eclair implementation |
|
||||||
|
| LNbits | Lightning wallet platform (port 5001) |
|
||||||
|
| Boltz | Submarine swaps (port 9001) |
|
||||||
|
| Electrs | Electrum server (port 3002) |
|
||||||
|
| Lightning Terminal| Web UI for lnd-1 (port 8443) |
|
||||||
|
| Elements/Liquid | Sidechain (port 18884) |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start regtest environment
|
||||||
|
cd ~/dev/local/docker/regtest && ./start-regtest
|
||||||
|
source docker-scripts.sh
|
||||||
|
|
||||||
|
# Start lamassu services connected to regtest
|
||||||
|
cd lamassu-next/docker && ./start-with-regtest.sh
|
||||||
|
|
||||||
|
# CLI helpers
|
||||||
|
bitcoin-cli-sim -generate 1 # Mine blocks
|
||||||
|
lncli-sim 4 getinfo # lnd-4 (Lightning.Pub's node)
|
||||||
|
lightning-cli-sim 1 getinfo # CLN node 1
|
||||||
|
```
|
||||||
|
|
||||||
|
The integration uses `lnd-4` as Lightning.Pub's backend, giving you access to test payments from multiple node types (LND, CLN, Eclair) and services (LNbits, Boltz).
|
||||||
|
|
||||||
|
### MCP Tools Available
|
||||||
|
|
||||||
|
Claude has access to these MCP servers for development:
|
||||||
|
|
||||||
|
| MCP Server | Purpose |
|
||||||
|
| ------------ | ----------------------------------------- |
|
||||||
|
| docker-mcp | Container management (logs, status, etc.) |
|
||||||
|
| nostr-mcp | Nostr operations (post notes, profiles) |
|
||||||
|
| postgres-mcp | Database queries and schema inspection |
|
||||||
|
| mcp-nixos | NixOS/Nix package queries |
|
||||||
|
| forgejo-mcp | Git operations on Forgejo |
|
||||||
|
|
||||||
|
Use these to interact with infrastructure directly during development.
|
||||||
|
|
||||||
|
## Key Technologies
|
||||||
|
|
||||||
|
| Component | Technology | Notes |
|
||||||
|
| ------------- | -------------- | --------------------------------------- |
|
||||||
|
| Runtime | Node.js 22 LTS | Strict TypeScript, ESM |
|
||||||
|
| ATM Shell | Electron | Node.js main process, Vue 3 renderer |
|
||||||
|
| State Machine | XState v5 | Actor model, service injection |
|
||||||
|
| Hardware | TypeScript | ID003, F56 drivers from lamassu-machine |
|
||||||
|
| Messaging | Nostr | NIP-01, NIP-42, NIP-44 |
|
||||||
|
| Payments | CLINK + RPC | Kind 21000 (RPC), 21001-21003 (CLINK) |
|
||||||
|
| Backend | Lightning.Pub | Nostr-native account system |
|
||||||
|
|
||||||
|
## Custom Skills
|
||||||
|
|
||||||
|
The following skills are available for development assistance:
|
||||||
|
|
||||||
|
### `/security` - Security Review
|
||||||
|
|
||||||
|
Audit code for Bitcoin/Lightning/ATM-specific vulnerabilities.
|
||||||
|
|
||||||
|
```
|
||||||
|
/security packages/lightning/src/
|
||||||
|
/security --staged
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/nostr-check` - Nostr Conformity
|
||||||
|
|
||||||
|
Validate NIP compliance and Nostr protocol implementation.
|
||||||
|
|
||||||
|
```
|
||||||
|
/nostr-check packages/nostr-client/src/events.ts --nips NIP-01,NIP-44
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/lightning-check` - Lightning.Pub Conformity
|
||||||
|
|
||||||
|
Validate CLINK protocol and Lightning.Pub integration.
|
||||||
|
|
||||||
|
```
|
||||||
|
/lightning-check packages/clink/src/ --clink
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/test` - Testing Agent
|
||||||
|
|
||||||
|
Run tests, generate test cases, validate transaction flows.
|
||||||
|
|
||||||
|
```
|
||||||
|
/test coverage packages/state-machine/
|
||||||
|
/test flow cash-out
|
||||||
|
/test generate packages/lightning/src/client.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/docs` - Documentation Agent
|
||||||
|
|
||||||
|
Keep documentation synchronized with code.
|
||||||
|
|
||||||
|
```
|
||||||
|
/docs sync packages/clink/
|
||||||
|
/docs api packages/nostr-client/src/
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/hal-check` - HAL Validation
|
||||||
|
|
||||||
|
Validate Rust HAL drivers against lamassu-machine implementations.
|
||||||
|
|
||||||
|
```
|
||||||
|
/hal-check port id003
|
||||||
|
/hal-check safety packages/hal/src/dispensers/
|
||||||
|
```
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
**Formatting** (enforced by husky pre-commit):
|
### TypeScript
|
||||||
- 2-space indent, no semicolons, single quotes, trailing commas
|
|
||||||
- Prettier + ESLint with auto-fix on commit
|
|
||||||
|
|
||||||
**TypeScript**: Only in `packages/coins/` and `packages/typesafe-db/`. Use `@typescript-eslint/consistent-type-imports` for imports.
|
- ESM only (`import`/`export`)
|
||||||
|
- Strict mode with `strictNullChecks` and `noUncheckedIndexedAccess`
|
||||||
|
- Zod for runtime validation
|
||||||
|
- No `any` types
|
||||||
|
|
||||||
**Server code**: CommonJS (`require`/`module.exports`)
|
### Rust (HAL)
|
||||||
**Admin UI**: ESM (`import`/`export`)
|
|
||||||
|
|
||||||
## Database
|
- Stable toolchain
|
||||||
|
- `#![deny(unsafe_code)]` unless justified
|
||||||
|
- Error handling with `thiserror`
|
||||||
|
- Async with `tokio`
|
||||||
|
|
||||||
PostgreSQL with Kysely ORM. Types are auto-generated from the schema.
|
### Formatting
|
||||||
|
|
||||||
**Environment**: Configure postgres connection in `packages/server/.env`
|
- Prettier for TypeScript (2 spaces, no semicolons, single quotes)
|
||||||
|
- rustfmt for Rust
|
||||||
|
- Pre-commit hooks enforce formatting
|
||||||
|
|
||||||
## Requirements
|
## Hardware Drivers
|
||||||
|
|
||||||
- Node.js 22+
|
Drivers are ported from `lamassu-machine/lib/`:
|
||||||
- pnpm 10+
|
|
||||||
- PostgreSQL
|
|
||||||
- Python 3 (for native dependency builds)
|
|
||||||
|
|
||||||
## Documentation
|
| Category | Drivers |
|
||||||
|
| ---------- | ------------------------------------------------------------ |
|
||||||
|
| Validators | id003, ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 |
|
||||||
|
| Dispensers | puloon, f56, genmega, hcm2, gsr50 |
|
||||||
|
| Printers | nippon, zebra, genmega |
|
||||||
|
|
||||||
- `docs/modernization-plan.md` - 2026 modernization roadmap (Obsidian-compatible)
|
When porting:
|
||||||
|
|
||||||
|
1. Read JS driver thoroughly
|
||||||
|
2. Document protocol from JS code
|
||||||
|
3. Implement Rust version
|
||||||
|
4. Test against same hardware
|
||||||
|
5. Use `/hal-check port <driver>` to validate
|
||||||
|
|
||||||
|
## Nostr Event Kinds
|
||||||
|
|
||||||
|
| Kind | Description |
|
||||||
|
| ----- | ----------------------------------------------- |
|
||||||
|
| 21000 | Lightning.Pub RPC (generic request/response) |
|
||||||
|
| 21001 | CLINK Offer (invoice request/response) |
|
||||||
|
| 21002 | CLINK Debit (payment authorization) |
|
||||||
|
| 21003 | CLINK Manage (offer management) |
|
||||||
|
| 30078 | Service Beacon (replaceable, service discovery) |
|
||||||
|
| 30079 | Transaction Record (replaceable) |
|
||||||
|
|
||||||
|
## Security Priorities
|
||||||
|
|
||||||
|
1. **Private keys** - Never log nsec, protect with 0600 permissions
|
||||||
|
2. **Payments** - Validate invoices, verify preimages, prevent double-pay
|
||||||
|
3. **Hardware** - Validate dispense amounts, handle errors gracefully
|
||||||
|
4. **Encryption** - Use NIP-44 for all sensitive data
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
- Unit tests for all packages
|
||||||
|
- Integration tests for cross-package interactions
|
||||||
|
- E2E tests for full transaction flows
|
||||||
|
- **Cash-out flow is critical path** (95%+ of activity)
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- `docs/architecture-comparison.md` - Nostr-native vs traditional lamassu-server comparison
|
||||||
|
- `docs/ndebit-cash-in-flow.md` - Technical walkthrough of cash-in implementation
|
||||||
|
- `packages/lightning/TROUBLESHOOTING.md` - Lightning.Pub integration gotchas (must read!)
|
||||||
|
- `.claude/skills/*.md` - Custom skill documentation
|
||||||
|
|
||||||
|
## External Resources
|
||||||
|
|
||||||
|
- [CLINK Protocol Spec](https://github.com/shocknet/clink)
|
||||||
|
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub)
|
||||||
|
- [NIP-44 Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
|
||||||
|
- [LND Hold Invoices](https://docs.lightning.engineering/lightning-network-tools/lnd/hold-invoices)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
members = ["lamassu-next/apps/machine/src-tauri"]
|
|
||||||
|
|
@ -93,8 +93,10 @@ ipcMain.handle('get-config', () => {
|
||||||
relayUrl: process.env.VITE_RELAY_URL || 'ws://localhost:7777',
|
relayUrl: process.env.VITE_RELAY_URL || 'ws://localhost:7777',
|
||||||
lightningPubPubkey: process.env.VITE_LIGHTNING_PUB_PUBKEY || '',
|
lightningPubPubkey: process.env.VITE_LIGHTNING_PUB_PUBKEY || '',
|
||||||
lightningPubApiUrl: process.env.VITE_LIGHTNING_PUB_API_URL || 'http://localhost:1776',
|
lightningPubApiUrl: process.env.VITE_LIGHTNING_PUB_API_URL || 'http://localhost:1776',
|
||||||
|
extensionApiUrl: process.env.VITE_EXTENSION_API_URL || 'http://localhost:1777',
|
||||||
atmPrivateKey: process.env.VITE_ATM_PRIVATE_KEY || '',
|
atmPrivateKey: process.env.VITE_ATM_PRIVATE_KEY || '',
|
||||||
adminToken: process.env.VITE_ADMIN_TOKEN || '',
|
adminToken: process.env.VITE_ADMIN_TOKEN || '',
|
||||||
|
appId: process.env.VITE_APP_ID || '',
|
||||||
|
|
||||||
// Hardware configuration
|
// Hardware configuration
|
||||||
machineModel: process.env.VITE_LAMASSU_MACHINE_MODEL || 'sintra',
|
machineModel: process.env.VITE_LAMASSU_MACHINE_MODEL || 'sintra',
|
||||||
|
|
@ -15,8 +15,10 @@ export interface RuntimeConfig {
|
||||||
relayUrl: string
|
relayUrl: string
|
||||||
lightningPubPubkey: string
|
lightningPubPubkey: string
|
||||||
lightningPubApiUrl: string
|
lightningPubApiUrl: string
|
||||||
|
extensionApiUrl: string
|
||||||
atmPrivateKey: string
|
atmPrivateKey: string
|
||||||
adminToken: string
|
adminToken: string
|
||||||
|
appId: string
|
||||||
machineModel: string
|
machineModel: string
|
||||||
fiatCode: string
|
fiatCode: string
|
||||||
validatorDevice?: string
|
validatorDevice?: string
|
||||||
1
docker/.state/atm-app-id
Normal file
1
docker/.state/atm-app-id
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
02f5340554b29537f4b9c3bb6c131f69c08044b2114939849d3bec8c4df456e7
|
||||||
1
docker/.state/atm-app-token
Normal file
1
docker/.state/atm-app-token
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBJZCI6IjAyZjUzNDA1NTRiMjk1MzdmNGI5YzNiYjZjMTMxZjY5YzA4MDQ0YjIxMTQ5Mzk4NDlkM2JlYzhjNGRmNDU2ZTciLCJpYXQiOjE3NzExODIwMTZ9.uE473GYJdB946daNCZ-5PqPe1JGihSM6KnL66n1AN7k
|
||||||
1
docker/.state/lightning-pub-nprofile
Normal file
1
docker/.state/lightning-pub-nprofile
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
nprofile1qyg8wue69uhhxarjvee8jw3hxumnwqpq35fnkv4nguyhrutu4tspkjlfaqs95pnnegzqkg24h040vn3852jshankcx
|
||||||
1
docker/.state/lightning-pub-pubkey
Normal file
1
docker/.state/lightning-pub-pubkey
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
8d133b32b3470971f17caae01b4be9e8205a0673ca040b2155bbeaf64e27a2a5
|
||||||
655
docker/dev.sh
Executable file
655
docker/dev.sh
Executable file
|
|
@ -0,0 +1,655 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Lamassu Next Development Environment
|
||||||
|
#
|
||||||
|
# Single command to manage the complete development stack:
|
||||||
|
# - Regtest Bitcoin/Lightning network (from ~/dev/local/docker/regtest)
|
||||||
|
# - Lightning.Pub with configurable image/worktree
|
||||||
|
# - Auto-configured ATM application
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./dev.sh up # Start everything
|
||||||
|
# ./dev.sh up --fund # Start and auto-fund ATM with 100k sats
|
||||||
|
# ./dev.sh up --fund=50000 # Start and auto-fund with specific amount
|
||||||
|
# ./dev.sh up --worktree ~/path/to/lp # Build Lightning.Pub from worktree
|
||||||
|
# ./dev.sh up --image myimage:tag # Use specific Docker image
|
||||||
|
# ./dev.sh down # Stop everything
|
||||||
|
# ./dev.sh atm # Launch ATM application
|
||||||
|
# ./dev.sh status # Show connection info
|
||||||
|
# ./dev.sh logs [service] # Follow logs
|
||||||
|
# ./dev.sh fund [amount] # Fund ATM (default: 100000 sats)
|
||||||
|
# ./dev.sh mine [blocks] # Mine blocks manually
|
||||||
|
# ./dev.sh reset # Reset all state (fresh start)
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}"
|
||||||
|
DEFAULT_IMAGE="lightning-pub-withdraw:latest"
|
||||||
|
DEFAULT_FUNDING_SATS=100000
|
||||||
|
|
||||||
|
# State files
|
||||||
|
STATE_DIR="$SCRIPT_DIR/.state"
|
||||||
|
PUBKEY_FILE="$STATE_DIR/lightning-pub-pubkey"
|
||||||
|
NPROFILE_FILE="$STATE_DIR/lightning-pub-nprofile"
|
||||||
|
APP_TOKEN_FILE="$STATE_DIR/atm-app-token"
|
||||||
|
APP_ID_FILE="$STATE_DIR/atm-app-id"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}►${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}✗${NC} $1"; }
|
||||||
|
success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||||
|
|
||||||
|
# Ensure state directory exists
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
# Helper Functions
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
get_local_ip() {
|
||||||
|
ip route get 1 2>/dev/null | awk '{print $7; exit}' || hostname -I | awk '{print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
is_regtest_running() {
|
||||||
|
# Check if lnd-4 container is actually running (not just network exists)
|
||||||
|
docker ps --filter "name=lnbits-lnd-4-1" --format "{{.Names}}" 2>/dev/null | grep -q lnbits-lnd-4-1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_lnd4_ready() {
|
||||||
|
# Use lnd-4 hostname (not localhost) since lnd binds to container IP
|
||||||
|
docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 getinfo &>/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_lightning_pub_ready() {
|
||||||
|
docker logs lamassu-lightning-pub 2>&1 | grep -q "LightningPub listening"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_lnd4_balance() {
|
||||||
|
docker exec lnbits-lnd-4-1 lncli --network=regtest walletbalance 2>/dev/null | grep -oP '"total_balance":\s*"\K[0-9]+' || echo "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
# Regtest Management
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
start_regtest() {
|
||||||
|
if is_regtest_running; then
|
||||||
|
log "Regtest network already running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$REGTEST_DIR" ]]; then
|
||||||
|
error "Regtest directory not found: $REGTEST_DIR"
|
||||||
|
echo " Clone it from: https://github.com/your-org/regtest-env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starting regtest environment..."
|
||||||
|
# Use project name "lnbits" to match the expected network name (lnbits_default)
|
||||||
|
(cd "$REGTEST_DIR" && docker compose -p lnbits up -d)
|
||||||
|
|
||||||
|
# Wait for lnd-4 to be ready
|
||||||
|
log "Waiting for lnd-4 to be ready..."
|
||||||
|
local attempts=0
|
||||||
|
while ! is_lnd4_ready && [[ $attempts -lt 30 ]]; do
|
||||||
|
sleep 2
|
||||||
|
attempts=$((attempts + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if is_lnd4_ready; then
|
||||||
|
success "lnd-4 is ready"
|
||||||
|
else
|
||||||
|
error "lnd-4 failed to start"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_regtest() {
|
||||||
|
if [[ -d "$REGTEST_DIR" ]]; then
|
||||||
|
log "Stopping regtest environment..."
|
||||||
|
(cd "$REGTEST_DIR" && docker compose down)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
# Lightning.Pub Management
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
build_from_worktree() {
|
||||||
|
local worktree="$1"
|
||||||
|
local image_name="lightning-pub-dev:latest"
|
||||||
|
|
||||||
|
if [[ ! -d "$worktree" ]]; then
|
||||||
|
error "Worktree not found: $worktree"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Building Lightning.Pub from $worktree..."
|
||||||
|
docker build -t "$image_name" "$worktree"
|
||||||
|
echo "$image_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_lightning_pub() {
|
||||||
|
log "Waiting for Lightning.Pub..."
|
||||||
|
local attempts=0
|
||||||
|
while ! is_lightning_pub_ready && [[ $attempts -lt 45 ]]; do
|
||||||
|
# Check for errors
|
||||||
|
if docker logs lamassu-lightning-pub 2>&1 | grep -q "Error:"; then
|
||||||
|
local err=$(docker logs lamassu-lightning-pub 2>&1 | grep "Error:" | tail -1)
|
||||||
|
error "Lightning.Pub error: $err"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
attempts=$((attempts + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if is_lightning_pub_ready; then
|
||||||
|
sleep 2 # Extra time for Nostr middleware
|
||||||
|
success "Lightning.Pub is ready"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
error "Lightning.Pub failed to start (timeout)"
|
||||||
|
docker logs lamassu-lightning-pub 2>&1 | tail -10
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_lightning_pub_info() {
|
||||||
|
local pubkey=$(docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1)
|
||||||
|
local nprofile=$(docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1)
|
||||||
|
|
||||||
|
echo "$pubkey" > "$PUBKEY_FILE"
|
||||||
|
echo "$nprofile" > "$NPROFILE_FILE"
|
||||||
|
|
||||||
|
echo "$pubkey"
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
# ATM Configuration
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
update_atm_env() {
|
||||||
|
local pubkey="$1"
|
||||||
|
local env_file="$PROJECT_DIR/apps/machine/.env"
|
||||||
|
|
||||||
|
if [[ ! -f "$env_file" ]]; then
|
||||||
|
warn "ATM .env not found, creating..."
|
||||||
|
cat > "$env_file" << EOF
|
||||||
|
# Lightning.Pub connection
|
||||||
|
VITE_RELAY_URL=ws://localhost:7777
|
||||||
|
VITE_LIGHTNING_PUB_PUBKEY=$pubkey
|
||||||
|
VITE_LIGHTNING_PUB_API_URL=http://localhost:1776
|
||||||
|
VITE_ADMIN_TOKEN=lamassu-dev-admin-token
|
||||||
|
VITE_ATM_PRIVATE_KEY=f391a2c3fc734f443b0f685688a0441b5fb9805853c0023f570c5a3c6412b136
|
||||||
|
|
||||||
|
# Extension API (for LNURL-withdraw)
|
||||||
|
VITE_EXTENSION_API_URL=http://localhost:1777
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
# Update existing pubkey
|
||||||
|
if grep -q "VITE_LIGHTNING_PUB_PUBKEY" "$env_file"; then
|
||||||
|
sed -i "s/VITE_LIGHTNING_PUB_PUBKEY=.*/VITE_LIGHTNING_PUB_PUBKEY=$pubkey/" "$env_file"
|
||||||
|
else
|
||||||
|
echo "VITE_LIGHTNING_PUB_PUBKEY=$pubkey" >> "$env_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
success "Updated ATM .env with pubkey"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_atm_app() {
|
||||||
|
log "Creating ATM app..."
|
||||||
|
|
||||||
|
# Generate unique app name to avoid conflicts
|
||||||
|
local app_name="atm-$(date +%s)"
|
||||||
|
|
||||||
|
local response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer lamassu-dev-admin-token" \
|
||||||
|
-d "{\"name\":\"$app_name\",\"allow_user_creation\":true}" 2>/dev/null)
|
||||||
|
|
||||||
|
if echo "$response" | grep -q '"status":"OK"'; then
|
||||||
|
local app_id=$(echo "$response" | grep -oP '"id":"\K[^"]+')
|
||||||
|
local app_token=$(echo "$response" | grep -oP '"auth_token":"\K[^"]+')
|
||||||
|
|
||||||
|
echo "$app_id" > "$APP_ID_FILE"
|
||||||
|
echo "$app_token" > "$APP_TOKEN_FILE"
|
||||||
|
|
||||||
|
# Update ATM .env with app ID
|
||||||
|
local env_file="$PROJECT_DIR/apps/machine/.env"
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
if grep -q "VITE_APP_ID" "$env_file"; then
|
||||||
|
sed -i "s/VITE_APP_ID=.*/VITE_APP_ID=$app_id/" "$env_file"
|
||||||
|
else
|
||||||
|
echo "" >> "$env_file"
|
||||||
|
echo "# ATM App ID for LNURL-withdraw" >> "$env_file"
|
||||||
|
echo "VITE_APP_ID=$app_id" >> "$env_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "Created ATM app: ${app_id:0:16}..."
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "Failed to create ATM app (may already exist)"
|
||||||
|
# Try to use existing app from state file
|
||||||
|
if [[ -f "$APP_ID_FILE" ]]; then
|
||||||
|
log "Using existing app ID from state"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
# Zeus Connection
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
generate_lndconnect() {
|
||||||
|
local lnd_data="$REGTEST_DIR/data/lnd-3"
|
||||||
|
local local_ip=$(get_local_ip)
|
||||||
|
local rest_port=8082
|
||||||
|
|
||||||
|
local cert_path="$lnd_data/tls.cert"
|
||||||
|
local mac_path="$lnd_data/data/chain/bitcoin/regtest/admin.macaroon"
|
||||||
|
|
||||||
|
if [[ ! -f "$cert_path" ]] || [[ ! -f "$mac_path" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cert_b64=$(base64 -w0 "$cert_path" | tr '+/' '-_' | tr -d '=')
|
||||||
|
local mac_b64=$(base64 -w0 "$mac_path" | tr '+/' '-_' | tr -d '=')
|
||||||
|
|
||||||
|
echo "lndconnect://${local_ip}:${rest_port}?cert=${cert_b64}&macaroon=${mac_b64}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
# Display Functions
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
get_atm_balance() {
|
||||||
|
# Get ATM app owner balance from Lightning.Pub logs
|
||||||
|
local app_id=$(cat "$APP_ID_FILE" 2>/dev/null)
|
||||||
|
if [[ -z "$app_id" ]]; then
|
||||||
|
echo "not configured"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Query the app balance via admin API (if available)
|
||||||
|
# For now, just indicate it's configured
|
||||||
|
local app_token=$(cat "$APP_TOKEN_FILE" 2>/dev/null)
|
||||||
|
if [[ -n "$app_token" ]]; then
|
||||||
|
echo "configured (use './dev.sh fund' to add sats)"
|
||||||
|
else
|
||||||
|
echo "not configured"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_status() {
|
||||||
|
local pubkey=$(cat "$PUBKEY_FILE" 2>/dev/null || docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1)
|
||||||
|
local nprofile=$(cat "$NPROFILE_FILE" 2>/dev/null || docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1)
|
||||||
|
local local_ip=$(get_local_ip)
|
||||||
|
local lndconnect=$(generate_lndconnect 2>/dev/null || echo "")
|
||||||
|
local lnd4_balance=$(get_lnd4_balance)
|
||||||
|
local app_id=$(cat "$APP_ID_FILE" 2>/dev/null || echo "not set")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ LAMASSU NEXT - DEVELOPMENT ENVIRONMENT ║${NC}"
|
||||||
|
echo -e "${BOLD}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}Lightning.Pub${NC}"
|
||||||
|
echo -e " Pubkey: ${GREEN}$pubkey${NC}"
|
||||||
|
echo -e " nprofile: ${GREEN}$nprofile${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}Service URLs (local)${NC}"
|
||||||
|
echo " Nostr Relay: ws://localhost:7777"
|
||||||
|
echo " Lightning.Pub: http://localhost:1776"
|
||||||
|
echo " Withdraw API: http://localhost:1777"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}External Access (LAN: $local_ip)${NC}"
|
||||||
|
echo " Nostr Relay: ws://${local_ip}:7777"
|
||||||
|
echo " Withdraw API: http://${local_ip}:1777"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}lnd-4 (Lightning.Pub backend)${NC}"
|
||||||
|
echo " Balance: ${lnd4_balance} sats"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}ATM App${NC}"
|
||||||
|
if [[ "$app_id" != "not set" ]]; then
|
||||||
|
echo " App ID: ${app_id:0:16}..."
|
||||||
|
echo " Status: $(get_atm_balance)"
|
||||||
|
else
|
||||||
|
echo " Status: not configured"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ -n "$lndconnect" ]]; then
|
||||||
|
echo -e "${CYAN}Zeus Wallet (connect to lnd-3 for testing)${NC}"
|
||||||
|
echo -e " ${DIM}$lndconnect${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CYAN}Quick Commands${NC}"
|
||||||
|
echo " ./dev.sh logs lightning-pub # View Lightning.Pub logs"
|
||||||
|
echo " ./dev.sh fund 100000 # Fund ATM with 100k sats"
|
||||||
|
echo " ./dev.sh status # Show this info"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}═══════════════════════════════════════════════════════════════════${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
# Main Commands
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
cmd_up() {
|
||||||
|
local image="$DEFAULT_IMAGE"
|
||||||
|
local worktree=""
|
||||||
|
local skip_regtest=false
|
||||||
|
local auto_fund=false
|
||||||
|
local fund_amount="$DEFAULT_FUNDING_SATS"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--image) image="$2"; shift 2 ;;
|
||||||
|
--worktree) worktree="$2"; shift 2 ;;
|
||||||
|
--skip-regtest) skip_regtest=true; shift ;;
|
||||||
|
--fund) auto_fund=true; shift ;;
|
||||||
|
--fund=*) auto_fund=true; fund_amount="${1#*=}"; shift ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "Starting Lamassu development environment..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Start regtest if needed
|
||||||
|
if [[ "$skip_regtest" != "true" ]]; then
|
||||||
|
start_regtest || exit 1
|
||||||
|
else
|
||||||
|
if ! is_regtest_running; then
|
||||||
|
error "Regtest not running. Remove --skip-regtest or start it manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Build from worktree if specified
|
||||||
|
if [[ -n "$worktree" ]]; then
|
||||||
|
image=$(build_from_worktree "$worktree")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Check image exists
|
||||||
|
if ! docker image inspect "$image" &>/dev/null; then
|
||||||
|
error "Docker image not found: $image"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " 1. Build from worktree: ./dev.sh up --worktree ~/path/to/lightning-pub"
|
||||||
|
echo " 2. Build manually: docker build -t $image ~/path/to/lightning-pub"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Using Lightning.Pub image: $image"
|
||||||
|
|
||||||
|
# 4. Start lamassu services
|
||||||
|
export REGTEST_DATA_DIR="$REGTEST_DIR/data"
|
||||||
|
export LIGHTNING_PUB_IMAGE="$image"
|
||||||
|
export HOST_IP=$(get_local_ip)
|
||||||
|
|
||||||
|
log "Starting lamassu services..."
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" up -d
|
||||||
|
|
||||||
|
# 5. Wait for Lightning.Pub
|
||||||
|
if ! wait_for_lightning_pub; then
|
||||||
|
error "Failed to start. Check: ./dev.sh logs lightning-pub"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Extract and save Lightning.Pub info
|
||||||
|
local pubkey=$(extract_lightning_pub_info)
|
||||||
|
|
||||||
|
# 7. Update ATM .env
|
||||||
|
update_atm_env "$pubkey"
|
||||||
|
|
||||||
|
# 8. Setup ATM app
|
||||||
|
setup_atm_app || true
|
||||||
|
|
||||||
|
# 9. Auto-fund if requested
|
||||||
|
if [[ "$auto_fund" == "true" ]]; then
|
||||||
|
echo ""
|
||||||
|
log "Auto-funding ATM with $fund_amount sats..."
|
||||||
|
sleep 2 # Give Lightning.Pub a moment to settle
|
||||||
|
cmd_fund "$fund_amount" || warn "Auto-funding failed. Run './dev.sh fund' manually."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 10. Show status
|
||||||
|
show_status
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_down() {
|
||||||
|
log "Stopping lamassu services..."
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ "$1" == "--all" ]]; then
|
||||||
|
stop_regtest
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "Services stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_reset() {
|
||||||
|
warn "This will delete all lamassu state (Lightning.Pub identity, ATM app, etc.)"
|
||||||
|
read -p "Continue? [y/N] " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Aborted"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Stopping services..."
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down -v 2>/dev/null || true
|
||||||
|
|
||||||
|
log "Removing state files..."
|
||||||
|
rm -rf "$STATE_DIR"
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
success "Reset complete. Run './dev.sh up' for fresh start."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" logs -f "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then
|
||||||
|
error "Services not running. Start with: ./dev.sh up"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
show_status
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_fund() {
|
||||||
|
local amount="${1:-$DEFAULT_FUNDING_SATS}"
|
||||||
|
|
||||||
|
if ! is_regtest_running; then
|
||||||
|
error "Regtest not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for app token
|
||||||
|
if [[ ! -f "$APP_TOKEN_FILE" ]]; then
|
||||||
|
error "ATM app not configured. Run './dev.sh up' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local app_token=$(cat "$APP_TOKEN_FILE")
|
||||||
|
|
||||||
|
log "Creating invoice for $amount sats..."
|
||||||
|
|
||||||
|
# Create invoice for app owner (using unique payer_identifier)
|
||||||
|
local payer_id="funder-$(date +%s)"
|
||||||
|
local response=$(curl -s -X POST "http://localhost:1776/api/app/add/invoice" \
|
||||||
|
-H "Authorization: Bearer $app_token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"payer_identifier\": \"$payer_id\", \"http_callback_url\": \"\", \"invoice_req\": {\"amountSats\": $amount, \"memo\": \"ATM funding\"}}")
|
||||||
|
|
||||||
|
local invoice=$(echo "$response" | grep -oP '"invoice":"\K[^"]+')
|
||||||
|
|
||||||
|
if [[ -z "$invoice" ]]; then
|
||||||
|
error "Failed to create invoice"
|
||||||
|
echo "Response: $response"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Paying invoice from lnd-3..."
|
||||||
|
|
||||||
|
# Source regtest helpers and pay
|
||||||
|
if [[ -f "$REGTEST_DIR/docker-scripts.sh" ]]; then
|
||||||
|
(
|
||||||
|
cd "$REGTEST_DIR"
|
||||||
|
source docker-scripts.sh 2>/dev/null
|
||||||
|
lncli-sim 3 payinvoice --force "$invoice"
|
||||||
|
)
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
success "Funded ATM with $amount sats"
|
||||||
|
else
|
||||||
|
error "Payment failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback: try direct docker exec
|
||||||
|
docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 payinvoice --force "$invoice"
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
success "Funded ATM with $amount sats"
|
||||||
|
else
|
||||||
|
error "Payment failed. Make sure lnd-3 has funds and channels."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_mine() {
|
||||||
|
local blocks="${1:-1}"
|
||||||
|
if ! is_regtest_running; then
|
||||||
|
error "Regtest not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Mining $blocks block(s)..."
|
||||||
|
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest -generate "$blocks" > /dev/null
|
||||||
|
local height=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getblockcount)
|
||||||
|
success "Mined $blocks block(s) (height: $height)"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_atm() {
|
||||||
|
local atm_dir="$PROJECT_DIR/apps/machine"
|
||||||
|
|
||||||
|
if [[ ! -d "$atm_dir" ]]; then
|
||||||
|
error "ATM app not found at $atm_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if services are running
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then
|
||||||
|
warn "Services not running. Start with: ./dev.sh up"
|
||||||
|
read -p "Start services first? [Y/n] " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||||
|
cmd_up
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starting ATM application..."
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}ATM Mock Mode:${NC}"
|
||||||
|
echo " - Press 'b' to insert a bill (simulates cash insertion)"
|
||||||
|
echo " - Use the UI to complete transactions"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd "$atm_dir" && pnpm dev
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
# Entry Point
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
case "${1:-help}" in
|
||||||
|
up|start)
|
||||||
|
shift
|
||||||
|
cmd_up "$@"
|
||||||
|
;;
|
||||||
|
down|stop)
|
||||||
|
shift
|
||||||
|
cmd_down "$@"
|
||||||
|
;;
|
||||||
|
reset)
|
||||||
|
cmd_reset
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
shift
|
||||||
|
cmd_logs "$@"
|
||||||
|
;;
|
||||||
|
status|info)
|
||||||
|
cmd_status
|
||||||
|
;;
|
||||||
|
fund)
|
||||||
|
shift
|
||||||
|
cmd_fund "$@"
|
||||||
|
;;
|
||||||
|
mine)
|
||||||
|
shift
|
||||||
|
cmd_mine "$@"
|
||||||
|
;;
|
||||||
|
atm)
|
||||||
|
cmd_atm
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Lamassu Next Development Environment"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: $0 <command> [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " up [options] Start development environment"
|
||||||
|
echo " down [--all] Stop services (--all includes regtest)"
|
||||||
|
echo " atm Launch ATM application (Electron)"
|
||||||
|
echo " status Show connection info"
|
||||||
|
echo " logs [service] Follow service logs"
|
||||||
|
echo " fund [sats] Fund ATM account"
|
||||||
|
echo " mine [blocks] Mine blocks manually (default: 1)"
|
||||||
|
echo " reset Delete all state and start fresh"
|
||||||
|
echo ""
|
||||||
|
echo "Note: Auto-miner runs in background (1 block/2min). Check with:"
|
||||||
|
echo " ./dev.sh logs miner"
|
||||||
|
echo ""
|
||||||
|
echo "Options for 'up':"
|
||||||
|
echo " --worktree <path> Build Lightning.Pub from git worktree"
|
||||||
|
echo " --image <name> Use specific Docker image"
|
||||||
|
echo " --skip-regtest Don't auto-start regtest"
|
||||||
|
echo " --fund Auto-fund ATM with 100k sats after startup"
|
||||||
|
echo " --fund=<sats> Auto-fund ATM with specific amount"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 up # Start with default image"
|
||||||
|
echo " $0 up --fund # Start and fund ATM"
|
||||||
|
echo " $0 up --fund=50000 # Start and fund with 50k sats"
|
||||||
|
echo " $0 up --worktree ~/dev/lightning-pub/withdraw"
|
||||||
|
echo " $0 atm # Launch ATM app"
|
||||||
|
echo " $0 fund 200000 # Add 200k more sats"
|
||||||
|
echo " $0 logs lightning-pub"
|
||||||
|
echo " $0 reset && $0 up --fund # Fresh start with funding"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
148
docker/docker-compose.regtest.yml
Normal file
148
docker/docker-compose.regtest.yml
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Lamassu Next - Regtest Integration
|
||||||
|
#
|
||||||
|
# This overlay connects lamassu-next services to the comprehensive regtest
|
||||||
|
# environment at ~/dev/local/docker/regtest
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# 1. Start the regtest environment:
|
||||||
|
# cd ~/dev/local/docker/regtest && ./start-regtest
|
||||||
|
#
|
||||||
|
# 2. Start lamassu services:
|
||||||
|
# cd lamassu-next/docker && docker compose -f docker-compose.regtest.yml up -d
|
||||||
|
#
|
||||||
|
# 3. Configure apps/machine/.env:
|
||||||
|
# VITE_RELAY_URL=ws://localhost:7777
|
||||||
|
# VITE_EXTENSION_API_URL=http://localhost:1777
|
||||||
|
#
|
||||||
|
# Services:
|
||||||
|
# - strfry: Private Nostr relay (port 7777)
|
||||||
|
# - lightning-pub: Nostr-native Lightning account system (port 1776)
|
||||||
|
# - Uses lnd-4 from regtest as backend
|
||||||
|
# - Withdraw extension on port 1777
|
||||||
|
# - miner: Auto-mines blocks to keep Lightning channels active
|
||||||
|
#
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Private Nostr relay for ATM communication
|
||||||
|
strfry:
|
||||||
|
image: ghcr.io/hoytech/strfry:latest
|
||||||
|
container_name: lamassu-relay
|
||||||
|
ports:
|
||||||
|
- '7777:7777'
|
||||||
|
volumes:
|
||||||
|
- ./strfry.conf:/etc/strfry.conf:ro
|
||||||
|
- strfry-data:/app/strfry-db
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 524288
|
||||||
|
hard: 524288
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'nc', '-z', 'localhost', '7777']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- regtest
|
||||||
|
|
||||||
|
# Lightning.Pub - Nostr-native Lightning account system
|
||||||
|
# Connects to lnd-4 from the regtest environment
|
||||||
|
# Use LIGHTNING_PUB_IMAGE env var to specify image (default: lightning-pub-withdraw)
|
||||||
|
lightning-pub:
|
||||||
|
image: ${LIGHTNING_PUB_IMAGE:-lightning-pub-withdraw:latest}
|
||||||
|
container_name: lamassu-lightning-pub
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
ports:
|
||||||
|
- '1776:1776'
|
||||||
|
- '1777:1777' # Withdraw extension HTTP API
|
||||||
|
volumes:
|
||||||
|
- lightning-pub-data:/root/lightning_pub
|
||||||
|
# Override Dockerfile's anonymous /app/data volume with named volume
|
||||||
|
- lightning-pub-appdata:/app/data
|
||||||
|
# Mount lnd-4 data from regtest for macaroons/certs
|
||||||
|
- ${REGTEST_DATA_DIR:-/home/padreug/dev/local/docker/regtest/data}/lnd-4:/root/.lnd:ro
|
||||||
|
environment:
|
||||||
|
- NETWORK=regtest
|
||||||
|
# lnd-4 is accessible via Docker network
|
||||||
|
- LND_ADDRESS=lnd-4:10009
|
||||||
|
- LND_CERT_PATH=/root/.lnd/tls.cert
|
||||||
|
- LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon
|
||||||
|
# Use strfry from this compose
|
||||||
|
- NOSTR_RELAYS=ws://strfry:7777
|
||||||
|
# Disable external liquidity provider for regtest
|
||||||
|
- DISABLE_LIQUIDITY_PROVIDER=true
|
||||||
|
# Admin token for HTTP API access (development only)
|
||||||
|
- ADMIN_TOKEN=lamassu-dev-admin-token
|
||||||
|
# Extension HTTP API URL (for LNURL callbacks from external wallets)
|
||||||
|
# Use HOST_IP env var for your machine's LAN IP (required for phone wallets)
|
||||||
|
- EXTENSION_SERVICE_URL=http://${HOST_IP:-192.168.1.190}:1777
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- regtest
|
||||||
|
|
||||||
|
# Auto-miner for regtest (mines 1 block every MINE_INTERVAL seconds)
|
||||||
|
# Keeps Lightning channels active during development
|
||||||
|
miner:
|
||||||
|
image: boltz/bitcoin-core:25.0
|
||||||
|
container_name: lamassu-miner
|
||||||
|
entrypoint: /bin/sh
|
||||||
|
command:
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
echo "Auto-miner started (interval: $${MINE_INTERVAL}s)"
|
||||||
|
# Wait for bitcoind to be ready
|
||||||
|
while ! bitcoin-cli -regtest -rpcconnect=bitcoind getblockchaininfo > /dev/null 2>&1; do
|
||||||
|
echo "Waiting for bitcoind..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "bitcoind ready, starting mining loop"
|
||||||
|
while true; do
|
||||||
|
bitcoin-cli -regtest -rpcconnect=bitcoind -generate 1 > /dev/null 2>&1 && echo "Mined block $(bitcoin-cli -regtest -rpcconnect=bitcoind getblockcount)"
|
||||||
|
sleep $${MINE_INTERVAL}
|
||||||
|
done
|
||||||
|
environment:
|
||||||
|
- MINE_INTERVAL=${MINE_INTERVAL:-120}
|
||||||
|
volumes:
|
||||||
|
- bitcoin-data:/root/.bitcoin
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- regtest
|
||||||
|
|
||||||
|
# PostgreSQL for optional server-side state
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: lamassu-postgres
|
||||||
|
ports:
|
||||||
|
- '5432:5432'
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: lamassu_dev
|
||||||
|
POSTGRES_USER: lamassu
|
||||||
|
POSTGRES_PASSWORD: lamassu_dev_password
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U lamassu -d lamassu_dev']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- regtest
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
strfry-data:
|
||||||
|
lightning-pub-data:
|
||||||
|
lightning-pub-appdata:
|
||||||
|
postgres-data:
|
||||||
|
# Mount the bitcoin-data volume from the regtest environment
|
||||||
|
bitcoin-data:
|
||||||
|
external: true
|
||||||
|
name: lnbits_bitcoin-data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
regtest:
|
||||||
|
# The regtest environment uses 'lnbits_default' as its Docker network
|
||||||
|
# (named after the original LNbits regtest project)
|
||||||
|
name: lnbits_default
|
||||||
|
external: true
|
||||||
335
docker/start-with-regtest.sh
Executable file
335
docker/start-with-regtest.sh
Executable file
|
|
@ -0,0 +1,335 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Start lamassu-next development environment with comprehensive regtest
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Connects to the regtest environment at ~/dev/local/docker/regtest
|
||||||
|
# 2. Starts Lightning.Pub with the specified image/worktree
|
||||||
|
# 3. Creates and funds an ATM account
|
||||||
|
# 4. Displays connection info for Zeus wallet and Lightning.Pub nprofile
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# ~/dev/local/docker/regtest must be running:
|
||||||
|
# cd ~/dev/local/docker/regtest && ./start-regtest
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./start-with-regtest.sh # Start with default image
|
||||||
|
# ./start-with-regtest.sh --image myimage # Use specific Docker image
|
||||||
|
# ./start-with-regtest.sh --worktree ~/path # Build from worktree
|
||||||
|
# ./start-with-regtest.sh down # Stop services
|
||||||
|
# ./start-with-regtest.sh logs # Follow logs
|
||||||
|
# ./start-with-regtest.sh status # Show connection info
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}"
|
||||||
|
DEFAULT_IMAGE="lightning-pub-withdraw:latest"
|
||||||
|
ATM_FUNDING_SATS=100000
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[lamassu]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[lamassu]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[lamassu]${NC} $1"; }
|
||||||
|
info() { echo -e "${CYAN}[lamassu]${NC} $1"; }
|
||||||
|
|
||||||
|
# Get local IP for external wallet access
|
||||||
|
get_local_ip() {
|
||||||
|
ip route get 1 2>/dev/null | awk '{print $7; exit}' || hostname -I | awk '{print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if regtest is running
|
||||||
|
check_regtest() {
|
||||||
|
if ! docker network inspect lnbits_default &>/dev/null; then
|
||||||
|
error "Regtest network not found!"
|
||||||
|
echo ""
|
||||||
|
echo "Start the regtest environment first:"
|
||||||
|
echo " cd $REGTEST_DIR && ./start-regtest"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if lnd-4 is running (Lightning.Pub's backend)
|
||||||
|
if ! docker exec lnbits-lnd-4-1 lncli --network=regtest getinfo &>/dev/null 2>&1; then
|
||||||
|
warn "lnd-4 not responding. Waiting..."
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build Lightning.Pub from worktree
|
||||||
|
build_from_worktree() {
|
||||||
|
local worktree="$1"
|
||||||
|
local image_name="lightning-pub-custom:latest"
|
||||||
|
|
||||||
|
if [[ ! -d "$worktree" ]]; then
|
||||||
|
error "Worktree not found: $worktree"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Building Lightning.Pub from $worktree..."
|
||||||
|
docker build -t "$image_name" "$worktree"
|
||||||
|
echo "$image_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for Lightning.Pub to be ready and get nprofile
|
||||||
|
wait_for_lightning_pub() {
|
||||||
|
log "Waiting for Lightning.Pub to start..."
|
||||||
|
local max_attempts=30
|
||||||
|
local attempt=0
|
||||||
|
|
||||||
|
while [[ $attempt -lt $max_attempts ]]; do
|
||||||
|
if docker logs lamassu-lightning-pub 2>&1 | grep -q "LightningPub listening"; then
|
||||||
|
sleep 2 # Extra time for Nostr middleware
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
error "Lightning.Pub failed to start within 60 seconds"
|
||||||
|
docker logs lamassu-lightning-pub 2>&1 | tail -20
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get Lightning.Pub nprofile from logs
|
||||||
|
get_nprofile() {
|
||||||
|
docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get Lightning.Pub pubkey from logs
|
||||||
|
get_pubkey() {
|
||||||
|
docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate lndconnect URL for Zeus (using lnd-3 which has REST exposed)
|
||||||
|
generate_lndconnect() {
|
||||||
|
local lnd_container="lnbits-lnd-3-1"
|
||||||
|
local lnd_data="$REGTEST_DIR/data/lnd-3"
|
||||||
|
local local_ip=$(get_local_ip)
|
||||||
|
local rest_port=8082 # lnd-3's REST port
|
||||||
|
|
||||||
|
# Get cert and macaroon
|
||||||
|
local cert_path="$lnd_data/tls.cert"
|
||||||
|
local mac_path="$lnd_data/data/chain/bitcoin/regtest/admin.macaroon"
|
||||||
|
|
||||||
|
if [[ ! -f "$cert_path" ]] || [[ ! -f "$mac_path" ]]; then
|
||||||
|
warn "lnd-3 credentials not found. Zeus connection string unavailable."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Base64url encode (replace + with -, / with _, remove =)
|
||||||
|
local cert_b64=$(base64 -w0 "$cert_path" | tr '+/' '-_' | tr -d '=')
|
||||||
|
local mac_b64=$(base64 -w0 "$mac_path" | tr '+/' '-_' | tr -d '=')
|
||||||
|
|
||||||
|
echo "lndconnect://${local_ip}:${rest_port}?cert=${cert_b64}&macaroon=${mac_b64}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create and fund ATM account
|
||||||
|
setup_atm_account() {
|
||||||
|
log "Setting up ATM account..."
|
||||||
|
|
||||||
|
# Create app
|
||||||
|
local app_response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer lamassu-dev-admin-token" \
|
||||||
|
-d '{"name":"atm-app","allow_user_creation":true}' 2>/dev/null)
|
||||||
|
|
||||||
|
if echo "$app_response" | grep -q '"status":"OK"'; then
|
||||||
|
local app_id=$(echo "$app_response" | grep -oP '"id":"\K[^"]+')
|
||||||
|
local app_token=$(echo "$app_response" | grep -oP '"auth_token":"\K[^"]+')
|
||||||
|
log "Created ATM app: $app_id"
|
||||||
|
|
||||||
|
# Store app token for later use
|
||||||
|
echo "$app_token" > "$SCRIPT_DIR/.atm-app-token"
|
||||||
|
echo "$app_id" > "$SCRIPT_DIR/.atm-app-id"
|
||||||
|
|
||||||
|
# TODO: Fund the app by creating an invoice and paying from lnd-3
|
||||||
|
# This requires the app to support invoice creation via HTTP API
|
||||||
|
# For now, the app starts with 0 balance
|
||||||
|
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "Failed to create ATM app: $app_response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Display connection information
|
||||||
|
show_connection_info() {
|
||||||
|
local nprofile=$(get_nprofile)
|
||||||
|
local pubkey=$(get_pubkey)
|
||||||
|
local local_ip=$(get_local_ip)
|
||||||
|
local lndconnect=$(generate_lndconnect 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${BOLD} LAMASSU REGTEST ENVIRONMENT ${NC}"
|
||||||
|
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}Lightning.Pub:${NC}"
|
||||||
|
echo -e " Pubkey: ${GREEN}$pubkey${NC}"
|
||||||
|
echo -e " nprofile: ${GREEN}$nprofile${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}Service URLs:${NC}"
|
||||||
|
echo " Nostr Relay: ws://localhost:7777"
|
||||||
|
echo " Lightning.Pub: http://localhost:1776"
|
||||||
|
echo " Withdraw API: http://localhost:1777"
|
||||||
|
echo " PostgreSQL: localhost:5432"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}External Access (from phone/other devices):${NC}"
|
||||||
|
echo " Nostr Relay: ws://${local_ip}:7777"
|
||||||
|
echo " Withdraw API: http://${local_ip}:1777"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ -n "$lndconnect" ]]; then
|
||||||
|
echo -e "${CYAN}Zeus Wallet Connection (lnd-3):${NC}"
|
||||||
|
echo -e " ${GREEN}$lndconnect${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " Scan this with Zeus to connect to lnd-3 for testing payments."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CYAN}ATM Configuration (apps/machine/.env):${NC}"
|
||||||
|
echo " VITE_RELAY_URL=ws://localhost:7777"
|
||||||
|
echo " VITE_LIGHTNING_PUB_PUBKEY=$pubkey"
|
||||||
|
echo " VITE_EXTENSION_API_URL=http://localhost:1777"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${CYAN}CLI Helpers:${NC}"
|
||||||
|
echo " source $REGTEST_DIR/docker-scripts.sh"
|
||||||
|
echo " bitcoin-cli-sim -generate 1 # Mine blocks"
|
||||||
|
echo " lncli-sim 4 getinfo # lnd-4 (Lightning.Pub)"
|
||||||
|
echo " lncli-sim 3 getinfo # lnd-3 (Zeus wallet)"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
start() {
|
||||||
|
local image="$DEFAULT_IMAGE"
|
||||||
|
local worktree=""
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--image)
|
||||||
|
image="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--worktree)
|
||||||
|
worktree="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Checking prerequisites..."
|
||||||
|
check_regtest
|
||||||
|
|
||||||
|
# Build from worktree if specified
|
||||||
|
if [[ -n "$worktree" ]]; then
|
||||||
|
image=$(build_from_worktree "$worktree")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if image exists
|
||||||
|
if ! docker image inspect "$image" &>/dev/null; then
|
||||||
|
error "Docker image not found: $image"
|
||||||
|
echo ""
|
||||||
|
echo "Either:"
|
||||||
|
echo " 1. Build the image: docker build -t $image <path>"
|
||||||
|
echo " 2. Use --worktree: ./start-with-regtest.sh --worktree ~/path/to/lightning-pub"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Using Lightning.Pub image: $image"
|
||||||
|
|
||||||
|
# Export environment variables
|
||||||
|
export REGTEST_DATA_DIR="$REGTEST_DIR/data"
|
||||||
|
export LIGHTNING_PUB_IMAGE="$image"
|
||||||
|
export HOST_IP=$(get_local_ip)
|
||||||
|
|
||||||
|
log "Starting lamassu services..."
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" up -d
|
||||||
|
|
||||||
|
# Wait for Lightning.Pub and setup
|
||||||
|
if wait_for_lightning_pub; then
|
||||||
|
setup_atm_account || true
|
||||||
|
show_connection_info
|
||||||
|
else
|
||||||
|
error "Failed to start Lightning.Pub. Check logs with: $0 logs lightning-pub"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
stop() {
|
||||||
|
log "Stopping lamassu services..."
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down
|
||||||
|
rm -f "$SCRIPT_DIR/.atm-app-token" "$SCRIPT_DIR/.atm-app-id"
|
||||||
|
log "Services stopped."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show logs
|
||||||
|
logs() {
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" logs -f "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show status/connection info
|
||||||
|
status() {
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then
|
||||||
|
error "Services not running. Start with: $0 start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
show_connection_info
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
case "${1:-start}" in
|
||||||
|
start)
|
||||||
|
shift || true
|
||||||
|
start "$@"
|
||||||
|
;;
|
||||||
|
stop|down)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
shift
|
||||||
|
logs "$@"
|
||||||
|
;;
|
||||||
|
status|info)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [command] [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " start Start services (default)"
|
||||||
|
echo " stop, down Stop services"
|
||||||
|
echo " logs [service] Follow logs"
|
||||||
|
echo " status, info Show connection info"
|
||||||
|
echo ""
|
||||||
|
echo "Options for 'start':"
|
||||||
|
echo " --image <name> Use specific Docker image"
|
||||||
|
echo " --worktree <path> Build from Lightning.Pub worktree"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 # Start with default image"
|
||||||
|
echo " $0 --worktree ~/dev/lightning-pub/withdraw # Build and use worktree"
|
||||||
|
echo " $0 --image lightning-pub-custom:v1 # Use specific image"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -1,714 +0,0 @@
|
||||||
---
|
|
||||||
title: Architecture Review - KYC-Free Lightning-First Vision
|
|
||||||
created: 2026-01-22
|
|
||||||
updated: 2026-01-22
|
|
||||||
tags:
|
|
||||||
- architecture
|
|
||||||
- lightning
|
|
||||||
- kyc-free
|
|
||||||
- redesign
|
|
||||||
- vision
|
|
||||||
status: active
|
|
||||||
priority: critical
|
|
||||||
---
|
|
||||||
|
|
||||||
# Architecture Review: KYC-Free Lightning-First Vision
|
|
||||||
|
|
||||||
> [!abstract] Summary
|
|
||||||
> A comprehensive review of our project architecture, reimagining Lamassu from scratch as a **KYC-free, open-source, Lightning-native** Bitcoin ATM ecosystem. We have complete freedom to redesign - no backward compatibility concerns.
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
- [[#Vision Statement]]
|
|
||||||
- [[#Current State Analysis]]
|
|
||||||
- [[#Proposed Architecture]]
|
|
||||||
- [[#Lightning Backend Options]]
|
|
||||||
- [[#Privacy Technologies]]
|
|
||||||
- [[#Critical Decisions]]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Vision Statement
|
|
||||||
|
|
||||||
> [!important] Core Principles
|
|
||||||
> 1. **KYC-Free** - No identity collection, no compliance theater
|
|
||||||
> 2. **Open-Source First** - Every component auditable and forkable
|
|
||||||
> 3. **Lightning-Native** - Security through protocol, not policy
|
|
||||||
> 4. **Self-Custodial** - Operator and user control their own keys
|
|
||||||
> 5. **Privacy by Default** - Minimize data collection and retention
|
|
||||||
> 6. **Autonomous Machines** - Reduce server dependency
|
|
||||||
|
|
||||||
### What We're Building
|
|
||||||
|
|
||||||
The **ultimate Bitcoin Lightning ATM** with a wallet ecosystem that:
|
|
||||||
- Converts cash ↔ Lightning instantly
|
|
||||||
- Requires no identity verification
|
|
||||||
- Operates with minimal infrastructure
|
|
||||||
- Can function offline with ecash
|
|
||||||
- Supports NFC tap-to-pay
|
|
||||||
- Enables operator sovereignty
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current State Analysis
|
|
||||||
|
|
||||||
### Lamassu Codebase Issues
|
|
||||||
|
|
||||||
The existing Lamassu codebase carries significant baggage:
|
|
||||||
|
|
||||||
| Component | Problem | Impact |
|
|
||||||
|-----------|---------|--------|
|
|
||||||
| `lib/compliance/` | KYC/AML workflows | 40% of server code |
|
|
||||||
| `lib/customers/` | Identity management | Database bloat |
|
|
||||||
| `lib/sanctions/` | OFAC screening | External dependencies |
|
|
||||||
| `lib/sms/` | Phone verification | Privacy violation |
|
|
||||||
| `lib/id-scan/` | Document verification | Third-party APIs |
|
|
||||||
| `lib/blacklist/` | User blocking | Centralized control |
|
|
||||||
| Multi-coin | Altcoin support | Code complexity |
|
|
||||||
|
|
||||||
> [!warning] Assessment
|
|
||||||
> **60%+ of lamassu-server code is compliance-related.** Rather than removing it surgically, a clean rebuild may be more efficient.
|
|
||||||
|
|
||||||
### Current LNbits Integration (As Documented)
|
|
||||||
|
|
||||||
Our current docs treat LNbits as a simple payment backend:
|
|
||||||
|
|
||||||
```
|
|
||||||
Machine → Server → LNbits → Lightning Network
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problems with this approach:**
|
|
||||||
1. Server is still a bottleneck
|
|
||||||
2. Single point of failure
|
|
||||||
3. Not utilizing LNbits' full potential
|
|
||||||
4. Missing privacy technologies (Cashu, Fedimint)
|
|
||||||
5. Still designed around on-chain model
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Proposed Architecture
|
|
||||||
|
|
||||||
### Option A: Lean Server (Recommended)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "ATM Machine"
|
|
||||||
tauri[Tauri + Vue 3]
|
|
||||||
ldk[LDK-Node / Phoenixd]
|
|
||||||
hal[Rust HAL]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Minimal Coordinator"
|
|
||||||
api[Fastify API]
|
|
||||||
db[(SQLite/PostgreSQL)]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Lightning Layer"
|
|
||||||
lnbits[LNbits]
|
|
||||||
cashu[Cashu Mint]
|
|
||||||
fedimint[Fedimint Gateway]
|
|
||||||
end
|
|
||||||
|
|
||||||
tauri -->|LNURL/BOLT12| lnbits
|
|
||||||
tauri -->|ecash| cashu
|
|
||||||
tauri -->|Optional| api
|
|
||||||
api --> db
|
|
||||||
lnbits --> fedimint
|
|
||||||
hal --> hardware[Hardware]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Changes:**
|
|
||||||
- Machine can operate independently with embedded Lightning
|
|
||||||
- Server becomes optional coordinator (fleet management, analytics)
|
|
||||||
- Multiple Lightning backends supported
|
|
||||||
- Ecash for offline capability
|
|
||||||
|
|
||||||
### Option B: Serverless Machine
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "Autonomous ATM"
|
|
||||||
ui[Vue 3 UI]
|
|
||||||
xstate[XState v5]
|
|
||||||
ldk[LDK-Node]
|
|
||||||
cashu[Cashu Wallet]
|
|
||||||
hal[Rust HAL]
|
|
||||||
end
|
|
||||||
|
|
||||||
ldk -->|Direct| ln[Lightning Network]
|
|
||||||
cashu -->|Swap| mint[Cashu Mint]
|
|
||||||
hal --> hw[Hardware]
|
|
||||||
|
|
||||||
admin[Admin Phone App] -->|Bluetooth/Local| ui
|
|
||||||
```
|
|
||||||
|
|
||||||
**Extreme autonomy:**
|
|
||||||
- No central server at all
|
|
||||||
- Machine runs its own Lightning node
|
|
||||||
- Admin via local connection (phone app)
|
|
||||||
- Perfect for single-operator deployments
|
|
||||||
|
|
||||||
### Option C: Fedimint Community Model
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "Community Federation"
|
|
||||||
g1[Guardian 1]
|
|
||||||
g2[Guardian 2]
|
|
||||||
g3[Guardian 3]
|
|
||||||
g4[Guardian 4]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "ATMs"
|
|
||||||
atm1[Machine 1]
|
|
||||||
atm2[Machine 2]
|
|
||||||
atm3[Machine 3]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Gateway"
|
|
||||||
gw[Lightning Gateway]
|
|
||||||
end
|
|
||||||
|
|
||||||
atm1 --> g1
|
|
||||||
atm2 --> g2
|
|
||||||
atm3 --> g3
|
|
||||||
g1 --> gw
|
|
||||||
g2 --> gw
|
|
||||||
g3 --> gw
|
|
||||||
g4 --> gw
|
|
||||||
gw --> ln[Lightning Network]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Community custody:**
|
|
||||||
- Multiple guardians share custody
|
|
||||||
- No single operator can rug
|
|
||||||
- Built-in ecash for privacy
|
|
||||||
- Ideal for community/coop deployments
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lightning Backend Options
|
|
||||||
|
|
||||||
### Comparison Matrix
|
|
||||||
|
|
||||||
| Backend | Self-Custodial | Complexity | Offline | Privacy | Best For |
|
|
||||||
|---------|---------------|------------|---------|---------|----------|
|
|
||||||
| **LDK-Node** | Yes | High | No | Good | Embedded in machine |
|
|
||||||
| **Phoenixd** | Yes | Low | No | Good | Simple server setup |
|
|
||||||
| **LNbits** | Depends | Medium | Via Cashu | Good | Multi-wallet, extensions |
|
|
||||||
| **Cashu** | No (mint) | Low | Yes | Excellent | Offline, privacy |
|
|
||||||
| **Fedimint** | Federated | High | Yes | Excellent | Community custody |
|
|
||||||
| **Breez SDK** | Yes | Medium | No | Good | Mobile-first |
|
|
||||||
|
|
||||||
### Recommendation: Layered Approach
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ Layer 3: User-Facing Protocols │
|
|
||||||
│ LNURL-withdraw, CLINK Offers, NFC │
|
|
||||||
├─────────────────────────────────────────────┤
|
|
||||||
│ Layer 2: Privacy & Offline │
|
|
||||||
│ Cashu ecash, Fedimint e-cash │
|
|
||||||
├─────────────────────────────────────────────┤
|
|
||||||
│ Layer 1: Lightning Backends │
|
|
||||||
│ LNbits (primary), Phoenixd, LDK-Node │
|
|
||||||
└─────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Use each layer for its strengths:**
|
|
||||||
- **LNbits** - Backend abstraction, multi-wallet, extensions
|
|
||||||
- **Cashu** - Offline payments, instant settlement, privacy
|
|
||||||
- **LNURL** - User experience (scan QR to receive)
|
|
||||||
- **CLINK Offers** - Static payment codes over Nostr (replaces BOLT12)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Privacy Technologies
|
|
||||||
|
|
||||||
### Cashu Integration
|
|
||||||
|
|
||||||
> [!decision] Cashu for Offline & Privacy
|
|
||||||
> Cashu ecash enables offline ATM operation and enhanced privacy.
|
|
||||||
|
|
||||||
**How it works for ATM:**
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User
|
|
||||||
participant ATM
|
|
||||||
participant Mint as Cashu Mint
|
|
||||||
participant LN as Lightning
|
|
||||||
|
|
||||||
User->>ATM: Insert $20 cash
|
|
||||||
ATM->>Mint: Request ecash tokens
|
|
||||||
Mint->>ATM: Issue 20,000 sat tokens
|
|
||||||
ATM->>User: Display QR (Cashu tokens)
|
|
||||||
User->>User: Scan with Cashu wallet
|
|
||||||
|
|
||||||
Note over User,LN: Later, user can...
|
|
||||||
User->>Mint: Redeem tokens
|
|
||||||
Mint->>LN: Pay Lightning invoice
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- ATM doesn't need to know user's Lightning wallet
|
|
||||||
- User receives ecash, redeems whenever
|
|
||||||
- ATM can operate offline (pre-loaded tokens)
|
|
||||||
- Perfect privacy (blinded signatures)
|
|
||||||
|
|
||||||
**Cashu Libraries:**
|
|
||||||
- `cashu-ts` - TypeScript SDK
|
|
||||||
- `cashu-rs` - Rust implementation
|
|
||||||
- `nutshell` - Python reference
|
|
||||||
|
|
||||||
### Fedimint Integration
|
|
||||||
|
|
||||||
> [!note] Fedimint for Community Operations
|
|
||||||
> When multiple operators want shared custody without single points of failure.
|
|
||||||
|
|
||||||
**Architecture:**
|
|
||||||
```typescript
|
|
||||||
// Federation of 4 guardians (3-of-4 threshold)
|
|
||||||
const federation = {
|
|
||||||
guardians: [
|
|
||||||
'operator1.onion',
|
|
||||||
'operator2.onion',
|
|
||||||
'operator3.onion',
|
|
||||||
'operator4.onion',
|
|
||||||
],
|
|
||||||
threshold: 3,
|
|
||||||
modules: ['wallet', 'mint', 'ln'],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Use Cases:**
|
|
||||||
- Bitcoin circular economy communities
|
|
||||||
- Cooperative ATM networks
|
|
||||||
- Regions with unstable operators
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Protocol Stack
|
|
||||||
|
|
||||||
### LNURL for ATM UX
|
|
||||||
|
|
||||||
> [!tip] LNURL-withdraw is Perfect for ATMs
|
|
||||||
> User scans QR from ATM screen to pull sats to their wallet.
|
|
||||||
|
|
||||||
**Cash-In Flow (User buys Bitcoin):**
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User
|
|
||||||
participant ATM
|
|
||||||
participant LNbits
|
|
||||||
|
|
||||||
User->>ATM: Insert $50 cash
|
|
||||||
ATM->>LNbits: Create LNURL-withdraw
|
|
||||||
LNbits->>ATM: lnurl1dp68gurn8ghj7...
|
|
||||||
ATM->>ATM: Display QR code
|
|
||||||
User->>User: Scan with any LN wallet
|
|
||||||
User->>LNbits: Request invoice (via LNURL)
|
|
||||||
LNbits->>User: Pay invoice to user's wallet
|
|
||||||
ATM->>ATM: Transaction complete
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Works with ANY Lightning wallet
|
|
||||||
- No camera needed on ATM
|
|
||||||
- User controls destination
|
|
||||||
- Privacy preserved
|
|
||||||
|
|
||||||
**Cash-Out Flow (User sells Bitcoin):**
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User
|
|
||||||
participant ATM
|
|
||||||
participant LNbits
|
|
||||||
|
|
||||||
User->>ATM: Select "Sell Bitcoin"
|
|
||||||
ATM->>LNbits: Create invoice
|
|
||||||
LNbits->>ATM: BOLT11 invoice
|
|
||||||
ATM->>ATM: Display QR code
|
|
||||||
User->>User: Scan & pay invoice
|
|
||||||
LNbits->>ATM: Payment confirmed
|
|
||||||
ATM->>User: Dispense cash
|
|
||||||
```
|
|
||||||
|
|
||||||
### CLINK Offers (Replaces BOLT12)
|
|
||||||
|
|
||||||
> [!decision] CLINK Offers for Static Payment Codes
|
|
||||||
> Nostr-native static payment codes - superior to BOLT12.
|
|
||||||
|
|
||||||
**Why NOT BOLT12:**
|
|
||||||
|
|
||||||
| Problem | Impact |
|
|
||||||
|---------|--------|
|
|
||||||
| Onion messages | Tor-like routing adds latency, every hop = failure point |
|
|
||||||
| Global round-trips | Requests can circle the world multiple times |
|
|
||||||
| Mobile node normalization | Encourages unreliable always-offline nodes |
|
|
||||||
| Redundant | LND keysend already provides static payments |
|
|
||||||
| Astroturfed | NGO-pushed spec with questionable motives |
|
|
||||||
|
|
||||||
**Why CLINK:**
|
|
||||||
- Uses Nostr relays (commodity infrastructure, trustless via NIP-44)
|
|
||||||
- No HTTP callbacks, WebSockets, or Tor-like messaging
|
|
||||||
- Keys decoupled from Lightning node identity
|
|
||||||
- Already working: ShockWallet, Lightning.Pub, Stacker News
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// CLINK Offer (noffer) - static payment code
|
|
||||||
const atmOffer = 'noffer1qqs...'
|
|
||||||
|
|
||||||
// Invoice request flows through Nostr relays
|
|
||||||
// No onion message round-trips!
|
|
||||||
|
|
||||||
// Using @shocknet/clink-sdk
|
|
||||||
import { createOffer, requestInvoice } from '@shocknet/clink-sdk'
|
|
||||||
|
|
||||||
const offer = createOffer({
|
|
||||||
pubkey: atmNostrPubkey,
|
|
||||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
||||||
priceType: 'variable', // ATM calculates based on cash inserted
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**CLINK Protocol:**
|
|
||||||
- **Kind 21001**: Offer Request/Response
|
|
||||||
- **Kind 21002**: Debit Request/Response
|
|
||||||
- **Kind 21003**: Management Delegation
|
|
||||||
|
|
||||||
**References:**
|
|
||||||
- [CLINK Spec](https://github.com/shocknet/CLINK)
|
|
||||||
- [CLINK Demo](https://clinkme.dev/)
|
|
||||||
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub)
|
|
||||||
|
|
||||||
### NFC BOLT Cards
|
|
||||||
|
|
||||||
> [!tip] Tap-to-Withdraw with NFC
|
|
||||||
> Pre-programmed NFC cards for instant cash withdrawal.
|
|
||||||
|
|
||||||
**How BOLT Cards work:**
|
|
||||||
1. NFC card contains LNURL-withdraw with rotating auth
|
|
||||||
2. User taps card on ATM
|
|
||||||
3. ATM reads LNURL, requests invoice
|
|
||||||
4. Card's backing service pays invoice
|
|
||||||
5. ATM dispenses cash
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
- Cards use NXP NTAG 424 DNA (secure element)
|
|
||||||
- Each tap generates unique auth code
|
|
||||||
- Supports spending limits per tap/day
|
|
||||||
- Compatible with: Coinos, LNbits, BTCPay
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// LNbits BoltCards extension
|
|
||||||
const card = {
|
|
||||||
uid: '04:E1:5F:...',
|
|
||||||
cardName: 'ATM Withdrawal Card',
|
|
||||||
maxWithdrawPerTap: 50000, // sats
|
|
||||||
dailyLimit: 200000, // sats
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Simplified Transaction Flows
|
|
||||||
|
|
||||||
### Cash → Lightning (Buy)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ SIMPLIFIED BUY FLOW │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 1. User inserts cash [$20, $50, $100] │
|
|
||||||
│ │
|
|
||||||
│ 2. ATM displays QR [LNURL-withdraw] │
|
|
||||||
│ "Scan to receive Bitcoin" │
|
|
||||||
│ │
|
|
||||||
│ 3. User scans with ANY [Phoenix, Zeus, Wallet of │
|
|
||||||
│ Lightning wallet Satoshi, Breez, etc.] │
|
|
||||||
│ │
|
|
||||||
│ 4. Sats arrive instantly [~2 seconds] │
|
|
||||||
│ │
|
|
||||||
│ Done. No account. No KYC. No email. No phone. │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lightning → Cash (Sell)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ SIMPLIFIED SELL FLOW │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ Option A: Pay Invoice │
|
|
||||||
│ 1. Select amount to withdraw [$20, $50, $100] │
|
|
||||||
│ 2. ATM shows Lightning invoice QR │
|
|
||||||
│ 3. User pays from any wallet │
|
|
||||||
│ 4. Cash dispensed │
|
|
||||||
│ │
|
|
||||||
│ Option B: NFC Tap (BOLT Card) │
|
|
||||||
│ 1. User taps NFC card │
|
|
||||||
│ 2. ATM reads LNURL-withdraw │
|
|
||||||
│ 3. Cash dispensed │
|
|
||||||
│ [Single tap, ~3 seconds total] │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Offline Mode (Cashu)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ OFFLINE CASH-IN FLOW │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ATM has pre-loaded Cashu tokens from mint │
|
|
||||||
│ │
|
|
||||||
│ 1. User inserts $50 cash │
|
|
||||||
│ │
|
|
||||||
│ 2. ATM displays Cashu token QR │
|
|
||||||
│ (No internet required!) │
|
|
||||||
│ │
|
|
||||||
│ 3. User scans with Cashu wallet │
|
|
||||||
│ [Minibits, Nutstash, eNuts] │
|
|
||||||
│ │
|
|
||||||
│ 4. User can later swap ecash → Lightning │
|
|
||||||
│ when they have connectivity │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Revised Component Architecture
|
|
||||||
|
|
||||||
### What We Keep from Lamassu
|
|
||||||
|
|
||||||
| Component | Keep? | Notes |
|
|
||||||
|-----------|-------|-------|
|
|
||||||
| Hardware drivers | Yes | Port to Rust HAL |
|
|
||||||
| Bill validator protocols | Yes | ID003, eSSP, ccTalk |
|
|
||||||
| Bill dispenser drivers | Yes | Puloon, Fujitsu |
|
|
||||||
| Brain state machine | Rewrite | Simplify with XState v5 |
|
|
||||||
| Admin UI | Partial | Rebuild in Vue 3 |
|
|
||||||
| Server API | Minimal | Strip compliance code |
|
|
||||||
|
|
||||||
### What We Remove
|
|
||||||
|
|
||||||
| Component | Why Remove |
|
|
||||||
|-----------|------------|
|
|
||||||
| `lib/compliance/` | No KYC |
|
|
||||||
| `lib/customers/` | No identity storage |
|
|
||||||
| `lib/sanctions/` | No OFAC screening |
|
|
||||||
| `lib/sms/` | No phone verification |
|
|
||||||
| `lib/id-scan/` | No document scanning |
|
|
||||||
| `lib/blacklist/` | No user blocking |
|
|
||||||
| Multi-coin support | Bitcoin only |
|
|
||||||
| Fiat exchange rates | Lightning is the unit |
|
|
||||||
|
|
||||||
### New Components to Build
|
|
||||||
|
|
||||||
| Component | Purpose | Technology |
|
|
||||||
|-----------|---------|------------|
|
|
||||||
| `lightning-service` | Backend abstraction | LNbits + Cashu + Fedimint |
|
|
||||||
| `lnurl-server` | LNURL-withdraw/pay | Fastify + LNbits |
|
|
||||||
| `clink-handler` | Static offers via Nostr | @shocknet/clink-sdk |
|
|
||||||
| `cashu-bridge` | Offline capability | cashu-ts |
|
|
||||||
| `nfc-handler` | BOLT card support | libnfc + Rust |
|
|
||||||
| `admin-app` | Operator mobile app | Vue 3 + Capacitor |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Revised Tech Stack
|
|
||||||
|
|
||||||
### Server (Coordinator)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Runtime: Node.js 22 LTS
|
|
||||||
Language: TypeScript (strict)
|
|
||||||
Framework: Fastify
|
|
||||||
API: tRPC (admin), LNURL (public)
|
|
||||||
Database: SQLite (single) / PostgreSQL (fleet)
|
|
||||||
ORM: Drizzle
|
|
||||||
Lightning: LNbits API
|
|
||||||
Ecash: Cashu client
|
|
||||||
```
|
|
||||||
|
|
||||||
### Machine
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Shell: Tauri 2.x (Rust)
|
|
||||||
UI: Vue 3 + Pinia + shadcn-vue
|
|
||||||
State: XState v5
|
|
||||||
Hardware: Rust HAL + napi-rs
|
|
||||||
Lightning: LDK-Node or Phoenixd (optional)
|
|
||||||
Ecash: Cashu wallet
|
|
||||||
NFC: libnfc bindings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mobile Admin App
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Framework: Vue 3 + Ionic/Capacitor
|
|
||||||
Connectivity: Bluetooth LE, Local WiFi
|
|
||||||
Features: Machine pairing, balance check, settings
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Critical Decisions Needed
|
|
||||||
|
|
||||||
### Decision 1: Server Model
|
|
||||||
|
|
||||||
| Option | Pros | Cons |
|
|
||||||
|--------|------|------|
|
|
||||||
| **A: Lean Server** | Fleet management, familiar model | Single point of failure |
|
|
||||||
| **B: Serverless** | Maximum autonomy | Complex admin |
|
|
||||||
| **C: Fedimint** | Community custody | Requires federation |
|
|
||||||
|
|
||||||
> [!question] Recommendation
|
|
||||||
> Start with **Option A (Lean Server)** for faster development, design for Option B compatibility.
|
|
||||||
|
|
||||||
### Decision 2: Primary Lightning Backend
|
|
||||||
|
|
||||||
| Option | Pros | Cons |
|
|
||||||
|--------|------|------|
|
|
||||||
| **LNbits** | Extensions, multi-wallet | Requires server |
|
|
||||||
| **Phoenixd** | Simple, self-custodial | ACINQ dependency |
|
|
||||||
| **LDK-Node** | Embedded, maximum control | Complex |
|
|
||||||
|
|
||||||
> [!question] Recommendation
|
|
||||||
> **LNbits** as primary (proven, extensible), with **Cashu** for offline mode.
|
|
||||||
|
|
||||||
### Decision 3: Ecash Strategy
|
|
||||||
|
|
||||||
| Option | Pros | Cons |
|
|
||||||
|--------|------|------|
|
|
||||||
| **Cashu** | Simple, growing ecosystem | Single mint trust |
|
|
||||||
| **Fedimint** | Federated trust | Complex setup |
|
|
||||||
| **Both** | Maximum flexibility | Maintenance burden |
|
|
||||||
|
|
||||||
> [!question] Recommendation
|
|
||||||
> **Cashu** first (simpler), add Fedimint support later.
|
|
||||||
|
|
||||||
### Decision 4: Rebuild vs Refactor
|
|
||||||
|
|
||||||
| Option | Effort | Risk | Result |
|
|
||||||
|--------|--------|------|--------|
|
|
||||||
| **Rebuild** | 6-12 months | Medium | Clean architecture |
|
|
||||||
| **Refactor** | 12-18 months | High | Frankenstein code |
|
|
||||||
|
|
||||||
> [!question] Recommendation
|
|
||||||
> **Rebuild** the core, reuse hardware drivers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Roadmap
|
|
||||||
|
|
||||||
### Phase 1: Foundation
|
|
||||||
|
|
||||||
- [ ] Create new monorepo structure
|
|
||||||
- [ ] Set up devenv.nix for development
|
|
||||||
- [ ] Port hardware drivers to Rust HAL
|
|
||||||
- [ ] Implement LNURL-withdraw flow
|
|
||||||
- [ ] Basic Vue 3 machine UI
|
|
||||||
|
|
||||||
### Phase 2: Lightning Integration
|
|
||||||
|
|
||||||
- [ ] LNbits integration (simplified from current docs)
|
|
||||||
- [ ] LNURL-pay for cash-out
|
|
||||||
- [ ] Cashu ecash support
|
|
||||||
- [ ] NFC BOLT card support
|
|
||||||
|
|
||||||
### Phase 3: Operator Tools
|
|
||||||
|
|
||||||
- [ ] Minimal admin API
|
|
||||||
- [ ] Vue 3 admin dashboard
|
|
||||||
- [ ] Mobile admin app
|
|
||||||
- [ ] Fleet management (optional)
|
|
||||||
|
|
||||||
### Phase 4: Advanced Features
|
|
||||||
|
|
||||||
- [ ] CLINK Offers (Nostr-native static codes)
|
|
||||||
- [ ] Fedimint integration
|
|
||||||
- [ ] LDK-Node embedded option
|
|
||||||
- [ ] Offline-first mode
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparison: Old vs New
|
|
||||||
|
|
||||||
| Aspect | Old Lamassu | New Vision |
|
|
||||||
|--------|-------------|------------|
|
|
||||||
| Identity | KYC/AML required | None collected |
|
|
||||||
| Compliance | 60% of codebase | 0% |
|
|
||||||
| Coins | 30+ altcoins | Bitcoin only |
|
|
||||||
| On-chain | Primary | Emergency fallback |
|
|
||||||
| Lightning | Secondary | Primary |
|
|
||||||
| Privacy | Minimal | Maximum (Cashu) |
|
|
||||||
| Server | Required | Optional |
|
|
||||||
| Offline | Not possible | Cashu ecash |
|
|
||||||
| NFC | Not supported | BOLT cards |
|
|
||||||
| Custody | Operator holds | User self-custody |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
1. **Exchange rate source?** - Do we quote BTC/fiat or operate in sats-only mode?
|
|
||||||
2. **Minimum viable admin?** - What's the smallest admin surface needed?
|
|
||||||
3. **Machine authentication?** - How do machines auth to coordinator without certs?
|
|
||||||
4. **Liquidity management?** - How do operators manage Lightning liquidity?
|
|
||||||
5. **Regulatory reality?** - What jurisdictions can this operate in?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related Notes
|
|
||||||
|
|
||||||
- [[nostr-native-architecture]] - **Nostr as infrastructure backbone**
|
|
||||||
- [[modernization-plan]] - Original tech stack decisions
|
|
||||||
- [[lnbits-integration]] - LNbits as alternative to Lightning.Pub
|
|
||||||
- [[membership-lightning-integration]] - Membership feature (simplify)
|
|
||||||
- [[hardware-recommendations]] - Hardware choices
|
|
||||||
- [[machine-ui-modernization]] - Vue 3 UI migration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
### Lightning
|
|
||||||
- [LDK Documentation](https://lightningdevkit.org/)
|
|
||||||
- [Phoenixd](https://github.com/ACINQ/phoenixd)
|
|
||||||
- [LNbits](https://lnbits.com/)
|
|
||||||
- [LNURL Specifications](https://github.com/lnurl/luds)
|
|
||||||
|
|
||||||
### CLINK (Nostr-Native Lightning)
|
|
||||||
- [CLINK Protocol Spec](https://github.com/shocknet/CLINK)
|
|
||||||
- [CLINK Demo](https://clinkme.dev/)
|
|
||||||
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub)
|
|
||||||
- [ShockWallet](https://github.com/shocknet/wallet2)
|
|
||||||
- [@shocknet/clink-sdk](https://www.npmjs.com/package/@shocknet/clink-sdk)
|
|
||||||
|
|
||||||
### Privacy/Ecash
|
|
||||||
- [Cashu Protocol](https://cashu.space/)
|
|
||||||
- [Fedimint](https://fedimint.org/)
|
|
||||||
- [Cashu TypeScript SDK](https://github.com/cashubtc/cashu-ts)
|
|
||||||
|
|
||||||
### NFC
|
|
||||||
- [BOLT Cards](https://bolt.cards/)
|
|
||||||
- [LNbits BoltCards Extension](https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards)
|
|
||||||
|
|
||||||
### Nostr Infrastructure
|
|
||||||
- [strfry](https://github.com/hoytech/strfry) - High-performance relay
|
|
||||||
- [rnostr](https://github.com/rnostr/rnostr) - Rust relay with NIP-42
|
|
||||||
- [NIP-42: Auth](https://github.com/nostr-protocol/nips/blob/master/42.md)
|
|
||||||
- [NIP-44: Encryption](https://github.com/paulmillr/nip44)
|
|
||||||
- [NIP-17: Private DMs](https://nips.nostr.com/17)
|
|
||||||
|
|
||||||
### Reference Implementations
|
|
||||||
- [FOSSA ATM](https://github.com/lnbits/fossa) - LNbits Lightning ATM
|
|
||||||
- [Bleskomat](https://github.com/samotari/bleskomat) - Minimal Lightning ATM
|
|
||||||
- [RoboSats](https://github.com/RoboSats/robosats) - KYC-free P2P exchange
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,970 +0,0 @@
|
||||||
---
|
|
||||||
title: Machine UI Modernization
|
|
||||||
created: 2026-01-22
|
|
||||||
updated: 2026-01-22
|
|
||||||
tags:
|
|
||||||
- feature
|
|
||||||
- vue
|
|
||||||
- ui
|
|
||||||
- lamassu-machine
|
|
||||||
- refactor
|
|
||||||
status: planning
|
|
||||||
priority: high
|
|
||||||
---
|
|
||||||
|
|
||||||
# Machine UI Modernization
|
|
||||||
|
|
||||||
> [!abstract] Summary
|
|
||||||
> Replace the legacy vanilla JavaScript + jQuery UI in `lamassu-machine` with a modern **Vue 3** application using TypeScript, Pinia for state management, and Vite for building.
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
- [[#Current State]]
|
|
||||||
- [[#Why Vue]]
|
|
||||||
- [[#Architecture]]
|
|
||||||
- [[#Migration Strategy]]
|
|
||||||
- [[#Implementation]]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
### Problems with Existing UI
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Current: lamassu-machine/ui/src/app.js (80KB single file)
|
|
||||||
/* globals $, URLSearchParams, WebSocket, Keyboard, BigNumber, ... */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
var fiatCode = null
|
|
||||||
var locale = null
|
|
||||||
var currentState
|
|
||||||
var websocket = null
|
|
||||||
// ... 50+ global variables
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!warning] Technical Debt
|
|
||||||
> - **Single 80KB file** with all logic
|
|
||||||
> - **50+ global variables** for state
|
|
||||||
> - **jQuery dependency** for DOM manipulation
|
|
||||||
> - **No type safety** - runtime errors only
|
|
||||||
> - **No component structure** - hard to test/maintain
|
|
||||||
> - **Babel 6** (2016) for transpilation
|
|
||||||
> - **Manual DOM updates** - error-prone
|
|
||||||
|
|
||||||
### Current Tech Stack
|
|
||||||
|
|
||||||
| Component | Current | Issues |
|
|
||||||
|-----------|---------|--------|
|
|
||||||
| Framework | Vanilla JS | No structure |
|
|
||||||
| DOM | jQuery | Dated, heavy |
|
|
||||||
| State | Global vars | Unmaintainable |
|
|
||||||
| Build | Babel 6 | Outdated |
|
|
||||||
| Styles | SCSS | OK, keep |
|
|
||||||
| i18n | Jed (gettext) | Works, but heavy |
|
|
||||||
|
|
||||||
#currentstate #technicaldebt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why Vue
|
|
||||||
|
|
||||||
> [!decision] Vue 3 over React/Svelte/Solid
|
|
||||||
|
|
||||||
| Framework | Bundle Size | Learning Curve | Kiosk Fit |
|
|
||||||
|-----------|-------------|----------------|-----------|
|
|
||||||
| **Vue 3** | ~33kb | Low | Excellent |
|
|
||||||
| React 19 | ~42kb | Medium | Good |
|
|
||||||
| Svelte 5 | ~2kb | Low | Excellent |
|
|
||||||
| Solid | ~7kb | Medium | Good |
|
|
||||||
|
|
||||||
### Vue Advantages for Kiosk
|
|
||||||
|
|
||||||
1. **Single-File Components (SFC)**
|
|
||||||
- HTML, CSS, JS in one file
|
|
||||||
- Natural for UI-focused development
|
|
||||||
- Easy to understand screen-by-screen
|
|
||||||
|
|
||||||
2. **Composition API**
|
|
||||||
- TypeScript-first design
|
|
||||||
- Reusable composables for hardware
|
|
||||||
- Better than Options API for complex state
|
|
||||||
|
|
||||||
3. **Progressive Adoption**
|
|
||||||
- Can migrate screen-by-screen
|
|
||||||
- Works alongside existing code during migration
|
|
||||||
|
|
||||||
4. **Smaller Bundle**
|
|
||||||
- Critical for kiosk boot time
|
|
||||||
- Tree-shakeable
|
|
||||||
|
|
||||||
5. **Vue Ecosystem**
|
|
||||||
- **Pinia** - Type-safe state management
|
|
||||||
- **VueUse** - Composables for common tasks
|
|
||||||
- **Vue I18n** - Internationalization
|
|
||||||
- **shadcn-vue** - Shared components with admin UI
|
|
||||||
|
|
||||||
> [!note] Why Not Svelte?
|
|
||||||
> Svelte has the smallest bundle, but Vue has:
|
|
||||||
> - Larger ecosystem for i18n, forms, etc.
|
|
||||||
> - More developers familiar with it
|
|
||||||
> - Better tooling maturity
|
|
||||||
|
|
||||||
> [!tip] Shared UI Components
|
|
||||||
> Use shadcn-vue for base components (Button, Card, etc.) to share code between machine and admin UIs via `@lamassu/ui-shared` package.
|
|
||||||
|
|
||||||
#vue #framework
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Target Stack
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ Tauri 2.x Shell │
|
|
||||||
│ (Rust core, WebView for UI) │
|
|
||||||
├─────────────────────────────────────────────┤
|
|
||||||
│ Vue 3 Application │
|
|
||||||
│ ┌─────────────────────────────────────┐ │
|
|
||||||
│ │ Screens (Vue Components) │ │
|
|
||||||
│ │ ├── IdleScreen.vue │ │
|
|
||||||
│ │ ├── ChooseCoinScreen.vue │ │
|
|
||||||
│ │ ├── InsertBillsScreen.vue │ │
|
|
||||||
│ │ └── ... │ │
|
|
||||||
│ └─────────────────────────────────────┘ │
|
|
||||||
│ ┌─────────────────────────────────────┐ │
|
|
||||||
│ │ State (Pinia Stores) │ │
|
|
||||||
│ │ ├── useTransactionStore │ │
|
|
||||||
│ │ ├── useMachineStore │ │
|
|
||||||
│ │ └── useUIStore │ │
|
|
||||||
│ └─────────────────────────────────────┘ │
|
|
||||||
│ ┌─────────────────────────────────────┐ │
|
|
||||||
│ │ Composables │ │
|
|
||||||
│ │ ├── useWebSocket │ │
|
|
||||||
│ │ ├── useKeyboard │ │
|
|
||||||
│ │ └── useQRScanner │ │
|
|
||||||
│ └─────────────────────────────────────┘ │
|
|
||||||
├─────────────────────────────────────────────┤
|
|
||||||
│ Hardware Bridge │
|
|
||||||
│ (WebSocket ↔ brain.js state machine) │
|
|
||||||
└─────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
lamassu-machine/
|
|
||||||
├── ui/
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── main.ts # Entry point
|
|
||||||
│ │ ├── App.vue # Root component
|
|
||||||
│ │ ├── router.ts # Screen routing
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── screens/ # Full-screen views
|
|
||||||
│ │ │ ├── IdleScreen.vue
|
|
||||||
│ │ │ ├── ChooseCoinScreen.vue
|
|
||||||
│ │ │ ├── ChooseLanguageScreen.vue
|
|
||||||
│ │ │ ├── ScanAddressScreen.vue
|
|
||||||
│ │ │ ├── InsertBillsScreen.vue
|
|
||||||
│ │ │ ├── SendingCoinsScreen.vue
|
|
||||||
│ │ │ ├── MembershipPromptScreen.vue
|
|
||||||
│ │ │ ├── MembershipScanScreen.vue
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── components/ # Reusable components
|
|
||||||
│ │ │ ├── common/
|
|
||||||
│ │ │ │ ├── BaseButton.vue
|
|
||||||
│ │ │ │ ├── QRCode.vue
|
|
||||||
│ │ │ │ ├── LoadingSpinner.vue
|
|
||||||
│ │ │ │ └── LanguageSelector.vue
|
|
||||||
│ │ │ ├── keyboard/
|
|
||||||
│ │ │ │ ├── VirtualKeyboard.vue
|
|
||||||
│ │ │ │ └── Keypad.vue
|
|
||||||
│ │ │ └── transaction/
|
|
||||||
│ │ │ ├── CoinSelector.vue
|
|
||||||
│ │ │ ├── BillAcceptor.vue
|
|
||||||
│ │ │ └── AmountDisplay.vue
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── stores/ # Pinia stores
|
|
||||||
│ │ │ ├── transaction.ts
|
|
||||||
│ │ │ ├── machine.ts
|
|
||||||
│ │ │ ├── ui.ts
|
|
||||||
│ │ │ └── i18n.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── composables/ # Reusable logic
|
|
||||||
│ │ │ ├── useWebSocket.ts
|
|
||||||
│ │ │ ├── useKeyboard.ts
|
|
||||||
│ │ │ ├── useQRScanner.ts
|
|
||||||
│ │ │ ├── useIdleTimeout.ts
|
|
||||||
│ │ │ └── useSounds.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── types/ # TypeScript types
|
|
||||||
│ │ │ ├── transaction.ts
|
|
||||||
│ │ │ ├── machine.ts
|
|
||||||
│ │ │ └── events.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── i18n/ # Internationalization
|
|
||||||
│ │ │ ├── index.ts
|
|
||||||
│ │ │ └── locales/
|
|
||||||
│ │ │ ├── en.json
|
|
||||||
│ │ │ ├── es.json
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ │
|
|
||||||
│ │ └── styles/ # Global styles
|
|
||||||
│ │ ├── main.scss
|
|
||||||
│ │ ├── variables.scss
|
|
||||||
│ │ └── themes/
|
|
||||||
│ │
|
|
||||||
│ ├── index.html
|
|
||||||
│ ├── vite.config.ts
|
|
||||||
│ ├── tsconfig.json
|
|
||||||
│ └── package.json
|
|
||||||
│
|
|
||||||
├── lib/
|
|
||||||
│ └── brain.js # State machine (unchanged)
|
|
||||||
│
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
#architecture #structure
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
### App.vue - Root Component
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- ui/src/App.vue -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useUIStore } from './stores/ui'
|
|
||||||
import { useWebSocket } from './composables/useWebSocket'
|
|
||||||
|
|
||||||
// Screens
|
|
||||||
import IdleScreen from './screens/IdleScreen.vue'
|
|
||||||
import ChooseCoinScreen from './screens/ChooseCoinScreen.vue'
|
|
||||||
import InsertBillsScreen from './screens/InsertBillsScreen.vue'
|
|
||||||
// ... other screens
|
|
||||||
|
|
||||||
const ui = useUIStore()
|
|
||||||
const { connected } = useWebSocket()
|
|
||||||
|
|
||||||
const screenComponent = computed(() => {
|
|
||||||
const screens: Record<string, Component> = {
|
|
||||||
idle: IdleScreen,
|
|
||||||
chooseCoin: ChooseCoinScreen,
|
|
||||||
insertBills: InsertBillsScreen,
|
|
||||||
// ... map all states to screens
|
|
||||||
}
|
|
||||||
return screens[ui.currentScreen] ?? IdleScreen
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="app"
|
|
||||||
:class="[ui.theme, { rtl: ui.isRTL }]"
|
|
||||||
:dir="ui.isRTL ? 'rtl' : 'ltr'"
|
|
||||||
>
|
|
||||||
<Transition name="screen" mode="out-in">
|
|
||||||
<component :is="screenComponent" :key="ui.currentScreen" />
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- Global overlays -->
|
|
||||||
<LoadingOverlay v-if="ui.isLoading" />
|
|
||||||
<ErrorOverlay v-if="ui.error" :message="ui.error" />
|
|
||||||
<ConnectionLost v-if="!connected" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import './styles/main.scss';
|
|
||||||
|
|
||||||
.app {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
|
|
||||||
&.rtl {
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen-enter-active,
|
|
||||||
.screen-leave-active {
|
|
||||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.screen-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-20px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transaction Store (Pinia)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ui/src/stores/transaction.ts
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import type { Coin, Membership, Transaction } from '../types'
|
|
||||||
|
|
||||||
export const useTransactionStore = defineStore('transaction', () => {
|
|
||||||
// State
|
|
||||||
const direction = ref<'cashIn' | 'cashOut' | null>(null)
|
|
||||||
const selectedCoin = ref<Coin | null>(null)
|
|
||||||
const fiatAmount = ref(0)
|
|
||||||
const cryptoAmount = ref(0)
|
|
||||||
const walletAddress = ref<string | null>(null)
|
|
||||||
const membership = ref<Membership | null>(null)
|
|
||||||
const bills = ref<number[]>([])
|
|
||||||
|
|
||||||
// Computed
|
|
||||||
const hasMembership = computed(() => membership.value !== null)
|
|
||||||
const discountPercent = computed(() => membership.value?.tier.discountPercentage ?? 0)
|
|
||||||
const totalFiat = computed(() => bills.value.reduce((sum, bill) => sum + bill, 0))
|
|
||||||
|
|
||||||
const effectiveRate = computed(() => {
|
|
||||||
if (!selectedCoin.value) return 0
|
|
||||||
const baseRate = selectedCoin.value.rate
|
|
||||||
return baseRate * (1 - discountPercent.value / 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
function startCashIn(coin: Coin) {
|
|
||||||
direction.value = 'cashIn'
|
|
||||||
selectedCoin.value = coin
|
|
||||||
bills.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
function startCashOut(coin: Coin) {
|
|
||||||
direction.value = 'cashOut'
|
|
||||||
selectedCoin.value = coin
|
|
||||||
}
|
|
||||||
|
|
||||||
function addBill(denomination: number) {
|
|
||||||
bills.value.push(denomination)
|
|
||||||
fiatAmount.value = totalFiat.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMembership(m: Membership) {
|
|
||||||
membership.value = m
|
|
||||||
if (m.lightningAddress) {
|
|
||||||
walletAddress.value = m.lightningAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
direction.value = null
|
|
||||||
selectedCoin.value = null
|
|
||||||
fiatAmount.value = 0
|
|
||||||
cryptoAmount.value = 0
|
|
||||||
walletAddress.value = null
|
|
||||||
membership.value = null
|
|
||||||
bills.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
direction,
|
|
||||||
selectedCoin,
|
|
||||||
fiatAmount,
|
|
||||||
cryptoAmount,
|
|
||||||
walletAddress,
|
|
||||||
membership,
|
|
||||||
bills,
|
|
||||||
|
|
||||||
// Computed
|
|
||||||
hasMembership,
|
|
||||||
discountPercent,
|
|
||||||
totalFiat,
|
|
||||||
effectiveRate,
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
startCashIn,
|
|
||||||
startCashOut,
|
|
||||||
addBill,
|
|
||||||
setMembership,
|
|
||||||
reset,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### WebSocket Composable
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ui/src/composables/useWebSocket.ts
|
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
import { useUIStore } from '../stores/ui'
|
|
||||||
import { useTransactionStore } from '../stores/transaction'
|
|
||||||
|
|
||||||
interface BrainMessage {
|
|
||||||
action: string
|
|
||||||
state?: string
|
|
||||||
data?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWebSocket() {
|
|
||||||
const ws = ref<WebSocket | null>(null)
|
|
||||||
const connected = ref(false)
|
|
||||||
const reconnectAttempts = ref(0)
|
|
||||||
|
|
||||||
const ui = useUIStore()
|
|
||||||
const transaction = useTransactionStore()
|
|
||||||
|
|
||||||
function connect() {
|
|
||||||
const host = import.meta.env.VITE_WS_HOST ?? 'localhost'
|
|
||||||
const port = import.meta.env.VITE_WS_PORT ?? '8080'
|
|
||||||
|
|
||||||
ws.value = new WebSocket(`ws://${host}:${port}`)
|
|
||||||
|
|
||||||
ws.value.onopen = () => {
|
|
||||||
connected.value = true
|
|
||||||
reconnectAttempts.value = 0
|
|
||||||
console.log('WebSocket connected')
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.value.onclose = () => {
|
|
||||||
connected.value = false
|
|
||||||
scheduleReconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.value.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.value.onmessage = (event) => {
|
|
||||||
const message: BrainMessage = JSON.parse(event.data)
|
|
||||||
handleMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMessage(message: BrainMessage) {
|
|
||||||
switch (message.action) {
|
|
||||||
case 'stateChange':
|
|
||||||
ui.setScreen(message.state!)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'billInserted':
|
|
||||||
transaction.addBill(message.data!.denomination as number)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'membershipValidated':
|
|
||||||
transaction.setMembership(message.data!.membership as Membership)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'transactionComplete':
|
|
||||||
transaction.reset()
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'error':
|
|
||||||
ui.setError(message.data!.message as string)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log('Unknown message:', message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(action: string, data?: Record<string, unknown>) {
|
|
||||||
if (ws.value?.readyState === WebSocket.OPEN) {
|
|
||||||
ws.value.send(JSON.stringify({ action, data }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleReconnect() {
|
|
||||||
if (reconnectAttempts.value < 10) {
|
|
||||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.value), 30000)
|
|
||||||
setTimeout(() => {
|
|
||||||
reconnectAttempts.value++
|
|
||||||
connect()
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => connect())
|
|
||||||
onUnmounted(() => ws.value?.close())
|
|
||||||
|
|
||||||
return {
|
|
||||||
connected,
|
|
||||||
send,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Screen Component
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- ui/src/screens/MembershipPromptScreen.vue -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useWebSocket } from '../composables/useWebSocket'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import BaseButton from '../components/common/BaseButton.vue'
|
|
||||||
|
|
||||||
const { send } = useWebSocket()
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
function handleYes() {
|
|
||||||
send('membershipYes')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNo() {
|
|
||||||
send('membershipNo')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="screen membership-prompt">
|
|
||||||
<div class="content">
|
|
||||||
<div class="icon">
|
|
||||||
<img src="/images/membership-card.svg" alt="" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="title">{{ t('membership.prompt.title') }}</h1>
|
|
||||||
<p class="subtitle">{{ t('membership.prompt.subtitle') }}</p>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<BaseButton
|
|
||||||
variant="primary"
|
|
||||||
size="large"
|
|
||||||
@click="handleYes"
|
|
||||||
>
|
|
||||||
{{ t('common.yes') }}
|
|
||||||
</BaseButton>
|
|
||||||
|
|
||||||
<BaseButton
|
|
||||||
variant="secondary"
|
|
||||||
size="large"
|
|
||||||
@click="handleNo"
|
|
||||||
>
|
|
||||||
{{ t('common.no') }}
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.membership-prompt {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
text-align: center;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
#components #vue
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## i18n Strategy
|
|
||||||
|
|
||||||
### Vue I18n Setup
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ui/src/i18n/index.ts
|
|
||||||
import { createI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
// Lazy load locales
|
|
||||||
const messages = Object.fromEntries(
|
|
||||||
Object.entries(
|
|
||||||
import.meta.glob('./locales/*.json', { eager: true })
|
|
||||||
).map(([path, module]) => {
|
|
||||||
const locale = path.match(/\/(\w+)\.json$/)?.[1] ?? 'en'
|
|
||||||
return [locale, (module as { default: Record<string, string> }).default]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
export const i18n = createI18n({
|
|
||||||
legacy: false, // Composition API
|
|
||||||
locale: 'en',
|
|
||||||
fallbackLocale: 'en',
|
|
||||||
messages,
|
|
||||||
})
|
|
||||||
|
|
||||||
export function setLocale(locale: string) {
|
|
||||||
i18n.global.locale.value = locale
|
|
||||||
document.documentElement.lang = locale
|
|
||||||
document.documentElement.dir = isRTL(locale) ? 'rtl' : 'ltr'
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRTL(locale: string): boolean {
|
|
||||||
return ['ar', 'he', 'fa', 'ur'].includes(locale)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Locale Files
|
|
||||||
|
|
||||||
```json
|
|
||||||
// ui/src/i18n/locales/en.json
|
|
||||||
{
|
|
||||||
"common": {
|
|
||||||
"yes": "Yes",
|
|
||||||
"no": "No",
|
|
||||||
"continue": "Continue",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"back": "Back"
|
|
||||||
},
|
|
||||||
"idle": {
|
|
||||||
"tapToStart": "Tap to Start",
|
|
||||||
"buyBitcoin": "Buy Bitcoin",
|
|
||||||
"sellBitcoin": "Sell Bitcoin"
|
|
||||||
},
|
|
||||||
"membership": {
|
|
||||||
"prompt": {
|
|
||||||
"title": "Do you have a membership card?",
|
|
||||||
"subtitle": "Scan your card for exclusive discounts"
|
|
||||||
},
|
|
||||||
"scan": {
|
|
||||||
"title": "Scan your membership card",
|
|
||||||
"instruction": "Hold your QR code to the scanner"
|
|
||||||
},
|
|
||||||
"valid": {
|
|
||||||
"welcome": "Welcome, {tierName}!",
|
|
||||||
"discount": "{percent}% discount applied",
|
|
||||||
"autoSend": "Bitcoin will be sent to your wallet automatically"
|
|
||||||
},
|
|
||||||
"invalid": {
|
|
||||||
"title": "Membership not recognized",
|
|
||||||
"tryAgain": "Try Again",
|
|
||||||
"skip": "Continue without membership"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"transaction": {
|
|
||||||
"insertBills": "Insert bills",
|
|
||||||
"currentAmount": "Current amount: {amount}",
|
|
||||||
"sendingCoins": "Sending {coin}...",
|
|
||||||
"complete": "Transaction complete!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#i18n #localization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build Configuration
|
|
||||||
|
|
||||||
### Vite Config
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ui/vite.config.ts
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import { resolve } from 'path'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, 'src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
build: {
|
|
||||||
target: 'chrome90', // Kiosk browser target
|
|
||||||
outDir: 'dist',
|
|
||||||
assetsDir: 'assets',
|
|
||||||
sourcemap: false,
|
|
||||||
minify: 'esbuild',
|
|
||||||
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
manualChunks: {
|
|
||||||
vue: ['vue', 'vue-router', 'pinia'],
|
|
||||||
i18n: ['vue-i18n'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
server: {
|
|
||||||
port: 3000,
|
|
||||||
host: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// For kiosk: inline assets to reduce HTTP requests
|
|
||||||
assetsInclude: ['**/*.svg', '**/*.png'],
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Package.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "lamassu-machine-ui",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"test": "vitest",
|
|
||||||
"test:ui": "vitest --ui",
|
|
||||||
"lint": "eslint src --ext .vue,.ts --fix",
|
|
||||||
"typecheck": "vue-tsc --noEmit"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"vue": "^3.5.0",
|
|
||||||
"vue-router": "^4.4.0",
|
|
||||||
"pinia": "^2.2.0",
|
|
||||||
"vue-i18n": "^10.0.0",
|
|
||||||
"@vueuse/core": "^11.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^5.1.0",
|
|
||||||
"vite": "^6.0.0",
|
|
||||||
"typescript": "^5.6.0",
|
|
||||||
"vue-tsc": "^2.1.0",
|
|
||||||
"vitest": "^2.1.0",
|
|
||||||
"@vue/test-utils": "^2.4.0",
|
|
||||||
"sass": "^1.80.0",
|
|
||||||
"eslint": "^9.14.0",
|
|
||||||
"eslint-plugin-vue": "^9.30.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#build #vite
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
### Phase 1: Setup & Parallel Development
|
|
||||||
|
|
||||||
> [!todo] Phase 1 Tasks
|
|
||||||
|
|
||||||
- [ ] Create new `ui/` directory structure
|
|
||||||
- [ ] Set up Vite + Vue + TypeScript
|
|
||||||
- [ ] Configure Pinia stores
|
|
||||||
- [ ] Set up Vue I18n with existing translations
|
|
||||||
- [ ] Create base components (Button, QRCode, etc.)
|
|
||||||
- [ ] Implement WebSocket composable
|
|
||||||
- [ ] Run Vue app alongside legacy app for testing
|
|
||||||
|
|
||||||
**Key principle:** Keep `brain.js` state machine unchanged. Only replace the UI layer.
|
|
||||||
|
|
||||||
### Phase 2: Screen Migration
|
|
||||||
|
|
||||||
> [!todo] Phase 2 Tasks
|
|
||||||
|
|
||||||
Migrate screens one-by-one, starting with simplest:
|
|
||||||
|
|
||||||
1. [ ] IdleScreen
|
|
||||||
2. [ ] ChooseLanguageScreen
|
|
||||||
3. [ ] ChooseCoinScreen
|
|
||||||
4. [ ] MembershipPromptScreen (new)
|
|
||||||
5. [ ] MembershipScanScreen (new)
|
|
||||||
6. [ ] ScanAddressScreen
|
|
||||||
7. [ ] InsertBillsScreen
|
|
||||||
8. [ ] SendingCoinsScreen
|
|
||||||
9. [ ] CompleteScreen
|
|
||||||
10. [ ] ErrorScreen
|
|
||||||
|
|
||||||
### Phase 3: Component Polish
|
|
||||||
|
|
||||||
> [!todo] Phase 3 Tasks
|
|
||||||
|
|
||||||
- [ ] Virtual keyboard component
|
|
||||||
- [ ] QR scanner integration
|
|
||||||
- [ ] Animations and transitions
|
|
||||||
- [ ] Touch gesture support
|
|
||||||
- [ ] Accessibility (a11y)
|
|
||||||
- [ ] RTL language support
|
|
||||||
|
|
||||||
### Phase 4: Testing & Cleanup
|
|
||||||
|
|
||||||
> [!todo] Phase 4 Tasks
|
|
||||||
|
|
||||||
- [ ] Unit tests for stores
|
|
||||||
- [ ] Component tests with Vue Test Utils
|
|
||||||
- [ ] E2E tests with Playwright
|
|
||||||
- [ ] Remove legacy `ui/src/app.js`
|
|
||||||
- [ ] Remove jQuery dependency
|
|
||||||
- [ ] Update documentation
|
|
||||||
|
|
||||||
#migration #phases
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Component Tests
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ui/src/screens/__tests__/MembershipPromptScreen.test.ts
|
|
||||||
import { describe, it, expect, vi } from 'vitest'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { createTestingPinia } from '@pinia/testing'
|
|
||||||
import MembershipPromptScreen from '../MembershipPromptScreen.vue'
|
|
||||||
|
|
||||||
describe('MembershipPromptScreen', () => {
|
|
||||||
it('renders prompt text', () => {
|
|
||||||
const wrapper = mount(MembershipPromptScreen, {
|
|
||||||
global: {
|
|
||||||
plugins: [createTestingPinia()],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('membership card')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('emits membershipYes when Yes clicked', async () => {
|
|
||||||
const send = vi.fn()
|
|
||||||
vi.mock('../composables/useWebSocket', () => ({
|
|
||||||
useWebSocket: () => ({ send, connected: ref(true) }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const wrapper = mount(MembershipPromptScreen)
|
|
||||||
await wrapper.find('[data-test="yes-btn"]').trigger('click')
|
|
||||||
|
|
||||||
expect(send).toHaveBeenCalledWith('membershipYes')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Store Tests
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ui/src/stores/__tests__/transaction.test.ts
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
|
||||||
import { setActivePinia, createPinia } from 'pinia'
|
|
||||||
import { useTransactionStore } from '../transaction'
|
|
||||||
|
|
||||||
describe('Transaction Store', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
setActivePinia(createPinia())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calculates total fiat from bills', () => {
|
|
||||||
const store = useTransactionStore()
|
|
||||||
|
|
||||||
store.addBill(20)
|
|
||||||
store.addBill(20)
|
|
||||||
store.addBill(10)
|
|
||||||
|
|
||||||
expect(store.totalFiat).toBe(50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('applies membership discount to rate', () => {
|
|
||||||
const store = useTransactionStore()
|
|
||||||
|
|
||||||
store.selectedCoin = { code: 'BTC', rate: 100 }
|
|
||||||
store.setMembership({
|
|
||||||
tier: { discountPercentage: 15 },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(store.effectiveRate).toBe(85) // 15% off
|
|
||||||
})
|
|
||||||
|
|
||||||
it('resets all state', () => {
|
|
||||||
const store = useTransactionStore()
|
|
||||||
|
|
||||||
store.startCashIn({ code: 'BTC', rate: 100 })
|
|
||||||
store.addBill(20)
|
|
||||||
store.reset()
|
|
||||||
|
|
||||||
expect(store.direction).toBeNull()
|
|
||||||
expect(store.bills).toEqual([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#testing #vitest
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### Bundle Size Targets
|
|
||||||
|
|
||||||
| Chunk | Target | Reason |
|
|
||||||
|-------|--------|--------|
|
|
||||||
| Vue core | < 40kb | Framework |
|
|
||||||
| App code | < 50kb | Screens + components |
|
|
||||||
| i18n | < 30kb | Lazy load locales |
|
|
||||||
| **Total** | **< 120kb** | Fast kiosk boot |
|
|
||||||
|
|
||||||
### Optimization Strategies
|
|
||||||
|
|
||||||
1. **Lazy load screens**
|
|
||||||
```typescript
|
|
||||||
const InsertBillsScreen = defineAsyncComponent(
|
|
||||||
() => import('./screens/InsertBillsScreen.vue')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Preload critical screens**
|
|
||||||
```typescript
|
|
||||||
// Preload next likely screen
|
|
||||||
router.beforeEach((to, from) => {
|
|
||||||
if (to.name === 'chooseCoin') {
|
|
||||||
import('./screens/ScanAddressScreen.vue')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Inline critical CSS**
|
|
||||||
- First-paint styles inlined in HTML
|
|
||||||
- Component styles loaded with components
|
|
||||||
|
|
||||||
4. **Image optimization**
|
|
||||||
- SVG for icons (scalable, small)
|
|
||||||
- WebP for photos
|
|
||||||
- Lazy load non-critical images
|
|
||||||
|
|
||||||
#performance #optimization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related Documents
|
|
||||||
|
|
||||||
- [[modernization-plan]] - Overall modernization roadmap
|
|
||||||
- [[membership-lightning-integration]] - Membership feature
|
|
||||||
- [[Machine State Management]] - XState migration (brain.js)
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,626 +0,0 @@
|
||||||
---
|
|
||||||
title: Hardware Recommendations for Lamassu Machine
|
|
||||||
created: 2026-01-22
|
|
||||||
updated: 2026-01-22
|
|
||||||
tags:
|
|
||||||
- hardware
|
|
||||||
- modernization
|
|
||||||
- open-source
|
|
||||||
- raspberry-pi
|
|
||||||
- bill-handling
|
|
||||||
status: draft
|
|
||||||
---
|
|
||||||
|
|
||||||
# Hardware Recommendations for Lamassu Machine
|
|
||||||
|
|
||||||
> [!abstract] Summary
|
|
||||||
> Hardware component recommendations for modernizing the Lamassu Bitcoin ATM, prioritizing **open-source compatibility**, **parts availability**, and **long-term support**. All components selected have existing open-source drivers or well-documented protocols.
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
- [[#Compute Platform]]
|
|
||||||
- [[#Bill Validators]]
|
|
||||||
- [[#Bill Dispensers]]
|
|
||||||
- [[#Touch Displays]]
|
|
||||||
- [[#Thermal Printers]]
|
|
||||||
- [[#NFC Readers]]
|
|
||||||
- [[#Protocol Support]]
|
|
||||||
- [[#DIY Reference Projects]]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Principles
|
|
||||||
|
|
||||||
> [!important] Selection Criteria
|
|
||||||
> 1. **Open-source drivers** - Existing community or vendor-provided open-source implementations
|
|
||||||
> 2. **Parts availability** - Globally accessible, not vendor-locked
|
|
||||||
> 3. **Protocol documentation** - Well-documented communication protocols
|
|
||||||
> 4. **Industrial longevity** - 5+ year production commitment
|
|
||||||
> 5. **NixOS compatibility** - Clean builds without binary blobs where possible
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Compute Platform
|
|
||||||
|
|
||||||
### Primary Recommendation: Raspberry Pi Compute Module 5
|
|
||||||
|
|
||||||
> [!decision] Raspberry Pi CM5 with Industrial Carrier Board
|
|
||||||
> Best balance of performance, ecosystem, and long-term availability (10-year production commitment).
|
|
||||||
|
|
||||||
| Specification | Value |
|
|
||||||
|--------------|-------|
|
|
||||||
| CPU | Broadcom BCM2712, 4-core Cortex-A76 @ 2.4GHz |
|
|
||||||
| RAM | 2GB / 4GB / 8GB LPDDR4X-4267 |
|
|
||||||
| Storage | 16GB / 32GB / 64GB eMMC (optional) |
|
|
||||||
| Connectivity | PCIe 2.0 x1, USB 3.0, Gigabit Ethernet |
|
|
||||||
| I/O | 2x MIPI DSI, 2x MIPI CSI, 30+ GPIO |
|
|
||||||
| Production | 10-year commitment through 2035 |
|
|
||||||
| Price | $45 (4GB) - $90 (8GB + 64GB eMMC) |
|
|
||||||
|
|
||||||
**Why CM5:**
|
|
||||||
- Raspberry Pi Foundation's industrial commitment
|
|
||||||
- Massive ecosystem of carrier boards
|
|
||||||
- NixOS has first-class ARM64 support
|
|
||||||
- Existing lamassu-machine runs on Pi
|
|
||||||
|
|
||||||
**Recommended Carrier Boards:**
|
|
||||||
|
|
||||||
| Board | Features | Price |
|
|
||||||
|-------|----------|-------|
|
|
||||||
| **Waveshare CM5-IO-BASE-A** | Full-size, HDMI x2, USB 3.0 x2, M.2 slot | ~$35 |
|
|
||||||
| **Waveshare CM5-DISP-BASE** | Built-in 7" touchscreen, compact | ~$75 |
|
|
||||||
| **Toradex Aster** | Industrial, wide temp, PoE | ~$150 |
|
|
||||||
| **BIGTREETECH CB1** | 3D printer heritage, robust | ~$40 |
|
|
||||||
|
|
||||||
> [!tip] Waveshare CM5-DISP-BASE
|
|
||||||
> Combines carrier board + 7" touchscreen in one unit. Ideal for compact kiosk designs.
|
|
||||||
|
|
||||||
### Alternative: Pine64 StarPro64 (RISC-V)
|
|
||||||
|
|
||||||
> [!note] Future-Proof Option
|
|
||||||
> For organizations wanting to support open silicon and avoid ARM licensing.
|
|
||||||
|
|
||||||
| Specification | Value |
|
|
||||||
|--------------|-------|
|
|
||||||
| CPU | StarFive JH7110, 4-core SiFive U74 @ 1.5GHz |
|
|
||||||
| RAM | 8GB LPDDR4 |
|
|
||||||
| Storage | M.2 NVMe, microSD, eMMC |
|
|
||||||
| GPU | IMG BXE-4-32 (open-source driver in progress) |
|
|
||||||
| Price | ~$90 |
|
|
||||||
|
|
||||||
**RISC-V Considerations:**
|
|
||||||
- Linux kernel support improving rapidly
|
|
||||||
- NixOS has experimental RISC-V builds
|
|
||||||
- Performance ~60% of Pi 5 currently
|
|
||||||
- Fully open ISA (no licensing fees)
|
|
||||||
|
|
||||||
**Verdict:** Use CM5 for production now, evaluate RISC-V for 2028+ deployments.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bill Validators
|
|
||||||
|
|
||||||
### Primary Recommendation: Innovative Technology NV200
|
|
||||||
|
|
||||||
> [!decision] ITL NV200 with eSSP Protocol
|
|
||||||
> Industry standard with excellent open-source library support.
|
|
||||||
|
|
||||||
| Specification | Value |
|
|
||||||
|--------------|-------|
|
|
||||||
| Capacity | Up to 600 notes stacked |
|
|
||||||
| Note Width | 60mm - 85mm |
|
|
||||||
| Validation Speed | <1 second |
|
|
||||||
| Interface | USB or TTL serial |
|
|
||||||
| Protocol | eSSP (encrypted SSP) |
|
|
||||||
| Recognition | 96 currencies, 4-way insertion |
|
|
||||||
|
|
||||||
**Open-Source Support:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Node.js eSSP library
|
|
||||||
npm install encrypted-ssp
|
|
||||||
|
|
||||||
# Python library
|
|
||||||
pip install ssp-protocol
|
|
||||||
```
|
|
||||||
|
|
||||||
| Library | Language | Repo |
|
|
||||||
|---------|----------|------|
|
|
||||||
| encrypted-ssp | Node.js | github.com/nickatnight/encrypted-ssp |
|
|
||||||
| ssp-server | Node.js | github.com/paysyslabs/ssp-server |
|
|
||||||
| ssp-protocol | Python | github.com/paysyslabs/ssp-protocol |
|
|
||||||
| eSSP.NET | C# | github.com/essp-library/essp-dotnet |
|
|
||||||
|
|
||||||
**NV200 Variants:**
|
|
||||||
|
|
||||||
| Model | Feature | Use Case |
|
|
||||||
|-------|---------|----------|
|
|
||||||
| NV200 | Stacker only | Standard ATM |
|
|
||||||
| NV200 Spectral | Enhanced counterfeit detection | High-risk areas |
|
|
||||||
| NV200 + SMART Payout | Recycling + dispensing | Two-way machines |
|
|
||||||
|
|
||||||
### Alternative: MEI Cashflow Series
|
|
||||||
|
|
||||||
> [!note] Alternative for US/Canada
|
|
||||||
> Strong in North American market with ccTalk protocol support.
|
|
||||||
|
|
||||||
| Model | Note Capacity | Protocol |
|
|
||||||
|-------|--------------|----------|
|
|
||||||
| MEI Cashflow SC66 | 600 | MDB, ccTalk |
|
|
||||||
| MEI Cashflow SC83 | 1,000 | MDB, ccTalk, eSSP |
|
|
||||||
| MEI Cashflow SC Advance | 1,500 | All protocols |
|
|
||||||
|
|
||||||
**ccTalk Library:**
|
|
||||||
```bash
|
|
||||||
# C++/Qt library
|
|
||||||
git clone https://github.com/nickatnight/cctalk-cpp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Existing Lamassu Drivers
|
|
||||||
|
|
||||||
The current lamassu-machine already supports:
|
|
||||||
|
|
||||||
| Driver | Protocol | File |
|
|
||||||
|--------|----------|------|
|
|
||||||
| `id003` | ID-003 | `lib/id003/` |
|
|
||||||
| `ccnet` | CCNET | `lib/ccnet.js` |
|
|
||||||
| `mei` | MEI proprietary | `lib/mei/` |
|
|
||||||
| `ssp` | SSP/eSSP | `lib/ssp.js` |
|
|
||||||
|
|
||||||
> [!success] Reuse Strategy
|
|
||||||
> Port existing JavaScript drivers to Rust HAL layer with napi-rs bindings.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bill Dispensers
|
|
||||||
|
|
||||||
### Primary Recommendation: Puloon LCDM-1000
|
|
||||||
|
|
||||||
> [!decision] Puloon LCDM Series
|
|
||||||
> Best-documented protocol with existing lamassu-machine support.
|
|
||||||
|
|
||||||
| Model | Cassettes | Capacity per Cassette | Interface |
|
|
||||||
|-------|-----------|----------------------|-----------|
|
|
||||||
| LCDM-1000 | 1 | 1,000 notes | RS-232 |
|
|
||||||
| LCDM-2000 | 2 | 1,000 notes each | RS-232 |
|
|
||||||
| LCDM-4000 | 4 | 500 notes each | RS-232 |
|
|
||||||
|
|
||||||
**Open-Source Driver:**
|
|
||||||
```bash
|
|
||||||
# Existing lamassu-machine driver
|
|
||||||
lib/puloon/puloonrs232.js
|
|
||||||
|
|
||||||
# Rust implementation available
|
|
||||||
github.com/nickatnight/puloon-rs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Puloon Protocol:**
|
|
||||||
- Simple ASCII command set
|
|
||||||
- Documented in public datasheet
|
|
||||||
- 9600 baud RS-232
|
|
||||||
|
|
||||||
### Alternative: Fujitsu F53/F56
|
|
||||||
|
|
||||||
> [!note] Higher Volume Option
|
|
||||||
> For high-traffic locations needing larger capacity.
|
|
||||||
|
|
||||||
| Model | Cassettes | Capacity | Interface |
|
|
||||||
|-------|-----------|----------|-----------|
|
|
||||||
| F53 | 4 | 2,500 notes total | USB, RS-232 |
|
|
||||||
| F56 | 6 | 4,000 notes total | USB, RS-232 |
|
|
||||||
|
|
||||||
**Open-Source Support:**
|
|
||||||
```bash
|
|
||||||
# Existing lamassu-machine driver
|
|
||||||
lib/f56/
|
|
||||||
|
|
||||||
# Python implementation
|
|
||||||
github.com/fujitsu-atm/f53-python
|
|
||||||
```
|
|
||||||
|
|
||||||
### Existing Lamassu Dispenser Support
|
|
||||||
|
|
||||||
| Driver | Hardware | File |
|
|
||||||
|--------|----------|------|
|
|
||||||
| `puloon` | LCDM series | `lib/puloon/` |
|
|
||||||
| `f56` | Fujitsu F53/F56 | `lib/f56/` |
|
|
||||||
| `genmega` | Genmega dispensers | `lib/genmega/` |
|
|
||||||
| `gsr50` | GSR50 recycler | `lib/gsr50/` |
|
|
||||||
| `hcm2` | Hitachi HCM2 | `lib/hcm2/` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Touch Displays
|
|
||||||
|
|
||||||
### Primary Recommendation: Elo Touch Solutions I-Series
|
|
||||||
|
|
||||||
> [!decision] Elo I-Series 4.0 (Linux)
|
|
||||||
> Industrial-grade with native Linux support and open-source touch drivers.
|
|
||||||
|
|
||||||
| Model | Size | Resolution | Features |
|
|
||||||
|-------|------|------------|----------|
|
|
||||||
| ESY15i5 | 15.6" | 1920x1080 | ARM Cortex-A73, Android/Linux |
|
|
||||||
| ESY22i5 | 21.5" | 1920x1080 | ARM Cortex-A73, Android/Linux |
|
|
||||||
|
|
||||||
**Linux Support:**
|
|
||||||
- Native Linux kernel touch driver
|
|
||||||
- No proprietary blobs required
|
|
||||||
- evdev/libinput compatible
|
|
||||||
|
|
||||||
**Advantages:**
|
|
||||||
- Designed for 24/7 kiosk operation
|
|
||||||
- Anti-glare, anti-fingerprint coating
|
|
||||||
- Wide temperature range (-20°C to 50°C)
|
|
||||||
- 3-year warranty
|
|
||||||
|
|
||||||
### Budget Alternative: Waveshare + Raspberry Pi
|
|
||||||
|
|
||||||
| Model | Size | Resolution | Price |
|
|
||||||
|-------|------|------------|-------|
|
|
||||||
| Waveshare 10.1" | 10.1" | 1280x800 | ~$90 |
|
|
||||||
| Waveshare 13.3" | 13.3" | 1920x1080 | ~$150 |
|
|
||||||
| Waveshare 15.6" | 15.6" | 1920x1080 | ~$180 |
|
|
||||||
|
|
||||||
**Advantages:**
|
|
||||||
- Direct DSI connection to CM5
|
|
||||||
- Single-cable solution (power + video + touch)
|
|
||||||
- Mainline Linux kernel support
|
|
||||||
|
|
||||||
### Industrial Open-Frame: Faytech
|
|
||||||
|
|
||||||
> [!note] Custom Enclosure Option
|
|
||||||
> For building into existing or custom ATM enclosures.
|
|
||||||
|
|
||||||
| Model | Size | Features |
|
|
||||||
|-------|------|----------|
|
|
||||||
| FT116TMBCAP | 11.6" | Open-frame, PCAP touch |
|
|
||||||
| FT156TMBCAP | 15.6" | Open-frame, PCAP touch |
|
|
||||||
| FT215TMBCAP | 21.5" | Open-frame, PCAP touch |
|
|
||||||
|
|
||||||
- IP65 front bezel available
|
|
||||||
- VESA mount compatible
|
|
||||||
- USB touch, HDMI video
|
|
||||||
|
|
||||||
### Experimental: E-Ink Displays
|
|
||||||
|
|
||||||
> [!warning] Experimental - Testing Only
|
|
||||||
> E-ink displays have significant trade-offs for interactive kiosk use. Document for evaluation purposes.
|
|
||||||
|
|
||||||
**Potential Benefits:**
|
|
||||||
- Perfect sunlight readability (reflective, no glare)
|
|
||||||
- Ultra-low power (~90% less than LCD)
|
|
||||||
- No eye strain, no flicker
|
|
||||||
- Unique aesthetic differentiator
|
|
||||||
- Solar-powered remote deployment possible
|
|
||||||
|
|
||||||
**Limitations:**
|
|
||||||
- Slow refresh rates (even 33-75Hz feels choppy)
|
|
||||||
- Limited touch options on large panels
|
|
||||||
- High cost for frontlit panels
|
|
||||||
- Color (Kaleido 3) only 150 PPI, washed out
|
|
||||||
- Ghosting requires periodic full refresh
|
|
||||||
|
|
||||||
#### High-Refresh E-Ink Options
|
|
||||||
|
|
||||||
| Display | Size | Resolution | Refresh | Frontlight | Price |
|
|
||||||
|---------|------|------------|---------|------------|-------|
|
|
||||||
| **Modos Paper** | 13.3" | 1600×1200 | 75Hz | No | ~$400 |
|
|
||||||
| **Modos Paper** | 6" | 1448×1072 | 75Hz | No | ~$199 |
|
|
||||||
| DASUNG Paperlike 253 | 25.3" | 3200×1800 | 33Hz | Yes | ~$2,250 |
|
|
||||||
| DASUNG Paperlike Color | 25.3" | Kaleido 3 | ~15Hz | Yes | ~$3,000+ |
|
|
||||||
|
|
||||||
#### Open Source: Modos Paper Monitor
|
|
||||||
|
|
||||||
> [!tip] Best Option for Testing
|
|
||||||
> Open-source FPGA controller with 75Hz refresh - ships January 2026.
|
|
||||||
|
|
||||||
- **Repository:** github.com/nickatnight/caster (FPGA controller)
|
|
||||||
- **Refresh:** 75Hz with sub-100ms latency
|
|
||||||
- **Power:** ~1.5W continuous
|
|
||||||
- **Controller:** AMD Spartan-6 FPGA with pixel-level management
|
|
||||||
- **Connectivity:** HDMI, USB-C
|
|
||||||
- **Limitation:** Monochrome only, no built-in touch
|
|
||||||
|
|
||||||
**Touch Integration:**
|
|
||||||
Pair with capacitive touch overlay (e.g., ILITEK controller) for touch input.
|
|
||||||
|
|
||||||
#### Development Boards
|
|
||||||
|
|
||||||
| Board | Size | Resolution | Interface | Price |
|
|
||||||
|-------|------|------------|-----------|-------|
|
|
||||||
| Waveshare 10.3" HAT | 10.3" | 1872×1404 | SPI/USB | ~$200 |
|
|
||||||
| Waveshare 7.8" HAT | 7.8" | 1872×1404 | SPI | ~$120 |
|
|
||||||
| GooDisplay GDEY042T81 | 4.2" | 400×300 | SPI | ~$25 |
|
|
||||||
|
|
||||||
#### Industrial/Outdoor E-Ink
|
|
||||||
|
|
||||||
For outdoor signage or secondary display:
|
|
||||||
|
|
||||||
| Vendor | Sizes | Features |
|
|
||||||
|--------|-------|----------|
|
|
||||||
| SEEKINK | 13.3" - 32" | IP65, -25°C to 65°C, solar option |
|
|
||||||
| Geniatech | 10" - 75" | CMS/API built-in, outdoor rated |
|
|
||||||
| E Ink Marquee | Various | Full color outdoor signage |
|
|
||||||
|
|
||||||
#### Recommended Use Cases
|
|
||||||
|
|
||||||
| Scenario | E-Ink Suitable? | Notes |
|
|
||||||
|----------|-----------------|-------|
|
|
||||||
| Outdoor/direct sunlight | Yes | Primary advantage |
|
|
||||||
| Solar-powered remote ATM | Yes | Ultra-low power |
|
|
||||||
| Standard indoor kiosk | No | LCD better for interaction |
|
|
||||||
| High-interaction UI | No | Refresh too slow |
|
|
||||||
| Idle-mode signage | Yes | Static content while waiting |
|
|
||||||
| Secondary info display | Yes | Rates, fees, location info |
|
|
||||||
|
|
||||||
#### Hybrid Approach
|
|
||||||
|
|
||||||
Consider dual-display architecture:
|
|
||||||
- **Primary:** Standard LCD for transactions
|
|
||||||
- **Secondary:** E-ink for idle advertising, static info, outdoor-facing
|
|
||||||
|
|
||||||
**References:**
|
|
||||||
- [Modos Paper - Crowd Supply](https://www.crowdsupply.com/modos-tech/modos-paper-monitor)
|
|
||||||
- [E-Paper 75Hz - IEEE Spectrum](https://spectrum.ieee.org/e-paper-display-modos)
|
|
||||||
- [DASUNG Paperlike 253](https://shop.dasung.com/products/dasung-25-3-e-ink-monitor-paperlike-253)
|
|
||||||
- [Waveshare E-Paper](https://www.waveshare.com/product/raspberry-pi/displays/e-paper.htm)
|
|
||||||
- [SEEKINK Outdoor](https://www.seekink.com/outdoor-e-ink-display/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Thermal Printers
|
|
||||||
|
|
||||||
### Protocol: ESC/POS
|
|
||||||
|
|
||||||
> [!decision] ESC/POS Compatible Printers
|
|
||||||
> Universal protocol with extensive open-source library support.
|
|
||||||
|
|
||||||
**ESC/POS Libraries:**
|
|
||||||
|
|
||||||
| Library | Language | Features |
|
|
||||||
|---------|----------|----------|
|
|
||||||
| `escpos-rs` | Rust | Async, image support |
|
|
||||||
| `node-thermal-printer` | Node.js | Multiple protocols |
|
|
||||||
| `python-escpos` | Python | Widely used |
|
|
||||||
| `escpos-php` | PHP | Legacy systems |
|
|
||||||
|
|
||||||
### Recommended Models
|
|
||||||
|
|
||||||
| Model | Paper Width | Interface | Price |
|
|
||||||
|-------|-------------|-----------|-------|
|
|
||||||
| **Epson TM-T88VI** | 80mm | USB, Ethernet, Bluetooth | ~$350 |
|
|
||||||
| **Star TSP143IV** | 80mm | USB, Ethernet | ~$280 |
|
|
||||||
| **Custom KUBE II** | 80mm | USB, Serial, Ethernet | ~$200 |
|
|
||||||
| **Goojprt JP-80H** | 80mm | USB, Serial | ~$60 |
|
|
||||||
|
|
||||||
> [!tip] Budget Option
|
|
||||||
> Goojprt/MUNBYN/Rongta Chinese printers are ESC/POS compatible at 1/5 the price. Suitable for testing and low-volume deployments.
|
|
||||||
|
|
||||||
**NixOS Integration:**
|
|
||||||
```nix
|
|
||||||
services.printing = {
|
|
||||||
enable = true;
|
|
||||||
drivers = [ pkgs.epson-escpr ];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NFC Readers
|
|
||||||
|
|
||||||
### Primary Recommendation: ACR122U
|
|
||||||
|
|
||||||
> [!decision] ACS ACR122U with libnfc
|
|
||||||
> Industry standard, excellent open-source support.
|
|
||||||
|
|
||||||
| Specification | Value |
|
|
||||||
|--------------|-------|
|
|
||||||
| Chip | NXP PN532 |
|
|
||||||
| Standards | ISO 14443A/B, MIFARE, FeliCa |
|
|
||||||
| Interface | USB 2.0 |
|
|
||||||
| Read Distance | Up to 50mm |
|
|
||||||
| Price | ~$35 |
|
|
||||||
|
|
||||||
**libnfc Support:**
|
|
||||||
```bash
|
|
||||||
# NixOS
|
|
||||||
environment.systemPackages = [ pkgs.libnfc pkgs.mfoc pkgs.mfcuk ];
|
|
||||||
|
|
||||||
# Rust
|
|
||||||
cargo add nfc
|
|
||||||
|
|
||||||
# Node.js
|
|
||||||
npm install nfc-pcsc
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alternative: PN532 Module
|
|
||||||
|
|
||||||
> [!note] DIY/Embedded Option
|
|
||||||
> Direct SPI/I2C connection to Raspberry Pi GPIO.
|
|
||||||
|
|
||||||
| Module | Interface | Price |
|
|
||||||
|--------|-----------|-------|
|
|
||||||
| Adafruit PN532 | SPI, I2C, UART | ~$40 |
|
|
||||||
| Elechouse PN532 | SPI, I2C, UART | ~$15 |
|
|
||||||
| Waveshare PN532 | SPI, I2C, UART | ~$12 |
|
|
||||||
|
|
||||||
**GPIO Connection:**
|
|
||||||
```
|
|
||||||
PN532 → Raspberry Pi
|
|
||||||
VCC → 3.3V (Pin 1)
|
|
||||||
GND → GND (Pin 6)
|
|
||||||
SDA → GPIO 2 (Pin 3)
|
|
||||||
SCL → GPIO 3 (Pin 5)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Protocol Support Summary
|
|
||||||
|
|
||||||
### Bill Handling Protocols
|
|
||||||
|
|
||||||
| Protocol | Description | Open-Source Support |
|
|
||||||
|----------|-------------|---------------------|
|
|
||||||
| **eSSP** | Encrypted SSP (ITL) | Node.js, Python, C# |
|
|
||||||
| **SSP** | Standard SSP (ITL) | Node.js, Python |
|
|
||||||
| **ccTalk** | Serial coin/note protocol | C++, Qt |
|
|
||||||
| **ID-003** | JCM bill validator | JavaScript (lamassu) |
|
|
||||||
| **CCNET** | CashCode protocol | JavaScript (lamassu) |
|
|
||||||
| **MDB** | Vending standard | C, Rust |
|
|
||||||
|
|
||||||
### Recommended Protocol Stack
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "Rust HAL Layer"
|
|
||||||
essp[eSSP Driver]
|
|
||||||
cctalk[ccTalk Driver]
|
|
||||||
puloon[Puloon RS232]
|
|
||||||
escpos[ESC/POS]
|
|
||||||
nfc[libnfc]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "napi-rs Bindings"
|
|
||||||
napi[Node.js FFI]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "TypeScript Application"
|
|
||||||
app[Tauri + Vue 3]
|
|
||||||
end
|
|
||||||
|
|
||||||
app --> napi
|
|
||||||
napi --> essp
|
|
||||||
napi --> cctalk
|
|
||||||
napi --> puloon
|
|
||||||
napi --> escpos
|
|
||||||
napi --> nfc
|
|
||||||
|
|
||||||
essp --> nv200[NV200]
|
|
||||||
cctalk --> mei[MEI Cashflow]
|
|
||||||
puloon --> lcdm[Puloon LCDM]
|
|
||||||
escpos --> printer[Thermal Printer]
|
|
||||||
nfc --> reader[ACR122U]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DIY Reference Projects
|
|
||||||
|
|
||||||
### FOSSA Bitcoin ATM
|
|
||||||
|
|
||||||
> [!example] LNbits + Lightning
|
|
||||||
> Open-source Lightning ATM using ESP32 + NV10 bill acceptor.
|
|
||||||
|
|
||||||
- **Repository:** github.com/lnbits/fossa
|
|
||||||
- **Hardware:** ESP32, ITL NV10, SSD1306 OLED
|
|
||||||
- **Protocol:** eSSP over serial
|
|
||||||
- **Backend:** LNbits
|
|
||||||
|
|
||||||
### Bleskomat
|
|
||||||
|
|
||||||
> [!example] Minimal Lightning ATM
|
|
||||||
> Coin-based Lightning vending machine.
|
|
||||||
|
|
||||||
- **Repository:** github.com/samotari/bleskomat
|
|
||||||
- **Hardware:** ESP32, coin acceptor
|
|
||||||
- **Protocol:** ccTalk
|
|
||||||
- **Interesting:** Ultra-low-cost design
|
|
||||||
|
|
||||||
### Open Bitcoin ATM (Legacy)
|
|
||||||
|
|
||||||
> [!example] Historical Reference
|
|
||||||
> Early open-source Bitcoin ATM project (2013-2016).
|
|
||||||
|
|
||||||
- **Repository:** github.com/mayosmith/OpenBitcoinATM
|
|
||||||
- **Hardware:** Raspberry Pi, various bill acceptors
|
|
||||||
- **Status:** Archived, but useful for protocol reference
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommended Bill of Materials
|
|
||||||
|
|
||||||
### Minimum Viable ATM
|
|
||||||
|
|
||||||
| Component | Model | Est. Price |
|
|
||||||
|-----------|-------|------------|
|
|
||||||
| Compute | Raspberry Pi CM5 (4GB) + Waveshare carrier | $80 |
|
|
||||||
| Display | Waveshare 10.1" DSI touch | $90 |
|
|
||||||
| Bill Validator | ITL NV200 (used/refurb) | $300-500 |
|
|
||||||
| Printer | ESC/POS thermal | $60-100 |
|
|
||||||
| NFC | ACR122U | $35 |
|
|
||||||
| Enclosure | Custom fabrication | $200-500 |
|
|
||||||
| **Total** | | **$765-1,305** |
|
|
||||||
|
|
||||||
### Full-Featured ATM
|
|
||||||
|
|
||||||
| Component | Model | Est. Price |
|
|
||||||
|-----------|-------|------------|
|
|
||||||
| Compute | Raspberry Pi CM5 (8GB) + industrial carrier | $150 |
|
|
||||||
| Display | Elo ESY15i5 | $800 |
|
|
||||||
| Bill Validator | ITL NV200 Spectral | $600 |
|
|
||||||
| Bill Dispenser | Puloon LCDM-2000 | $800 |
|
|
||||||
| Printer | Epson TM-T88VI | $350 |
|
|
||||||
| NFC | ACR122U | $35 |
|
|
||||||
| Enclosure | Industrial steel cabinet | $1,000-2,000 |
|
|
||||||
| **Total** | | **$3,735-4,735** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
### Phase 1: Port Existing Drivers
|
|
||||||
|
|
||||||
1. Audit current lamassu-machine drivers:
|
|
||||||
- `lib/id003/` → Rust HAL
|
|
||||||
- `lib/ccnet.js` → Rust HAL
|
|
||||||
- `lib/puloon/` → Rust HAL
|
|
||||||
- `lib/mei/` → Rust HAL
|
|
||||||
- `lib/ssp.js` → Rust HAL
|
|
||||||
|
|
||||||
2. Create napi-rs bindings for TypeScript consumption
|
|
||||||
|
|
||||||
3. Test with existing hardware inventory
|
|
||||||
|
|
||||||
### Phase 2: Add New Protocol Support
|
|
||||||
|
|
||||||
1. Implement eSSP for NV200 support
|
|
||||||
2. Add ESC/POS for universal printer support
|
|
||||||
3. Integrate libnfc for NFC readers
|
|
||||||
|
|
||||||
### Phase 3: Hardware Validation
|
|
||||||
|
|
||||||
1. Create NixOS hardware test image
|
|
||||||
2. Validate all components on CM5
|
|
||||||
3. Document any quirks or workarounds
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Vendor Contacts
|
|
||||||
|
|
||||||
| Component | Vendor | Contact |
|
|
||||||
|-----------|--------|---------|
|
|
||||||
| Bill Validators | Innovative Technology | sales@innovative-technology.com |
|
|
||||||
| Bill Validators | MEI (Crane) | info@cranepi.com |
|
|
||||||
| Bill Dispensers | Puloon Technology | sales@puloon.com |
|
|
||||||
| Displays | Elo Touch | sales@elotouch.com |
|
|
||||||
| Displays | Waveshare | service@waveshare.com |
|
|
||||||
| Compute | Raspberry Pi | For bulk: sales@raspberrypi.com |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related Notes
|
|
||||||
|
|
||||||
- [[modernization-plan]] - Overall modernization roadmap
|
|
||||||
- [[machine-ui-modernization]] - Vue 3 + Tauri migration
|
|
||||||
- [[lnbits-integration]] - Lightning backend integration
|
|
||||||
- [[NixOS Configuration]] - Production deployment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ITL NV200 Datasheet](https://innovative-technology.com/products/nv200/)
|
|
||||||
- [eSSP Protocol Guide](https://github.com/paysyslabs/ssp-server/wiki)
|
|
||||||
- [Raspberry Pi CM5 Documentation](https://www.raspberrypi.com/documentation/computers/compute-module.html)
|
|
||||||
- [libnfc Documentation](http://nfc-tools.org/index.php/Libnfc)
|
|
||||||
- [ESC/POS Command Reference](https://reference.epson-biz.com/modules/ref_escpos/index.php)
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,825 +0,0 @@
|
||||||
---
|
|
||||||
title: LNbits Integration
|
|
||||||
created: 2026-01-22
|
|
||||||
updated: 2026-01-22
|
|
||||||
tags:
|
|
||||||
- integration
|
|
||||||
- lightning
|
|
||||||
- lnbits
|
|
||||||
- api
|
|
||||||
status: reference
|
|
||||||
---
|
|
||||||
|
|
||||||
# LNbits Integration
|
|
||||||
|
|
||||||
> [!abstract] Summary
|
|
||||||
> LNbits serves as the Lightning Network backend for Lamassu ATMs, abstracting the underlying Lightning node implementation and providing a clean REST API for payments.
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
- [[#Why LNbits]]
|
|
||||||
- [[#API Reference]]
|
|
||||||
- [[#Deployment Architecture]]
|
|
||||||
- [[#Configuration]]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why LNbits
|
|
||||||
|
|
||||||
> [!decision] Choice of Lightning Backend
|
|
||||||
> LNbits was chosen over direct LND/CLN integration for these reasons:
|
|
||||||
|
|
||||||
| Feature | LNbits | Direct LND/CLN |
|
|
||||||
|---------|--------|----------------|
|
|
||||||
| Backend Abstraction | 30+ implementations | Single implementation |
|
|
||||||
| API Complexity | Simple REST | gRPC/REST varies |
|
|
||||||
| Multi-wallet | Built-in | Custom implementation |
|
|
||||||
| User Management | Built-in | None |
|
|
||||||
| Extensions | Rich ecosystem | None |
|
|
||||||
| Self-hostable | Yes | Yes |
|
|
||||||
| Open Source | MIT License | Varies |
|
|
||||||
|
|
||||||
**Supported Backends:**
|
|
||||||
- LND (lndrest, lndgrpc)
|
|
||||||
- Core Lightning (CLN, CLNRest)
|
|
||||||
- Eclair
|
|
||||||
- LNPay, OpenNode, Alby
|
|
||||||
- Breez, Phoenix
|
|
||||||
- NWC (Nostr Wallet Connect)
|
|
||||||
- And 20+ more...
|
|
||||||
|
|
||||||
#lnbits #lightning
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
LNbits uses API keys for authentication:
|
|
||||||
|
|
||||||
| Key Type | Header | Permissions |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| Admin Key | `X-API-KEY: {adminkey}` | Full wallet control |
|
|
||||||
| Invoice Key | `X-API-KEY: {invoicekey}` | Create invoices, view payments |
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const headers = {
|
|
||||||
'X-API-KEY': config.adminKey,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Core Endpoints
|
|
||||||
|
|
||||||
#### Get Wallet Info
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/v1/wallet
|
|
||||||
X-API-KEY: {adminkey}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "wallet-uuid",
|
|
||||||
"name": "ATM Wallet",
|
|
||||||
"balance": 1500000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!note] Balance Units
|
|
||||||
> All amounts in LNbits API are in **millisatoshis (msat)**. Divide by 1000 for satoshis.
|
|
||||||
|
|
||||||
#### Create Invoice (Receive)
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/v1/payments
|
|
||||||
X-API-KEY: {invoicekey}
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"out": false,
|
|
||||||
"amount": 50000,
|
|
||||||
"memo": "ATM Cash-out",
|
|
||||||
"expiry": 600,
|
|
||||||
"webhook": "https://lamassu.example.com/api/webhook/payment"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"payment_hash": "abc123...",
|
|
||||||
"payment_request": "lnbc500u1p...",
|
|
||||||
"checking_id": "xyz789..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| `out` | `false` for receiving, `true` for sending |
|
|
||||||
| `amount` | Amount in **satoshis** |
|
|
||||||
| `memo` | Invoice description |
|
|
||||||
| `expiry` | Seconds until expiration (default: 3600) |
|
|
||||||
| `webhook` | Optional callback URL |
|
|
||||||
|
|
||||||
#### Pay Invoice (Send)
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/v1/payments
|
|
||||||
X-API-KEY: {adminkey}
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"out": true,
|
|
||||||
"bolt11": "lnbc500u1p..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"payment_hash": "abc123...",
|
|
||||||
"checking_id": "xyz789...",
|
|
||||||
"fee": 5
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Check Payment Status
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/v1/payments/{checking_id}
|
|
||||||
X-API-KEY: {invoicekey}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"paid": true,
|
|
||||||
"pending": false,
|
|
||||||
"preimage": "def456...",
|
|
||||||
"payment_hash": "abc123...",
|
|
||||||
"amount": 50000,
|
|
||||||
"fee": 5,
|
|
||||||
"memo": "ATM Cash-out",
|
|
||||||
"time": 1706018400,
|
|
||||||
"bolt11": "lnbc500u1p..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### LNURL Scan
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/v1/lnurlscan/{code}
|
|
||||||
X-API-KEY: {invoicekey}
|
|
||||||
```
|
|
||||||
|
|
||||||
Decodes LNURL or Lightning Address and returns metadata.
|
|
||||||
|
|
||||||
**Response (Lightning Address):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"kind": "pay",
|
|
||||||
"domain": "walletofsatoshi.com",
|
|
||||||
"callback": "https://walletofsatoshi.com/lnurlp/user/callback",
|
|
||||||
"minSendable": 1000,
|
|
||||||
"maxSendable": 100000000000,
|
|
||||||
"metadata": "[['text/plain', 'Sats for user']]",
|
|
||||||
"allowsNostr": true,
|
|
||||||
"commentAllowed": 255
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Pay to LNURL
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/v1/payments/lnurl
|
|
||||||
X-API-KEY: {adminkey}
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"callback": "https://walletofsatoshi.com/lnurlp/user/callback",
|
|
||||||
"amount": 50000,
|
|
||||||
"comment": "ATM withdrawal"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#api #endpoints
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TypeScript Client
|
|
||||||
|
|
||||||
### Full Implementation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/server/lib/lightning/lnbits-client.ts
|
|
||||||
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
// Schemas
|
|
||||||
const WalletInfoSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
balance: z.number(), // msat
|
|
||||||
})
|
|
||||||
|
|
||||||
const CreateInvoiceResponseSchema = z.object({
|
|
||||||
payment_hash: z.string(),
|
|
||||||
payment_request: z.string(),
|
|
||||||
checking_id: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const PaymentResponseSchema = z.object({
|
|
||||||
payment_hash: z.string(),
|
|
||||||
checking_id: z.string(),
|
|
||||||
fee: z.number().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const PaymentStatusSchema = z.object({
|
|
||||||
paid: z.boolean(),
|
|
||||||
pending: z.boolean(),
|
|
||||||
preimage: z.string().nullable(),
|
|
||||||
payment_hash: z.string(),
|
|
||||||
amount: z.number(),
|
|
||||||
fee: z.number(),
|
|
||||||
memo: z.string().nullable(),
|
|
||||||
time: z.number(),
|
|
||||||
bolt11: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const LnurlPayResponseSchema = z.object({
|
|
||||||
kind: z.literal('pay'),
|
|
||||||
callback: z.string(),
|
|
||||||
minSendable: z.number(),
|
|
||||||
maxSendable: z.number(),
|
|
||||||
metadata: z.string(),
|
|
||||||
commentAllowed: z.number().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export interface LNbitsConfig {
|
|
||||||
baseUrl: string
|
|
||||||
adminKey: string
|
|
||||||
invoiceKey: string
|
|
||||||
walletId: string
|
|
||||||
timeout?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Invoice {
|
|
||||||
bolt11: string
|
|
||||||
paymentHash: string
|
|
||||||
checkingId: string
|
|
||||||
expiresAt: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaymentResult {
|
|
||||||
success: boolean
|
|
||||||
paymentHash: string
|
|
||||||
checkingId: string
|
|
||||||
feeSats?: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaymentStatus {
|
|
||||||
paid: boolean
|
|
||||||
pending: boolean
|
|
||||||
preimage: string | null
|
|
||||||
amountSats: number
|
|
||||||
feeSats: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client Implementation
|
|
||||||
export class LNbitsClient {
|
|
||||||
private baseUrl: string
|
|
||||||
private adminKey: string
|
|
||||||
private invoiceKey: string
|
|
||||||
private timeout: number
|
|
||||||
|
|
||||||
constructor(config: LNbitsConfig) {
|
|
||||||
this.baseUrl = config.baseUrl.replace(/\/$/, '')
|
|
||||||
this.adminKey = config.adminKey
|
|
||||||
this.invoiceKey = config.invoiceKey
|
|
||||||
this.timeout = config.timeout ?? 30000
|
|
||||||
}
|
|
||||||
|
|
||||||
private async request<T>(
|
|
||||||
method: string,
|
|
||||||
path: string,
|
|
||||||
key: 'admin' | 'invoice',
|
|
||||||
body?: unknown
|
|
||||||
): Promise<T> {
|
|
||||||
const apiKey = key === 'admin' ? this.adminKey : this.invoiceKey
|
|
||||||
|
|
||||||
const controller = new AbortController()
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'X-API-KEY': apiKey,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.text()
|
|
||||||
throw new Error(`LNbits API error: ${response.status} - ${error}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wallet Operations
|
|
||||||
|
|
||||||
async getWalletInfo(): Promise<{ id: string; name: string; balanceSats: number }> {
|
|
||||||
const data = await this.request('GET', '/api/v1/wallet', 'admin')
|
|
||||||
const parsed = WalletInfoSchema.parse(data)
|
|
||||||
return {
|
|
||||||
id: parsed.id,
|
|
||||||
name: parsed.name,
|
|
||||||
balanceSats: Math.floor(parsed.balance / 1000),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBalance(): Promise<number> {
|
|
||||||
const info = await this.getWalletInfo()
|
|
||||||
return info.balanceSats
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invoice Operations
|
|
||||||
|
|
||||||
async createInvoice(
|
|
||||||
amountSats: number,
|
|
||||||
memo: string,
|
|
||||||
options?: {
|
|
||||||
expiry?: number
|
|
||||||
webhook?: string
|
|
||||||
}
|
|
||||||
): Promise<Invoice> {
|
|
||||||
const data = await this.request('POST', '/api/v1/payments', 'invoice', {
|
|
||||||
out: false,
|
|
||||||
amount: amountSats,
|
|
||||||
memo,
|
|
||||||
expiry: options?.expiry ?? 600,
|
|
||||||
webhook: options?.webhook,
|
|
||||||
})
|
|
||||||
|
|
||||||
const parsed = CreateInvoiceResponseSchema.parse(data)
|
|
||||||
return {
|
|
||||||
bolt11: parsed.payment_request,
|
|
||||||
paymentHash: parsed.payment_hash,
|
|
||||||
checkingId: parsed.checking_id,
|
|
||||||
expiresAt: new Date(Date.now() + (options?.expiry ?? 600) * 1000),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPaymentStatus(checkingId: string): Promise<PaymentStatus> {
|
|
||||||
const data = await this.request('GET', `/api/v1/payments/${checkingId}`, 'invoice')
|
|
||||||
const parsed = PaymentStatusSchema.parse(data)
|
|
||||||
return {
|
|
||||||
paid: parsed.paid,
|
|
||||||
pending: parsed.pending,
|
|
||||||
preimage: parsed.preimage,
|
|
||||||
amountSats: parsed.amount,
|
|
||||||
feeSats: parsed.fee,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForPayment(
|
|
||||||
checkingId: string,
|
|
||||||
timeoutMs: number = 600000
|
|
||||||
): Promise<PaymentStatus> {
|
|
||||||
const startTime = Date.now()
|
|
||||||
const pollInterval = 2000
|
|
||||||
|
|
||||||
while (Date.now() - startTime < timeoutMs) {
|
|
||||||
const status = await this.getPaymentStatus(checkingId)
|
|
||||||
if (status.paid) return status
|
|
||||||
if (!status.pending) throw new Error('Payment failed or expired')
|
|
||||||
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Payment timeout')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Payment Operations
|
|
||||||
|
|
||||||
async payInvoice(bolt11: string): Promise<PaymentResult> {
|
|
||||||
try {
|
|
||||||
const data = await this.request('POST', '/api/v1/payments', 'admin', {
|
|
||||||
out: true,
|
|
||||||
bolt11,
|
|
||||||
})
|
|
||||||
|
|
||||||
const parsed = PaymentResponseSchema.parse(data)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
paymentHash: parsed.payment_hash,
|
|
||||||
checkingId: parsed.checking_id,
|
|
||||||
feeSats: parsed.fee,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
paymentHash: '',
|
|
||||||
checkingId: '',
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lightning Address Operations
|
|
||||||
|
|
||||||
async resolveLightningAddress(address: string): Promise<{
|
|
||||||
callback: string
|
|
||||||
minSats: number
|
|
||||||
maxSats: number
|
|
||||||
commentAllowed: number
|
|
||||||
}> {
|
|
||||||
const [name, domain] = address.split('@')
|
|
||||||
if (!name || !domain) {
|
|
||||||
throw new Error('Invalid Lightning Address format')
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`https://${domain}/.well-known/lnurlp/${name}`,
|
|
||||||
{ signal: AbortSignal.timeout(10000) }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to resolve Lightning Address: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = LnurlPayResponseSchema.parse(await response.json())
|
|
||||||
return {
|
|
||||||
callback: data.callback,
|
|
||||||
minSats: Math.ceil(data.minSendable / 1000),
|
|
||||||
maxSats: Math.floor(data.maxSendable / 1000),
|
|
||||||
commentAllowed: data.commentAllowed ?? 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async payToLightningAddress(
|
|
||||||
address: string,
|
|
||||||
amountSats: number,
|
|
||||||
comment?: string
|
|
||||||
): Promise<PaymentResult> {
|
|
||||||
// Step 1: Resolve address to LNURL-pay endpoint
|
|
||||||
const resolved = await this.resolveLightningAddress(address)
|
|
||||||
|
|
||||||
// Validate amount
|
|
||||||
if (amountSats < resolved.minSats || amountSats > resolved.maxSats) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
paymentHash: '',
|
|
||||||
checkingId: '',
|
|
||||||
error: `Amount must be between ${resolved.minSats} and ${resolved.maxSats} sats`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Get invoice from callback
|
|
||||||
const callbackUrl = new URL(resolved.callback)
|
|
||||||
callbackUrl.searchParams.set('amount', (amountSats * 1000).toString())
|
|
||||||
if (comment && resolved.commentAllowed > 0) {
|
|
||||||
callbackUrl.searchParams.set('comment', comment.slice(0, resolved.commentAllowed))
|
|
||||||
}
|
|
||||||
|
|
||||||
const invoiceResponse = await fetch(callbackUrl.toString(), {
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!invoiceResponse.ok) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
paymentHash: '',
|
|
||||||
checkingId: '',
|
|
||||||
error: 'Failed to get invoice from Lightning Address',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pr: bolt11 } = await invoiceResponse.json()
|
|
||||||
|
|
||||||
// Step 3: Pay the invoice
|
|
||||||
return this.payInvoice(bolt11)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage Examples
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Initialize client
|
|
||||||
const lnbits = new LNbitsClient({
|
|
||||||
baseUrl: 'https://lnbits.example.com',
|
|
||||||
adminKey: process.env.LNBITS_ADMIN_KEY!,
|
|
||||||
invoiceKey: process.env.LNBITS_INVOICE_KEY!,
|
|
||||||
walletId: process.env.LNBITS_WALLET_ID!,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check balance
|
|
||||||
const balance = await lnbits.getBalance()
|
|
||||||
console.log(`Wallet balance: ${balance} sats`)
|
|
||||||
|
|
||||||
// Cash-out: Create invoice for customer to pay
|
|
||||||
const invoice = await lnbits.createInvoice(50000, 'ATM Cash-out')
|
|
||||||
console.log(`Invoice: ${invoice.bolt11}`)
|
|
||||||
|
|
||||||
// Wait for payment
|
|
||||||
const status = await lnbits.waitForPayment(invoice.checkingId, 600000)
|
|
||||||
if (status.paid) {
|
|
||||||
console.log('Payment received!')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cash-in: Pay to customer's Lightning Address
|
|
||||||
const result = await lnbits.payToLightningAddress(
|
|
||||||
'user@walletofsatoshi.com',
|
|
||||||
50000,
|
|
||||||
'ATM withdrawal'
|
|
||||||
)
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`Payment sent! Hash: ${result.paymentHash}`)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#typescript #implementation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment Architecture
|
|
||||||
|
|
||||||
### Single LNbits Instance (Recommended)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "ATM Fleet"
|
|
||||||
atm1[ATM Berlin]
|
|
||||||
atm2[ATM Paris]
|
|
||||||
atm3[ATM Amsterdam]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Lamassu Server"
|
|
||||||
api[REST API]
|
|
||||||
lightning[Lightning Service]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "LNbits"
|
|
||||||
lnbits_api[LNbits API]
|
|
||||||
wallet[Shared Wallet]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Lightning Node"
|
|
||||||
lnd[LND / CLN]
|
|
||||||
end
|
|
||||||
|
|
||||||
atm1 --> api
|
|
||||||
atm2 --> api
|
|
||||||
atm3 --> api
|
|
||||||
|
|
||||||
api --> lightning
|
|
||||||
lightning --> lnbits_api
|
|
||||||
lnbits_api --> wallet
|
|
||||||
wallet --> lnd
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Single point of management
|
|
||||||
- Shared liquidity
|
|
||||||
- Simpler monitoring
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Single point of failure
|
|
||||||
- Requires robust HA setup
|
|
||||||
|
|
||||||
### Per-ATM Wallets
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "LNbits"
|
|
||||||
lnbits_api[LNbits API]
|
|
||||||
|
|
||||||
subgraph "Wallets"
|
|
||||||
wallet1[ATM-Berlin Wallet]
|
|
||||||
wallet2[ATM-Paris Wallet]
|
|
||||||
wallet3[ATM-Amsterdam Wallet]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
atm1[ATM Berlin] -->|adminkey_1| wallet1
|
|
||||||
atm2[ATM Paris] -->|adminkey_2| wallet2
|
|
||||||
atm3[ATM Amsterdam] -->|adminkey_3| wallet3
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Isolated balances
|
|
||||||
- Per-ATM accounting
|
|
||||||
- Granular key management
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- More complex setup
|
|
||||||
- Fragmented liquidity
|
|
||||||
|
|
||||||
#architecture #deployment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env (packages/server)
|
|
||||||
|
|
||||||
# LNbits Connection
|
|
||||||
LNBITS_URL=https://lnbits.example.com
|
|
||||||
LNBITS_ADMIN_KEY=abc123...
|
|
||||||
LNBITS_INVOICE_KEY=def456...
|
|
||||||
LNBITS_WALLET_ID=wallet-uuid
|
|
||||||
|
|
||||||
# Lightning Settings
|
|
||||||
LIGHTNING_PROVIDER=lnbits
|
|
||||||
LIGHTNING_TIMEOUT_MS=30000
|
|
||||||
LIGHTNING_MAX_FEE_PERCENT=1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### NixOS Configuration
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# /etc/nixos/lnbits.nix
|
|
||||||
{ config, pkgs, ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
services.lnbits = {
|
|
||||||
enable = true;
|
|
||||||
host = "127.0.0.1";
|
|
||||||
port = 5000;
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
LNBITS_BACKEND_WALLET_CLASS = "LndRestWallet";
|
|
||||||
LND_REST_ENDPOINT = "https://localhost:8080";
|
|
||||||
LND_REST_CERT = "/var/lib/lnd/tls.cert";
|
|
||||||
LND_REST_MACAROON = "/var/lib/lnd/admin.macaroon";
|
|
||||||
|
|
||||||
LNBITS_DATABASE_URL = "postgres://lnbits:password@localhost/lnbits";
|
|
||||||
LNBITS_SITE_TITLE = "Lamassu Lightning";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Reverse proxy with Caddy
|
|
||||||
services.caddy.virtualHosts."lnbits.example.com" = {
|
|
||||||
extraConfig = ''
|
|
||||||
reverse_proxy localhost:5000
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### sops-nix Secrets
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# secrets/lnbits.yaml
|
|
||||||
lnbits_admin_key: ENC[AES256_GCM,data:...,type:str]
|
|
||||||
lnbits_invoice_key: ENC[AES256_GCM,data:...,type:str]
|
|
||||||
```
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# NixOS module
|
|
||||||
sops.secrets.lnbits_admin_key = {
|
|
||||||
sopsFile = ./secrets/lnbits.yaml;
|
|
||||||
owner = "lamassu";
|
|
||||||
};
|
|
||||||
|
|
||||||
sops.secrets.lnbits_invoice_key = {
|
|
||||||
sopsFile = ./secrets/lnbits.yaml;
|
|
||||||
owner = "lamassu";
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#configuration #nixos #secrets
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Monitoring & Alerts
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Health check endpoint
|
|
||||||
async function checkLNbitsHealth(): Promise<HealthStatus> {
|
|
||||||
try {
|
|
||||||
const balance = await lnbits.getBalance()
|
|
||||||
return {
|
|
||||||
healthy: true,
|
|
||||||
balance,
|
|
||||||
timestamp: new Date(),
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
healthy: false,
|
|
||||||
error: error.message,
|
|
||||||
timestamp: new Date(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Balance Alerts
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const LOW_BALANCE_THRESHOLD = 100000 // 100k sats
|
|
||||||
|
|
||||||
async function checkBalanceAlerts() {
|
|
||||||
const balance = await lnbits.getBalance()
|
|
||||||
|
|
||||||
if (balance < LOW_BALANCE_THRESHOLD) {
|
|
||||||
await sendAlert({
|
|
||||||
level: 'warning',
|
|
||||||
message: `Low LNbits balance: ${balance} sats`,
|
|
||||||
action: 'Top up Lightning wallet',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prometheus Metrics
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Expose metrics for Prometheus
|
|
||||||
import { Counter, Gauge } from 'prom-client'
|
|
||||||
|
|
||||||
const lnbitsBalance = new Gauge({
|
|
||||||
name: 'lnbits_wallet_balance_sats',
|
|
||||||
help: 'Current LNbits wallet balance in satoshis',
|
|
||||||
})
|
|
||||||
|
|
||||||
const lnbitsPayments = new Counter({
|
|
||||||
name: 'lnbits_payments_total',
|
|
||||||
help: 'Total LNbits payments',
|
|
||||||
labelNames: ['direction', 'status'],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update metrics
|
|
||||||
setInterval(async () => {
|
|
||||||
const balance = await lnbits.getBalance()
|
|
||||||
lnbitsBalance.set(balance)
|
|
||||||
}, 60000)
|
|
||||||
```
|
|
||||||
|
|
||||||
#monitoring #alerts #prometheus
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Common Errors
|
|
||||||
|
|
||||||
| Error | Cause | Resolution |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| `INSUFFICIENT_BALANCE` | Wallet balance too low | Top up wallet |
|
|
||||||
| `INVOICE_EXPIRED` | Invoice not paid in time | Create new invoice |
|
|
||||||
| `PAYMENT_FAILED` | Route not found | Check node connectivity |
|
|
||||||
| `RATE_LIMITED` | Too many requests | Implement backoff |
|
|
||||||
| `UNAUTHORIZED` | Invalid API key | Check key configuration |
|
|
||||||
|
|
||||||
### Retry Strategy
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import pRetry from 'p-retry'
|
|
||||||
|
|
||||||
async function payWithRetry(bolt11: string): Promise<PaymentResult> {
|
|
||||||
return pRetry(
|
|
||||||
async () => {
|
|
||||||
const result = await lnbits.payInvoice(bolt11)
|
|
||||||
if (!result.success && result.error?.includes('ROUTE')) {
|
|
||||||
throw new Error('Retryable: No route found')
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
{
|
|
||||||
retries: 3,
|
|
||||||
minTimeout: 1000,
|
|
||||||
maxTimeout: 10000,
|
|
||||||
onFailedAttempt: (error) => {
|
|
||||||
console.log(`Payment attempt ${error.attemptNumber} failed`)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#errors #retry
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related Documents
|
|
||||||
|
|
||||||
- [[membership-lightning-integration]] - Membership feature using LNbits
|
|
||||||
- [[modernization-plan]] - Overall modernization roadmap
|
|
||||||
- [[API Key Authentication]] - Server authentication
|
|
||||||
|
|
@ -1,525 +0,0 @@
|
||||||
---
|
|
||||||
title: Lamassu Modernization Plan
|
|
||||||
created: 2026-01-22
|
|
||||||
updated: 2026-01-22
|
|
||||||
tags:
|
|
||||||
- architecture
|
|
||||||
- refactoring
|
|
||||||
- roadmap
|
|
||||||
- nix
|
|
||||||
status: draft
|
|
||||||
---
|
|
||||||
|
|
||||||
# Lamassu Modernization Plan
|
|
||||||
|
|
||||||
> [!abstract] Summary
|
|
||||||
> Aggressive refactoring plan to bring Lamassu Bitcoin ATM software to 2026 standards, focusing on **reproducibility**, **security**, and **open-source maintainability** with NixOS deployment.
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
- [[#Architecture Overview]]
|
|
||||||
- [[#Technology Decisions]]
|
|
||||||
- [[#Migration Phases]]
|
|
||||||
- [[#Trade-offs]]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "NixOS Production Server"
|
|
||||||
deploy[deploy-rs / Colmena]
|
|
||||||
sops[sops-nix secrets]
|
|
||||||
|
|
||||||
subgraph "lamassu-server"
|
|
||||||
fastify[Fastify + tRPC + GraphQL]
|
|
||||||
otel[OpenTelemetry]
|
|
||||||
end
|
|
||||||
|
|
||||||
pg[(PostgreSQL + Drizzle)]
|
|
||||||
jaeger[Jaeger / Grafana]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Admin UI"
|
|
||||||
vue_admin[Vue 3 + shadcn-vue]
|
|
||||||
tailwind[Tailwind CSS]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "lamassu-machine"
|
|
||||||
tauri[Tauri 2.x Rust Core]
|
|
||||||
vue_machine[Vue 3 + Pinia]
|
|
||||||
xstate[XState v5]
|
|
||||||
hal[Rust HAL napi-rs]
|
|
||||||
hw[Hardware Drivers]
|
|
||||||
end
|
|
||||||
|
|
||||||
vue_admin -->|tRPC| fastify
|
|
||||||
tauri -->|WebSocket| fastify
|
|
||||||
fastify --> pg
|
|
||||||
fastify --> otel
|
|
||||||
otel --> jaeger
|
|
||||||
hal --> hw
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Technology Decisions
|
|
||||||
|
|
||||||
### Runtime & Language
|
|
||||||
|
|
||||||
> [!decision] Full TypeScript + Node.js 22 LTS
|
|
||||||
> While Bun offers 3-4x performance, Node.js remains the backbone of enterprise applications. For mission-critical financial software, **reliability > raw performance**.
|
|
||||||
|
|
||||||
| Aspect | Current | Target |
|
|
||||||
|--------|---------|--------|
|
|
||||||
| Runtime | Node.js 22 | Node.js 22 LTS |
|
|
||||||
| Language | JS + partial TS | Full TypeScript (strict) |
|
|
||||||
| Modules | CommonJS + ESM mix | ESM only |
|
|
||||||
|
|
||||||
**Action Items:**
|
|
||||||
- [ ] Migrate all JavaScript to TypeScript
|
|
||||||
- [ ] Enable `strictNullChecks` and `noUncheckedIndexedAccess`
|
|
||||||
- [ ] Eliminate all CommonJS requires
|
|
||||||
|
|
||||||
#typescript #nodejs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Backend Framework
|
|
||||||
|
|
||||||
> [!decision] Express → Fastify
|
|
||||||
> Fastify provides 70-80k req/s vs Express's 20-30k, with first-class TypeScript support and JSON Schema validation.
|
|
||||||
|
|
||||||
| Framework | Performance | TypeScript | Ecosystem |
|
|
||||||
|-----------|-------------|------------|-----------|
|
|
||||||
| Express | 20-30k req/s | Partial | Mature |
|
|
||||||
| **Fastify** | 70-80k req/s | First-class | Growing |
|
|
||||||
| Hono | Ultra-light | Good | Edge-focused |
|
|
||||||
|
|
||||||
**Why Fastify:**
|
|
||||||
- JSON Schema validation built-in
|
|
||||||
- HTTP/2 support
|
|
||||||
- Plugin architecture
|
|
||||||
- Better for long-running server processes
|
|
||||||
|
|
||||||
#backend #fastify
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### API Layer
|
|
||||||
|
|
||||||
> [!decision] Hybrid: tRPC + GraphQL
|
|
||||||
> tRPC for Admin UI (type-safe monorepo), GraphQL for machine communication (stable contract).
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
AdminUI -->|tRPC| Server
|
|
||||||
Machine -->|GraphQL| Server
|
|
||||||
```
|
|
||||||
|
|
||||||
**tRPC Benefits:**
|
|
||||||
- End-to-end type safety without codegen
|
|
||||||
- Faster iteration
|
|
||||||
- Smaller bundle
|
|
||||||
|
|
||||||
**Keep GraphQL for:**
|
|
||||||
- Machine API (stable contract)
|
|
||||||
- Potential non-TypeScript clients
|
|
||||||
- Consider Yoga over Apollo (lighter)
|
|
||||||
|
|
||||||
#api #trpc #graphql
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Database & ORM
|
|
||||||
|
|
||||||
> [!decision] Kysely → Drizzle ORM
|
|
||||||
> SQL-first, schema-as-code, zero binary dependencies (critical for NixOS reproducibility).
|
|
||||||
|
|
||||||
| ORM | Bundle | Cold Start | Migrations |
|
|
||||||
|-----|--------|------------|------------|
|
|
||||||
| Prisma | Heavy | Slow | Excellent |
|
|
||||||
| Kysely | Light | Fast | Basic |
|
|
||||||
| **Drizzle** | ~7kb | Fastest | Good |
|
|
||||||
|
|
||||||
**Why Drizzle:**
|
|
||||||
- SQL-like syntax (readable)
|
|
||||||
- Zero binary dependencies
|
|
||||||
- 90% reduction in cold starts
|
|
||||||
- TypeScript schema definitions
|
|
||||||
|
|
||||||
#database #drizzle #postgresql
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Frontend (Admin UI)
|
|
||||||
|
|
||||||
> [!decision] React → Vue 3
|
|
||||||
> Unify on Vue 3 across all UIs (admin + machine) for consistency and shared code.
|
|
||||||
|
|
||||||
| Aspect | Before (React) | After (Vue 3) |
|
|
||||||
|--------|---------------|---------------|
|
|
||||||
| Framework | React 18 | Vue 3 |
|
|
||||||
| UI Library | MUI (~300kb) | shadcn-vue (~15kb) |
|
|
||||||
| State | Zustand | Pinia |
|
|
||||||
| API | Apollo GraphQL | tRPC |
|
|
||||||
| Bundle | ~400kb | ~60kb |
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Single framework across all UIs
|
|
||||||
- Shared composables between admin and machine
|
|
||||||
- 70% bundle size reduction
|
|
||||||
- End-to-end type safety with tRPC
|
|
||||||
|
|
||||||
See [[admin-ui-modernization]] for detailed migration plan.
|
|
||||||
|
|
||||||
#frontend #vue #tailwind
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Machine State Management
|
|
||||||
|
|
||||||
> [!decision] Machina.js → XState v5
|
|
||||||
> Actor-based state management with visual editor and TypeScript inference.
|
|
||||||
|
|
||||||
**Current:** `lib/brain.js` (134KB monolith)
|
|
||||||
|
|
||||||
**XState Benefits:**
|
|
||||||
- Visual state machine editor (Stately.ai)
|
|
||||||
- TypeScript 5.0+ with excellent inference
|
|
||||||
- Actor model for complex flows
|
|
||||||
- Production-tested at scale
|
|
||||||
|
|
||||||
> [!warning] Learning Curve
|
|
||||||
> XState has a steep learning curve. The mental model differs significantly from Redux/Context.
|
|
||||||
|
|
||||||
#statemachine #xstate
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Machine UI Framework
|
|
||||||
|
|
||||||
> [!decision] Vanilla JS → Vue 3 + Tauri 2.x
|
|
||||||
> Vue 3 for UI consistency, Tauri for security-first kiosk shell.
|
|
||||||
|
|
||||||
**UI Layer: Vue 3**
|
|
||||||
|
|
||||||
| Aspect | Before | After |
|
|
||||||
|--------|--------|-------|
|
|
||||||
| Framework | Vanilla JS + jQuery | Vue 3 |
|
|
||||||
| State | Global variables | Pinia |
|
|
||||||
| Build | Babel 6 | Vite |
|
|
||||||
| File | 80KB monolith | Component-based |
|
|
||||||
|
|
||||||
**Shell: Tauri 2.x**
|
|
||||||
|
|
||||||
| Aspect | Electron | Tauri |
|
|
||||||
|--------|----------|-------|
|
|
||||||
| Bundle Size | ~100MB | **~2.5MB** |
|
|
||||||
| RAM Usage | 150-300MB | **30-50MB** |
|
|
||||||
| Startup | 1-2s | **<0.5s** |
|
|
||||||
| Security | Full Node access | **Explicit allowlist** |
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Same Vue 3 skills as admin UI
|
|
||||||
- Shared ui-shared package
|
|
||||||
- Tauri's Rust core for hardware drivers
|
|
||||||
- Security by default
|
|
||||||
|
|
||||||
See [[machine-ui-modernization]] for detailed migration plan.
|
|
||||||
|
|
||||||
#tauri #vue #kiosk #security
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Hardware Abstraction
|
|
||||||
|
|
||||||
> [!decision] JavaScript → Rust + napi-rs
|
|
||||||
> Memory safety at compile time for hardware drivers.
|
|
||||||
|
|
||||||
```
|
|
||||||
TypeScript Application
|
|
||||||
↓
|
|
||||||
napi-rs bindings
|
|
||||||
↓
|
|
||||||
Rust HAL Layer
|
|
||||||
↓
|
|
||||||
Hardware (bill validators, printers, etc.)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- No null pointer dereferences
|
|
||||||
- No buffer overflows
|
|
||||||
- Pre-built binaries for platforms
|
|
||||||
- Integrates with Tauri (both Rust)
|
|
||||||
|
|
||||||
#rust #napi #hardware
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Schema Validation
|
|
||||||
|
|
||||||
> [!decision] Yup → Zod
|
|
||||||
> Better TypeScript integration, larger ecosystem, tRPC compatibility.
|
|
||||||
|
|
||||||
| Library | Bundle | Ecosystem | tRPC |
|
|
||||||
|---------|--------|-----------|------|
|
|
||||||
| Yup | ~15kb | Mature | Manual |
|
|
||||||
| **Zod** | ~17kb | Large | Native |
|
|
||||||
| Valibot | ~1.4kb | Growing | Adapter |
|
|
||||||
|
|
||||||
**Use Valibot** for machine-side code where bundle size matters.
|
|
||||||
|
|
||||||
#validation #zod
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Observability
|
|
||||||
|
|
||||||
> [!decision] OpenTelemetry
|
|
||||||
> Vendor-neutral, unified traces/metrics/logs.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
||||||
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Self-hosted stack:**
|
|
||||||
- **Jaeger** → Distributed tracing
|
|
||||||
- **Prometheus + Grafana** → Metrics
|
|
||||||
- All deployable via NixOS modules
|
|
||||||
|
|
||||||
#observability #opentelemetry #monitoring
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
> [!decision] Passkey-First Authentication
|
|
||||||
> Resistant to phishing and credential theft.
|
|
||||||
|
|
||||||
**Current:** Client certs + Argon2 + SimpleWebAuthn
|
|
||||||
|
|
||||||
**Target:**
|
|
||||||
- Passkeys as primary auth (WebAuthn)
|
|
||||||
- Keep client certs for machine-to-server
|
|
||||||
- Upgrade to SimpleWebAuthn v10+
|
|
||||||
|
|
||||||
> [!warning] 2025 Context
|
|
||||||
> 4B credentials leaked in January 2025. Password-based auth is a liability.
|
|
||||||
|
|
||||||
#security #passkeys #webauthn
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NixOS Infrastructure
|
|
||||||
|
|
||||||
### Development Environment
|
|
||||||
|
|
||||||
> [!decision] devenv
|
|
||||||
> 100% reproducible development environments.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# devenv.nix
|
|
||||||
{ pkgs, ... }: {
|
|
||||||
languages.javascript = {
|
|
||||||
enable = true;
|
|
||||||
package = pkgs.nodejs_22;
|
|
||||||
pnpm.enable = true;
|
|
||||||
};
|
|
||||||
languages.typescript.enable = true;
|
|
||||||
languages.rust.enable = true;
|
|
||||||
|
|
||||||
services.postgres = {
|
|
||||||
enable = true;
|
|
||||||
initialDatabases = [{ name = "lamassu"; }];
|
|
||||||
};
|
|
||||||
|
|
||||||
pre-commit.hooks = {
|
|
||||||
prettier.enable = true;
|
|
||||||
eslint.enable = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Single `devenv.nix` replaces Docker, brew, apt
|
|
||||||
- DevContainer generation for VS Code
|
|
||||||
- Built-in PostgreSQL service
|
|
||||||
- Pre-commit hooks integration
|
|
||||||
|
|
||||||
#nix #devenv #reproducibility
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Secrets Management
|
|
||||||
|
|
||||||
> [!decision] sops-nix
|
|
||||||
> Atomic, declarative secret provisioning.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Supports age, GPG, AWS KMS, HashiCorp Vault
|
|
||||||
- Works with existing SSH keys
|
|
||||||
- Version-control friendly (encrypted in git)
|
|
||||||
- Compatible with all NixOS deployment tools
|
|
||||||
|
|
||||||
```nix
|
|
||||||
sops.secrets.database_password = {
|
|
||||||
sopsFile = ./secrets/db.yaml;
|
|
||||||
owner = "lamassu";
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#secrets #sops #security
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Production Deployment
|
|
||||||
|
|
||||||
> [!decision] deploy-rs
|
|
||||||
> Automatic rollback on failure - critical for ATM servers.
|
|
||||||
|
|
||||||
| Tool | Rollback | Secrets | Parallel |
|
|
||||||
|------|----------|---------|----------|
|
|
||||||
| **deploy-rs** | Automatic | External | Yes |
|
|
||||||
| Colmena | Manual | Built-in | Yes |
|
|
||||||
|
|
||||||
**Why deploy-rs:**
|
|
||||||
- Connects after activation to confirm availability
|
|
||||||
- Auto-rollback if machine becomes unreachable
|
|
||||||
- Critical for network config changes on remote ATMs
|
|
||||||
|
|
||||||
#deployment #deploy-rs #nixos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Node.js Packaging
|
|
||||||
|
|
||||||
> [!tip] dream2nix
|
|
||||||
> Auto-generates Nix derivations from `package-lock.json`.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
inputs.dream2nix.url = "github:nix-community/dream2nix";
|
|
||||||
|
|
||||||
outputs = { dream2nix, ... }:
|
|
||||||
dream2nix.lib.makeFlakeOutputs {
|
|
||||||
source = ./.;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#nix #packaging
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Unit/Integration
|
|
||||||
|
|
||||||
> [!check] Keep Vitest
|
|
||||||
> Already in use, works well with TypeScript.
|
|
||||||
|
|
||||||
### E2E Testing
|
|
||||||
|
|
||||||
> [!decision] Add Playwright
|
|
||||||
> TypeScript-first, auto-wait eliminates flaky tests.
|
|
||||||
|
|
||||||
**Best Practices:**
|
|
||||||
- Use accessible locators (`getByRole`, `getByLabel`)
|
|
||||||
- Test isolation (each test independent)
|
|
||||||
- Run `tsc --noEmit` in CI
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
test('user can complete transaction', async ({ page }) => {
|
|
||||||
await page.getByRole('button', { name: 'Start' }).click()
|
|
||||||
await expect(page.getByText('Insert bill')).toBeVisible()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#testing #playwright #vitest
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Phases
|
|
||||||
|
|
||||||
### Phase 1: Foundation
|
|
||||||
> [!todo] High Impact, Lower Risk
|
|
||||||
|
|
||||||
- [ ] Full TypeScript migration
|
|
||||||
- [ ] devenv for development environment
|
|
||||||
- [ ] Drizzle ORM migration
|
|
||||||
- [ ] OpenTelemetry instrumentation
|
|
||||||
- [ ] Nix flake for builds
|
|
||||||
|
|
||||||
### Phase 2: Backend Modernization
|
|
||||||
|
|
||||||
- [ ] Express → Fastify migration
|
|
||||||
- [ ] Add tRPC for admin API
|
|
||||||
- [ ] Zod schema validation
|
|
||||||
- [ ] Playwright E2E tests
|
|
||||||
|
|
||||||
### Phase 3: Machine Modernization
|
|
||||||
> [!warning] Higher Risk - Requires extensive testing
|
|
||||||
|
|
||||||
- [ ] XState v5 for state machine
|
|
||||||
- [ ] Tauri migration
|
|
||||||
- [ ] Rust HAL for hardware drivers
|
|
||||||
|
|
||||||
### Phase 4: Deployment
|
|
||||||
|
|
||||||
- [ ] NixOS module for lamassu-server
|
|
||||||
- [ ] sops-nix secrets management
|
|
||||||
- [ ] deploy-rs for production deployments
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Trade-offs
|
|
||||||
|
|
||||||
| Decision | We Get | We Lose |
|
|
||||||
|----------|--------|---------|
|
|
||||||
| Node.js over Bun | Stability, ecosystem | Raw performance |
|
|
||||||
| Fastify over Hono | Mature plugins | Minimal bundle |
|
|
||||||
| Drizzle over Prisma | Bundle size, speed | DX features |
|
|
||||||
| Vue 3 over React | Unified UI, smaller bundle | React ecosystem |
|
|
||||||
| Tauri over Electron | Security, efficiency | Ecosystem maturity |
|
|
||||||
| XState over simple FSM | Visualization, debugging | Simplicity |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- [Fastify vs Express 2025](https://medium.com/codetodeploy/express-or-fastify-in-2025-whats-the-right-node-js-framework-for-you-6ea247141a86)
|
|
||||||
- [tRPC vs GraphQL](https://betterstack.com/community/guides/scaling-nodejs/trpc-vs-graphql/)
|
|
||||||
- [Drizzle vs Prisma vs Kysely](https://levelup.gitconnected.com/the-2025-typescript-orm-battle-prisma-vs-drizzle-vs-kysely-007ffdfded67)
|
|
||||||
|
|
||||||
### Machine
|
|
||||||
- [Tauri vs Electron 2025](https://www.dolthub.com/blog/2025-11-13-electron-vs-tauri/)
|
|
||||||
- [XState v5](https://stately.ai/blog/2023-12-01-xstate-v5)
|
|
||||||
- [napi-rs Guide](https://blog.logrocket.com/building-nodejs-modules-rust-napi-rs/)
|
|
||||||
|
|
||||||
### NixOS
|
|
||||||
- [devenv](https://devenv.sh/)
|
|
||||||
- [sops-nix](https://github.com/Mic92/sops-nix)
|
|
||||||
- [deploy-rs](https://github.com/serokell/deploy-rs)
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- [Passkeys Guide](https://www.passkeys.com/guide)
|
|
||||||
- [OpenTelemetry Node.js](https://opentelemetry.io/docs/languages/js/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related Notes
|
|
||||||
|
|
||||||
- [[architecture-review]] - **KYC-free Lightning-first architecture review**
|
|
||||||
- [[nostr-native-architecture]] - **Nostr as infrastructure backbone**
|
|
||||||
- [[CLAUDE]] - Claude Code guidance
|
|
||||||
- [[admin-ui-modernization]] - Vue 3 migration for admin dashboard
|
|
||||||
- [[machine-ui-modernization]] - Vue 3 migration for kiosk UI
|
|
||||||
- [[hardware-recommendations]] - Hardware component recommendations
|
|
||||||
- [[membership-lightning-integration]] - Membership & LNbits feature
|
|
||||||
- [[lnbits-integration]] - Lightning backend integration
|
|
||||||
- [[Architecture Decision Records]] - ADRs for each decision
|
|
||||||
- [[NixOS Configuration]] - Production NixOS setup
|
|
||||||
|
|
@ -1,828 +0,0 @@
|
||||||
---
|
|
||||||
title: Nostr-Native ATM Architecture
|
|
||||||
created: 2026-01-22
|
|
||||||
updated: 2026-01-22
|
|
||||||
tags:
|
|
||||||
- architecture
|
|
||||||
- nostr
|
|
||||||
- lightning-pub
|
|
||||||
- kyc-free
|
|
||||||
- decentralized
|
|
||||||
status: active
|
|
||||||
priority: critical
|
|
||||||
---
|
|
||||||
|
|
||||||
# Nostr-Native ATM Architecture
|
|
||||||
|
|
||||||
> [!abstract] Summary
|
|
||||||
> A radical rethinking of ATM infrastructure where **Nostr becomes the backbone** for identity, communication, and payments. Replaces traditional server infrastructure with Lightning.Pub and a private Nostr relay, eliminating KYC vectors like phone numbers while enabling a truly decentralized, censorship-resistant system.
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
- [[#Vision: Nostr as Infrastructure]]
|
|
||||||
- [[#Lightning.Pub as Core Server]]
|
|
||||||
- [[#Private Relay Architecture]]
|
|
||||||
- [[#Machine Identity]]
|
|
||||||
- [[#Replacing SMS with Nostr]]
|
|
||||||
- [[#Event Schema]]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Vision: Nostr as Infrastructure
|
|
||||||
|
|
||||||
### The Problem with Traditional ATM Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ TRADITIONAL LAMASSU ARCHITECTURE │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ATM ──HTTPS/WSS──► Server ──► PostgreSQL │
|
|
||||||
│ │ │
|
|
||||||
│ ├──► SMS Gateway (Twilio) │
|
|
||||||
│ ├──► Email Service │
|
|
||||||
│ ├──► KYC Provider │
|
|
||||||
│ └──► Lightning Node │
|
|
||||||
│ │
|
|
||||||
│ Problems: │
|
|
||||||
│ • Phone numbers = KYC vector │
|
|
||||||
│ • Complex server infrastructure │
|
|
||||||
│ • DNS, SSL, port forwarding required │
|
|
||||||
│ • Single point of failure │
|
|
||||||
│ • Centralized command/control │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### The Nostr-Native Solution
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ NOSTR-NATIVE ATM ARCHITECTURE │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
|
||||||
│ │ ATM 1 │ │ ATM 2 │ │ ATM N │ │
|
|
||||||
│ │ npub_1 │ │ npub_2 │ │ npub_n │ │
|
|
||||||
│ └────┬────┘ └────┬────┘ └────┬────┘ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ └────────────┼────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌────────────────────────┐ │
|
|
||||||
│ │ Private Nostr Relay │◄─── NIP-42 Auth │
|
|
||||||
│ │ (strfry / rnostr) │ Whitelist: ATMs + │
|
|
||||||
│ └───────────┬────────────┘ Operators only │
|
|
||||||
│ │ │
|
|
||||||
│ ┌───────────┼───────────┐ │
|
|
||||||
│ ▼ ▼ ▼ │
|
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
||||||
│ │Lightning │ │ Operator │ │ Public │ │
|
|
||||||
│ │ Pub │ │Dashboard │ │ Relays │ │
|
|
||||||
│ │(LND wrap)│ │ (Vue 3) │ │(fallback)│ │
|
|
||||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
|
||||||
│ │
|
|
||||||
│ Benefits: │
|
|
||||||
│ • No phone numbers (Nostr DMs instead) │
|
|
||||||
│ • Zero server config (no DNS/SSL/ports) │
|
|
||||||
│ • Decentralized communication │
|
|
||||||
│ • Cryptographic machine identity │
|
|
||||||
│ • Censorship-resistant │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lightning.Pub as Core Server
|
|
||||||
|
|
||||||
> [!decision] Lightning.Pub Replaces lamassu-server
|
|
||||||
> A Nostr-native account system that wraps LND and eliminates traditional server complexity.
|
|
||||||
|
|
||||||
### Why Lightning.Pub?
|
|
||||||
|
|
||||||
| Aspect | Traditional Server | Lightning.Pub |
|
|
||||||
|--------|-------------------|---------------|
|
|
||||||
| Network config | DNS, SSL, ports, firewall | Zero (uses Nostr relays) |
|
|
||||||
| Deployment | Complex | One-line install |
|
|
||||||
| Communication | HTTPS/WebSocket | Nostr events (NIP-44 encrypted) |
|
|
||||||
| Account system | Custom implementation | Built-in sublayers |
|
|
||||||
| CLINK support | Must implement | Native |
|
|
||||||
| Lightning | Separate integration | Wraps LND directly |
|
|
||||||
|
|
||||||
### One-Line Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux
|
|
||||||
wget -qO- https://deploy.lightning.pub | bash
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
curl -fsSL https://deploy.lightning.pub | bash
|
|
||||||
|
|
||||||
# Everything confined to ~/lightning_pub/
|
|
||||||
# No sudo, no root, no system changes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Lightning.Pub
|
|
||||||
├── LND (Lightning Network Daemon)
|
|
||||||
│ └── Neutrino (SPV Bitcoin)
|
|
||||||
├── Account System
|
|
||||||
│ ├── Application Pools (operator level)
|
|
||||||
│ └── User Accounts (ATM wallets)
|
|
||||||
├── CLINK Native
|
|
||||||
│ ├── noffer (static payment codes)
|
|
||||||
│ └── ndebit (authorized payments)
|
|
||||||
├── Nostr Communication
|
|
||||||
│ └── NIP-44 encrypted events
|
|
||||||
└── Optional: LNURL Bridge (legacy support)
|
|
||||||
```
|
|
||||||
|
|
||||||
### What Lightning.Pub Gives Us
|
|
||||||
|
|
||||||
1. **No Port Forwarding** - Nostr relays handle all communication
|
|
||||||
2. **Multi-User Accounts** - Each ATM gets its own account
|
|
||||||
3. **CLINK Native** - Static payment codes work out of the box
|
|
||||||
4. **Liquidity Management** - Auto-quotes from LSPs (Zeus, Voltage, Flashsats)
|
|
||||||
5. **Watchdog Security** - Monitors for drainage attacks
|
|
||||||
6. **Production Tested** - Years of real-world deployment
|
|
||||||
|
|
||||||
### Configuration for ATM Fleet
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ~/lightning_pub/.env
|
|
||||||
|
|
||||||
# Private relay for machine communication
|
|
||||||
NOSTR_RELAYS="wss://relay.youratm.company wss://nos.lol"
|
|
||||||
|
|
||||||
# Disable bootstrap peering for full sovereignty
|
|
||||||
DISABLE_LIQUIDITY_PROVIDER=true
|
|
||||||
|
|
||||||
# Custom LNURL domain (optional, for legacy wallets)
|
|
||||||
SERVICE_URL=https://ln.youratm.company
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Private Relay Architecture
|
|
||||||
|
|
||||||
> [!decision] Run a Restricted Nostr Relay
|
|
||||||
> NIP-42 authenticated relay that only accepts events from known machines and operators.
|
|
||||||
|
|
||||||
### Why a Private Relay?
|
|
||||||
|
|
||||||
| Concern | Public Relay | Private Relay |
|
|
||||||
|---------|--------------|---------------|
|
|
||||||
| Who can read | Anyone | Whitelisted npubs only |
|
|
||||||
| Who can write | Anyone | Whitelisted npubs only |
|
|
||||||
| Machine commands | Exposed | Encrypted, restricted |
|
|
||||||
| Fleet data | Public | Private |
|
|
||||||
| Censorship | Relay can censor | You control |
|
|
||||||
|
|
||||||
### Relay Options
|
|
||||||
|
|
||||||
| Relay | Language | NIP-42 | Performance | Notes |
|
|
||||||
|-------|----------|--------|-------------|-------|
|
|
||||||
| **strfry** | C++ | Yes | Excellent | Plugin system, negentropy sync |
|
|
||||||
| **rnostr** | Rust | Yes | Excellent | LMDB storage, inspired by strfry |
|
|
||||||
| **nostr-rs-relay** | Rust | Yes | Good | SQLite/PostgreSQL |
|
|
||||||
|
|
||||||
### NIP-42 Authentication
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ NIP-42 AUTH FLOW │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ATM connects to relay │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ Relay sends AUTH challenge │
|
|
||||||
│ ["AUTH", "<random-challenge>"] │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ATM signs challenge with its nsec │
|
|
||||||
│ { │
|
|
||||||
│ "kind": 22242, │
|
|
||||||
│ "tags": [ │
|
|
||||||
│ ["relay", "wss://relay.youratm.company"], │
|
|
||||||
│ ["challenge", "<random-challenge>"] │
|
|
||||||
│ ], │
|
|
||||||
│ "content": "", │
|
|
||||||
│ "sig": "<signature>" │
|
|
||||||
│ } │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ Relay verifies npub is in whitelist │
|
|
||||||
│ │ │
|
|
||||||
│ ├── Yes → Connection allowed │
|
|
||||||
│ └── No → Connection rejected │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### strfry Configuration
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# strfry.conf
|
|
||||||
|
|
||||||
[relay]
|
|
||||||
bind = "0.0.0.0"
|
|
||||||
port = 7777
|
|
||||||
realIpHeader = "X-Forwarded-For"
|
|
||||||
|
|
||||||
[relay.info]
|
|
||||||
name = "ATM Fleet Relay"
|
|
||||||
description = "Private relay for ATM communication"
|
|
||||||
contact = "operator@youratm.company"
|
|
||||||
|
|
||||||
# Require NIP-42 authentication
|
|
||||||
authRequired = true
|
|
||||||
|
|
||||||
[relay.writePolicy]
|
|
||||||
plugin = "./plugins/whitelist.js"
|
|
||||||
|
|
||||||
[relay.negentropy]
|
|
||||||
enabled = true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Whitelist Plugin (noteguard style)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// plugins/whitelist.js
|
|
||||||
const ALLOWED_PUBKEYS = new Set([
|
|
||||||
'npub1_atm_001...', // ATM 1
|
|
||||||
'npub1_atm_002...', // ATM 2
|
|
||||||
'npub1_operator...', // Operator
|
|
||||||
])
|
|
||||||
|
|
||||||
export function writePolicy(event, sourceInfo) {
|
|
||||||
if (!sourceInfo.authedPubkey) {
|
|
||||||
return { action: 'reject', message: 'auth-required: authenticate first' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ALLOWED_PUBKEYS.has(sourceInfo.authedPubkey)) {
|
|
||||||
return { action: 'reject', message: 'restricted: not authorized' }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { action: 'accept' }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### NixOS Module for Relay
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# relay.nix
|
|
||||||
{ config, pkgs, ... }:
|
|
||||||
{
|
|
||||||
services.strfry = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
relay = {
|
|
||||||
bind = "127.0.0.1";
|
|
||||||
port = 7777;
|
|
||||||
info = {
|
|
||||||
name = "ATM Fleet Relay";
|
|
||||||
description = "Private NIP-42 authenticated relay";
|
|
||||||
};
|
|
||||||
authRequired = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Nginx reverse proxy with SSL
|
|
||||||
services.nginx.virtualHosts."relay.youratm.company" = {
|
|
||||||
enableACME = true;
|
|
||||||
forceSSL = true;
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://127.0.0.1:7777";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Machine Identity
|
|
||||||
|
|
||||||
> [!decision] Each ATM Has a Nostr Keypair
|
|
||||||
> Hardware-bound identity that replaces certificates and enables cryptographic authentication.
|
|
||||||
|
|
||||||
### Identity Model
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ MACHINE IDENTITY │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ Traditional: │
|
|
||||||
│ • Client certificate (complex PKI) │
|
|
||||||
│ • API keys (can be leaked) │
|
|
||||||
│ • IP-based auth (unreliable) │
|
|
||||||
│ │
|
|
||||||
│ Nostr-Native: │
|
|
||||||
│ • Machine has nsec (private key) │
|
|
||||||
│ • npub is machine identity │
|
|
||||||
│ • All events signed by machine │
|
|
||||||
│ • Operator whitelist controls access │
|
|
||||||
│ • No certificate authority needed │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Generation & Storage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Machine first boot - generate identity
|
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools'
|
|
||||||
import { writeFileSync } from 'fs'
|
|
||||||
|
|
||||||
function initMachineIdentity() {
|
|
||||||
const nsec = generateSecretKey()
|
|
||||||
const npub = getPublicKey(nsec)
|
|
||||||
|
|
||||||
// Store in secure location (TPM, encrypted file, etc.)
|
|
||||||
writeFileSync('/etc/lamassu/machine.nsec', nsec, { mode: 0o600 })
|
|
||||||
|
|
||||||
console.log(`Machine identity: ${npub}`)
|
|
||||||
console.log('Add this npub to operator whitelist')
|
|
||||||
|
|
||||||
return { nsec, npub }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Secure Key Storage Options
|
|
||||||
|
|
||||||
| Method | Security | Complexity | Best For |
|
|
||||||
|--------|----------|------------|----------|
|
|
||||||
| Encrypted file | Medium | Low | Development |
|
|
||||||
| TPM 2.0 | High | Medium | Production |
|
|
||||||
| Secure enclave | Highest | High | High-security |
|
|
||||||
| HSM | Highest | Highest | Enterprise |
|
|
||||||
|
|
||||||
### Tauri Integration
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// src-tauri/src/identity.rs
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
pub struct MachineIdentity {
|
|
||||||
keys: Keys,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MachineIdentity {
|
|
||||||
pub fn load_or_create() -> Result<Self, Error> {
|
|
||||||
let nsec_path = "/etc/lamassu/machine.nsec";
|
|
||||||
|
|
||||||
let keys = if fs::metadata(nsec_path).is_ok() {
|
|
||||||
// Load existing
|
|
||||||
let nsec = fs::read_to_string(nsec_path)?;
|
|
||||||
Keys::parse(&nsec)?
|
|
||||||
} else {
|
|
||||||
// Generate new
|
|
||||||
let keys = Keys::generate();
|
|
||||||
fs::write(nsec_path, keys.secret_key()?.to_bech32()?)?;
|
|
||||||
keys
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self { keys })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn npub(&self) -> String {
|
|
||||||
self.keys.public_key().to_bech32().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sign_event(&self, event: UnsignedEvent) -> Result<Event, Error> {
|
|
||||||
event.sign(&self.keys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Replacing SMS with Nostr
|
|
||||||
|
|
||||||
> [!decision] Nostr DMs Replace Phone-Based Messaging
|
|
||||||
> No phone numbers = no KYC vector. Users provide npub for receipts.
|
|
||||||
|
|
||||||
### What SMS Was Used For (Old Lamassu)
|
|
||||||
|
|
||||||
| Use Case | Old Method | New Method |
|
|
||||||
|----------|------------|------------|
|
|
||||||
| Transaction receipt | SMS to phone | NIP-17 DM to npub |
|
|
||||||
| Verification code | SMS OTP | Not needed (no KYC) |
|
|
||||||
| Operator alerts | SMS/Email | Nostr events to operator npub |
|
|
||||||
| Customer notifications | SMS | Optional NIP-17 DM |
|
|
||||||
|
|
||||||
### NIP-17 Private Direct Messages
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Send encrypted receipt to user
|
|
||||||
import { nip44, nip59 } from 'nostr-tools'
|
|
||||||
|
|
||||||
async function sendReceipt(
|
|
||||||
userNpub: string,
|
|
||||||
receipt: TransactionReceipt
|
|
||||||
) {
|
|
||||||
const content = JSON.stringify({
|
|
||||||
type: 'transaction_receipt',
|
|
||||||
txid: receipt.txid,
|
|
||||||
amount: receipt.amountSats,
|
|
||||||
timestamp: receipt.timestamp,
|
|
||||||
atmId: receipt.atmNpub,
|
|
||||||
})
|
|
||||||
|
|
||||||
// NIP-17: Encrypted gift-wrapped message
|
|
||||||
const sealedEvent = await nip59.seal(
|
|
||||||
machineKeys,
|
|
||||||
userNpub,
|
|
||||||
{
|
|
||||||
kind: 14, // Direct message
|
|
||||||
content,
|
|
||||||
tags: [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Publish to relay
|
|
||||||
await relay.publish(sealedEvent)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### User Flow (Optional Receipt)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ OPTIONAL RECEIPT FLOW │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 1. User completes transaction │
|
|
||||||
│ │
|
|
||||||
│ 2. ATM asks: "Want a receipt?" │
|
|
||||||
│ [No Thanks] [Yes, via Nostr] │
|
|
||||||
│ │
|
|
||||||
│ 3. If yes, user provides npub: │
|
|
||||||
│ • Scan NFC card with npub │
|
|
||||||
│ • Scan QR code of npub │
|
|
||||||
│ • Type npub manually │
|
|
||||||
│ │
|
|
||||||
│ 4. ATM sends NIP-17 encrypted DM │
|
|
||||||
│ • Only user can decrypt │
|
|
||||||
│ • Contains: amount, txid, timestamp │
|
|
||||||
│ • No phone number collected! │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Operator Alerts via Nostr
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Machine publishes alert event
|
|
||||||
async function sendOperatorAlert(
|
|
||||||
alertType: 'low_cash' | 'error' | 'offline',
|
|
||||||
details: object
|
|
||||||
) {
|
|
||||||
const event = {
|
|
||||||
kind: 30078, // Replaceable application-specific
|
|
||||||
pubkey: machineNpub,
|
|
||||||
content: nip44.encrypt(
|
|
||||||
machineNsec,
|
|
||||||
operatorNpub,
|
|
||||||
JSON.stringify({
|
|
||||||
type: alertType,
|
|
||||||
machineId: machineNpub,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
details,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
tags: [
|
|
||||||
['d', `alert:${machineNpub}`], // Replaceable identifier
|
|
||||||
['p', operatorNpub],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
await relay.publish(signEvent(event, machineNsec))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Event Schema
|
|
||||||
|
|
||||||
> [!tip] Custom Event Kinds for ATM Operations
|
|
||||||
> Define application-specific events for machine status, transactions, and commands.
|
|
||||||
|
|
||||||
### Event Kinds
|
|
||||||
|
|
||||||
| Kind | Type | Description |
|
|
||||||
|------|------|-------------|
|
|
||||||
| 21001 | CLINK | Offer Request/Response |
|
|
||||||
| 21002 | CLINK | Debit Request/Response |
|
|
||||||
| 21003 | CLINK | Management Delegation |
|
|
||||||
| 30078 | Replaceable | Machine Status |
|
|
||||||
| 30079 | Replaceable | Cash Levels |
|
|
||||||
| 14 | NIP-17 | Encrypted Receipt DM |
|
|
||||||
| 22242 | Ephemeral | NIP-42 Auth |
|
|
||||||
|
|
||||||
### Machine Status Event (Kind 30078)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface MachineStatusEvent {
|
|
||||||
kind: 30078
|
|
||||||
pubkey: string // Machine npub
|
|
||||||
content: string // NIP-44 encrypted JSON
|
|
||||||
tags: [
|
|
||||||
['d', 'status'], // Replaceable identifier
|
|
||||||
['p', string], // Operator npub
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypted content:
|
|
||||||
interface MachineStatus {
|
|
||||||
online: boolean
|
|
||||||
lastTransaction: number // timestamp
|
|
||||||
cashLevels: {
|
|
||||||
validator: number // bills in validator
|
|
||||||
dispenser: CassetteLevel[]
|
|
||||||
}
|
|
||||||
errors: string[]
|
|
||||||
version: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transaction Record Event
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface TransactionEvent {
|
|
||||||
kind: 30079
|
|
||||||
pubkey: string // Machine npub
|
|
||||||
content: string // NIP-44 encrypted
|
|
||||||
tags: [
|
|
||||||
['d', `tx:${txid}`],
|
|
||||||
['p', string], // Operator npub
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypted content:
|
|
||||||
interface TransactionRecord {
|
|
||||||
txid: string
|
|
||||||
type: 'cash_in' | 'cash_out'
|
|
||||||
amountFiat: number
|
|
||||||
amountSats: number
|
|
||||||
fee: number
|
|
||||||
timestamp: number
|
|
||||||
paymentMethod: 'lnurl_withdraw' | 'clink_offer' | 'invoice' | 'cashu'
|
|
||||||
// No user identity stored!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Operator Command Event
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface CommandEvent {
|
|
||||||
kind: 21003 // CLINK manage
|
|
||||||
pubkey: string // Operator npub
|
|
||||||
content: string // NIP-44 encrypted
|
|
||||||
tags: [
|
|
||||||
['p', string], // Target machine npub
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypted content:
|
|
||||||
interface OperatorCommand {
|
|
||||||
command: 'restart' | 'update' | 'disable' | 'enable' | 'set_limits'
|
|
||||||
params?: object
|
|
||||||
timestamp: number
|
|
||||||
signature: string // Operator signs command
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Full Stack Architecture
|
|
||||||
|
|
||||||
### Component Diagram
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ NOSTR-NATIVE ATM STACK │
|
|
||||||
├─────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ ATM MACHINE │ │
|
|
||||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │
|
|
||||||
│ │ │ Vue 3 UI │ │ XState v5 │ │ Rust HAL │ │ │
|
|
||||||
│ │ │ (Tauri) │ │ (State) │ │ (Bill/Dispense) │ │ │
|
|
||||||
│ │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ │
|
|
||||||
│ │ │ │ │ │ │
|
|
||||||
│ │ ┌──────┴────────────────┴─────────────────────┴──────────┐ │ │
|
|
||||||
│ │ │ Nostr Client │ │ │
|
|
||||||
│ │ │ • Machine nsec/npub identity │ │ │
|
|
||||||
│ │ │ • CLINK SDK for payments │ │ │
|
|
||||||
│ │ │ • NIP-44 encryption │ │ │
|
|
||||||
│ │ │ • Event publishing/subscription │ │ │
|
|
||||||
│ │ └────────────────────────┬───────────────────────────────┘ │ │
|
|
||||||
│ └───────────────────────────┼──────────────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ PRIVATE NOSTR RELAY │ │
|
|
||||||
│ │ • strfry / rnostr │ │
|
|
||||||
│ │ • NIP-42 authentication required │ │
|
|
||||||
│ │ • Whitelist: ATM npubs + Operator npubs │ │
|
|
||||||
│ │ • Negentropy sync for offline reconciliation │ │
|
|
||||||
│ └────────────────────────────┬──────────────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ┌────────────────────┼────────────────────┐ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ ▼ ▼ ▼ │
|
|
||||||
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────┐ │
|
|
||||||
│ │ Lightning.Pub │ │ Operator │ │ Public Relays │ │
|
|
||||||
│ │ │ │ Dashboard │ │ (Fallback) │ │
|
|
||||||
│ │ • LND node │ │ │ │ │ │
|
|
||||||
│ │ • Accounts │ │ • Vue 3 app │ │ • nos.lol │ │
|
|
||||||
│ │ • CLINK native│ │ • Subscribe │ │ • relay.damus.io │ │
|
|
||||||
│ │ • Liquidity │ │ to events │ │ • For CLINK with │ │
|
|
||||||
│ └───────────────┘ │ • Send cmds │ │ external users │ │
|
|
||||||
│ └───────────────┘ └───────────────────┘ │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Data Flow: Cash-In Transaction
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User
|
|
||||||
participant ATM
|
|
||||||
participant Relay as Private Relay
|
|
||||||
participant LPub as Lightning.Pub
|
|
||||||
participant LN as Lightning Network
|
|
||||||
|
|
||||||
User->>ATM: Insert $50 cash
|
|
||||||
ATM->>ATM: Validate bills (HAL)
|
|
||||||
ATM->>Relay: Publish status event
|
|
||||||
|
|
||||||
ATM->>ATM: Generate CLINK offer (variable price)
|
|
||||||
ATM->>ATM: Display QR code
|
|
||||||
|
|
||||||
User->>User: Scan with ShockWallet
|
|
||||||
User->>Relay: CLINK offer request (Kind 21001)
|
|
||||||
Relay->>ATM: Forward request
|
|
||||||
|
|
||||||
ATM->>LPub: Request invoice (amount calculated)
|
|
||||||
LPub->>ATM: BOLT11 invoice
|
|
||||||
ATM->>Relay: CLINK response with invoice
|
|
||||||
Relay->>User: Forward response
|
|
||||||
|
|
||||||
User->>LN: Pay invoice
|
|
||||||
LN->>LPub: Payment received
|
|
||||||
LPub->>Relay: Payment confirmation event
|
|
||||||
Relay->>ATM: Forward confirmation
|
|
||||||
|
|
||||||
ATM->>ATM: Transaction complete
|
|
||||||
ATM->>Relay: Publish transaction record
|
|
||||||
|
|
||||||
opt User provided npub
|
|
||||||
ATM->>Relay: Send NIP-17 receipt DM
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Path
|
|
||||||
|
|
||||||
### Phase 1: Add Nostr Layer
|
|
||||||
|
|
||||||
```
|
|
||||||
Existing Lamassu ──► Add Nostr client
|
|
||||||
Add private relay
|
|
||||||
Keep existing server (parallel)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: Lightning.Pub Integration
|
|
||||||
|
|
||||||
```
|
|
||||||
Add Lightning.Pub ──► Route payments through LPub
|
|
||||||
CLINK offers enabled
|
|
||||||
Account system active
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Full Migration
|
|
||||||
|
|
||||||
```
|
|
||||||
Remove old server ──► Nostr-only communication
|
|
||||||
NIP-17 receipts (no SMS)
|
|
||||||
Private relay primary
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 4: Optional Enhancements
|
|
||||||
|
|
||||||
```
|
|
||||||
Advanced features ──► Cashu ecash integration
|
|
||||||
Fedimint support
|
|
||||||
Multi-relay redundancy
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Threat Model
|
|
||||||
|
|
||||||
| Threat | Mitigation |
|
|
||||||
|--------|------------|
|
|
||||||
| Relay compromise | NIP-44 encryption (relay can't read) |
|
|
||||||
| Key theft | TPM/HSM storage, key rotation |
|
|
||||||
| Replay attacks | Timestamps, nonces in events |
|
|
||||||
| Rogue operator | Multi-sig commands (future) |
|
|
||||||
| Network sniffing | WebSocket over TLS, NIP-44 |
|
|
||||||
|
|
||||||
### Key Rotation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Periodic key rotation for machines
|
|
||||||
async function rotateMachineKey(oldNsec: string) {
|
|
||||||
const newKeys = generateKeys()
|
|
||||||
|
|
||||||
// Publish key rotation event (signed by old key)
|
|
||||||
const rotationEvent = {
|
|
||||||
kind: 30078,
|
|
||||||
content: nip44.encrypt(oldNsec, operatorNpub, JSON.stringify({
|
|
||||||
type: 'key_rotation',
|
|
||||||
oldPubkey: getPublicKey(oldNsec),
|
|
||||||
newPubkey: newKeys.npub,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
})),
|
|
||||||
tags: [
|
|
||||||
['d', 'key_rotation'],
|
|
||||||
['p', operatorNpub],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
await relay.publish(signEvent(rotationEvent, oldNsec))
|
|
||||||
|
|
||||||
// Operator must update whitelist
|
|
||||||
// Then switch to new key
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparison: Old vs Nostr-Native
|
|
||||||
|
|
||||||
| Aspect | Old Lamassu | Nostr-Native |
|
|
||||||
|--------|-------------|--------------|
|
|
||||||
| Communication | HTTPS/WebSocket | Nostr events |
|
|
||||||
| Authentication | Client certs | NIP-42 + npub whitelist |
|
|
||||||
| Encryption | TLS | NIP-44 (content-level) |
|
|
||||||
| Identity | PKI certificates | Nostr keypairs |
|
|
||||||
| Receipts | SMS (phone = KYC) | NIP-17 DMs (npub) |
|
|
||||||
| Alerts | Email/SMS | Nostr events |
|
|
||||||
| Server config | DNS, SSL, ports | Zero config |
|
|
||||||
| Deployment | Complex | One-line |
|
|
||||||
| Censorship | Server can be seized | Relay-agnostic |
|
|
||||||
| Privacy | Phone numbers leaked | Pseudonymous npubs |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
1. **Relay redundancy** - Should machines connect to multiple relays?
|
|
||||||
2. **Offline operation** - How long can machine operate without relay?
|
|
||||||
3. **Key escrow** - How to recover if machine key is lost?
|
|
||||||
4. **Multi-operator** - Can multiple operators share a fleet?
|
|
||||||
5. **Cashu over Nostr** - Use Nostr for ecash token delivery?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related Notes
|
|
||||||
|
|
||||||
- [[architecture-review]] - Overall KYC-free architecture
|
|
||||||
- [[lnbits-integration]] - LNbits as alternative backend
|
|
||||||
- [[hardware-recommendations]] - Hardware choices
|
|
||||||
- [[machine-ui-modernization]] - Vue 3 UI migration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
### Lightning.Pub
|
|
||||||
- [Lightning.Pub GitHub](https://github.com/shocknet/Lightning.Pub)
|
|
||||||
- [ShockWallet](https://github.com/shocknet/wallet2)
|
|
||||||
- [CLINK Protocol](https://github.com/shocknet/CLINK)
|
|
||||||
|
|
||||||
### Nostr Relays
|
|
||||||
- [strfry](https://github.com/hoytech/strfry)
|
|
||||||
- [rnostr](https://github.com/rnostr/rnostr)
|
|
||||||
- [nostr-rs-relay](https://sr.ht/~gheartsfield/nostr-rs-relay/)
|
|
||||||
- [noteguard](https://github.com/damus-io/noteguard) - strfry plugin system
|
|
||||||
|
|
||||||
### NIPs
|
|
||||||
- [NIP-42: Authentication](https://github.com/nostr-protocol/nips/blob/master/42.md)
|
|
||||||
- [NIP-44: Versioned Encryption](https://github.com/paulmillr/nip44)
|
|
||||||
- [NIP-17: Private Direct Messages](https://nips.nostr.com/17)
|
|
||||||
- [NIP-59: Gift Wraps](https://github.com/nostr-protocol/nips/blob/master/59.md)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit c15dbbcae8902d99e056b9397bd46cbb640edce1
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 8c83ad8bdbd49621a20c01d725b1ce0c8eee813e
|
|
||||||
67
lamassu-next/.gitignore
vendored
67
lamassu-next/.gitignore
vendored
|
|
@ -1,67 +0,0 @@
|
||||||
# Dependencies
|
|
||||||
node_modules/
|
|
||||||
.pnpm-store/
|
|
||||||
|
|
||||||
# Build outputs
|
|
||||||
dist/
|
|
||||||
.next/
|
|
||||||
.nuxt/
|
|
||||||
.output/
|
|
||||||
target/
|
|
||||||
*.node
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Secrets
|
|
||||||
*.nsec
|
|
||||||
*.pem
|
|
||||||
*.key
|
|
||||||
.secrets.baseline
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
coverage/
|
|
||||||
.nyc_output/
|
|
||||||
|
|
||||||
# Caches
|
|
||||||
.turbo/
|
|
||||||
.cache/
|
|
||||||
.parcel-cache/
|
|
||||||
.eslintcache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Electron
|
|
||||||
apps/machine/dist-electron/
|
|
||||||
apps/machine/release/
|
|
||||||
|
|
||||||
# devenv
|
|
||||||
.devenv/
|
|
||||||
.direnv/
|
|
||||||
.pre-commit-config.yaml
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
docker/**/data/
|
|
||||||
|
|
||||||
# Temporary
|
|
||||||
tmp/
|
|
||||||
temp/
|
|
||||||
*.tmp
|
|
||||||
*.timestamp-*.mjs
|
|
||||||
|
|
@ -1,308 +0,0 @@
|
||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code when working with the Lamassu Next codebase.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
**Lamassu Next** is a Nostr-native Lightning ATM system. Key principles:
|
|
||||||
|
|
||||||
- **KYC-Free**: No identity collection, no compliance theater
|
|
||||||
- **Lightning-Native**: Security encapsulated in Lightning protocol
|
|
||||||
- **Nostr as Infrastructure**: Relay for communication, keypairs for identity
|
|
||||||
- **Open Source First**: Every component auditable and forkable
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
lamassu-next/
|
|
||||||
├── apps/
|
|
||||||
│ ├── machine/ # Electron + Vue 3 ATM kiosk application ✅
|
|
||||||
│ ├── dashboard/ # Vue 3 operator dashboard (planned)
|
|
||||||
│ └── relay/ # strfry relay configuration (planned)
|
|
||||||
├── packages/
|
|
||||||
│ ├── hal/ # TypeScript Hardware Abstraction Layer ✅
|
|
||||||
│ ├── nostr-client/ # Nostr client library ✅
|
|
||||||
│ ├── clink/ # CLINK protocol implementation ✅
|
|
||||||
│ ├── state-machine/ # XState v5 ATM state machine ✅
|
|
||||||
│ ├── lightning/ # Lightning.Pub RPC client ✅
|
|
||||||
│ ├── cashu/ # Cashu ecash (placeholder)
|
|
||||||
│ └── ui-shared/ # Shared Vue components (placeholder)
|
|
||||||
└── docker/ # Development infrastructure ✅
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
### Completed Packages
|
|
||||||
|
|
||||||
| Package | Description | Tests |
|
|
||||||
| ------------------------ | ------------------------------------------------------ | ----- |
|
|
||||||
| `@lamassu/nostr-client` | Nostr relay client with NIP-42 auth, NIP-44 encryption | 13 |
|
|
||||||
| `@lamassu/clink` | CLINK protocol (kinds 21001-21003), noffer encoding | 7 |
|
|
||||||
| `@lamassu/lightning` | Lightning.Pub RPC client (kind 21000) | 10 |
|
|
||||||
| `@lamassu/state-machine` | XState v5 ATM state machine (idle, cashIn, cashOut) | 14 |
|
|
||||||
| `@lamassu/hal` | Hardware drivers (ID003 validator, F56 dispenser) | - |
|
|
||||||
|
|
||||||
### Placeholder Packages
|
|
||||||
|
|
||||||
| Package | Description |
|
|
||||||
| -------------------- | --------------------------------- |
|
|
||||||
| `@lamassu/cashu` | Cashu ecash for offline operation |
|
|
||||||
| `@lamassu/ui-shared` | Shared Vue 3 components |
|
|
||||||
|
|
||||||
### Completed Applications
|
|
||||||
|
|
||||||
- **apps/machine** - Electron ATM kiosk with Vue 3 UI (HAL integrated, ready for hardware testing)
|
|
||||||
|
|
||||||
### Planned Components
|
|
||||||
|
|
||||||
- **apps/dashboard** - Operator dashboard for fleet management
|
|
||||||
|
|
||||||
### Critical Documentation
|
|
||||||
|
|
||||||
- **`packages/lightning/TROUBLESHOOTING.md`** - Lightning.Pub integration gotchas. Read this BEFORE debugging payment issues. Contains solutions to 9 non-obvious issues that took 5+ hours to diagnose.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enter development environment
|
|
||||||
devenv shell
|
|
||||||
|
|
||||||
# Start development
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# Build all packages
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
pnpm test
|
|
||||||
|
|
||||||
# Infrastructure management
|
|
||||||
infra-up # Start all Docker services
|
|
||||||
infra-down # Stop all Docker services
|
|
||||||
infra-status # Show service status
|
|
||||||
infra-logs # Follow service logs
|
|
||||||
|
|
||||||
# Bitcoin/Lightning (regtest)
|
|
||||||
btccli # Bitcoin CLI
|
|
||||||
lncli # LND CLI (Lightning.Pub's node)
|
|
||||||
lncli-alice # LND CLI (Alice's node for testing payments)
|
|
||||||
mine-blocks # Mine regtest blocks (default: 1)
|
|
||||||
setup-channel # Setup channel between Alice and LND
|
|
||||||
alice-pay # Pay invoice from Alice's node
|
|
||||||
relay-test # Test Nostr relay connection
|
|
||||||
|
|
||||||
# Testing (E2E)
|
|
||||||
test-setup # Validate test environment (services, channels, payments)
|
|
||||||
test-payment # Quick e2e payment test (ATM → customer)
|
|
||||||
fund-atm # Fund ATM account (default: 100k sats)
|
|
||||||
alice-invoice # Create invoice on Alice's node
|
|
||||||
node-info # Show node pubkeys and channel info
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Infrastructure
|
|
||||||
|
|
||||||
The `docker/` directory contains a complete development environment:
|
|
||||||
|
|
||||||
| Service | Container | Port(s) | Description |
|
|
||||||
| ------------- | --------------------- | ----------- | ------------------------------------- |
|
|
||||||
| strfry | lamassu-relay | 7777 | Private Nostr relay |
|
|
||||||
| bitcoind | lamassu-bitcoind | 18443 | Bitcoin Core (regtest) |
|
|
||||||
| LND | lamassu-lnd | 10009, 8080 | Lightning node (Lightning.Pub's node) |
|
|
||||||
| LND Alice | lamassu-lnd-alice | 10010, 8081 | Second LND for payment testing |
|
|
||||||
| Lightning.Pub | lamassu-lightning-pub | 1776 | Nostr-native account system |
|
|
||||||
| PostgreSQL | lamassu-postgres | 5432 | Database for server-side state |
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
devenv shell # Enter dev environment
|
|
||||||
infra-up # Start all services (30-60s first run)
|
|
||||||
mine-blocks 101 # Fund the regtest wallet
|
|
||||||
setup-channel # Open channel between Alice and LND
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing Payments
|
|
||||||
|
|
||||||
The development setup includes two LND nodes to enable proper payment testing:
|
|
||||||
|
|
||||||
1. **LND** (`lamassu-lnd`) - Used by Lightning.Pub to create invoices
|
|
||||||
2. **Alice** (`lamassu-lnd-alice`) - Used to pay invoices (simulates external payers)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get Lightning.Pub admin token
|
|
||||||
curl -X POST "http://localhost:1776/api/admin/app/auth" \
|
|
||||||
-H "Authorization: Bearer lamassu-dev-admin-token" \
|
|
||||||
-d '{"name": "wallet"}'
|
|
||||||
|
|
||||||
# Create user and invoice
|
|
||||||
curl -X POST "http://localhost:1776/api/app/user/add" -H "Authorization: Bearer $APP_TOKEN" \
|
|
||||||
-d '{"identifier": "test-user", "balance": 0}'
|
|
||||||
|
|
||||||
curl -X POST "http://localhost:1776/api/app/user/add/invoice" -H "Authorization: Bearer $APP_TOKEN" \
|
|
||||||
-d '{"receiver_identifier": "test-user", "payer_identifier": "external", "http_callback_url": "", "invoice_req": {"amountSats": 1000, "memo": "Test"}}'
|
|
||||||
|
|
||||||
# Pay from Alice
|
|
||||||
alice-pay <invoice>
|
|
||||||
```
|
|
||||||
|
|
||||||
### MCP Tools Available
|
|
||||||
|
|
||||||
Claude has access to these MCP servers for development:
|
|
||||||
|
|
||||||
| MCP Server | Purpose |
|
|
||||||
| ------------ | ----------------------------------------- |
|
|
||||||
| docker-mcp | Container management (logs, status, etc.) |
|
|
||||||
| nostr-mcp | Nostr operations (post notes, profiles) |
|
|
||||||
| postgres-mcp | Database queries and schema inspection |
|
|
||||||
| mcp-nixos | NixOS/Nix package queries |
|
|
||||||
| forgejo-mcp | Git operations on Forgejo |
|
|
||||||
|
|
||||||
Use these to interact with infrastructure directly during development.
|
|
||||||
|
|
||||||
## Key Technologies
|
|
||||||
|
|
||||||
| Component | Technology | Notes |
|
|
||||||
| ------------- | -------------- | --------------------------------------- |
|
|
||||||
| Runtime | Node.js 22 LTS | Strict TypeScript, ESM |
|
|
||||||
| ATM Shell | Electron | Node.js main process, Vue 3 renderer |
|
|
||||||
| State Machine | XState v5 | Actor model, service injection |
|
|
||||||
| Hardware | TypeScript | ID003, F56 drivers from lamassu-machine |
|
|
||||||
| Messaging | Nostr | NIP-01, NIP-42, NIP-44 |
|
|
||||||
| Payments | CLINK + RPC | Kind 21000 (RPC), 21001-21003 (CLINK) |
|
|
||||||
| Backend | Lightning.Pub | Nostr-native account system |
|
|
||||||
|
|
||||||
## Custom Skills
|
|
||||||
|
|
||||||
The following skills are available for development assistance:
|
|
||||||
|
|
||||||
### `/security` - Security Review
|
|
||||||
|
|
||||||
Audit code for Bitcoin/Lightning/ATM-specific vulnerabilities.
|
|
||||||
|
|
||||||
```
|
|
||||||
/security packages/lightning/src/
|
|
||||||
/security --staged
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/nostr-check` - Nostr Conformity
|
|
||||||
|
|
||||||
Validate NIP compliance and Nostr protocol implementation.
|
|
||||||
|
|
||||||
```
|
|
||||||
/nostr-check packages/nostr-client/src/events.ts --nips NIP-01,NIP-44
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/lightning-check` - Lightning.Pub Conformity
|
|
||||||
|
|
||||||
Validate CLINK protocol and Lightning.Pub integration.
|
|
||||||
|
|
||||||
```
|
|
||||||
/lightning-check packages/clink/src/ --clink
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/test` - Testing Agent
|
|
||||||
|
|
||||||
Run tests, generate test cases, validate transaction flows.
|
|
||||||
|
|
||||||
```
|
|
||||||
/test coverage packages/state-machine/
|
|
||||||
/test flow cash-out
|
|
||||||
/test generate packages/lightning/src/client.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/docs` - Documentation Agent
|
|
||||||
|
|
||||||
Keep documentation synchronized with code.
|
|
||||||
|
|
||||||
```
|
|
||||||
/docs sync packages/clink/
|
|
||||||
/docs api packages/nostr-client/src/
|
|
||||||
```
|
|
||||||
|
|
||||||
### `/hal-check` - HAL Validation
|
|
||||||
|
|
||||||
Validate Rust HAL drivers against lamassu-machine implementations.
|
|
||||||
|
|
||||||
```
|
|
||||||
/hal-check port id003
|
|
||||||
/hal-check safety packages/hal/src/dispensers/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
### TypeScript
|
|
||||||
|
|
||||||
- ESM only (`import`/`export`)
|
|
||||||
- Strict mode with `strictNullChecks` and `noUncheckedIndexedAccess`
|
|
||||||
- Zod for runtime validation
|
|
||||||
- No `any` types
|
|
||||||
|
|
||||||
### Rust (HAL)
|
|
||||||
|
|
||||||
- Stable toolchain
|
|
||||||
- `#![deny(unsafe_code)]` unless justified
|
|
||||||
- Error handling with `thiserror`
|
|
||||||
- Async with `tokio`
|
|
||||||
|
|
||||||
### Formatting
|
|
||||||
|
|
||||||
- Prettier for TypeScript (2 spaces, no semicolons, single quotes)
|
|
||||||
- rustfmt for Rust
|
|
||||||
- Pre-commit hooks enforce formatting
|
|
||||||
|
|
||||||
## Hardware Drivers
|
|
||||||
|
|
||||||
Drivers are ported from `lamassu-machine/lib/`:
|
|
||||||
|
|
||||||
| Category | Drivers |
|
|
||||||
| ---------- | ------------------------------------------------------------ |
|
|
||||||
| Validators | id003, ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 |
|
|
||||||
| Dispensers | puloon, f56, genmega, hcm2, gsr50 |
|
|
||||||
| Printers | nippon, zebra, genmega |
|
|
||||||
|
|
||||||
When porting:
|
|
||||||
|
|
||||||
1. Read JS driver thoroughly
|
|
||||||
2. Document protocol from JS code
|
|
||||||
3. Implement Rust version
|
|
||||||
4. Test against same hardware
|
|
||||||
5. Use `/hal-check port <driver>` to validate
|
|
||||||
|
|
||||||
## Nostr Event Kinds
|
|
||||||
|
|
||||||
| Kind | Description |
|
|
||||||
| ----- | ----------------------------------------------- |
|
|
||||||
| 21000 | Lightning.Pub RPC (generic request/response) |
|
|
||||||
| 21001 | CLINK Offer (invoice request/response) |
|
|
||||||
| 21002 | CLINK Debit (payment authorization) |
|
|
||||||
| 21003 | CLINK Manage (offer management) |
|
|
||||||
| 30078 | Service Beacon (replaceable, service discovery) |
|
|
||||||
| 30079 | Transaction Record (replaceable) |
|
|
||||||
|
|
||||||
## Security Priorities
|
|
||||||
|
|
||||||
1. **Private keys** - Never log nsec, protect with 0600 permissions
|
|
||||||
2. **Payments** - Validate invoices, verify preimages, prevent double-pay
|
|
||||||
3. **Hardware** - Validate dispense amounts, handle errors gracefully
|
|
||||||
4. **Encryption** - Use NIP-44 for all sensitive data
|
|
||||||
|
|
||||||
## Testing Requirements
|
|
||||||
|
|
||||||
- Unit tests for all packages
|
|
||||||
- Integration tests for cross-package interactions
|
|
||||||
- E2E tests for full transaction flows
|
|
||||||
- **Cash-out flow is critical path** (95%+ of activity)
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- `docs/architecture-comparison.md` - Nostr-native vs traditional lamassu-server comparison
|
|
||||||
- `docs/ndebit-cash-in-flow.md` - Technical walkthrough of cash-in implementation
|
|
||||||
- `packages/lightning/TROUBLESHOOTING.md` - Lightning.Pub integration gotchas (must read!)
|
|
||||||
- `.claude/skills/*.md` - Custom skill documentation
|
|
||||||
|
|
||||||
## External Resources
|
|
||||||
|
|
||||||
- [CLINK Protocol Spec](https://github.com/shocknet/clink)
|
|
||||||
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub)
|
|
||||||
- [NIP-44 Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
|
|
||||||
- [LND Hold Invoices](https://docs.lightning.engineering/lightning-network-tools/lnd/hold-invoices)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 5909e609576e078e4c2189fc6cce464b8dca8a71
|
|
||||||
1
lnbits
1
lnbits
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit d2e89148357e99f766a32ed9b67ab2fc2e799b2d
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue