Merge pull request #698 from shocknet/fix/bootstrap

Fix/bootstrap
This commit is contained in:
boufni95 2024-07-20 17:55:33 +02:00 committed by GitHub
commit 84395e7b94
98 changed files with 17367 additions and 12640 deletions

7
.gitignore vendored
View file

@ -10,4 +10,9 @@ db.sqlite
metrics.sqlite metrics.sqlite
.key/ .key/
logs logs
.jwt_secret .jwt_secret
data/
.wallet_secret
.wallet_password
.admin_enroll
admin.npub

View file

@ -1,25 +1,31 @@
import { DataSource } from "typeorm" import { DataSource } from "typeorm"
import { User } from "./build/src/services/storage/entity/User.js" import { User } from "./build/src/services/storage/entity/User.js"
import { UserReceivingInvoice } from "./build/src/services/storage/entity/UserReceivingInvoice.js" import { UserReceivingInvoice } from "./build/src/services/storage/entity/UserReceivingInvoice.js"
import { AddressReceivingTransaction } from "./build/src/services/storage/entity/AddressReceivingTransaction.js" import { AddressReceivingTransaction } from "./build/src/services/storage/entity/AddressReceivingTransaction.js"
import { Application } from "./build/src/services/storage/entity/Application.js" import { Application } from "./build/src/services/storage/entity/Application.js"
import { ApplicationUser } from "./build/src/services/storage/entity/ApplicationUser.js" import { ApplicationUser } from "./build/src/services/storage/entity/ApplicationUser.js"
import { BalanceEvent } from "./build/src/services/storage/entity/BalanceEvent.js" import { Product } from "./build/src/services/storage/entity/Product.js"
import { ChannelBalanceEvent } from "./build/src/services/storage/entity/ChannelsBalanceEvent.js" import { UserBasicAuth } from "./build/src/services/storage/entity/UserBasicAuth.js"
import { Product } from "./build/src/services/storage/entity/Product.js" import { UserEphemeralKey } from "./build/src/services/storage/entity/UserEphemeralKey.js"
import { RoutingEvent } from "./build/src/services/storage/entity/RoutingEvent.js" import { UserInvoicePayment } from "./build/src/services/storage/entity/UserInvoicePayment.js"
import { UserBasicAuth } from "./build/src/services/storage/entity/UserBasicAuth.js" import { UserReceivingAddress } from "./build/src/services/storage/entity/UserReceivingAddress.js"
import { UserEphemeralKey } from "./build/src/services/storage/entity/UserEphemeralKey.js" import { UserToUserPayment } from "./build/src/services/storage/entity/UserToUserPayment.js"
import { UserInvoicePayment } from "./build/src/services/storage/entity/UserInvoicePayment.js" import { UserTransactionPayment } from "./build/src/services/storage/entity/UserTransactionPayment.js"
import { UserReceivingAddress } from "./build/src/services/storage/entity/UserReceivingAddress.js" import { LspOrder } from "./build/src/services/storage/entity/LspOrder.js"
import { UserToUserPayment } from "./build/src/services/storage/entity/UserToUserPayment.js" import { LndNodeInfo } from "./build/src/services/storage/entity/LndNodeInfo.js"
import { UserTransactionPayment } from "./build/src/services/storage/entity/UserTransactionPayment.js" import { TrackedProvider } from "./build/src/services/storage/entity/TrackedProvider.js"
export default new DataSource({ import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
type: "sqlite", import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
database: "source.sqlite", import { LndNodeInfo1720187506189 } from './build/src/services/storage/migrations/1720187506189-lnd_node_info.js'
// logging: true, import { LiquidityProvider1719335699480 } from './build/src/services/storage/migrations/1719335699480-liquidity_provider.js'
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, export default new DataSource({
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, RoutingEvent, BalanceEvent, ChannelBalanceEvent], type: "sqlite",
// synchronize: true, database: "db.sqlite",
}) // logging: true,
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider],
// synchronize: true,
})
//npx typeorm migration:generate ./src/services/storage/migrations/lnd_node_info -d ./datasource.js

399
deploy.sh
View file

@ -1,399 +0,0 @@
#!/bin/bash
set -e
PRIMARY_COLOR="\e[38;5;208m" # #f59322
SECONDARY_COLOR="\e[38;5;165m" # #c740c7
RESET_COLOR="\e[0m"
LOG_FILE="/var/log/deploy.log"
touch $LOG_FILE
chmod 644 $LOG_FILE
log() {
local message="$(date '+%Y-%m-%d %H:%M:%S') $1"
if [ -t 1 ]; then
echo -e "$message"
fi
echo -e "$(echo $message | sed 's/\\e\[[0-9;]*m//g')" >> $LOG_FILE
}
if [ "$EUID" -ne 0 ]; then
log "${PRIMARY_COLOR}Please run as root or use sudo.${RESET_COLOR}"
exit 1
fi
check_homebrew() {
if ! command -v brew &> /dev/null; then
log "${PRIMARY_COLOR}Homebrew not found. Installing Homebrew...${RESET_COLOR}"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || {
log "${PRIMARY_COLOR}Failed to install Homebrew.${RESET_COLOR}"
exit 1
}
fi
}
install_rsync_mac() {
check_homebrew
log "${PRIMARY_COLOR}Installing${RESET_COLOR} rsync using Homebrew..."
brew install rsync || {
log "${PRIMARY_COLOR}Failed to install rsync.${RESET_COLOR}"
exit 1
}
}
create_launchd_plist() {
create_plist() {
local plist_path=$1
local label=$2
local program_args=$3
local working_dir=$4
if [ -f "$plist_path" ]; then
log "${PRIMARY_COLOR}${label} already exists. Skipping creation.${RESET_COLOR}"
else
cat <<EOF > "$plist_path"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${label}</string>
<key>ProgramArguments</key>
<array>
${program_args}
</array>
<key>WorkingDirectory</key>
<string>${working_dir}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
EOF
fi
}
USER_HOME=$(eval echo ~$(whoami))
NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${USER_HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
LAUNCH_AGENTS_DIR="${USER_HOME}/Library/LaunchAgents"
create_plist "${LAUNCH_AGENTS_DIR}/local.lnd.plist" "local.lnd" "<string>${USER_HOME}/lnd/lnd</string>" ""
create_plist "${LAUNCH_AGENTS_DIR}/local.lightning_pub.plist" "local.lightning_pub" "<string>/bin/bash</string><string>-c</string><string>source ${NVM_DIR}/nvm.sh && npm start</string>" "${USER_HOME}/lightning_pub"
log "${PRIMARY_COLOR}Created launchd plists. Please load them using launchctl.${RESET_COLOR}"
}
start_services_mac() {
create_launchd_plist
launchctl load "${LAUNCH_AGENTS_DIR}/local.lnd.plist"
launchctl load "${LAUNCH_AGENTS_DIR}/local.lightning_pub.plist"
log "${SECONDARY_COLOR}LND${RESET_COLOR} and ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} services started using launchd."
}
handle_macos() {
check_homebrew
install_rsync_mac
install_nodejs
install_lightning_pub
create_launchd_plist # Ensure this function is called
start_services_mac
}
detect_os_arch() {
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
Linux*) OS=Linux;;
Darwin*) OS=Mac;;
CYGWIN*) OS=Cygwin;;
MINGW*) OS=MinGw;;
*) OS="UNKNOWN"
esac
case "$ARCH" in
x86_64) ARCH=amd64;;
arm64) ARCH=arm64;;
*) ARCH="UNKNOWN"
esac
if [ "$OS" = "Linux" ] && command -v systemctl &> /dev/null; then
SYSTEMCTL_AVAILABLE=true
else
SYSTEMCTL_AVAILABLE=false
fi
}
install_lnd() {
LND_VERSION=$(wget -qO- https://api.github.com/repos/lightningnetwork/lnd/releases/latest | grep 'tag_name' | cut -d\" -f4)
LND_URL="https://github.com/lightningnetwork/lnd/releases/download/${LND_VERSION}/lnd-${OS}-${ARCH}-${LND_VERSION}.tar.gz"
# Check if LND is already installed
if [ -d "$HOME/lnd" ]; then
CURRENT_VERSION=$("$HOME/lnd/lnd" --version | grep -oP 'version \K[^\s]+')
if [ "$CURRENT_VERSION" == "${LND_VERSION#v}" ]; then
log "${SECONDARY_COLOR}LND${RESET_COLOR} is already up-to-date (version $CURRENT_VERSION)."
return
else
if [ "$SKIP_PROMPT" != true ]; then
read -p "LND version $CURRENT_VERSION is installed. Do you want to upgrade to version $LND_VERSION? (y/N): " response
case "$response" in
[yY][eE][sS]|[yY])
log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from version $CURRENT_VERSION to $LND_VERSION..."
;;
*)
log "$(date '+%Y-%m-%d %H:%M:%S') Upgrade cancelled."
return
;;
esac
else
log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from version $CURRENT_VERSION to $LND_VERSION..."
fi
fi
fi
log "${PRIMARY_COLOR}Downloading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..."
# Start the download
wget -q $LND_URL -O lnd.tar.gz || {
log "${PRIMARY_COLOR}Failed to download LND.${RESET_COLOR}"
exit 1
}
# Check if LND is already running and stop it if necessary (Linux)
if [ "$OS" = "Linux" ] && [ "$SYSTEMCTL_AVAILABLE" = true ]; then
if systemctl is-active --quiet lnd; then
log "${PRIMARY_COLOR}Stopping${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} service..."
sudo systemctl stop lnd
fi
else
log "${PRIMARY_COLOR}systemctl not found. Please stop ${SECONDARY_COLOR}LND${RESET_COLOR} manually if it is running.${RESET_COLOR}"
fi
tar -xzf lnd.tar.gz -C ~/ > /dev/null || {
log "${PRIMARY_COLOR}Failed to extract LND.${RESET_COLOR}"
exit 1
}
rm lnd.tar.gz
mv lnd-* lnd
# Create .lnd directory if it doesn't exist
mkdir -p ~/.lnd
# Check if lnd.conf already exists and avoid overwriting it
if [ -f ~/.lnd/lnd.conf ]; then
log "${PRIMARY_COLOR}lnd.conf already exists. Skipping creation of new lnd.conf file.${RESET_COLOR}"
else
cat <<EOF > ~/.lnd/lnd.conf
bitcoin.mainnet=true
bitcoin.node=neutrino
neutrino.addpeer=neutrino.shock.network
feeurl=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json
EOF
fi
log "${SECONDARY_COLOR}LND${RESET_COLOR} installation and configuration completed."
}
# Use nvm to install nodejs
install_nodejs() {
log "${PRIMARY_COLOR}Checking${RESET_COLOR} for Node.js..."
MINIMUM_VERSION="18.0.0"
# Load nvm if it already exists
export NVM_DIR="${NVM_DIR}"
[ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh"
if ! command -v nvm &> /dev/null; then
NVM_VERSION=$(wget -qO- https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")')
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh | bash > /dev/null 2>&1
export NVM_DIR="${NVM_DIR}"
[ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh"
fi
if command -v node &> /dev/null; then
NODE_VERSION=$(node -v | sed 's/v//')
if [ "$(printf '%s\n' "$MINIMUM_VERSION" "$NODE_VERSION" | sort -V | head -n1)" = "$MINIMUM_VERSION" ]; then
log "Node.js is already installed and meets the minimum version requirement."
return
else
log "${PRIMARY_COLOR}Updating${RESET_COLOR} Node.js to the LTS version..."
fi
else
log "Node.js is not installed. ${PRIMARY_COLOR}Installing the LTS version...${RESET_COLOR}"
fi
nvm install --lts || {
log "${PRIMARY_COLOR}Failed to install Node.js.${RESET_COLOR}"
exit 1
}
log "Node.js LTS installation completed."
}
install_lightning_pub() {
log "${PRIMARY_COLOR}Installing${RESET_COLOR} ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR}..."
REPO_URL="https://github.com/shocknet/Lightning.Pub/tarball/master"
wget $REPO_URL -O lightning_pub.tar.gz > /dev/null 2>&1 || {
log "${PRIMARY_COLOR}Failed to download Lightning.Pub.${RESET_COLOR}"
exit 1
}
mkdir -p lightning_pub_temp
tar -xvzf lightning_pub.tar.gz -C lightning_pub_temp --strip-components=1 > /dev/null 2>&1 || {
log "${PRIMARY_COLOR}Failed to extract Lightning.Pub.${RESET_COLOR}"
exit 1
}
rm lightning_pub.tar.gz
if ! command -v rsync &> /dev/null; then
log "${PRIMARY_COLOR}rsync not found, installing...${RESET_COLOR}"
if [ "$OS" = "Mac" ]; then
brew install rsync
elif [ "$OS" = "Linux" ]; then
if [ -x "$(command -v apt-get)" ]; then
sudo apt-get update > /dev/null 2>&1
sudo apt-get install -y rsync > /dev/null 2>&1
elif [ -x "$(command -v yum)" ]; then
sudo yum install -y rsync > /dev/null 2>&1
else
log "${PRIMARY_COLOR}Package manager not found. Please install rsync manually.${RESET_COLOR}"
exit 1
fi
else
log "${PRIMARY_COLOR}Package manager not found. Please install rsync manually.${RESET_COLOR}"
exit 1
fi
fi
# Merge if upgrade
rsync -av --exclude='*.sqlite' --exclude='.env' --exclude='logs' --exclude='node_modules' lightning_pub_temp/ lightning_pub/ > /dev/null 2>&1
rm -rf lightning_pub_temp
# Load nvm and npm
export NVM_DIR="${NVM_DIR}"
[ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh"
cd lightning_pub
log "${PRIMARY_COLOR}Installing${RESET_COLOR} npm dependencies..."
npm install > npm_install.log 2>&1 || {
log "${PRIMARY_COLOR}Failed to install npm dependencies.${RESET_COLOR}"
exit 1
}
log "${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} installation completed."
}
create_start_script() {
cat <<EOF > start.sh
#!/bin/bash
${USER_HOME}/lnd/lnd &
LND_PID=\$!
sleep 10
npm start &
NODE_PID=\$!
wait \$LND_PID
wait \$NODE_PID
EOF
chmod +x start.sh
log "systemctl not available. Created start.sh. Please use this script to start the services manually."
}
start_services() {
USER_HOME=$(eval echo ~$(whoami))
if [ "$OS" = "Linux" ]; then
if [ "$SYSTEMCTL_AVAILABLE" = true ]; then
sudo bash -c "cat > /etc/systemd/system/lnd.service <<EOF
[Unit]
Description=LND Service
After=network.target
[Service]
ExecStart=${USER_HOME}/lnd/lnd
User=$(whoami)
Restart=always
[Install]
WantedBy=multi-user.target
EOF"
sudo bash -c "cat > /etc/systemd/system/lightning_pub.service <<EOF
[Unit]
Description=Lightning.Pub Service
After=network.target
[Service]
ExecStart=/bin/bash -c 'source ${NVM_DIR}/nvm.sh && npm start'
WorkingDirectory=${USER_HOME}/lightning_pub
User=$(whoami)
Restart=always
[Install]
WantedBy=multi-user.target
EOF"
sudo systemctl daemon-reload
sudo systemctl enable lnd
sudo systemctl enable lightning_pub
log "${PRIMARY_COLOR}Starting${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} service..."
sudo systemctl start lnd &
lnd_pid=$!
wait $lnd_pid
if systemctl is-active --quiet lnd; then
log "${SECONDARY_COLOR}LND${RESET_COLOR} started successfully using systemd."
else
log "Failed to start ${SECONDARY_COLOR}LND${RESET_COLOR} using systemd."
exit 1
fi
log "Giving ${SECONDARY_COLOR}LND${RESET_COLOR} a few seconds to start before starting ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR}..."
sleep 10
log "${PRIMARY_COLOR}Starting${RESET_COLOR} ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} service..."
sudo systemctl start lightning_pub &
lightning_pub_pid=$!
wait $lightning_pub_pid
if systemctl is-active --quiet lightning_pub; then
log "${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} started successfully using systemd."
else
log "Failed to start ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} using systemd."
exit 1
fi
else
create_start_script
log "systemctl not available. Created start.sh. Please use this script to start the services manually."
fi
elif [ "$OS" = "Mac" ]; then
log "macOS detected. Please configure launchd manually to start ${SECONDARY_COLOR}LND${RESET_COLOR} and ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} at startup."
create_start_script
elif [ "$OS" = "Cygwin" ] || [ "$OS" = "MinGw" ]; then
log "Windows detected. Please configure your startup scripts manually to start ${SECONDARY_COLOR}LND${RESET_COLOR} and ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} at startup."
create_start_script
else
log "Unsupported OS detected. Please configure your startup scripts manually."
create_start_script
fi
}
# Upgrade flag
SKIP_PROMPT=false
for arg in "$@"; do
case $arg in
--yes)
SKIP_PROMPT=true
shift
;;
esac
done
detect_os_arch
if [ "$OS" = "Mac" ]; then
handle_macos
else
install_lnd
install_nodejs
install_lightning_pub
start_services
fi

View file

@ -78,6 +78,7 @@ LSP_MAX_FEE_BPS=100
#ALLOW_BALANCE_MIGRATION=false #ALLOW_BALANCE_MIGRATION=false
#MIGRATE_DB=false #MIGRATE_DB=false
#LOG_LEVEL=DEBUG #LOG_LEVEL=DEBUG
#HIDE_LOGS= <space separated list of log providers to ignore>
#METRICS #METRICS
#RECORD_PERFORMANCE=true #RECORD_PERFORMANCE=true

View file

@ -1,2 +1,5 @@
create lnd classes: `npx protoc -I ./others --ts_out=./lnd others/*` create lnd classes: `npx protoc -I ./others --ts_out=./lnd others/*`
create server classes: `npx protoc -I ./service --pub_out=. service/*` create server classes: `npx protoc -I ./service --pub_out=. service/*`
create wizard classes: `npx protoc -I ./wizard --pub_out=./wizard_service wizard/*`
export PATH=$PATH:~/Lightning.Pub/proto

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,282 +1,400 @@
// This file was autogenerated from a .proto file, DO NOT EDIT! // This file was autogenerated from a .proto file, DO NOT EDIT!
import { NostrRequest } from './nostr_transport.js' import { NostrRequest } from './nostr_transport.js'
import * as Types from './types.js' import * as Types from './types.js'
export type ResultError = { status: 'ERROR', reason: string } export type ResultError = { status: 'ERROR', reason: string }
export type NostrClientParams = { export type NostrClientParams = {
pubDestination: string pubDestination: string
retrieveNostrUserAuth: () => Promise<string | null> retrieveNostrAdminAuth: () => Promise<string | null>
checkResult?: true retrieveNostrMetricsAuth: () => Promise<string | null>
} retrieveNostrUserAuth: () => Promise<string | null>
export default (params: NostrClientParams, send: (to:string, message: NostrRequest) => Promise<any>, subscribe: (to:string, message: NostrRequest, cb:(res:any)=> void) => void) => ({ checkResult?: true
LinkNPubThroughToken: async (request: Types.LinkNPubThroughTokenRequest): Promise<ResultError | ({ status: 'OK' })> => { }
const auth = await params.retrieveNostrUserAuth() export default (params: NostrClientParams, send: (to:string, message: NostrRequest) => Promise<any>, subscribe: (to:string, message: NostrRequest, cb:(res:any)=> void) => void) => ({
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') LndGetInfo: async (request: Types.LndGetInfoRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndGetInfoResponse)> => {
const nostrRequest: NostrRequest = {} const auth = await params.retrieveNostrAdminAuth()
nostrRequest.body = request if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const data = await send(params.pubDestination, {rpcName:'LinkNPubThroughToken',authIdentifier:auth, ...nostrRequest }) const nostrRequest: NostrRequest = {}
if (data.status === 'ERROR' && typeof data.reason === 'string') return data nostrRequest.body = request
if (data.status === 'OK') { const data = await send(params.pubDestination, {rpcName:'LndGetInfo',authIdentifier:auth, ...nostrRequest })
return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
} if (data.status === 'OK') {
return { status: 'ERROR', reason: 'invalid response' } const result = data
}, if(!params.checkResult) return { status: 'OK', ...result }
UserHealth: async (): Promise<ResultError | ({ status: 'OK' })> => { const error = Types.LndGetInfoResponseValidate(result)
const auth = await params.retrieveNostrUserAuth() if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') }
const nostrRequest: NostrRequest = {} return { status: 'ERROR', reason: 'invalid response' }
const data = await send(params.pubDestination, {rpcName:'UserHealth',authIdentifier:auth, ...nostrRequest }) },
if (data.status === 'ERROR' && typeof data.reason === 'string') return data AddApp: async (request: Types.AddAppRequest): Promise<ResultError | ({ status: 'OK' }& Types.AuthApp)> => {
if (data.status === 'OK') { const auth = await params.retrieveNostrAdminAuth()
return data if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
} const nostrRequest: NostrRequest = {}
return { status: 'ERROR', reason: 'invalid response' } nostrRequest.body = request
}, const data = await send(params.pubDestination, {rpcName:'AddApp',authIdentifier:auth, ...nostrRequest })
GetUserInfo: async (): Promise<ResultError | ({ status: 'OK' }& Types.UserInfo)> => { if (data.status === 'ERROR' && typeof data.reason === 'string') return data
const auth = await params.retrieveNostrUserAuth() if (data.status === 'OK') {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const result = data
const nostrRequest: NostrRequest = {} if(!params.checkResult) return { status: 'OK', ...result }
const data = await send(params.pubDestination, {rpcName:'GetUserInfo',authIdentifier:auth, ...nostrRequest }) const error = Types.AuthAppValidate(result)
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
if (data.status === 'OK') { }
const result = data return { status: 'ERROR', reason: 'invalid response' }
if(!params.checkResult) return { status: 'OK', ...result } },
const error = Types.UserInfoValidate(result) AuthApp: async (request: Types.AuthAppRequest): Promise<ResultError | ({ status: 'OK' }& Types.AuthApp)> => {
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } const auth = await params.retrieveNostrAdminAuth()
} if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
return { status: 'ERROR', reason: 'invalid response' } const nostrRequest: NostrRequest = {}
}, nostrRequest.body = request
AddProduct: async (request: Types.AddProductRequest): Promise<ResultError | ({ status: 'OK' }& Types.Product)> => { const data = await send(params.pubDestination, {rpcName:'AuthApp',authIdentifier:auth, ...nostrRequest })
const auth = await params.retrieveNostrUserAuth() if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (data.status === 'OK') {
const nostrRequest: NostrRequest = {} const result = data
nostrRequest.body = request if(!params.checkResult) return { status: 'OK', ...result }
const data = await send(params.pubDestination, {rpcName:'AddProduct',authIdentifier:auth, ...nostrRequest }) const error = Types.AuthAppValidate(result)
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
if (data.status === 'OK') { }
const result = data return { status: 'ERROR', reason: 'invalid response' }
if(!params.checkResult) return { status: 'OK', ...result } },
const error = Types.ProductValidate(result) BanUser: async (request: Types.BanUserRequest): Promise<ResultError | ({ status: 'OK' }& Types.BanUserResponse)> => {
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } const auth = await params.retrieveNostrAdminAuth()
} if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
return { status: 'ERROR', reason: 'invalid response' } const nostrRequest: NostrRequest = {}
}, nostrRequest.body = request
NewProductInvoice: async (query: Types.NewProductInvoice_Query): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => { const data = await send(params.pubDestination, {rpcName:'BanUser',authIdentifier:auth, ...nostrRequest })
const auth = await params.retrieveNostrUserAuth() if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (data.status === 'OK') {
const nostrRequest: NostrRequest = {} const result = data
nostrRequest.query = query if(!params.checkResult) return { status: 'OK', ...result }
const data = await send(params.pubDestination, {rpcName:'NewProductInvoice',authIdentifier:auth, ...nostrRequest }) const error = Types.BanUserResponseValidate(result)
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
if (data.status === 'OK') { }
const result = data return { status: 'ERROR', reason: 'invalid response' }
if(!params.checkResult) return { status: 'OK', ...result } },
const error = Types.NewInvoiceResponseValidate(result) GetUsageMetrics: async (): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => {
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } const auth = await params.retrieveNostrMetricsAuth()
} if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
return { status: 'ERROR', reason: 'invalid response' } const nostrRequest: NostrRequest = {}
}, const data = await send(params.pubDestination, {rpcName:'GetUsageMetrics',authIdentifier:auth, ...nostrRequest })
GetUserOperations: async (request: Types.GetUserOperationsRequest): Promise<ResultError | ({ status: 'OK' }& Types.GetUserOperationsResponse)> => { if (data.status === 'ERROR' && typeof data.reason === 'string') return data
const auth = await params.retrieveNostrUserAuth() if (data.status === 'OK') {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const result = data
const nostrRequest: NostrRequest = {} if(!params.checkResult) return { status: 'OK', ...result }
nostrRequest.body = request const error = Types.UsageMetricsValidate(result)
const data = await send(params.pubDestination, {rpcName:'GetUserOperations',authIdentifier:auth, ...nostrRequest }) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
if (data.status === 'ERROR' && typeof data.reason === 'string') return data }
if (data.status === 'OK') { return { status: 'ERROR', reason: 'invalid response' }
const result = data },
if(!params.checkResult) return { status: 'OK', ...result } GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => {
const error = Types.GetUserOperationsResponseValidate(result) const auth = await params.retrieveNostrMetricsAuth()
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
} const nostrRequest: NostrRequest = {}
return { status: 'ERROR', reason: 'invalid response' } nostrRequest.body = request
}, const data = await send(params.pubDestination, {rpcName:'GetAppsMetrics',authIdentifier:auth, ...nostrRequest })
NewAddress: async (request: Types.NewAddressRequest): Promise<ResultError | ({ status: 'OK' }& Types.NewAddressResponse)> => { if (data.status === 'ERROR' && typeof data.reason === 'string') return data
const auth = await params.retrieveNostrUserAuth() if (data.status === 'OK') {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const result = data
const nostrRequest: NostrRequest = {} if(!params.checkResult) return { status: 'OK', ...result }
nostrRequest.body = request const error = Types.AppsMetricsValidate(result)
const data = await send(params.pubDestination, {rpcName:'NewAddress',authIdentifier:auth, ...nostrRequest }) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
if (data.status === 'ERROR' && typeof data.reason === 'string') return data }
if (data.status === 'OK') { return { status: 'ERROR', reason: 'invalid response' }
const result = data },
if(!params.checkResult) return { status: 'OK', ...result } GetLndMetrics: async (request: Types.LndMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndMetrics)> => {
const error = Types.NewAddressResponseValidate(result) const auth = await params.retrieveNostrMetricsAuth()
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
} const nostrRequest: NostrRequest = {}
return { status: 'ERROR', reason: 'invalid response' } nostrRequest.body = request
}, const data = await send(params.pubDestination, {rpcName:'GetLndMetrics',authIdentifier:auth, ...nostrRequest })
PayAddress: async (request: Types.PayAddressRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayAddressResponse)> => { if (data.status === 'ERROR' && typeof data.reason === 'string') return data
const auth = await params.retrieveNostrUserAuth() if (data.status === 'OK') {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const result = data
const nostrRequest: NostrRequest = {} if(!params.checkResult) return { status: 'OK', ...result }
nostrRequest.body = request const error = Types.LndMetricsValidate(result)
const data = await send(params.pubDestination, {rpcName:'PayAddress',authIdentifier:auth, ...nostrRequest }) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
if (data.status === 'ERROR' && typeof data.reason === 'string') return data }
if (data.status === 'OK') { return { status: 'ERROR', reason: 'invalid response' }
const result = data },
if(!params.checkResult) return { status: 'OK', ...result } LinkNPubThroughToken: async (request: Types.LinkNPubThroughTokenRequest): Promise<ResultError | ({ status: 'OK' })> => {
const error = Types.PayAddressResponseValidate(result) const auth = await params.retrieveNostrUserAuth()
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
} const nostrRequest: NostrRequest = {}
return { status: 'ERROR', reason: 'invalid response' } nostrRequest.body = request
}, const data = await send(params.pubDestination, {rpcName:'LinkNPubThroughToken',authIdentifier:auth, ...nostrRequest })
NewInvoice: async (request: Types.NewInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => { if (data.status === 'ERROR' && typeof data.reason === 'string') return data
const auth = await params.retrieveNostrUserAuth() if (data.status === 'OK') {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') return data
const nostrRequest: NostrRequest = {} }
nostrRequest.body = request return { status: 'ERROR', reason: 'invalid response' }
const data = await send(params.pubDestination, {rpcName:'NewInvoice',authIdentifier:auth, ...nostrRequest }) },
if (data.status === 'ERROR' && typeof data.reason === 'string') return data EnrollAdminToken: async (request: Types.EnrollAdminTokenRequest): Promise<ResultError | ({ status: 'OK' })> => {
if (data.status === 'OK') { const auth = await params.retrieveNostrUserAuth()
const result = data if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
if(!params.checkResult) return { status: 'OK', ...result } const nostrRequest: NostrRequest = {}
const error = Types.NewInvoiceResponseValidate(result) nostrRequest.body = request
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } const data = await send(params.pubDestination, {rpcName:'EnrollAdminToken',authIdentifier:auth, ...nostrRequest })
} if (data.status === 'ERROR' && typeof data.reason === 'string') return data
return { status: 'ERROR', reason: 'invalid response' } if (data.status === 'OK') {
}, return data
DecodeInvoice: async (request: Types.DecodeInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.DecodeInvoiceResponse)> => { }
const auth = await params.retrieveNostrUserAuth() return { status: 'ERROR', reason: 'invalid response' }
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') },
const nostrRequest: NostrRequest = {} UserHealth: async (): Promise<ResultError | ({ status: 'OK' })> => {
nostrRequest.body = request const auth = await params.retrieveNostrUserAuth()
const data = await send(params.pubDestination, {rpcName:'DecodeInvoice',authIdentifier:auth, ...nostrRequest }) if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
if (data.status === 'ERROR' && typeof data.reason === 'string') return data const nostrRequest: NostrRequest = {}
if (data.status === 'OK') { const data = await send(params.pubDestination, {rpcName:'UserHealth',authIdentifier:auth, ...nostrRequest })
const result = data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if(!params.checkResult) return { status: 'OK', ...result } if (data.status === 'OK') {
const error = Types.DecodeInvoiceResponseValidate(result) return data
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } }
} return { status: 'ERROR', reason: 'invalid response' }
return { status: 'ERROR', reason: 'invalid response' } },
}, GetUserInfo: async (): Promise<ResultError | ({ status: 'OK' }& Types.UserInfo)> => {
PayInvoice: async (request: Types.PayInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => { const auth = await params.retrieveNostrUserAuth()
const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const nostrRequest: NostrRequest = {}
const nostrRequest: NostrRequest = {} const data = await send(params.pubDestination, {rpcName:'GetUserInfo',authIdentifier:auth, ...nostrRequest })
nostrRequest.body = request if (data.status === 'ERROR' && typeof data.reason === 'string') return data
const data = await send(params.pubDestination, {rpcName:'PayInvoice',authIdentifier:auth, ...nostrRequest }) if (data.status === 'OK') {
if (data.status === 'ERROR' && typeof data.reason === 'string') return data const result = data
if (data.status === 'OK') { if(!params.checkResult) return { status: 'OK', ...result }
const result = data const error = Types.UserInfoValidate(result)
if(!params.checkResult) return { status: 'OK', ...result } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
const error = Types.PayInvoiceResponseValidate(result) }
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } return { status: 'ERROR', reason: 'invalid response' }
} },
return { status: 'ERROR', reason: 'invalid response' } AddProduct: async (request: Types.AddProductRequest): Promise<ResultError | ({ status: 'OK' }& Types.Product)> => {
}, const auth = await params.retrieveNostrUserAuth()
OpenChannel: async (request: Types.OpenChannelRequest): Promise<ResultError | ({ status: 'OK' }& Types.OpenChannelResponse)> => { if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const auth = await params.retrieveNostrUserAuth() const nostrRequest: NostrRequest = {}
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') nostrRequest.body = request
const nostrRequest: NostrRequest = {} const data = await send(params.pubDestination, {rpcName:'AddProduct',authIdentifier:auth, ...nostrRequest })
nostrRequest.body = request if (data.status === 'ERROR' && typeof data.reason === 'string') return data
const data = await send(params.pubDestination, {rpcName:'OpenChannel',authIdentifier:auth, ...nostrRequest }) if (data.status === 'OK') {
if (data.status === 'ERROR' && typeof data.reason === 'string') return data const result = data
if (data.status === 'OK') { if(!params.checkResult) return { status: 'OK', ...result }
const result = data const error = Types.ProductValidate(result)
if(!params.checkResult) return { status: 'OK', ...result } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
const error = Types.OpenChannelResponseValidate(result) }
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } return { status: 'ERROR', reason: 'invalid response' }
} },
return { status: 'ERROR', reason: 'invalid response' } NewProductInvoice: async (query: Types.NewProductInvoice_Query): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => {
}, const auth = await params.retrieveNostrUserAuth()
GetLnurlWithdrawLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => { if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const auth = await params.retrieveNostrUserAuth() const nostrRequest: NostrRequest = {}
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') nostrRequest.query = query
const nostrRequest: NostrRequest = {} const data = await send(params.pubDestination, {rpcName:'NewProductInvoice',authIdentifier:auth, ...nostrRequest })
const data = await send(params.pubDestination, {rpcName:'GetLnurlWithdrawLink',authIdentifier:auth, ...nostrRequest }) if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'OK') {
if (data.status === 'OK') { const result = data
const result = data if(!params.checkResult) return { status: 'OK', ...result }
if(!params.checkResult) return { status: 'OK', ...result } const error = Types.NewInvoiceResponseValidate(result)
const error = Types.LnurlLinkResponseValidate(result) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } }
} return { status: 'ERROR', reason: 'invalid response' }
return { status: 'ERROR', reason: 'invalid response' } },
}, GetUserOperations: async (request: Types.GetUserOperationsRequest): Promise<ResultError | ({ status: 'OK' }& Types.GetUserOperationsResponse)> => {
GetLnurlPayLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => { const auth = await params.retrieveNostrUserAuth()
const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const nostrRequest: NostrRequest = {}
const nostrRequest: NostrRequest = {} nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'GetLnurlPayLink',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'GetUserOperations',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.LnurlLinkResponseValidate(result) const error = Types.GetUserOperationsResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetLNURLChannelLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => { NewAddress: async (request: Types.NewAddressRequest): Promise<ResultError | ({ status: 'OK' }& Types.NewAddressResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetLNURLChannelLink',authIdentifier:auth, ...nostrRequest }) nostrRequest.body = request
if (data.status === 'ERROR' && typeof data.reason === 'string') return data const data = await send(params.pubDestination, {rpcName:'NewAddress',authIdentifier:auth, ...nostrRequest })
if (data.status === 'OK') { if (data.status === 'ERROR' && typeof data.reason === 'string') return data
const result = data if (data.status === 'OK') {
if(!params.checkResult) return { status: 'OK', ...result } const result = data
const error = Types.LnurlLinkResponseValidate(result) if(!params.checkResult) return { status: 'OK', ...result }
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } const error = Types.NewAddressResponseValidate(result)
} if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
return { status: 'ERROR', reason: 'invalid response' } }
}, return { status: 'ERROR', reason: 'invalid response' }
GetLiveUserOperations: async (cb: (res:ResultError | ({ status: 'OK' }& Types.LiveUserOperation)) => void): Promise<void> => { },
const auth = await params.retrieveNostrUserAuth() PayAddress: async (request: Types.PayAddressRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayAddressResponse)> => {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const auth = await params.retrieveNostrUserAuth()
const nostrRequest: NostrRequest = {} if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
subscribe(params.pubDestination, {rpcName:'GetLiveUserOperations',authIdentifier:auth, ...nostrRequest }, (data) => { const nostrRequest: NostrRequest = {}
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) nostrRequest.body = request
if (data.status === 'OK') { const data = await send(params.pubDestination, {rpcName:'PayAddress',authIdentifier:auth, ...nostrRequest })
const result = data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if(!params.checkResult) return cb({ status: 'OK', ...result }) if (data.status === 'OK') {
const error = Types.LiveUserOperationValidate(result) const result = data
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) if(!params.checkResult) return { status: 'OK', ...result }
} const error = Types.PayAddressResponseValidate(result)
return cb({ status: 'ERROR', reason: 'invalid response' }) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}) }
}, return { status: 'ERROR', reason: 'invalid response' }
GetMigrationUpdate: async (cb: (res:ResultError | ({ status: 'OK' }& Types.MigrationUpdate)) => void): Promise<void> => { },
const auth = await params.retrieveNostrUserAuth() NewInvoice: async (request: Types.NewInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const auth = await params.retrieveNostrUserAuth()
const nostrRequest: NostrRequest = {} if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
subscribe(params.pubDestination, {rpcName:'GetMigrationUpdate',authIdentifier:auth, ...nostrRequest }, (data) => { const nostrRequest: NostrRequest = {}
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) nostrRequest.body = request
if (data.status === 'OK') { const data = await send(params.pubDestination, {rpcName:'NewInvoice',authIdentifier:auth, ...nostrRequest })
const result = data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if(!params.checkResult) return cb({ status: 'OK', ...result }) if (data.status === 'OK') {
const error = Types.MigrationUpdateValidate(result) const result = data
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) if(!params.checkResult) return { status: 'OK', ...result }
} const error = Types.NewInvoiceResponseValidate(result)
return cb({ status: 'ERROR', reason: 'invalid response' }) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}) }
}, return { status: 'ERROR', reason: 'invalid response' }
GetHttpCreds: async (cb: (res:ResultError | ({ status: 'OK' }& Types.HttpCreds)) => void): Promise<void> => { },
const auth = await params.retrieveNostrUserAuth() DecodeInvoice: async (request: Types.DecodeInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.DecodeInvoiceResponse)> => {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const auth = await params.retrieveNostrUserAuth()
const nostrRequest: NostrRequest = {} if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
subscribe(params.pubDestination, {rpcName:'GetHttpCreds',authIdentifier:auth, ...nostrRequest }, (data) => { const nostrRequest: NostrRequest = {}
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) nostrRequest.body = request
if (data.status === 'OK') { const data = await send(params.pubDestination, {rpcName:'DecodeInvoice',authIdentifier:auth, ...nostrRequest })
const result = data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if(!params.checkResult) return cb({ status: 'OK', ...result }) if (data.status === 'OK') {
const error = Types.HttpCredsValidate(result) const result = data
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) if(!params.checkResult) return { status: 'OK', ...result }
} const error = Types.DecodeInvoiceResponseValidate(result)
return cb({ status: 'ERROR', reason: 'invalid response' }) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}) }
}, return { status: 'ERROR', reason: 'invalid response' }
BatchUser: async (requests:Types.UserMethodInputs[]): Promise<ResultError | ({ status: 'OK', responses:(Types.UserMethodOutputs)[] })> => { },
const auth = await params.retrieveNostrUserAuth() PayInvoice: async (request: Types.PayInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => {
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const auth = await params.retrieveNostrUserAuth()
const nostrRequest: NostrRequest = {body:{requests}} if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const data = await send(params.pubDestination, {rpcName:'BatchUser',authIdentifier:auth, ...nostrRequest }) const nostrRequest: NostrRequest = {}
if (data.status === 'ERROR' && typeof data.reason === 'string') return data nostrRequest.body = request
if (data.status === 'OK') { const data = await send(params.pubDestination, {rpcName:'PayInvoice',authIdentifier:auth, ...nostrRequest })
return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
} if (data.status === 'OK') {
return { status: 'ERROR', reason: 'invalid response' } const result = data
} if(!params.checkResult) return { status: 'OK', ...result }
}) const error = Types.PayInvoiceResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
OpenChannel: async (request: Types.OpenChannelRequest): Promise<ResultError | ({ status: 'OK' }& Types.OpenChannelResponse)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'OpenChannel',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.OpenChannelResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetLnurlWithdrawLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetLnurlWithdrawLink',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.LnurlLinkResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetLnurlPayLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetLnurlPayLink',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.LnurlLinkResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetLNURLChannelLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetLNURLChannelLink',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.LnurlLinkResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetLiveUserOperations: async (cb: (res:ResultError | ({ status: 'OK' }& Types.LiveUserOperation)) => void): Promise<void> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
subscribe(params.pubDestination, {rpcName:'GetLiveUserOperations',authIdentifier:auth, ...nostrRequest }, (data) => {
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data)
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return cb({ status: 'OK', ...result })
const error = Types.LiveUserOperationValidate(result)
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message })
}
return cb({ status: 'ERROR', reason: 'invalid response' })
})
},
GetMigrationUpdate: async (cb: (res:ResultError | ({ status: 'OK' }& Types.MigrationUpdate)) => void): Promise<void> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
subscribe(params.pubDestination, {rpcName:'GetMigrationUpdate',authIdentifier:auth, ...nostrRequest }, (data) => {
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data)
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return cb({ status: 'OK', ...result })
const error = Types.MigrationUpdateValidate(result)
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message })
}
return cb({ status: 'ERROR', reason: 'invalid response' })
})
},
GetHttpCreds: async (cb: (res:ResultError | ({ status: 'OK' }& Types.HttpCreds)) => void): Promise<void> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
subscribe(params.pubDestination, {rpcName:'GetHttpCreds',authIdentifier:auth, ...nostrRequest }, (data) => {
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data)
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return cb({ status: 'OK', ...result })
const error = Types.HttpCredsValidate(result)
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message })
}
return cb({ status: 'ERROR', reason: 'invalid response' })
})
},
BatchUser: async (requests:Types.UserMethodInputs[]): Promise<ResultError | ({ status: 'OK', responses:(Types.UserMethodOutputs)[] })> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {body:{requests}}
const data = await send(params.pubDestination, {rpcName:'BatchUser',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
}
})

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,180 @@
// @generated by protobuf-ts 2.8.1
// @generated from protobuf file "walletunlocker.proto" (package "lnrpc", syntax proto3)
// tslint:disable
import type { RpcTransport } from "@protobuf-ts/runtime-rpc";
import type { ServiceInfo } from "@protobuf-ts/runtime-rpc";
import { WalletUnlocker } from "./walletunlocker.js";
import type { ChangePasswordResponse } from "./walletunlocker.js";
import type { ChangePasswordRequest } from "./walletunlocker.js";
import type { UnlockWalletResponse } from "./walletunlocker.js";
import type { UnlockWalletRequest } from "./walletunlocker.js";
import type { InitWalletResponse } from "./walletunlocker.js";
import type { InitWalletRequest } from "./walletunlocker.js";
import { stackIntercept } from "@protobuf-ts/runtime-rpc";
import type { GenSeedResponse } from "./walletunlocker.js";
import type { GenSeedRequest } from "./walletunlocker.js";
import type { UnaryCall } from "@protobuf-ts/runtime-rpc";
import type { RpcOptions } from "@protobuf-ts/runtime-rpc";
//
// Comments in this file will be directly parsed into the API
// Documentation as descriptions of the associated method, message, or field.
// These descriptions should go right above the definition of the object, and
// can be in either block or // comment format.
//
// An RPC method can be matched to an lncli command by placing a line in the
// beginning of the description in exactly the following format:
// lncli: `methodname`
//
// Failure to specify the exact name of the command will cause documentation
// generation to fail.
//
// More information on how exactly the gRPC documentation is generated from
// this proto file can be found here:
// https://github.com/lightninglabs/lightning-api
/**
* WalletUnlocker is a service that is used to set up a wallet password for
* lnd at first startup, and unlock a previously set up wallet.
*
* @generated from protobuf service lnrpc.WalletUnlocker
*/
export interface IWalletUnlockerClient {
/**
*
* GenSeed is the first method that should be used to instantiate a new lnd
* instance. This method allows a caller to generate a new aezeed cipher seed
* given an optional passphrase. If provided, the passphrase will be necessary
* to decrypt the cipherseed to expose the internal wallet seed.
*
* Once the cipherseed is obtained and verified by the user, the InitWallet
* method should be used to commit the newly generated seed, and create the
* wallet.
*
* @generated from protobuf rpc: GenSeed(lnrpc.GenSeedRequest) returns (lnrpc.GenSeedResponse);
*/
genSeed(input: GenSeedRequest, options?: RpcOptions): UnaryCall<GenSeedRequest, GenSeedResponse>;
/**
*
* InitWallet is used when lnd is starting up for the first time to fully
* initialize the daemon and its internal wallet. At the very least a wallet
* password must be provided. This will be used to encrypt sensitive material
* on disk.
*
* In the case of a recovery scenario, the user can also specify their aezeed
* mnemonic and passphrase. If set, then the daemon will use this prior state
* to initialize its internal wallet.
*
* Alternatively, this can be used along with the GenSeed RPC to obtain a
* seed, then present it to the user. Once it has been verified by the user,
* the seed can be fed into this RPC in order to commit the new wallet.
*
* @generated from protobuf rpc: InitWallet(lnrpc.InitWalletRequest) returns (lnrpc.InitWalletResponse);
*/
initWallet(input: InitWalletRequest, options?: RpcOptions): UnaryCall<InitWalletRequest, InitWalletResponse>;
/**
* lncli: `unlock`
* UnlockWallet is used at startup of lnd to provide a password to unlock
* the wallet database.
*
* @generated from protobuf rpc: UnlockWallet(lnrpc.UnlockWalletRequest) returns (lnrpc.UnlockWalletResponse);
*/
unlockWallet(input: UnlockWalletRequest, options?: RpcOptions): UnaryCall<UnlockWalletRequest, UnlockWalletResponse>;
/**
* lncli: `changepassword`
* ChangePassword changes the password of the encrypted wallet. This will
* automatically unlock the wallet database if successful.
*
* @generated from protobuf rpc: ChangePassword(lnrpc.ChangePasswordRequest) returns (lnrpc.ChangePasswordResponse);
*/
changePassword(input: ChangePasswordRequest, options?: RpcOptions): UnaryCall<ChangePasswordRequest, ChangePasswordResponse>;
}
//
// Comments in this file will be directly parsed into the API
// Documentation as descriptions of the associated method, message, or field.
// These descriptions should go right above the definition of the object, and
// can be in either block or // comment format.
//
// An RPC method can be matched to an lncli command by placing a line in the
// beginning of the description in exactly the following format:
// lncli: `methodname`
//
// Failure to specify the exact name of the command will cause documentation
// generation to fail.
//
// More information on how exactly the gRPC documentation is generated from
// this proto file can be found here:
// https://github.com/lightninglabs/lightning-api
/**
* WalletUnlocker is a service that is used to set up a wallet password for
* lnd at first startup, and unlock a previously set up wallet.
*
* @generated from protobuf service lnrpc.WalletUnlocker
*/
export class WalletUnlockerClient implements IWalletUnlockerClient, ServiceInfo {
typeName = WalletUnlocker.typeName;
methods = WalletUnlocker.methods;
options = WalletUnlocker.options;
constructor(private readonly _transport: RpcTransport) {
}
/**
*
* GenSeed is the first method that should be used to instantiate a new lnd
* instance. This method allows a caller to generate a new aezeed cipher seed
* given an optional passphrase. If provided, the passphrase will be necessary
* to decrypt the cipherseed to expose the internal wallet seed.
*
* Once the cipherseed is obtained and verified by the user, the InitWallet
* method should be used to commit the newly generated seed, and create the
* wallet.
*
* @generated from protobuf rpc: GenSeed(lnrpc.GenSeedRequest) returns (lnrpc.GenSeedResponse);
*/
genSeed(input: GenSeedRequest, options?: RpcOptions): UnaryCall<GenSeedRequest, GenSeedResponse> {
const method = this.methods[0], opt = this._transport.mergeOptions(options);
return stackIntercept<GenSeedRequest, GenSeedResponse>("unary", this._transport, method, opt, input);
}
/**
*
* InitWallet is used when lnd is starting up for the first time to fully
* initialize the daemon and its internal wallet. At the very least a wallet
* password must be provided. This will be used to encrypt sensitive material
* on disk.
*
* In the case of a recovery scenario, the user can also specify their aezeed
* mnemonic and passphrase. If set, then the daemon will use this prior state
* to initialize its internal wallet.
*
* Alternatively, this can be used along with the GenSeed RPC to obtain a
* seed, then present it to the user. Once it has been verified by the user,
* the seed can be fed into this RPC in order to commit the new wallet.
*
* @generated from protobuf rpc: InitWallet(lnrpc.InitWalletRequest) returns (lnrpc.InitWalletResponse);
*/
initWallet(input: InitWalletRequest, options?: RpcOptions): UnaryCall<InitWalletRequest, InitWalletResponse> {
const method = this.methods[1], opt = this._transport.mergeOptions(options);
return stackIntercept<InitWalletRequest, InitWalletResponse>("unary", this._transport, method, opt, input);
}
/**
* lncli: `unlock`
* UnlockWallet is used at startup of lnd to provide a password to unlock
* the wallet database.
*
* @generated from protobuf rpc: UnlockWallet(lnrpc.UnlockWalletRequest) returns (lnrpc.UnlockWalletResponse);
*/
unlockWallet(input: UnlockWalletRequest, options?: RpcOptions): UnaryCall<UnlockWalletRequest, UnlockWalletResponse> {
const method = this.methods[2], opt = this._transport.mergeOptions(options);
return stackIntercept<UnlockWalletRequest, UnlockWalletResponse>("unary", this._transport, method, opt, input);
}
/**
* lncli: `changepassword`
* ChangePassword changes the password of the encrypted wallet. This will
* automatically unlock the wallet database if successful.
*
* @generated from protobuf rpc: ChangePassword(lnrpc.ChangePasswordRequest) returns (lnrpc.ChangePasswordResponse);
*/
changePassword(input: ChangePasswordRequest, options?: RpcOptions): UnaryCall<ChangePasswordRequest, ChangePasswordResponse> {
const method = this.methods[3], opt = this._transport.mergeOptions(options);
return stackIntercept<ChangePasswordRequest, ChangePasswordResponse>("unary", this._transport, method, opt, input);
}
}

991
proto/lnd/walletunlocker.ts Normal file
View file

@ -0,0 +1,991 @@
// @generated by protobuf-ts 2.8.1
// @generated from protobuf file "walletunlocker.proto" (package "lnrpc", syntax proto3)
// tslint:disable
import { ServiceType } from "@protobuf-ts/runtime-rpc";
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
import type { IBinaryWriter } from "@protobuf-ts/runtime";
import { WireType } from "@protobuf-ts/runtime";
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
import type { IBinaryReader } from "@protobuf-ts/runtime";
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
import { ChanBackupSnapshot } from "./lightning.js";
/**
* @generated from protobuf message lnrpc.GenSeedRequest
*/
export interface GenSeedRequest {
/**
*
* aezeed_passphrase is an optional user provided passphrase that will be used
* to encrypt the generated aezeed cipher seed. When using REST, this field
* must be encoded as base64.
*
* @generated from protobuf field: bytes aezeed_passphrase = 1;
*/
aezeedPassphrase: Uint8Array;
/**
*
* seed_entropy is an optional 16-bytes generated via CSPRNG. If not
* specified, then a fresh set of randomness will be used to create the seed.
* When using REST, this field must be encoded as base64.
*
* @generated from protobuf field: bytes seed_entropy = 2;
*/
seedEntropy: Uint8Array;
}
/**
* @generated from protobuf message lnrpc.GenSeedResponse
*/
export interface GenSeedResponse {
/**
*
* cipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed
* cipher seed obtained by the user. This field is optional, as if not
* provided, then the daemon will generate a new cipher seed for the user.
* Otherwise, then the daemon will attempt to recover the wallet state linked
* to this cipher seed.
*
* @generated from protobuf field: repeated string cipher_seed_mnemonic = 1;
*/
cipherSeedMnemonic: string[];
/**
*
* enciphered_seed are the raw aezeed cipher seed bytes. This is the raw
* cipher text before run through our mnemonic encoding scheme.
*
* @generated from protobuf field: bytes enciphered_seed = 2;
*/
encipheredSeed: Uint8Array;
}
/**
* @generated from protobuf message lnrpc.InitWalletRequest
*/
export interface InitWalletRequest {
/**
*
* wallet_password is the passphrase that should be used to encrypt the
* wallet. This MUST be at least 8 chars in length. After creation, this
* password is required to unlock the daemon. When using REST, this field
* must be encoded as base64.
*
* @generated from protobuf field: bytes wallet_password = 1;
*/
walletPassword: Uint8Array;
/**
*
* cipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed
* cipher seed obtained by the user. This may have been generated by the
* GenSeed method, or be an existing seed.
*
* @generated from protobuf field: repeated string cipher_seed_mnemonic = 2;
*/
cipherSeedMnemonic: string[];
/**
*
* aezeed_passphrase is an optional user provided passphrase that will be used
* to encrypt the generated aezeed cipher seed. When using REST, this field
* must be encoded as base64.
*
* @generated from protobuf field: bytes aezeed_passphrase = 3;
*/
aezeedPassphrase: Uint8Array;
/**
*
* recovery_window is an optional argument specifying the address lookahead
* when restoring a wallet seed. The recovery window applies to each
* individual branch of the BIP44 derivation paths. Supplying a recovery
* window of zero indicates that no addresses should be recovered, such after
* the first initialization of the wallet.
*
* @generated from protobuf field: int32 recovery_window = 4;
*/
recoveryWindow: number;
/**
*
* channel_backups is an optional argument that allows clients to recover the
* settled funds within a set of channels. This should be populated if the
* user was unable to close out all channels and sweep funds before partial or
* total data loss occurred. If specified, then after on-chain recovery of
* funds, lnd begin to carry out the data loss recovery protocol in order to
* recover the funds in each channel from a remote force closed transaction.
*
* @generated from protobuf field: lnrpc.ChanBackupSnapshot channel_backups = 5;
*/
channelBackups?: ChanBackupSnapshot;
/**
*
* stateless_init is an optional argument instructing the daemon NOT to create
* any *.macaroon files in its filesystem. If this parameter is set, then the
* admin macaroon returned in the response MUST be stored by the caller of the
* RPC as otherwise all access to the daemon will be lost!
*
* @generated from protobuf field: bool stateless_init = 6;
*/
statelessInit: boolean;
/**
*
* extended_master_key is an alternative to specifying cipher_seed_mnemonic and
* aezeed_passphrase. Instead of deriving the master root key from the entropy
* of an aezeed cipher seed, the given extended master root key is used
* directly as the wallet's master key. This allows users to import/use a
* master key from another wallet. When doing so, lnd still uses its default
* SegWit only (BIP49/84) derivation paths and funds from custom/non-default
* derivation paths will not automatically appear in the on-chain wallet. Using
* an 'xprv' instead of an aezeed also has the disadvantage that the wallet's
* birthday is not known as that is an information that's only encoded in the
* aezeed, not the xprv. Therefore a birthday needs to be specified in
* extended_master_key_birthday_timestamp or a "safe" default value will be
* used.
*
* @generated from protobuf field: string extended_master_key = 7;
*/
extendedMasterKey: string;
/**
*
* extended_master_key_birthday_timestamp is the optional unix timestamp in
* seconds to use as the wallet's birthday when using an extended master key
* to restore the wallet. lnd will only start scanning for funds in blocks that
* are after the birthday which can speed up the process significantly. If the
* birthday is not known, this should be left at its default value of 0 in
* which case lnd will start scanning from the first SegWit block (481824 on
* mainnet).
*
* @generated from protobuf field: uint64 extended_master_key_birthday_timestamp = 8;
*/
extendedMasterKeyBirthdayTimestamp: bigint;
/**
*
* watch_only is the third option of initializing a wallet: by importing
* account xpubs only and therefore creating a watch-only wallet that does not
* contain any private keys. That means the wallet won't be able to sign for
* any of the keys and _needs_ to be run with a remote signer that has the
* corresponding private keys and can serve signing RPC requests.
*
* @generated from protobuf field: lnrpc.WatchOnly watch_only = 9;
*/
watchOnly?: WatchOnly;
/**
*
* macaroon_root_key is an optional 32 byte macaroon root key that can be
* provided when initializing the wallet rather than letting lnd generate one
* on its own.
*
* @generated from protobuf field: bytes macaroon_root_key = 10;
*/
macaroonRootKey: Uint8Array;
}
/**
* @generated from protobuf message lnrpc.InitWalletResponse
*/
export interface InitWalletResponse {
/**
*
* The binary serialized admin macaroon that can be used to access the daemon
* after creating the wallet. If the stateless_init parameter was set to true,
* this is the ONLY copy of the macaroon and MUST be stored safely by the
* caller. Otherwise a copy of this macaroon is also persisted on disk by the
* daemon, together with other macaroon files.
*
* @generated from protobuf field: bytes admin_macaroon = 1;
*/
adminMacaroon: Uint8Array;
}
/**
* @generated from protobuf message lnrpc.WatchOnly
*/
export interface WatchOnly {
/**
*
* The unix timestamp in seconds of when the master key was created. lnd will
* only start scanning for funds in blocks that are after the birthday which
* can speed up the process significantly. If the birthday is not known, this
* should be left at its default value of 0 in which case lnd will start
* scanning from the first SegWit block (481824 on mainnet).
*
* @generated from protobuf field: uint64 master_key_birthday_timestamp = 1;
*/
masterKeyBirthdayTimestamp: bigint;
/**
*
* The fingerprint of the root key (also known as the key with derivation path
* m/) from which the account public keys were derived from. This may be
* required by some hardware wallets for proper identification and signing. The
* bytes must be in big-endian order.
*
* @generated from protobuf field: bytes master_key_fingerprint = 2;
*/
masterKeyFingerprint: Uint8Array;
/**
*
* The list of accounts to import. There _must_ be an account for all of lnd's
* main key scopes: BIP49/BIP84 (m/49'/0'/0', m/84'/0'/0', note that the
* coin type is always 0, even for testnet/regtest) and lnd's internal key
* scope (m/1017'/<coin_type>'/<account>'), where account is the key family as
* defined in `keychain/derivation.go` (currently indices 0 to 9).
*
* @generated from protobuf field: repeated lnrpc.WatchOnlyAccount accounts = 3;
*/
accounts: WatchOnlyAccount[];
}
/**
* @generated from protobuf message lnrpc.WatchOnlyAccount
*/
export interface WatchOnlyAccount {
/**
*
* Purpose is the first number in the derivation path, must be either 49, 84
* or 1017.
*
* @generated from protobuf field: uint32 purpose = 1;
*/
purpose: number;
/**
*
* Coin type is the second number in the derivation path, this is _always_ 0
* for purposes 49 and 84. It only needs to be set to 1 for purpose 1017 on
* testnet or regtest.
*
* @generated from protobuf field: uint32 coin_type = 2;
*/
coinType: number;
/**
*
* Account is the third number in the derivation path. For purposes 49 and 84
* at least the default account (index 0) needs to be created but optional
* additional accounts are allowed. For purpose 1017 there needs to be exactly
* one account for each of the key families defined in `keychain/derivation.go`
* (currently indices 0 to 9)
*
* @generated from protobuf field: uint32 account = 3;
*/
account: number;
/**
*
* The extended public key at depth 3 for the given account.
*
* @generated from protobuf field: string xpub = 4;
*/
xpub: string;
}
/**
* @generated from protobuf message lnrpc.UnlockWalletRequest
*/
export interface UnlockWalletRequest {
/**
*
* wallet_password should be the current valid passphrase for the daemon. This
* will be required to decrypt on-disk material that the daemon requires to
* function properly. When using REST, this field must be encoded as base64.
*
* @generated from protobuf field: bytes wallet_password = 1;
*/
walletPassword: Uint8Array;
/**
*
* recovery_window is an optional argument specifying the address lookahead
* when restoring a wallet seed. The recovery window applies to each
* individual branch of the BIP44 derivation paths. Supplying a recovery
* window of zero indicates that no addresses should be recovered, such after
* the first initialization of the wallet.
*
* @generated from protobuf field: int32 recovery_window = 2;
*/
recoveryWindow: number;
/**
*
* channel_backups is an optional argument that allows clients to recover the
* settled funds within a set of channels. This should be populated if the
* user was unable to close out all channels and sweep funds before partial or
* total data loss occurred. If specified, then after on-chain recovery of
* funds, lnd begin to carry out the data loss recovery protocol in order to
* recover the funds in each channel from a remote force closed transaction.
*
* @generated from protobuf field: lnrpc.ChanBackupSnapshot channel_backups = 3;
*/
channelBackups?: ChanBackupSnapshot;
/**
*
* stateless_init is an optional argument instructing the daemon NOT to create
* any *.macaroon files in its file system.
*
* @generated from protobuf field: bool stateless_init = 4;
*/
statelessInit: boolean;
}
/**
* @generated from protobuf message lnrpc.UnlockWalletResponse
*/
export interface UnlockWalletResponse {
}
/**
* @generated from protobuf message lnrpc.ChangePasswordRequest
*/
export interface ChangePasswordRequest {
/**
*
* current_password should be the current valid passphrase used to unlock the
* daemon. When using REST, this field must be encoded as base64.
*
* @generated from protobuf field: bytes current_password = 1;
*/
currentPassword: Uint8Array;
/**
*
* new_password should be the new passphrase that will be needed to unlock the
* daemon. When using REST, this field must be encoded as base64.
*
* @generated from protobuf field: bytes new_password = 2;
*/
newPassword: Uint8Array;
/**
*
* stateless_init is an optional argument instructing the daemon NOT to create
* any *.macaroon files in its filesystem. If this parameter is set, then the
* admin macaroon returned in the response MUST be stored by the caller of the
* RPC as otherwise all access to the daemon will be lost!
*
* @generated from protobuf field: bool stateless_init = 3;
*/
statelessInit: boolean;
/**
*
* new_macaroon_root_key is an optional argument instructing the daemon to
* rotate the macaroon root key when set to true. This will invalidate all
* previously generated macaroons.
*
* @generated from protobuf field: bool new_macaroon_root_key = 4;
*/
newMacaroonRootKey: boolean;
}
/**
* @generated from protobuf message lnrpc.ChangePasswordResponse
*/
export interface ChangePasswordResponse {
/**
*
* The binary serialized admin macaroon that can be used to access the daemon
* after rotating the macaroon root key. If both the stateless_init and
* new_macaroon_root_key parameter were set to true, this is the ONLY copy of
* the macaroon that was created from the new root key and MUST be stored
* safely by the caller. Otherwise a copy of this macaroon is also persisted on
* disk by the daemon, together with other macaroon files.
*
* @generated from protobuf field: bytes admin_macaroon = 1;
*/
adminMacaroon: Uint8Array;
}
// @generated message type with reflection information, may provide speed optimized methods
class GenSeedRequest$Type extends MessageType<GenSeedRequest> {
constructor() {
super("lnrpc.GenSeedRequest", [
{ no: 1, name: "aezeed_passphrase", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 2, name: "seed_entropy", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<GenSeedRequest>): GenSeedRequest {
const message = { aezeedPassphrase: new Uint8Array(0), seedEntropy: new Uint8Array(0) };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<GenSeedRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GenSeedRequest): GenSeedRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bytes aezeed_passphrase */ 1:
message.aezeedPassphrase = reader.bytes();
break;
case /* bytes seed_entropy */ 2:
message.seedEntropy = reader.bytes();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: GenSeedRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bytes aezeed_passphrase = 1; */
if (message.aezeedPassphrase.length)
writer.tag(1, WireType.LengthDelimited).bytes(message.aezeedPassphrase);
/* bytes seed_entropy = 2; */
if (message.seedEntropy.length)
writer.tag(2, WireType.LengthDelimited).bytes(message.seedEntropy);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.GenSeedRequest
*/
export const GenSeedRequest = new GenSeedRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class GenSeedResponse$Type extends MessageType<GenSeedResponse> {
constructor() {
super("lnrpc.GenSeedResponse", [
{ no: 1, name: "cipher_seed_mnemonic", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "enciphered_seed", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<GenSeedResponse>): GenSeedResponse {
const message = { cipherSeedMnemonic: [], encipheredSeed: new Uint8Array(0) };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<GenSeedResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GenSeedResponse): GenSeedResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* repeated string cipher_seed_mnemonic */ 1:
message.cipherSeedMnemonic.push(reader.string());
break;
case /* bytes enciphered_seed */ 2:
message.encipheredSeed = reader.bytes();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: GenSeedResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated string cipher_seed_mnemonic = 1; */
for (let i = 0; i < message.cipherSeedMnemonic.length; i++)
writer.tag(1, WireType.LengthDelimited).string(message.cipherSeedMnemonic[i]);
/* bytes enciphered_seed = 2; */
if (message.encipheredSeed.length)
writer.tag(2, WireType.LengthDelimited).bytes(message.encipheredSeed);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.GenSeedResponse
*/
export const GenSeedResponse = new GenSeedResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InitWalletRequest$Type extends MessageType<InitWalletRequest> {
constructor() {
super("lnrpc.InitWalletRequest", [
{ no: 1, name: "wallet_password", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 2, name: "cipher_seed_mnemonic", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "aezeed_passphrase", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 4, name: "recovery_window", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 5, name: "channel_backups", kind: "message", T: () => ChanBackupSnapshot },
{ no: 6, name: "stateless_init", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 7, name: "extended_master_key", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 8, name: "extended_master_key_birthday_timestamp", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 9, name: "watch_only", kind: "message", T: () => WatchOnly },
{ no: 10, name: "macaroon_root_key", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<InitWalletRequest>): InitWalletRequest {
const message = { walletPassword: new Uint8Array(0), cipherSeedMnemonic: [], aezeedPassphrase: new Uint8Array(0), recoveryWindow: 0, statelessInit: false, extendedMasterKey: "", extendedMasterKeyBirthdayTimestamp: 0n, macaroonRootKey: new Uint8Array(0) };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InitWalletRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InitWalletRequest): InitWalletRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bytes wallet_password */ 1:
message.walletPassword = reader.bytes();
break;
case /* repeated string cipher_seed_mnemonic */ 2:
message.cipherSeedMnemonic.push(reader.string());
break;
case /* bytes aezeed_passphrase */ 3:
message.aezeedPassphrase = reader.bytes();
break;
case /* int32 recovery_window */ 4:
message.recoveryWindow = reader.int32();
break;
case /* lnrpc.ChanBackupSnapshot channel_backups */ 5:
message.channelBackups = ChanBackupSnapshot.internalBinaryRead(reader, reader.uint32(), options, message.channelBackups);
break;
case /* bool stateless_init */ 6:
message.statelessInit = reader.bool();
break;
case /* string extended_master_key */ 7:
message.extendedMasterKey = reader.string();
break;
case /* uint64 extended_master_key_birthday_timestamp */ 8:
message.extendedMasterKeyBirthdayTimestamp = reader.uint64().toBigInt();
break;
case /* lnrpc.WatchOnly watch_only */ 9:
message.watchOnly = WatchOnly.internalBinaryRead(reader, reader.uint32(), options, message.watchOnly);
break;
case /* bytes macaroon_root_key */ 10:
message.macaroonRootKey = reader.bytes();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InitWalletRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bytes wallet_password = 1; */
if (message.walletPassword.length)
writer.tag(1, WireType.LengthDelimited).bytes(message.walletPassword);
/* repeated string cipher_seed_mnemonic = 2; */
for (let i = 0; i < message.cipherSeedMnemonic.length; i++)
writer.tag(2, WireType.LengthDelimited).string(message.cipherSeedMnemonic[i]);
/* bytes aezeed_passphrase = 3; */
if (message.aezeedPassphrase.length)
writer.tag(3, WireType.LengthDelimited).bytes(message.aezeedPassphrase);
/* int32 recovery_window = 4; */
if (message.recoveryWindow !== 0)
writer.tag(4, WireType.Varint).int32(message.recoveryWindow);
/* lnrpc.ChanBackupSnapshot channel_backups = 5; */
if (message.channelBackups)
ChanBackupSnapshot.internalBinaryWrite(message.channelBackups, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
/* bool stateless_init = 6; */
if (message.statelessInit !== false)
writer.tag(6, WireType.Varint).bool(message.statelessInit);
/* string extended_master_key = 7; */
if (message.extendedMasterKey !== "")
writer.tag(7, WireType.LengthDelimited).string(message.extendedMasterKey);
/* uint64 extended_master_key_birthday_timestamp = 8; */
if (message.extendedMasterKeyBirthdayTimestamp !== 0n)
writer.tag(8, WireType.Varint).uint64(message.extendedMasterKeyBirthdayTimestamp);
/* lnrpc.WatchOnly watch_only = 9; */
if (message.watchOnly)
WatchOnly.internalBinaryWrite(message.watchOnly, writer.tag(9, WireType.LengthDelimited).fork(), options).join();
/* bytes macaroon_root_key = 10; */
if (message.macaroonRootKey.length)
writer.tag(10, WireType.LengthDelimited).bytes(message.macaroonRootKey);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.InitWalletRequest
*/
export const InitWalletRequest = new InitWalletRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InitWalletResponse$Type extends MessageType<InitWalletResponse> {
constructor() {
super("lnrpc.InitWalletResponse", [
{ no: 1, name: "admin_macaroon", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<InitWalletResponse>): InitWalletResponse {
const message = { adminMacaroon: new Uint8Array(0) };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InitWalletResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InitWalletResponse): InitWalletResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bytes admin_macaroon */ 1:
message.adminMacaroon = reader.bytes();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InitWalletResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bytes admin_macaroon = 1; */
if (message.adminMacaroon.length)
writer.tag(1, WireType.LengthDelimited).bytes(message.adminMacaroon);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.InitWalletResponse
*/
export const InitWalletResponse = new InitWalletResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class WatchOnly$Type extends MessageType<WatchOnly> {
constructor() {
super("lnrpc.WatchOnly", [
{ no: 1, name: "master_key_birthday_timestamp", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 2, name: "master_key_fingerprint", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 3, name: "accounts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => WatchOnlyAccount }
]);
}
create(value?: PartialMessage<WatchOnly>): WatchOnly {
const message = { masterKeyBirthdayTimestamp: 0n, masterKeyFingerprint: new Uint8Array(0), accounts: [] };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<WatchOnly>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: WatchOnly): WatchOnly {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* uint64 master_key_birthday_timestamp */ 1:
message.masterKeyBirthdayTimestamp = reader.uint64().toBigInt();
break;
case /* bytes master_key_fingerprint */ 2:
message.masterKeyFingerprint = reader.bytes();
break;
case /* repeated lnrpc.WatchOnlyAccount accounts */ 3:
message.accounts.push(WatchOnlyAccount.internalBinaryRead(reader, reader.uint32(), options));
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: WatchOnly, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* uint64 master_key_birthday_timestamp = 1; */
if (message.masterKeyBirthdayTimestamp !== 0n)
writer.tag(1, WireType.Varint).uint64(message.masterKeyBirthdayTimestamp);
/* bytes master_key_fingerprint = 2; */
if (message.masterKeyFingerprint.length)
writer.tag(2, WireType.LengthDelimited).bytes(message.masterKeyFingerprint);
/* repeated lnrpc.WatchOnlyAccount accounts = 3; */
for (let i = 0; i < message.accounts.length; i++)
WatchOnlyAccount.internalBinaryWrite(message.accounts[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.WatchOnly
*/
export const WatchOnly = new WatchOnly$Type();
// @generated message type with reflection information, may provide speed optimized methods
class WatchOnlyAccount$Type extends MessageType<WatchOnlyAccount> {
constructor() {
super("lnrpc.WatchOnlyAccount", [
{ no: 1, name: "purpose", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
{ no: 2, name: "coin_type", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
{ no: 3, name: "account", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
{ no: 4, name: "xpub", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<WatchOnlyAccount>): WatchOnlyAccount {
const message = { purpose: 0, coinType: 0, account: 0, xpub: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<WatchOnlyAccount>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: WatchOnlyAccount): WatchOnlyAccount {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* uint32 purpose */ 1:
message.purpose = reader.uint32();
break;
case /* uint32 coin_type */ 2:
message.coinType = reader.uint32();
break;
case /* uint32 account */ 3:
message.account = reader.uint32();
break;
case /* string xpub */ 4:
message.xpub = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: WatchOnlyAccount, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* uint32 purpose = 1; */
if (message.purpose !== 0)
writer.tag(1, WireType.Varint).uint32(message.purpose);
/* uint32 coin_type = 2; */
if (message.coinType !== 0)
writer.tag(2, WireType.Varint).uint32(message.coinType);
/* uint32 account = 3; */
if (message.account !== 0)
writer.tag(3, WireType.Varint).uint32(message.account);
/* string xpub = 4; */
if (message.xpub !== "")
writer.tag(4, WireType.LengthDelimited).string(message.xpub);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.WatchOnlyAccount
*/
export const WatchOnlyAccount = new WatchOnlyAccount$Type();
// @generated message type with reflection information, may provide speed optimized methods
class UnlockWalletRequest$Type extends MessageType<UnlockWalletRequest> {
constructor() {
super("lnrpc.UnlockWalletRequest", [
{ no: 1, name: "wallet_password", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 2, name: "recovery_window", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 3, name: "channel_backups", kind: "message", T: () => ChanBackupSnapshot },
{ no: 4, name: "stateless_init", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
]);
}
create(value?: PartialMessage<UnlockWalletRequest>): UnlockWalletRequest {
const message = { walletPassword: new Uint8Array(0), recoveryWindow: 0, statelessInit: false };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<UnlockWalletRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UnlockWalletRequest): UnlockWalletRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bytes wallet_password */ 1:
message.walletPassword = reader.bytes();
break;
case /* int32 recovery_window */ 2:
message.recoveryWindow = reader.int32();
break;
case /* lnrpc.ChanBackupSnapshot channel_backups */ 3:
message.channelBackups = ChanBackupSnapshot.internalBinaryRead(reader, reader.uint32(), options, message.channelBackups);
break;
case /* bool stateless_init */ 4:
message.statelessInit = reader.bool();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: UnlockWalletRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bytes wallet_password = 1; */
if (message.walletPassword.length)
writer.tag(1, WireType.LengthDelimited).bytes(message.walletPassword);
/* int32 recovery_window = 2; */
if (message.recoveryWindow !== 0)
writer.tag(2, WireType.Varint).int32(message.recoveryWindow);
/* lnrpc.ChanBackupSnapshot channel_backups = 3; */
if (message.channelBackups)
ChanBackupSnapshot.internalBinaryWrite(message.channelBackups, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
/* bool stateless_init = 4; */
if (message.statelessInit !== false)
writer.tag(4, WireType.Varint).bool(message.statelessInit);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.UnlockWalletRequest
*/
export const UnlockWalletRequest = new UnlockWalletRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class UnlockWalletResponse$Type extends MessageType<UnlockWalletResponse> {
constructor() {
super("lnrpc.UnlockWalletResponse", []);
}
create(value?: PartialMessage<UnlockWalletResponse>): UnlockWalletResponse {
const message = {};
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<UnlockWalletResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UnlockWalletResponse): UnlockWalletResponse {
return target ?? this.create();
}
internalBinaryWrite(message: UnlockWalletResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.UnlockWalletResponse
*/
export const UnlockWalletResponse = new UnlockWalletResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ChangePasswordRequest$Type extends MessageType<ChangePasswordRequest> {
constructor() {
super("lnrpc.ChangePasswordRequest", [
{ no: 1, name: "current_password", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 2, name: "new_password", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 3, name: "stateless_init", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 4, name: "new_macaroon_root_key", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
]);
}
create(value?: PartialMessage<ChangePasswordRequest>): ChangePasswordRequest {
const message = { currentPassword: new Uint8Array(0), newPassword: new Uint8Array(0), statelessInit: false, newMacaroonRootKey: false };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<ChangePasswordRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ChangePasswordRequest): ChangePasswordRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bytes current_password */ 1:
message.currentPassword = reader.bytes();
break;
case /* bytes new_password */ 2:
message.newPassword = reader.bytes();
break;
case /* bool stateless_init */ 3:
message.statelessInit = reader.bool();
break;
case /* bool new_macaroon_root_key */ 4:
message.newMacaroonRootKey = reader.bool();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: ChangePasswordRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bytes current_password = 1; */
if (message.currentPassword.length)
writer.tag(1, WireType.LengthDelimited).bytes(message.currentPassword);
/* bytes new_password = 2; */
if (message.newPassword.length)
writer.tag(2, WireType.LengthDelimited).bytes(message.newPassword);
/* bool stateless_init = 3; */
if (message.statelessInit !== false)
writer.tag(3, WireType.Varint).bool(message.statelessInit);
/* bool new_macaroon_root_key = 4; */
if (message.newMacaroonRootKey !== false)
writer.tag(4, WireType.Varint).bool(message.newMacaroonRootKey);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.ChangePasswordRequest
*/
export const ChangePasswordRequest = new ChangePasswordRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ChangePasswordResponse$Type extends MessageType<ChangePasswordResponse> {
constructor() {
super("lnrpc.ChangePasswordResponse", [
{ no: 1, name: "admin_macaroon", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<ChangePasswordResponse>): ChangePasswordResponse {
const message = { adminMacaroon: new Uint8Array(0) };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<ChangePasswordResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ChangePasswordResponse): ChangePasswordResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bytes admin_macaroon */ 1:
message.adminMacaroon = reader.bytes();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: ChangePasswordResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bytes admin_macaroon = 1; */
if (message.adminMacaroon.length)
writer.tag(1, WireType.LengthDelimited).bytes(message.adminMacaroon);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message lnrpc.ChangePasswordResponse
*/
export const ChangePasswordResponse = new ChangePasswordResponse$Type();
/**
* @generated ServiceType for protobuf service lnrpc.WalletUnlocker
*/
export const WalletUnlocker = new ServiceType("lnrpc.WalletUnlocker", [
{ name: "GenSeed", options: {}, I: GenSeedRequest, O: GenSeedResponse },
{ name: "InitWallet", options: {}, I: InitWalletRequest, O: InitWalletResponse },
{ name: "UnlockWallet", options: {}, I: UnlockWalletRequest, O: UnlockWalletResponse },
{ name: "ChangePassword", options: {}, I: ChangePasswordRequest, O: ChangePasswordResponse }
]);

View file

@ -0,0 +1,338 @@
syntax = "proto3";
package lnrpc;
import "lightning.proto";
option go_package = "github.com/lightningnetwork/lnd/lnrpc";
/*
* Comments in this file will be directly parsed into the API
* Documentation as descriptions of the associated method, message, or field.
* These descriptions should go right above the definition of the object, and
* can be in either block or // comment format.
*
* An RPC method can be matched to an lncli command by placing a line in the
* beginning of the description in exactly the following format:
* lncli: `methodname`
*
* Failure to specify the exact name of the command will cause documentation
* generation to fail.
*
* More information on how exactly the gRPC documentation is generated from
* this proto file can be found here:
* https://github.com/lightninglabs/lightning-api
*/
// WalletUnlocker is a service that is used to set up a wallet password for
// lnd at first startup, and unlock a previously set up wallet.
service WalletUnlocker {
/*
GenSeed is the first method that should be used to instantiate a new lnd
instance. This method allows a caller to generate a new aezeed cipher seed
given an optional passphrase. If provided, the passphrase will be necessary
to decrypt the cipherseed to expose the internal wallet seed.
Once the cipherseed is obtained and verified by the user, the InitWallet
method should be used to commit the newly generated seed, and create the
wallet.
*/
rpc GenSeed (GenSeedRequest) returns (GenSeedResponse);
/*
InitWallet is used when lnd is starting up for the first time to fully
initialize the daemon and its internal wallet. At the very least a wallet
password must be provided. This will be used to encrypt sensitive material
on disk.
In the case of a recovery scenario, the user can also specify their aezeed
mnemonic and passphrase. If set, then the daemon will use this prior state
to initialize its internal wallet.
Alternatively, this can be used along with the GenSeed RPC to obtain a
seed, then present it to the user. Once it has been verified by the user,
the seed can be fed into this RPC in order to commit the new wallet.
*/
rpc InitWallet (InitWalletRequest) returns (InitWalletResponse);
/* lncli: `unlock`
UnlockWallet is used at startup of lnd to provide a password to unlock
the wallet database.
*/
rpc UnlockWallet (UnlockWalletRequest) returns (UnlockWalletResponse);
/* lncli: `changepassword`
ChangePassword changes the password of the encrypted wallet. This will
automatically unlock the wallet database if successful.
*/
rpc ChangePassword (ChangePasswordRequest) returns (ChangePasswordResponse);
}
message GenSeedRequest {
/*
aezeed_passphrase is an optional user provided passphrase that will be used
to encrypt the generated aezeed cipher seed. When using REST, this field
must be encoded as base64.
*/
bytes aezeed_passphrase = 1;
/*
seed_entropy is an optional 16-bytes generated via CSPRNG. If not
specified, then a fresh set of randomness will be used to create the seed.
When using REST, this field must be encoded as base64.
*/
bytes seed_entropy = 2;
}
message GenSeedResponse {
/*
cipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed
cipher seed obtained by the user. This field is optional, as if not
provided, then the daemon will generate a new cipher seed for the user.
Otherwise, then the daemon will attempt to recover the wallet state linked
to this cipher seed.
*/
repeated string cipher_seed_mnemonic = 1;
/*
enciphered_seed are the raw aezeed cipher seed bytes. This is the raw
cipher text before run through our mnemonic encoding scheme.
*/
bytes enciphered_seed = 2;
}
message InitWalletRequest {
/*
wallet_password is the passphrase that should be used to encrypt the
wallet. This MUST be at least 8 chars in length. After creation, this
password is required to unlock the daemon. When using REST, this field
must be encoded as base64.
*/
bytes wallet_password = 1;
/*
cipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed
cipher seed obtained by the user. This may have been generated by the
GenSeed method, or be an existing seed.
*/
repeated string cipher_seed_mnemonic = 2;
/*
aezeed_passphrase is an optional user provided passphrase that will be used
to encrypt the generated aezeed cipher seed. When using REST, this field
must be encoded as base64.
*/
bytes aezeed_passphrase = 3;
/*
recovery_window is an optional argument specifying the address lookahead
when restoring a wallet seed. The recovery window applies to each
individual branch of the BIP44 derivation paths. Supplying a recovery
window of zero indicates that no addresses should be recovered, such after
the first initialization of the wallet.
*/
int32 recovery_window = 4;
/*
channel_backups is an optional argument that allows clients to recover the
settled funds within a set of channels. This should be populated if the
user was unable to close out all channels and sweep funds before partial or
total data loss occurred. If specified, then after on-chain recovery of
funds, lnd begin to carry out the data loss recovery protocol in order to
recover the funds in each channel from a remote force closed transaction.
*/
ChanBackupSnapshot channel_backups = 5;
/*
stateless_init is an optional argument instructing the daemon NOT to create
any *.macaroon files in its filesystem. If this parameter is set, then the
admin macaroon returned in the response MUST be stored by the caller of the
RPC as otherwise all access to the daemon will be lost!
*/
bool stateless_init = 6;
/*
extended_master_key is an alternative to specifying cipher_seed_mnemonic and
aezeed_passphrase. Instead of deriving the master root key from the entropy
of an aezeed cipher seed, the given extended master root key is used
directly as the wallet's master key. This allows users to import/use a
master key from another wallet. When doing so, lnd still uses its default
SegWit only (BIP49/84) derivation paths and funds from custom/non-default
derivation paths will not automatically appear in the on-chain wallet. Using
an 'xprv' instead of an aezeed also has the disadvantage that the wallet's
birthday is not known as that is an information that's only encoded in the
aezeed, not the xprv. Therefore a birthday needs to be specified in
extended_master_key_birthday_timestamp or a "safe" default value will be
used.
*/
string extended_master_key = 7;
/*
extended_master_key_birthday_timestamp is the optional unix timestamp in
seconds to use as the wallet's birthday when using an extended master key
to restore the wallet. lnd will only start scanning for funds in blocks that
are after the birthday which can speed up the process significantly. If the
birthday is not known, this should be left at its default value of 0 in
which case lnd will start scanning from the first SegWit block (481824 on
mainnet).
*/
uint64 extended_master_key_birthday_timestamp = 8;
/*
watch_only is the third option of initializing a wallet: by importing
account xpubs only and therefore creating a watch-only wallet that does not
contain any private keys. That means the wallet won't be able to sign for
any of the keys and _needs_ to be run with a remote signer that has the
corresponding private keys and can serve signing RPC requests.
*/
WatchOnly watch_only = 9;
/*
macaroon_root_key is an optional 32 byte macaroon root key that can be
provided when initializing the wallet rather than letting lnd generate one
on its own.
*/
bytes macaroon_root_key = 10;
}
message InitWalletResponse {
/*
The binary serialized admin macaroon that can be used to access the daemon
after creating the wallet. If the stateless_init parameter was set to true,
this is the ONLY copy of the macaroon and MUST be stored safely by the
caller. Otherwise a copy of this macaroon is also persisted on disk by the
daemon, together with other macaroon files.
*/
bytes admin_macaroon = 1;
}
message WatchOnly {
/*
The unix timestamp in seconds of when the master key was created. lnd will
only start scanning for funds in blocks that are after the birthday which
can speed up the process significantly. If the birthday is not known, this
should be left at its default value of 0 in which case lnd will start
scanning from the first SegWit block (481824 on mainnet).
*/
uint64 master_key_birthday_timestamp = 1;
/*
The fingerprint of the root key (also known as the key with derivation path
m/) from which the account public keys were derived from. This may be
required by some hardware wallets for proper identification and signing. The
bytes must be in big-endian order.
*/
bytes master_key_fingerprint = 2;
/*
The list of accounts to import. There _must_ be an account for all of lnd's
main key scopes: BIP49/BIP84 (m/49'/0'/0', m/84'/0'/0', note that the
coin type is always 0, even for testnet/regtest) and lnd's internal key
scope (m/1017'/<coin_type>'/<account>'), where account is the key family as
defined in `keychain/derivation.go` (currently indices 0 to 9).
*/
repeated WatchOnlyAccount accounts = 3;
}
message WatchOnlyAccount {
/*
Purpose is the first number in the derivation path, must be either 49, 84
or 1017.
*/
uint32 purpose = 1;
/*
Coin type is the second number in the derivation path, this is _always_ 0
for purposes 49 and 84. It only needs to be set to 1 for purpose 1017 on
testnet or regtest.
*/
uint32 coin_type = 2;
/*
Account is the third number in the derivation path. For purposes 49 and 84
at least the default account (index 0) needs to be created but optional
additional accounts are allowed. For purpose 1017 there needs to be exactly
one account for each of the key families defined in `keychain/derivation.go`
(currently indices 0 to 9)
*/
uint32 account = 3;
/*
The extended public key at depth 3 for the given account.
*/
string xpub = 4;
}
message UnlockWalletRequest {
/*
wallet_password should be the current valid passphrase for the daemon. This
will be required to decrypt on-disk material that the daemon requires to
function properly. When using REST, this field must be encoded as base64.
*/
bytes wallet_password = 1;
/*
recovery_window is an optional argument specifying the address lookahead
when restoring a wallet seed. The recovery window applies to each
individual branch of the BIP44 derivation paths. Supplying a recovery
window of zero indicates that no addresses should be recovered, such after
the first initialization of the wallet.
*/
int32 recovery_window = 2;
/*
channel_backups is an optional argument that allows clients to recover the
settled funds within a set of channels. This should be populated if the
user was unable to close out all channels and sweep funds before partial or
total data loss occurred. If specified, then after on-chain recovery of
funds, lnd begin to carry out the data loss recovery protocol in order to
recover the funds in each channel from a remote force closed transaction.
*/
ChanBackupSnapshot channel_backups = 3;
/*
stateless_init is an optional argument instructing the daemon NOT to create
any *.macaroon files in its file system.
*/
bool stateless_init = 4;
}
message UnlockWalletResponse {
}
message ChangePasswordRequest {
/*
current_password should be the current valid passphrase used to unlock the
daemon. When using REST, this field must be encoded as base64.
*/
bytes current_password = 1;
/*
new_password should be the new passphrase that will be needed to unlock the
daemon. When using REST, this field must be encoded as base64.
*/
bytes new_password = 2;
/*
stateless_init is an optional argument instructing the daemon NOT to create
any *.macaroon files in its filesystem. If this parameter is set, then the
admin macaroon returned in the response MUST be stored by the caller of the
RPC as otherwise all access to the daemon will be lost!
*/
bool stateless_init = 3;
/*
new_macaroon_root_key is an optional argument instructing the daemon to
rotate the macaroon root key when set to true. This will invalidate all
previously generated macaroons.
*/
bool new_macaroon_root_key = 4;
}
message ChangePasswordResponse {
/*
The binary serialized admin macaroon that can be used to access the daemon
after rotating the macaroon root key. If both the stateless_init and
new_macaroon_root_key parameter were set to true, this is the ONLY copy of
the macaroon that was created from the new root key and MUST be stored
safely by the caller. Otherwise a copy of this macaroon is also persisted on
disk by the daemon, together with other macaroon files.
*/
bytes admin_macaroon = 1;
}

Binary file not shown.

BIN
proto/protoc-gen-pub_old Executable file

Binary file not shown.

View file

@ -92,42 +92,49 @@ service LightningPub {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/admin/lnd/getinfo"; option (http_route) = "/api/admin/lnd/getinfo";
option (nostr) = true;
}; };
rpc AddApp(structs.AddAppRequest) returns (structs.AuthApp) { rpc AddApp(structs.AddAppRequest) returns (structs.AuthApp) {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/admin/app/add"; option (http_route) = "/api/admin/app/add";
option (nostr) = true;
}; };
rpc AuthApp(structs.AuthAppRequest) returns (structs.AuthApp) { rpc AuthApp(structs.AuthAppRequest) returns (structs.AuthApp) {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/admin/app/auth"; option (http_route) = "/api/admin/app/auth";
option (nostr) = true;
} }
rpc BanUser(structs.BanUserRequest) returns (structs.BanUserResponse) { rpc BanUser(structs.BanUserRequest) returns (structs.BanUserResponse) {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/admin/user/ban"; option (http_route) = "/api/admin/user/ban";
option (nostr) = true;
} }
rpc GetUsageMetrics(structs.Empty) returns (structs.UsageMetrics) { rpc GetUsageMetrics(structs.Empty) returns (structs.UsageMetrics) {
option (auth_type) = "Metrics"; option (auth_type) = "Metrics";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/reports/usage"; option (http_route) = "/api/reports/usage";
option (nostr) = true;
} }
rpc GetAppsMetrics(structs.AppsMetricsRequest) returns (structs.AppsMetrics) { rpc GetAppsMetrics(structs.AppsMetricsRequest) returns (structs.AppsMetrics) {
option (auth_type) = "Metrics"; option (auth_type) = "Metrics";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/reports/apps"; option (http_route) = "/api/reports/apps";
option (nostr) = true;
} }
rpc GetLndMetrics(structs.LndMetricsRequest) returns (structs.LndMetrics) { rpc GetLndMetrics(structs.LndMetricsRequest) returns (structs.LndMetrics) {
option (auth_type) = "Metrics"; option (auth_type) = "Metrics";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/reports/lnd"; option (http_route) = "/api/reports/lnd";
option (nostr) = true;
} }
@ -182,10 +189,16 @@ service LightningPub {
rpc LinkNPubThroughToken(structs.LinkNPubThroughTokenRequest) returns (structs.Empty) { rpc LinkNPubThroughToken(structs.LinkNPubThroughTokenRequest) returns (structs.Empty) {
option (auth_type) = "User"; option (auth_type) = "User";
option(http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/guest/npub/link"; option (http_route) = "/api/guest/npub/link";
option (nostr) = true; option (nostr) = true;
} }
rpc EnrollAdminToken(structs.EnrollAdminTokenRequest) returns (structs.Empty) {
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/guest/npub/enroll/admin";
option (nostr) = true;
}
//</Guest> //</Guest>
// <App> // <App>

View file

@ -349,6 +349,9 @@ message UserInfo{
int64 balance = 2; int64 balance = 2;
int64 max_withdrawable = 3; int64 max_withdrawable = 3;
string user_identifier = 4; string user_identifier = 4;
int64 service_fee_bps = 5;
int64 network_max_fee_bps = 6;
int64 network_max_fee_fixed = 7;
} }
message GetUserOperationsRequest{ message GetUserOperationsRequest{
@ -446,4 +449,8 @@ message LinkNPubThroughTokenRequest {
message HttpCreds { message HttpCreds {
string url = 1; string url = 1;
string token = 2; string token = 2;
}
message EnrollAdminTokenRequest {
string admin_token = 1;
} }

View file

@ -0,0 +1,70 @@
syntax = "proto3";
package wizard_methods;
import "google/protobuf/descriptor.proto";
import "wizard_structs.proto";
option go_package = "github.com/shocknet/lightning.pub";
option (file_options) = {
supported_http_methods:["post", "get"];
supported_auths:{
id: "guest"
name: "Guest"
context:[]
};
};
message MethodQueryOptions {
repeated string items = 1;
}
extend google.protobuf.MethodOptions { // TODO: move this stuff to dep repo?
string auth_type = 50003;
string http_method = 50004;
string http_route = 50005;
MethodQueryOptions query = 50006;
bool nostr = 50007;
bool batch = 50008;
}
message ProtoFileOptions {
message SupportedAuth {
string id = 1;
string name = 2;
bool encrypted = 3;
map<string,string> context = 4;
}
repeated SupportedAuth supported_auths = 1;
repeated string supported_http_methods = 2;
}
extend google.protobuf.FileOptions {
ProtoFileOptions file_options = 50004;
}
service Wizard {
// <Guest>
rpc WizardState(wizard_structs.Empty) returns (wizard_structs.StateResponse){
option (auth_type) = "Guest";
option (http_method) = "get";
option (http_route) = "/wizard/state";
};
rpc WizardConfig(wizard_structs.ConfigRequest) returns (wizard_structs.Empty){
option (auth_type) = "Guest";
option (http_method) = "post";
option (http_route) = "/wizard/config";
};
rpc GetAdminConnectInfo(wizard_structs.Empty) returns (wizard_structs.AdminConnectInfoResponse){
option (auth_type) = "Guest";
option (http_method) = "get";
option (http_route) = "/wizard/admin_connect_info";
};
rpc GetServiceState(wizard_structs.Empty) returns (wizard_structs.ServiceStateResponse){
option (auth_type) = "Guest";
option (http_method) = "get";
option (http_route) = "/wizard/service_state";
};
// </Guest>
}

View file

@ -0,0 +1,40 @@
syntax = "proto3";
package wizard_structs;
option go_package = "github.com/shocknet/lightning.pub";
message Empty {}
message StateResponse {
bool config_sent = 1;
bool admin_linked = 2;
}
message ConfigRequest {
string source_name = 1;
string relay_url = 2;
bool automate_liquidity = 3;
bool push_backups_to_nostr = 4;
}
message AdminConnectInfoResponse {
string nprofile = 1;
oneof connect_info {
string admin_token = 2;
string enrolled_npub = 3;
}
}
enum LndState {
OFFLINE = 0;
SYNCING = 1;
ONLINE = 2;
}
message ServiceStateResponse {
string provider_name = 1;
repeated string relays = 2;
string admin_npub = 3;
bool relay_connected = 4;
LndState lnd_state = 5;
bool watchdog_ok = 6;
string http_url = 7;
string nprofile = 8;
}

View file

@ -0,0 +1,91 @@
# NOSTR API DEFINITION
A nostr request will take the same parameter and give the same response as an http request, but it will use nostr as transport, to do that it will send encrypted events to the server public key, in the event 6 thing are required:
- __rpcName__: string containing the name of the method
- __params__: a map with the all the url params for the method
- __query__: a map with the the url query for the method
- __body__: the body of the method request
- __requestId__: id of the request to be able to get a response
The nostr server will send back a message response, and inside the body there will also be a __requestId__ to identify the request this response is answering
## NOSTR Methods
### These are the nostr methods the client implements to communicate with the API via nostr
# HTTP API DEFINITION
## Supported HTTP Auths
### These are the supported http auth types, to give different type of access to the API users
- __Guest__:
- expected context content
## HTTP Methods
### These are the http methods the client implements to communicate with the API
- WizardState
- auth type: __Guest__
- http method: __get__
- http route: __/wizard/state__
- This methods has an __empty__ __request__ body
- output: [StateResponse](#StateResponse)
- WizardConfig
- auth type: __Guest__
- http method: __post__
- http route: __/wizard/config__
- input: [ConfigRequest](#ConfigRequest)
- This methods has an __empty__ __response__ body
- GetAdminConnectInfo
- auth type: __Guest__
- http method: __get__
- http route: __/wizard/admin_connect_info__
- This methods has an __empty__ __request__ body
- output: [AdminConnectInfoResponse](#AdminConnectInfoResponse)
- GetServiceState
- auth type: __Guest__
- http method: __get__
- http route: __/wizard/service_state__
- This methods has an __empty__ __request__ body
- output: [ServiceStateResponse](#ServiceStateResponse)
# INPUTS AND OUTPUTS
## Messages
### The content of requests and response from the methods
### StateResponse
- __config_sent__: _boolean_
- __admin_linked__: _boolean_
### ConfigRequest
- __source_name__: _string_
- __relay_url__: _string_
- __automate_liquidity__: _boolean_
- __push_backups_to_nostr__: _boolean_
### AdminConnectInfoResponse
- __nprofile__: _string_
- __connect_info__: _AdminConnectInfoResponse_connect_info_
### ServiceStateResponse
- __http_url__: _string_
- __nprofile__: _string_
- __provider_name__: _string_
- __relays__: ARRAY of: _string_
- __admin_npub__: _string_
- __relay_connected__: _boolean_
- __lnd_state__: _[LndState](#LndState)_
- __watchdog_ok__: _boolean_
### Empty
## Enums
### The enumerators used in the messages
### LndState
- __OFFLINE__
- __SYNCING__
- __ONLINE__

View file

@ -0,0 +1,359 @@
([]*main.Method) (len=4 cap=4) {
(*main.Method)(0xc00022a280)({
in: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
},
name: (string) (len=11) "WizardState",
out: (main.MethodMessage) {
name: (string) (len=13) "StateResponse",
hasZeroFields: (bool) false
},
opts: (*main.methodOptions)(0xc00009a9c0)({
authType: (*main.supportedAuth)(0xc0003c9aa0)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: (map[string]string) {
}
}),
method: (string) (len=3) "get",
route: (main.decodedRoute) {
route: (string) (len=13) "/wizard/state",
params: ([]string) <nil>
},
query: ([]string) <nil>,
nostr: (bool) false,
batch: (bool) false
}),
serverStream: (bool) false
}),
(*main.Method)(0xc00022a2d0)({
in: (main.MethodMessage) {
name: (string) (len=13) "ConfigRequest",
hasZeroFields: (bool) false
},
name: (string) (len=12) "WizardConfig",
out: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
},
opts: (*main.methodOptions)(0xc00009ab40)({
authType: (*main.supportedAuth)(0xc0003c9b60)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: (map[string]string) {
}
}),
method: (string) (len=4) "post",
route: (main.decodedRoute) {
route: (string) (len=14) "/wizard/config",
params: ([]string) <nil>
},
query: ([]string) <nil>,
nostr: (bool) false,
batch: (bool) false
}),
serverStream: (bool) false
}),
(*main.Method)(0xc00022a640)({
in: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
},
name: (string) (len=19) "GetAdminConnectInfo",
out: (main.MethodMessage) {
name: (string) (len=24) "AdminConnectInfoResponse",
hasZeroFields: (bool) false
},
opts: (*main.methodOptions)(0xc00009acc0)({
authType: (*main.supportedAuth)(0xc0003c9c20)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: (map[string]string) {
}
}),
method: (string) (len=3) "get",
route: (main.decodedRoute) {
route: (string) (len=26) "/wizard/admin_connect_info",
params: ([]string) <nil>
},
query: ([]string) <nil>,
nostr: (bool) false,
batch: (bool) false
}),
serverStream: (bool) false
}),
(*main.Method)(0xc00022a690)({
in: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
},
name: (string) (len=15) "GetServiceState",
out: (main.MethodMessage) {
name: (string) (len=20) "ServiceStateResponse",
hasZeroFields: (bool) false
},
opts: (*main.methodOptions)(0xc00009ae40)({
authType: (*main.supportedAuth)(0xc0003c9ce0)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: (map[string]string) {
}
}),
method: (string) (len=3) "get",
route: (main.decodedRoute) {
route: (string) (len=21) "/wizard/service_state",
params: ([]string) <nil>
},
query: ([]string) <nil>,
nostr: (bool) false,
batch: (bool) false
}),
serverStream: (bool) false
})
}
([]*main.Enum) (len=1 cap=1) {
(*main.Enum)(0xc0003c9680)({
name: (string) (len=8) "LndState",
values: ([]main.EnumValue) (len=3 cap=4) {
(main.EnumValue) {
number: (int64) 0,
name: (string) (len=7) "OFFLINE"
},
(main.EnumValue) {
number: (int64) 1,
name: (string) (len=7) "SYNCING"
},
(main.EnumValue) {
number: (int64) 2,
name: (string) (len=6) "ONLINE"
}
}
})
}
(map[string]*main.Message) (len=5) {
(string) (len=5) "Empty": (*main.Message)(0xc0003c94a0)({
fullName: (string) (len=5) "Empty",
name: (string) (len=5) "Empty",
fields: (map[string]*main.Field) {
}
}),
(string) (len=13) "StateResponse": (*main.Message)(0xc0003c9500)({
fullName: (string) (len=13) "StateResponse",
name: (string) (len=13) "StateResponse",
fields: (map[string]*main.Field) (len=2) {
(string) (len=11) "config_sent": (*main.Field)(0xc0003ee440)({
name: (string) (len=11) "config_sent",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=12) "admin_linked": (*main.Field)(0xc0003ee480)({
name: (string) (len=12) "admin_linked",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
})
}
}),
(string) (len=13) "ConfigRequest": (*main.Message)(0xc0003c9560)({
fullName: (string) (len=13) "ConfigRequest",
name: (string) (len=13) "ConfigRequest",
fields: (map[string]*main.Field) (len=4) {
(string) (len=9) "relay_url": (*main.Field)(0xc0003ee500)({
name: (string) (len=9) "relay_url",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=18) "automate_liquidity": (*main.Field)(0xc0003ee540)({
name: (string) (len=18) "automate_liquidity",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=21) "push_backups_to_nostr": (*main.Field)(0xc0003ee580)({
name: (string) (len=21) "push_backups_to_nostr",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=11) "source_name": (*main.Field)(0xc0003ee4c0)({
name: (string) (len=11) "source_name",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
})
}
}),
(string) (len=24) "AdminConnectInfoResponse": (*main.Message)(0xc0003c95c0)({
fullName: (string) (len=24) "AdminConnectInfoResponse",
name: (string) (len=24) "AdminConnectInfoResponse",
fields: (map[string]*main.Field) (len=2) {
(string) (len=8) "nprofile": (*main.Field)(0xc0003ee5c0)({
name: (string) (len=8) "nprofile",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=37) "AdminConnectInfoResponse_connect_info": (*main.Field)(0xc0003eea80)({
name: (string) (len=12) "connect_info",
kind: (string) (len=37) "AdminConnectInfoResponse_connect_info",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) (len=12) "connect_info"
})
}
}),
(string) (len=20) "ServiceStateResponse": (*main.Message)(0xc0003c9620)({
fullName: (string) (len=20) "ServiceStateResponse",
name: (string) (len=20) "ServiceStateResponse",
fields: (map[string]*main.Field) (len=8) {
(string) (len=10) "admin_npub": (*main.Field)(0xc0003ee700)({
name: (string) (len=10) "admin_npub",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=15) "relay_connected": (*main.Field)(0xc0003ee740)({
name: (string) (len=15) "relay_connected",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=9) "lnd_state": (*main.Field)(0xc0003ee780)({
name: (string) (len=9) "lnd_state",
kind: (string) (len=8) "LndState",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) true,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=11) "watchdog_ok": (*main.Field)(0xc0003ee7c0)({
name: (string) (len=11) "watchdog_ok",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=8) "http_url": (*main.Field)(0xc0003ee800)({
name: (string) (len=8) "http_url",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=8) "nprofile": (*main.Field)(0xc0003ee840)({
name: (string) (len=8) "nprofile",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=13) "provider_name": (*main.Field)(0xc0003ee680)({
name: (string) (len=13) "provider_name",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=6) "relays": (*main.Field)(0xc0003ee6c0)({
name: (string) (len=6) "relays",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) true,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
})
}
})
}
(map[string][]*main.Field) (len=1) {
(string) (len=37) "AdminConnectInfoResponse_connect_info": ([]*main.Field) (len=2 cap=2) {
(*main.Field)(0xc0003ee600)({
name: (string) (len=11) "admin_token",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) (len=12) "connect_info"
}),
(*main.Field)(0xc0003ee640)({
name: (string) (len=13) "enrolled_npub",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) (len=12) "connect_info"
})
}
}
parsing file: wizard_structs 5
parsing file: wizard_methods 2
-> [{guest Guest map[]}]
([]interface {}) <nil>

View file

@ -0,0 +1,120 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
import express, { Response, json, urlencoded } from 'express'
import cors from 'cors'
import * as Types from './types.js'
export type Logger = { log: (v: any) => void, error: (v: any) => void }
export type ServerOptions = {
allowCors?: true
staticFiles?: string
allowNotImplementedMethods?: true
logger?: Logger
throwErrors?: true
logMethod?: true
logBody?: true
metricsCallback: (metrics: Types.RequestMetric[]) => void
GuestAuthGuard: (authorizationHeader?: string) => Promise<Types.GuestContext>
}
declare module 'express-serve-static-core' { interface Request { startTime?: bigint, bodySize?: number, startTimeMs: number } }
const logErrorAndReturnResponse = (error: Error, response: string, res: Response, logger: Logger, metric: Types.RequestMetric, metricsCallback: (metrics: Types.RequestMetric[]) => void) => {
logger.error(error.message || error); metricsCallback([{ ...metric, error: response }]); res.json({ status: 'ERROR', reason: response })
}
export default (methods: Types.ServerMethods, opts: ServerOptions) => {
const logger = opts.logger || { log: console.log, error: console.error }
const app = express()
if (opts.allowCors) {
app.use(cors())
}
app.use((req, _, next) => { req.startTime = process.hrtime.bigint(); req.startTimeMs = Date.now(); next() })
app.use(json())
app.use(urlencoded({ extended: true }))
if (opts.logMethod) app.use((req, _, next) => { console.log(req.method, req.path); if (opts.logBody) console.log(req.body); next() })
if (!opts.allowNotImplementedMethods && !methods.WizardState) throw new Error('method: WizardState is not implemented')
app.get('/wizard/state', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'WizardState', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.WizardState) throw new Error('method: WizardState is not implemented')
const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.WizardState({rpcName:'WizardState', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.WizardConfig) throw new Error('method: WizardConfig is not implemented')
app.post('/wizard/config', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'WizardConfig', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.WizardConfig) throw new Error('method: WizardConfig is not implemented')
const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.ConfigRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
await methods.WizardConfig({rpcName:'WizardConfig', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetAdminConnectInfo) throw new Error('method: GetAdminConnectInfo is not implemented')
app.get('/wizard/admin_connect_info', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminConnectInfo', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.GetAdminConnectInfo) throw new Error('method: GetAdminConnectInfo is not implemented')
const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.GetAdminConnectInfo({rpcName:'GetAdminConnectInfo', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetServiceState) throw new Error('method: GetServiceState is not implemented')
app.get('/wizard/service_state', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetServiceState', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.GetServiceState) throw new Error('method: GetServiceState is not implemented')
const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.GetServiceState({rpcName:'GetServiceState', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (opts.staticFiles) {
app.use(express.static(opts.staticFiles))
app.get('*', function (_, res) { res.sendFile('index.html', { root: opts.staticFiles })})
}
var server: { close: () => void } | undefined
return {
Close: () => { if (!server) { throw new Error('tried closing server before starting') } else server.close() },
Listen: (port: number) => { server = app.listen(port, () => logger.log('Wizard listening on port ' + port)) }
}
}

View file

@ -0,0 +1,68 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
import axios from 'axios'
import * as Types from './types.js'
export type ResultError = { status: 'ERROR', reason: string }
export type ClientParams = {
baseUrl: string
retrieveGuestAuth: () => Promise<string | null>
encryptCallback: (plain: any) => Promise<any>
decryptCallback: (encrypted: any) => Promise<any>
deviceId: string
checkResult?: true
}
export default (params: ClientParams) => ({
WizardState: async (): Promise<ResultError | ({ status: 'OK' }& Types.StateResponse)> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/wizard/state'
const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.StateResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
WizardConfig: async (request: Types.ConfigRequest): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/wizard/config'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminConnectInfo: async (): Promise<ResultError | ({ status: 'OK' }& Types.AdminConnectInfoResponse)> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/wizard/admin_connect_info'
const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminConnectInfoResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetServiceState: async (): Promise<ResultError | ({ status: 'OK' }& Types.ServiceStateResponse)> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/wizard/service_state'
const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.ServiceStateResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
})

View file

@ -0,0 +1,11 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
import { NostrRequest } from './nostr_transport.js'
import * as Types from './types.js'
export type ResultError = { status: 'ERROR', reason: string }
export type NostrClientParams = {
pubDestination: string
checkResult?: true
}
export default (params: NostrClientParams, send: (to:string, message: NostrRequest) => Promise<any>, subscribe: (to:string, message: NostrRequest, cb:(res:any)=> void) => void) => ({
})

View file

@ -0,0 +1,34 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
import * as Types from './types.js'
export type Logger = { log: (v: any) => void, error: (v: any) => void }
type NostrResponse = (message: object) => void
export type NostrRequest = {
rpcName?: string
params?: Record<string, string>
query?: Record<string, string>
body?: any
authIdentifier?: string
requestId?: string
appId?: string
}
export type NostrOptions = {
logger?: Logger
throwErrors?: true
metricsCallback: (metrics: Types.RequestMetric[]) => void
}
const logErrorAndReturnResponse = (error: Error, response: string, res: NostrResponse, logger: Logger, metric: Types.RequestMetric, metricsCallback: (metrics: Types.RequestMetric[]) => void) => {
logger.error(error.message || error); metricsCallback([{ ...metric, error: response }]); res({ status: 'ERROR', reason: response })
}
export default (methods: Types.ServerMethods, opts: NostrOptions) => {
const logger = opts.logger || { log: console.log, error: console.error }
return async (req: NostrRequest, res: NostrResponse, startString: string, startMs: number) => {
const startTime = BigInt(startString)
const info: Types.RequestInfo = { rpcName: req.rpcName || 'unkown', batch: false, nostr: true, batchSize: 0 }
const stats: Types.RequestStats = { startMs, start: startTime, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
switch (req.rpcName) {
default: logger.error('unknown rpc call name from nostr event:'+req.rpcName)
}
}
}

View file

@ -0,0 +1,214 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
export type ResultError = { status: 'ERROR', reason: string }
export type RequestInfo = { rpcName: string, batch: boolean, nostr: boolean, batchSize: number }
export type RequestStats = { startMs:number, start:bigint, parse: bigint, guard: bigint, validate: bigint, handle: bigint }
export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: string }
export type GuestContext = {
}
export type GuestMethodInputs = WizardState_Input | WizardConfig_Input | GetAdminConnectInfo_Input | GetServiceState_Input
export type GuestMethodOutputs = WizardState_Output | WizardConfig_Output | GetAdminConnectInfo_Output | GetServiceState_Output
export type AuthContext = GuestContext
export type WizardState_Input = {rpcName:'WizardState'}
export type WizardState_Output = ResultError | ({ status: 'OK' } & StateResponse)
export type WizardConfig_Input = {rpcName:'WizardConfig', req: ConfigRequest}
export type WizardConfig_Output = ResultError | { status: 'OK' }
export type GetAdminConnectInfo_Input = {rpcName:'GetAdminConnectInfo'}
export type GetAdminConnectInfo_Output = ResultError | ({ status: 'OK' } & AdminConnectInfoResponse)
export type GetServiceState_Input = {rpcName:'GetServiceState'}
export type GetServiceState_Output = ResultError | ({ status: 'OK' } & ServiceStateResponse)
export type ServerMethods = {
WizardState?: (req: WizardState_Input & {ctx: GuestContext }) => Promise<StateResponse>
WizardConfig?: (req: WizardConfig_Input & {ctx: GuestContext }) => Promise<void>
GetAdminConnectInfo?: (req: GetAdminConnectInfo_Input & {ctx: GuestContext }) => Promise<AdminConnectInfoResponse>
GetServiceState?: (req: GetServiceState_Input & {ctx: GuestContext }) => Promise<ServiceStateResponse>
}
export enum LndState {
OFFLINE = 'OFFLINE',
SYNCING = 'SYNCING',
ONLINE = 'ONLINE',
}
export const enumCheckLndState = (e?: LndState): boolean => {
for (const v in LndState) if (e === v) return true
return false
}
export type OptionsBaseMessage = {
allOptionalsAreSet?: true
}
export type Empty = {
}
export const EmptyOptionalFields: [] = []
export type EmptyOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
}
export const EmptyValidate = (o?: Empty, opts: EmptyOptions = {}, path: string = 'Empty::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
return null
}
export type StateResponse = {
config_sent: boolean
admin_linked: boolean
}
export const StateResponseOptionalFields: [] = []
export type StateResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
config_sent_CustomCheck?: (v: boolean) => boolean
admin_linked_CustomCheck?: (v: boolean) => boolean
}
export const StateResponseValidate = (o?: StateResponse, opts: StateResponseOptions = {}, path: string = 'StateResponse::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.config_sent !== 'boolean') return new Error(`${path}.config_sent: is not a boolean`)
if (opts.config_sent_CustomCheck && !opts.config_sent_CustomCheck(o.config_sent)) return new Error(`${path}.config_sent: custom check failed`)
if (typeof o.admin_linked !== 'boolean') return new Error(`${path}.admin_linked: is not a boolean`)
if (opts.admin_linked_CustomCheck && !opts.admin_linked_CustomCheck(o.admin_linked)) return new Error(`${path}.admin_linked: custom check failed`)
return null
}
export type ConfigRequest = {
source_name: string
relay_url: string
automate_liquidity: boolean
push_backups_to_nostr: boolean
}
export const ConfigRequestOptionalFields: [] = []
export type ConfigRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
push_backups_to_nostr_CustomCheck?: (v: boolean) => boolean
source_name_CustomCheck?: (v: string) => boolean
relay_url_CustomCheck?: (v: string) => boolean
automate_liquidity_CustomCheck?: (v: boolean) => boolean
}
export const ConfigRequestValidate = (o?: ConfigRequest, opts: ConfigRequestOptions = {}, path: string = 'ConfigRequest::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.source_name !== 'string') return new Error(`${path}.source_name: is not a string`)
if (opts.source_name_CustomCheck && !opts.source_name_CustomCheck(o.source_name)) return new Error(`${path}.source_name: custom check failed`)
if (typeof o.relay_url !== 'string') return new Error(`${path}.relay_url: is not a string`)
if (opts.relay_url_CustomCheck && !opts.relay_url_CustomCheck(o.relay_url)) return new Error(`${path}.relay_url: custom check failed`)
if (typeof o.automate_liquidity !== 'boolean') return new Error(`${path}.automate_liquidity: is not a boolean`)
if (opts.automate_liquidity_CustomCheck && !opts.automate_liquidity_CustomCheck(o.automate_liquidity)) return new Error(`${path}.automate_liquidity: custom check failed`)
if (typeof o.push_backups_to_nostr !== 'boolean') return new Error(`${path}.push_backups_to_nostr: is not a boolean`)
if (opts.push_backups_to_nostr_CustomCheck && !opts.push_backups_to_nostr_CustomCheck(o.push_backups_to_nostr)) return new Error(`${path}.push_backups_to_nostr: custom check failed`)
return null
}
export type AdminConnectInfoResponse = {
nprofile: string
connect_info: AdminConnectInfoResponse_connect_info
}
export const AdminConnectInfoResponseOptionalFields: [] = []
export type AdminConnectInfoResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
nprofile_CustomCheck?: (v: string) => boolean
connect_info_CustomCheck?: (v: AdminConnectInfoResponse_connect_info) => boolean
}
export const AdminConnectInfoResponseValidate = (o?: AdminConnectInfoResponse, opts: AdminConnectInfoResponseOptions = {}, path: string = 'AdminConnectInfoResponse::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.nprofile !== 'string') return new Error(`${path}.nprofile: is not a string`)
if (opts.nprofile_CustomCheck && !opts.nprofile_CustomCheck(o.nprofile)) return new Error(`${path}.nprofile: custom check failed`)
const connect_infoErr = AdminConnectInfoResponse_connect_infoValidate(o.connect_info,{}, `${path}.connect_info`)
if (connect_infoErr !== null) return connect_infoErr
return null
}
export type ServiceStateResponse = {
http_url: string
nprofile: string
provider_name: string
relays: string[]
admin_npub: string
relay_connected: boolean
lnd_state: LndState
watchdog_ok: boolean
}
export const ServiceStateResponseOptionalFields: [] = []
export type ServiceStateResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
http_url_CustomCheck?: (v: string) => boolean
nprofile_CustomCheck?: (v: string) => boolean
provider_name_CustomCheck?: (v: string) => boolean
relays_CustomCheck?: (v: string[]) => boolean
admin_npub_CustomCheck?: (v: string) => boolean
relay_connected_CustomCheck?: (v: boolean) => boolean
lnd_state_CustomCheck?: (v: LndState) => boolean
watchdog_ok_CustomCheck?: (v: boolean) => boolean
}
export const ServiceStateResponseValidate = (o?: ServiceStateResponse, opts: ServiceStateResponseOptions = {}, path: string = 'ServiceStateResponse::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (!Array.isArray(o.relays)) return new Error(`${path}.relays: is not an array`)
for (let index = 0; index < o.relays.length; index++) {
if (typeof o.relays[index] !== 'string') return new Error(`${path}.relays[${index}]: is not a string`)
}
if (opts.relays_CustomCheck && !opts.relays_CustomCheck(o.relays)) return new Error(`${path}.relays: custom check failed`)
if (typeof o.admin_npub !== 'string') return new Error(`${path}.admin_npub: is not a string`)
if (opts.admin_npub_CustomCheck && !opts.admin_npub_CustomCheck(o.admin_npub)) return new Error(`${path}.admin_npub: custom check failed`)
if (typeof o.relay_connected !== 'boolean') return new Error(`${path}.relay_connected: is not a boolean`)
if (opts.relay_connected_CustomCheck && !opts.relay_connected_CustomCheck(o.relay_connected)) return new Error(`${path}.relay_connected: custom check failed`)
if (!enumCheckLndState(o.lnd_state)) return new Error(`${path}.lnd_state: is not a valid LndState`)
if (opts.lnd_state_CustomCheck && !opts.lnd_state_CustomCheck(o.lnd_state)) return new Error(`${path}.lnd_state: custom check failed`)
if (typeof o.watchdog_ok !== 'boolean') return new Error(`${path}.watchdog_ok: is not a boolean`)
if (opts.watchdog_ok_CustomCheck && !opts.watchdog_ok_CustomCheck(o.watchdog_ok)) return new Error(`${path}.watchdog_ok: custom check failed`)
if (typeof o.http_url !== 'string') return new Error(`${path}.http_url: is not a string`)
if (opts.http_url_CustomCheck && !opts.http_url_CustomCheck(o.http_url)) return new Error(`${path}.http_url: custom check failed`)
if (typeof o.nprofile !== 'string') return new Error(`${path}.nprofile: is not a string`)
if (opts.nprofile_CustomCheck && !opts.nprofile_CustomCheck(o.nprofile)) return new Error(`${path}.nprofile: custom check failed`)
if (typeof o.provider_name !== 'string') return new Error(`${path}.provider_name: is not a string`)
if (opts.provider_name_CustomCheck && !opts.provider_name_CustomCheck(o.provider_name)) return new Error(`${path}.provider_name: custom check failed`)
return null
}
export enum AdminConnectInfoResponse_connect_info_type {
ADMIN_TOKEN = 'admin_token',
ENROLLED_NPUB = 'enrolled_npub',
}
export type AdminConnectInfoResponse_connect_info =
{type:AdminConnectInfoResponse_connect_info_type.ADMIN_TOKEN, admin_token:string}|
{type:AdminConnectInfoResponse_connect_info_type.ENROLLED_NPUB, enrolled_npub:string}
export const AdminConnectInfoResponse_connect_infoValidate = (o?: AdminConnectInfoResponse_connect_info, opts = {}, path: string = 'AdminConnectInfoResponse_connect_info::root.'): Error | null => {
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
switch (o.type) {
case 'admin_token':
if (typeof o.admin_token !== 'string') return new Error(`${path}.admin_token: is not a string`)
break
case 'enrolled_npub':
if (typeof o.enrolled_npub !== 'string') return new Error(`${path}.enrolled_npub: is not a string`)
break
}
return new Error(path + ': unknown type'+ o.type)
}

11
scripts/check_homebrew.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/bash
check_homebrew() {
if ! command -v brew &> /dev/null; then
log "${PRIMARY_COLOR}Homebrew not found. Installing Homebrew...${RESET_COLOR}"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || {
log "${PRIMARY_COLOR}Failed to install Homebrew.${RESET_COLOR}"
exit 1
}
fi
}

43
scripts/create_launchd_plist.sh Executable file
View file

@ -0,0 +1,43 @@
#!/bin/bash
create_launchd_plist() {
create_plist() {
local plist_path=$1
local label=$2
local program_args=$3
local working_dir=$4
if [ -f "$plist_path" ]; then
log "${PRIMARY_COLOR}${label} already exists. Skipping creation.${RESET_COLOR}"
else
cat <<EOF > "$plist_path"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${label}</string>
<key>ProgramArguments</key>
<array>
${program_args}
</array>
<key>WorkingDirectory</key>
<string>${working_dir}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
EOF
fi
}
USER_HOME=$(eval echo ~$(whoami))
NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${USER_HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
LAUNCH_AGENTS_DIR="${USER_HOME}/Library/LaunchAgents"
create_plist "${LAUNCH_AGENTS_DIR}/local.lnd.plist" "local.lnd" "<string>${USER_HOME}/lnd/lnd</string>" ""
create_plist "${LAUNCH_AGENTS_DIR}/local.lightning_pub.plist" "local.lightning_pub" "<string>/bin/bash</string><string>-c</string><string>source ${NVM_DIR}/nvm.sh && npm start</string>" "${USER_HOME}/lightning_pub"
log "${PRIMARY_COLOR}Created launchd plists. Please load them using launchctl.${RESET_COLOR}"
}

View file

@ -0,0 +1,80 @@
#!/bin/bash
get_log_info() {
if [ "$EUID" -eq 0 ]; then
USER_HOME=$(getent passwd ${SUDO_USER} | cut -d: -f6)
USER_NAME=$SUDO_USER
else
USER_HOME=$HOME
USER_NAME=$(whoami)
fi
LOG_DIR="$USER_HOME/lightning_pub/logs"
MAX_ATTEMPTS=4
ATTEMPT=0
find_latest_log() {
ls -1t ${LOG_DIR}/components/nostrMiddleware_*.log 2>/dev/null | head -n 1
}
TIMEOUT=180
while [ ! -f ${LOG_DIR}/components/unlocker_*.log ] && [ $TIMEOUT -gt 0 ]; do
log "Waiting for build..."
sleep 10
TIMEOUT=$((TIMEOUT - 10))
done
if [ $TIMEOUT -le 0 ]; then
log "Timeout waiting for unlocker log file, make sure the system has adequate resources."
exit 1
fi
TIMEOUT=45
while [ $TIMEOUT -gt 0 ]; do
if grep -q -e "unlocker >> macaroon not found, creating wallet..." -e "unlocker >> the wallet is already unlocked" -e "unlocker >> wallet is locked, unlocking" ${LOG_DIR}/components/unlocker_*.log; then
break
fi
sleep 1
TIMEOUT=$((TIMEOUT - 1))
done
if [ $TIMEOUT -le 0 ]; then
log "Timeout waiting for wallet status message."
exit 1
fi
latest_unlocker_log=$(ls -1t ${LOG_DIR}/components/unlocker_*.log 2>/dev/null | head -n 1)
latest_entry=$(grep -E "unlocker >> macaroon not found, creating wallet|unlocker >> wallet is locked, unlocking|unlocker >> the wallet is already unlocked" "$latest_unlocker_log" | tail -n 1)
if echo "$latest_entry" | grep -q "unlocker >> macaroon not found, creating wallet"; then
log "Creating wallet..."
elif echo "$latest_entry" | grep -q "unlocker >> wallet is locked, unlocking"; then
log "Unlocking wallet..."
else
log "Wallet is already unlocked."
fi
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
LATEST_LOG=$(find_latest_log)
if [ -n "$LATEST_LOG" ]; then
break
fi
log "Awaiting connection details..."
sleep 4
ATTEMPT=$((ATTEMPT + 1))
done
if [ -z "$LATEST_LOG" ]; then
log "Failed to find the log file, check service status"
exit 1
fi
LATEST_LOG=$(find_latest_log)
latest_nprofile_key=$(grep -oP 'nprofile: \K\w+' "$LATEST_LOG" | tail -n 1)
if [ -z "$latest_nprofile_key" ]; then
log "There was a problem fetching the connection details."
exit 1
fi
log "Paste this string into ShockWallet to connect to the node: $latest_nprofile_key"
}

44
scripts/install.sh Executable file
View file

@ -0,0 +1,44 @@
#!/bin/bash
set -e
BASE_URL="https://bolt12.info/deploy/"
modules=(
"utils"
"check_homebrew"
"install_rsync_mac"
"create_launchd_plist"
"start_services_mac"
"install_lnd"
"install_nodejs"
"install_lightning_pub"
"start_services"
"extract_nprofile"
)
for module in "${modules[@]}"; do
wget -q "${BASE_URL}/${module}.sh" -O "/tmp/${module}.sh"
source "/tmp/${module}.sh"
done
# Upgrade flag
SKIP_PROMPT=false
for arg in "$@"; do
case $arg in
--yes)
SKIP_PROMPT=true
shift
;;
esac
done
detect_os_arch
if [ "$OS" = "Mac" ]; then
handle_macos
else
install_lnd
install_nodejs
install_lightning_pub
start_services
get_log_info
fi

View file

@ -0,0 +1,65 @@
#!/bin/bash
install_lightning_pub() {
if [ "$EUID" -eq 0 ]; then
USER_HOME=$(getent passwd ${SUDO_USER} | cut -d: -f6)
USER_NAME=$SUDO_USER
else
USER_HOME=$HOME
USER_NAME=$(whoami)
fi
log "${PRIMARY_COLOR}Installing${RESET_COLOR} ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR}..."
REPO_URL="https://github.com/shocknet/Lightning.Pub/tarball/fix/bootstrap"
sudo -u $USER_NAME wget $REPO_URL -O $USER_HOME/lightning_pub.tar.gz > /dev/null 2>&1 || {
log "${PRIMARY_COLOR}Failed to download Lightning.Pub.${RESET_COLOR}"
exit 1
}
sudo -u $USER_NAME mkdir -p $USER_HOME/lightning_pub_temp
sudo -u $USER_NAME tar -xvzf $USER_HOME/lightning_pub.tar.gz -C $USER_HOME/lightning_pub_temp --strip-components=1 > /dev/null 2>&1 || {
log "${PRIMARY_COLOR}Failed to extract Lightning.Pub.${RESET_COLOR}"
exit 1
}
rm $USER_HOME/lightning_pub.tar.gz
if ! command -v rsync &> /dev/null; then
log "${PRIMARY_COLOR}rsync not found, installing...${RESET_COLOR}"
if [ "$OS" = "Mac" ]; then
brew install rsync
elif [ "$OS" = "Linux" ]; then
if [ -x "$(command -v apt-get)" ]; then
sudo apt-get update > /dev/null 2>&1
sudo apt-get install -y rsync > /dev/null 2>&1
elif [ -x "$(command -v yum)" ]; then
sudo yum install -y rsync > /dev/null 2>&1
else
log "${PRIMARY_COLOR}Package manager not found. Please install rsync manually.${RESET_COLOR}"
exit 1
fi
else
log "${PRIMARY_COLOR}Package manager not found. Please install rsync manually.${RESET_COLOR}"
exit 1
fi
fi
# Merge if upgrade
rsync -av --exclude='*.sqlite' --exclude='.env' --exclude='logs' --exclude='node_modules' lightning_pub_temp/ lightning_pub/ > /dev/null 2>&1
rm -rf lightning_pub_temp
# Load nvm and npm
export NVM_DIR="${NVM_DIR}"
[ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh"
cd lightning_pub
log "${PRIMARY_COLOR}Installing${RESET_COLOR} npm dependencies..."
npm install > npm_install.log 2>&1 || {
log "${PRIMARY_COLOR}Failed to install npm dependencies.${RESET_COLOR}"
exit 1
}
log "${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} installation completed."
}

80
scripts/install_lnd.sh Executable file
View file

@ -0,0 +1,80 @@
#!/bin/bash
install_lnd() {
if [ "$EUID" -eq 0 ]; then
USER_HOME=$(getent passwd ${SUDO_USER} | cut -d: -f6)
USER_NAME=$SUDO_USER
else
USER_HOME=$HOME
USER_NAME=$(whoami)
fi
LND_VERSION=$(wget -qO- https://api.github.com/repos/lightningnetwork/lnd/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")')
LND_URL="https://github.com/lightningnetwork/lnd/releases/download/${LND_VERSION}/lnd-${OS}-${ARCH}-${LND_VERSION}.tar.gz"
# Check if LND is already installed
if [ -d "$USER_HOME/lnd" ]; then
CURRENT_VERSION=$("$USER_HOME/lnd/lnd" --version | grep -oP 'version \K[^\s]+')
if [ "$CURRENT_VERSION" == "${LND_VERSION#v}" ]; then
log "${SECONDARY_COLOR}LND${RESET_COLOR} is already up-to-date (version $CURRENT_VERSION)."
return
else
if [ "$SKIP_PROMPT" != true ]; then
read -p "LND version $CURRENT_VERSION is installed. Do you want to upgrade to version $LND_VERSION? (y/N): " response
case "$response" in
[yY][eE][sS]|[yY])
log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from version $CURRENT_VERSION to $LND_VERSION..."
;;
*)
log "$(date '+%Y-%m-%d %H:%M:%S') Upgrade cancelled."
return
;;
esac
else
log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from version $CURRENT_VERSION to $LND_VERSION..."
fi
fi
fi
log "${PRIMARY_COLOR}Downloading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..."
# Start the download
sudo -u $USER_NAME wget -q $LND_URL -O $USER_HOME/lnd.tar.gz || {
log "${PRIMARY_COLOR}Failed to download LND.${RESET_COLOR}"
exit 1
}
# Check if LND is already running and stop it if necessary (Linux)
if [ "$OS" = "Linux" ] && [ "$SYSTEMCTL_AVAILABLE" = true ]; then
if systemctl is-active --quiet lnd; then
log "${PRIMARY_COLOR}Stopping${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} service..."
sudo systemctl stop lnd
fi
else
log "${PRIMARY_COLOR}systemctl not found. Please stop ${SECONDARY_COLOR}LND${RESET_COLOR} manually if it is running.${RESET_COLOR}"
fi
sudo -u $USER_NAME tar -xzf $USER_HOME/lnd.tar.gz -C $USER_HOME > /dev/null || {
log "${PRIMARY_COLOR}Failed to extract LND.${RESET_COLOR}"
exit 1
}
rm $USER_HOME/lnd.tar.gz
sudo -u $USER_NAME mv $USER_HOME/lnd-* $USER_HOME/lnd
# Create .lnd directory if it doesn't exist
sudo -u $USER_NAME mkdir -p $USER_HOME/.lnd
# Check if lnd.conf already exists and avoid overwriting it
if [ -f $USER_HOME/.lnd/lnd.conf ]; then
log "${PRIMARY_COLOR}lnd.conf already exists. Skipping creation of new lnd.conf file.${RESET_COLOR}"
else
sudo -u $USER_NAME bash -c "cat <<EOF > $USER_HOME/.lnd/lnd.conf
bitcoin.mainnet=true
bitcoin.node=neutrino
neutrino.addpeer=neutrino.shock.network
feeurl=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json
EOF"
fi
log "${SECONDARY_COLOR}LND${RESET_COLOR} installation and configuration completed."
}

45
scripts/install_nodejs.sh Executable file
View file

@ -0,0 +1,45 @@
#!/bin/bash
install_nodejs() {
if [ "$EUID" -eq 0 ] && [ -n "$SUDO_USER" ]; then
USER_HOME=$(getent passwd ${SUDO_USER} | cut -d: -f6)
USER_NAME=${SUDO_USER}
else
USER_HOME=$HOME
USER_NAME=$(whoami)
fi
NVM_DIR="$USER_HOME/.nvm"
log "${PRIMARY_COLOR}Checking${RESET_COLOR} for Node.js..."
MINIMUM_VERSION="18.0.0"
# Load nvm if it already exists
export NVM_DIR="${NVM_DIR}"
[ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh"
if ! command -v nvm &> /dev/null; then
NVM_VERSION=$(wget -qO- https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")')
sudo -u $USER_NAME bash -c "wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh | bash > /dev/null 2>&1"
export NVM_DIR="${NVM_DIR}"
[ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh"
fi
if command -v node &> /dev/null; then
NODE_VERSION=$(node -v | sed 's/v//')
if [ "$(printf '%s\n' "$MINIMUM_VERSION" "$NODE_VERSION" | sort -V | head -n1)" = "$MINIMUM_VERSION" ]; then
log "Node.js is already installed and meets the minimum version requirement."
return
else
log "${PRIMARY_COLOR}Updating${RESET_COLOR} Node.js to the LTS version..."
fi
else
log "Node.js is not installed. ${PRIMARY_COLOR}Installing the LTS version...${RESET_COLOR}"
fi
sudo -u $USER_NAME bash -c "source ${NVM_DIR}/nvm.sh && nvm install --lts" || {
log "${PRIMARY_COLOR}Failed to install Node.js.${RESET_COLOR}"
exit 1
}
log "Node.js LTS installation completed."
}

10
scripts/install_rsync_mac.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/bash
install_rsync_mac() {
check_homebrew
log "${PRIMARY_COLOR}Installing${RESET_COLOR} rsync using Homebrew..."
brew install rsync || {
log "${PRIMARY_COLOR}Failed to install rsync.${RESET_COLOR}"
exit 1
}
}

100
scripts/start_services.sh Executable file
View file

@ -0,0 +1,100 @@
#!/bin/bash
start_services() {
if [ "$EUID" -eq 0 ]; then
USER_HOME=$(getent passwd ${SUDO_USER} | cut -d: -f6)
USER_NAME=$SUDO_USER
else
USER_HOME=$HOME
USER_NAME=$(whoami)
fi
if [ "$OS" = "Linux" ]; then
if [ "$SYSTEMCTL_AVAILABLE" = true ]; then
sudo bash -c "cat > /etc/systemd/system/lnd.service <<EOF
[Unit]
Description=LND Service
After=network.target
[Service]
ExecStart=${USER_HOME}/lnd/lnd
User=${USER_NAME}
Restart=always
[Install]
WantedBy=multi-user.target
EOF"
sudo bash -c "cat > /etc/systemd/system/lightning_pub.service <<EOF
[Unit]
Description=Lightning.Pub Service
After=network.target
[Service]
ExecStart=/bin/bash -c 'source ${NVM_DIR}/nvm.sh && npm start'
WorkingDirectory=${USER_HOME}/lightning_pub
User=${USER_NAME}
Restart=always
[Install]
WantedBy=multi-user.target
EOF"
sudo systemctl daemon-reload
sudo systemctl enable lnd >/dev/null 2>&1
sudo systemctl enable lightning_pub >/dev/null 2>&1
log "${PRIMARY_COLOR}Starting${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} service..."
sudo systemctl start lnd &
lnd_pid=$!
wait $lnd_pid
if systemctl is-active --quiet lnd; then
log "${SECONDARY_COLOR}LND${RESET_COLOR} started successfully using systemd."
else
log "Failed to start ${SECONDARY_COLOR}LND${RESET_COLOR} using systemd."
exit 1
fi
log "Giving ${SECONDARY_COLOR}LND${RESET_COLOR} a few seconds to start before starting ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR}..."
sleep 10
log "${PRIMARY_COLOR}Starting${RESET_COLOR} ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} service..."
sudo systemctl start lightning_pub &
lightning_pub_pid=$!
wait $lightning_pub_pid
if systemctl is-active --quiet lightning_pub; then
log "${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} started successfully using systemd."
else
log "Failed to start ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} using systemd."
exit 1
fi
else
create_start_script
log "systemctl not available. Created start.sh. Please use this script to start the services manually."
fi
elif [ "$OS" = "Mac" ]; then
log "macOS detected. Please configure launchd manually to start ${SECONDARY_COLOR}LND${RESET_COLOR} and ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} at startup."
create_start_script
elif [ "$OS" = "Cygwin" ] || [ "$OS" = "MinGw" ]; then
log "Windows detected. Please configure your startup scripts manually to start ${SECONDARY_COLOR}LND${RESET_COLOR} and ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} at startup."
create_start_script
else
log "Unsupported OS detected. Please configure your startup scripts manually."
create_start_script
fi
}
create_start_script() {
cat <<EOF > start.sh
#!/bin/bash
${USER_HOME}/lnd/lnd &
LND_PID=\$!
sleep 10
npm start &
NODE_PID=\$!
wait \$LND_PID
wait \$NODE_PID
EOF
chmod +x start.sh
log "systemctl not available. Created start.sh. Please use this script to start the services manually."
}

17
scripts/start_services_mac.sh Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
start_services_mac() {
create_launchd_plist
launchctl load "${LAUNCH_AGENTS_DIR}/local.lnd.plist"
launchctl load "${LAUNCH_AGENTS_DIR}/local.lightning_pub.plist"
log "${SECONDARY_COLOR}LND${RESET_COLOR} and ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} services started using launchd."
}
handle_macos() {
check_homebrew
install_rsync_mac
install_nodejs
install_lightning_pub
create_launchd_plist
start_services_mac
}

40
scripts/utils.sh Executable file
View file

@ -0,0 +1,40 @@
#!/bin/bash
PRIMARY_COLOR="\e[38;5;208m"
SECONDARY_COLOR="\e[38;5;165m"
RESET_COLOR="\e[0m"
LOG_FILE="/var/log/pubdeploy.log"
touch $LOG_FILE
chmod 644 $LOG_FILE
log() {
local message="$(date '+%Y-%m-%d %H:%M:%S') $1"
if [ -t 1 ]; then
echo -e "$message"
fi
echo -e "$(echo $message | sed 's/\\e\[[0-9;]*m//g')" >> $LOG_FILE
}
detect_os_arch() {
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
Linux*) OS=Linux;;
Darwin*) OS=Mac;;
CYGWIN*) OS=Cygwin;;
MINGW*) OS=MinGw;;
*) OS="UNKNOWN"
esac
case "$ARCH" in
x86_64) ARCH=amd64;;
arm64) ARCH=arm64;;
*) ARCH="UNKNOWN"
esac
if [ "$OS" = "Linux" ] && command -v systemctl &> /dev/null; then
SYSTEMCTL_AVAILABLE=true
else
SYSTEMCTL_AVAILABLE=false
fi
}

View file

@ -1,5 +1,5 @@
import express from 'express'; import express from 'express';
import path from 'path';
import { ServerOptions } from "../proto/autogenerated/ts/express_server"; import { ServerOptions } from "../proto/autogenerated/ts/express_server";
import { AdminContext, MetricsContext } from "../proto/autogenerated/ts/types"; import { AdminContext, MetricsContext } from "../proto/autogenerated/ts/types";
import Main from './services/main' import Main from './services/main'
@ -8,7 +8,6 @@ const serverOptions = (mainHandler: Main): ServerOptions => {
const log = getLogger({}) const log = getLogger({})
return { return {
logger: { log, error: err => log(ERROR, err) }, logger: { log, error: err => log(ERROR, err) },
staticFiles: path.resolve('static'),
AdminAuthGuard: adminAuth, AdminAuthGuard: adminAuth,
MetricsAuthGuard: metricsAuth, MetricsAuthGuard: metricsAuth,
AppAuthGuard: async (authHeader) => { return { app_id: mainHandler.applicationManager.DecodeAppToken(stripBearer(authHeader)) } }, AppAuthGuard: async (authHeader) => { return { app_id: mainHandler.applicationManager.DecodeAppToken(stripBearer(authHeader)) } },

View file

@ -1,31 +1,37 @@
import 'dotenv/config' import 'dotenv/config'
import NewServer from '../proto/autogenerated/ts/express_server.js' import NewServer from '../proto/autogenerated/ts/express_server.js'
import GetServerMethods from './services/serverMethods/index.js' import GetServerMethods from './services/serverMethods/index.js'
import serverOptions from './auth.js'; import serverOptions from './auth.js';
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js' import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js'
import nostrMiddleware from './nostrMiddleware.js' import nostrMiddleware from './nostrMiddleware.js'
import { getLogger } from './services/helpers/logger.js'; import { getLogger } from './services/helpers/logger.js';
import { initMainHandler } from './services/main/init.js'; import { initMainHandler } from './services/main/init.js';
import { LoadMainSettingsFromEnv } from './services/main/settings.js'; import { LoadMainSettingsFromEnv } from './services/main/settings.js';
import { encodeNprofile } from './custom-nip19.js';
const start = async () => {
const log = getLogger({}) const start = async () => {
const mainSettings = LoadMainSettingsFromEnv() const log = getLogger({})
const keepOn = await initMainHandler(log, mainSettings) const mainSettings = LoadMainSettingsFromEnv()
if (!keepOn) { const keepOn = await initMainHandler(log, mainSettings)
log("manual process ended") if (!keepOn) {
return log("manual process ended")
} return
const { apps, mainHandler, liquidityProviderInfo } = keepOn }
const serverMethods = GetServerMethods(mainHandler) const { apps, mainHandler, liquidityProviderInfo, wizard } = keepOn
const nostrSettings = LoadNosrtSettingsFromEnv() const serverMethods = GetServerMethods(mainHandler)
const { Send } = nostrMiddleware(serverMethods, mainHandler, const nostrSettings = LoadNosrtSettingsFromEnv()
{ ...nostrSettings, apps, clients: [liquidityProviderInfo] }, log("initializing nostr middleware")
(e, p) => mainHandler.liquidProvider.onEvent(e, p) const { Send } = nostrMiddleware(serverMethods, mainHandler,
) { ...nostrSettings, apps, clients: [liquidityProviderInfo] },
mainHandler.attachNostrSend(Send) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
mainHandler.StartBeacons() )
const Server = NewServer(serverMethods, serverOptions(mainHandler)) log("starting server")
Server.Listen(mainSettings.servicePort) mainHandler.attachNostrSend(Send)
} mainHandler.StartBeacons()
start() if (wizard) {
wizard.AddConnectInfo(encodeNprofile({ pubkey: liquidityProviderInfo.publicKey, relays: nostrSettings.relays }), nostrSettings.relays)
}
const Server = NewServer(serverMethods, serverOptions(mainHandler))
Server.Listen(mainSettings.servicePort)
}
start()

View file

@ -13,6 +13,20 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
const nostrUser = await mainHandler.storage.applicationStorage.GetOrCreateNostrAppUser(app, pub || "") const nostrUser = await mainHandler.storage.applicationStorage.GetOrCreateNostrAppUser(app, pub || "")
return { user_id: nostrUser.user.user_id, app_user_id: nostrUser.identifier, app_id: appId || "" } return { user_id: nostrUser.user.user_id, app_user_id: nostrUser.identifier, app_id: appId || "" }
}, },
NostrAdminAuthGuard: async (appId, pub) => {
const adminNpub = mainHandler.adminManager.GetAdminNpub()
if (!adminNpub) { throw new Error("admin access not configured") }
if (pub !== adminNpub) { throw new Error("admin access denied") }
log("admin access from", pub)
return { admin_id: pub }
},
NostrMetricsAuthGuard: async (appId, pub) => {
const adminNpub = mainHandler.adminManager.GetAdminNpub()
if (!adminNpub) { throw new Error("admin access not configured") }
if (pub !== adminNpub) { throw new Error("admin access denied") }
log("operator access from", pub)
return { operator_id: pub }
},
metricsCallback: metrics => mainHandler.settings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null, metricsCallback: metrics => mainHandler.settings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null,
logger: { log: console.log, error: err => log(ERROR, err) } logger: { log: console.log, error: err => log(ERROR, err) }
}) })
@ -20,7 +34,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
let j: NostrRequest let j: NostrRequest
try { try {
j = JSON.parse(event.content) j = JSON.parse(event.content)
log("nostr event", j.rpcName || 'no rpc name') //log("nostr event", j.rpcName || 'no rpc name')
} catch { } catch {
log(ERROR, "invalid json event received", event.content) log(ERROR, "invalid json event received", event.content)
return return

View file

@ -1,105 +1,114 @@
import fs from 'fs' import fs from 'fs'
export const DEBUG = Symbol("DEBUG") export const DEBUG = Symbol("DEBUG")
export const ERROR = Symbol("ERROR") export const ERROR = Symbol("ERROR")
export const WARN = Symbol("WARN") export const WARN = Symbol("WARN")
type LoggerParams = { appName?: string, userId?: string, component?: string } type LoggerParams = { appName?: string, userId?: string, component?: string }
export type PubLogger = (...message: (string | number | object | symbol)[]) => void export type PubLogger = (...message: (string | number | object | symbol)[]) => void
type Writer = (message: string) => void type Writer = (message: string) => void
const logsDir = process.env.LOGS_DIR || "logs" const logsDir = process.env.LOGS_DIR || "logs"
const logLevel = process.env.LOG_LEVEL || "DEBUG" const logLevel = process.env.LOG_LEVEL || "DEBUG"
try { try {
fs.mkdirSync(logsDir) fs.mkdirSync(logsDir)
} catch { } } catch { }
if (logLevel !== "DEBUG" && logLevel !== "WARN" && logLevel !== "ERROR") { if (logLevel !== "DEBUG" && logLevel !== "WARN" && logLevel !== "ERROR") {
throw new Error("Invalid log level " + logLevel + " must be one of (DEBUG, WARN, ERROR)") throw new Error("Invalid log level " + logLevel + " must be one of (DEBUG, WARN, ERROR)")
} }
const z = (n: number) => n < 10 ? `0${n}` : `${n}` const z = (n: number) => n < 10 ? `0${n}` : `${n}`
const openWriter = (fileName: string): Writer => { const openWriter = (fileName: string): Writer => {
const now = new Date() const now = new Date()
const date = `${now.getFullYear()}-${z(now.getMonth() + 1)}-${z(now.getDate())}` const date = `${now.getFullYear()}-${z(now.getMonth() + 1)}-${z(now.getDate())}`
const logStream = fs.createWriteStream(`${logsDir}/${fileName}_${date}.log`, { flags: 'a' }); const logStream = fs.createWriteStream(`${logsDir}/${fileName}_${date}.log`, { flags: 'a' });
return (message) => { return (message) => {
logStream.write(message + "\n") logStream.write(message + "\n")
} }
} }
const rootWriter = openWriter("ROOT.log") const rootWriter = openWriter("ROOT.log")
if (!fs.existsSync(`${logsDir}/apps`)) { if (!fs.existsSync(`${logsDir}/apps`)) {
fs.mkdirSync(`${logsDir}/apps`, { recursive: true }); fs.mkdirSync(`${logsDir}/apps`, { recursive: true });
} }
if (!fs.existsSync(`${logsDir}/users`)) { if (!fs.existsSync(`${logsDir}/users`)) {
fs.mkdirSync(`${logsDir}/users`, { recursive: true }); fs.mkdirSync(`${logsDir}/users`, { recursive: true });
} }
if (!fs.existsSync(`${logsDir}/components`)) { if (!fs.existsSync(`${logsDir}/components`)) {
fs.mkdirSync(`${logsDir}/components`, { recursive: true }); fs.mkdirSync(`${logsDir}/components`, { recursive: true });
} }
export const getLogger = (params: LoggerParams): PubLogger => { export const getLogger = (params: LoggerParams): PubLogger => {
const writers: Writer[] = [] const writers: Writer[] = []
if (params.appName) { if (params.appName) {
writers.push(openWriter(`apps/${params.appName}`)) writers.push(openWriter(`apps/${params.appName}`))
} }
if (params.userId) { if (params.userId) {
writers.push(openWriter(`users/${params.userId}`)) writers.push(openWriter(`users/${params.userId}`))
} }
if (params.component) { if (params.component) {
writers.push(openWriter(`components/${params.component}`)) writers.push(openWriter(`components/${params.component}`))
} }
if (writers.length === 0) { if (writers.length === 0) {
writers.push(rootWriter) writers.push(rootWriter)
} }
return (...message) => { return (...message) => {
switch (message[0]) { switch (message[0]) {
case DEBUG: case DEBUG:
if (logLevel !== "DEBUG") { if (logLevel !== "DEBUG") {
return return
} }
message[0] = "DEBUG" message[0] = "DEBUG"
break; break;
case WARN: case WARN:
if (logLevel === "ERROR") { if (logLevel === "ERROR") {
return return
} }
message[0] = "WARN" message[0] = "WARN"
break; break;
case ERROR: case ERROR:
message[0] = "ERROR" message[0] = "ERROR"
break; break;
default: default:
if (logLevel !== "DEBUG") { if (logLevel !== "DEBUG") {
return return
} }
} }
const now = new Date() const now = new Date()
const timestamp = `${now.getFullYear()}-${z(now.getMonth() + 1)}-${z(now.getDate())} ${z(now.getHours())}:${z(now.getMinutes())}:${z(now.getSeconds())}` const timestamp = `${now.getFullYear()}-${z(now.getMonth() + 1)}-${z(now.getDate())} ${z(now.getHours())}:${z(now.getMinutes())}:${z(now.getSeconds())}`
const toLog = [timestamp] const toLog = [timestamp]
if (params.appName) { if (params.appName) {
if (disabledApps.includes(params.appName)) { if (disabledApps.includes(params.appName)) {
return return
} }
toLog.push(params.appName) toLog.push(params.appName)
} }
if (params.component) { if (params.component) {
if (disabledComponents.includes(params.component)) { if (disabledComponents.includes(params.component)) {
return return
} }
toLog.push(params.component) toLog.push(params.component)
} }
if (params.userId) { if (params.userId) {
toLog.push(params.userId) toLog.push(params.userId)
} }
const parsed = message.map(m => typeof m === 'object' ? JSON.stringify(m, (_, v) => typeof v === 'bigint' ? v.toString() : v) : m) const parsed = message.map(m => typeof m === 'object' ? JSON.stringify(m, (_, v) => typeof v === 'bigint' ? v.toString() : v) : m)
const final = `${toLog.join(" ")} >> ${parsed.join(" ")}` const final = `${toLog.join(" ")} >> ${parsed.join(" ")}`
console.log(final) console.log(final)
writers.forEach(w => w(final)) writers.forEach(w => w(final))
} }
} }
let disabledApps: string[] = [] let disabledApps: string[] = []
let disabledComponents: string[] = [] let disabledComponents: string[] = []
export const resetDisabledLoggers = () => { export const resetDisabledLoggers = () => {
disabledApps = [] disabledApps = []
disabledComponents = [] disabledComponents = []
} }
export const disableLoggers = (appNamesToDisable: string[], componentsToDisable: string[]) => { export const disableLoggers = (appNamesToDisable: string[], componentsToDisable: string[]) => {
disabledApps.push(...appNamesToDisable) disabledApps.push(...appNamesToDisable)
disabledComponents.push(...componentsToDisable) disabledComponents.push(...componentsToDisable)
} }
const disableFromEnv = () => {
const disabledApps = process.env.HIDE_LOGS
if (disabledApps) {
const loggers = disabledApps.split(" ")
resetDisabledLoggers()
disableLoggers(loggers, loggers)
}
}
disableFromEnv()

View file

@ -0,0 +1,11 @@
import { MainSettings } from "../main/settings.js";
import { StateBundler } from "../storage/stateBundler.js";
export class Utils {
stateBundler: StateBundler
settings: MainSettings
constructor(settings: MainSettings) {
this.settings = settings
this.stateBundler = new StateBundler()
}
}

View file

@ -4,18 +4,22 @@ import os from 'os'
import path from 'path' import path from 'path'
const resolveHome = (filepath: string) => { const resolveHome = (filepath: string) => {
if (filepath[0] === '~') { let homeDir;
return path.join(os.homedir(), filepath.slice(1)) if (process.env.SUDO_USER) {
homeDir = path.join('/home', process.env.SUDO_USER);
} else {
homeDir = os.homedir();
} }
return filepath return path.join(homeDir, filepath);
} }
export const LoadLndSettingsFromEnv = (): LndSettings => { export const LoadLndSettingsFromEnv = (): LndSettings => {
const lndAddr = process.env.LND_ADDRESS || "127.0.0.1:10009" const lndAddr = process.env.LND_ADDRESS || "127.0.0.1:10009"
const lndCertPath = process.env.LND_CERT_PATH || resolveHome("~/.lnd/tls.cert") const lndCertPath = process.env.LND_CERT_PATH || resolveHome("/.lnd/tls.cert")
const lndMacaroonPath = process.env.LND_MACAROON_PATH || resolveHome("~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon") const lndMacaroonPath = process.env.LND_MACAROON_PATH || resolveHome("/.lnd/data/chain/bitcoin/mainnet/admin.macaroon")
const feeRateLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_BPS", 60) / 10000 const feeRateBps = EnvCanBeInteger("OUTBOUND_MAX_FEE_BPS", 60)
const feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100) const feeRateLimit = feeRateBps / 10000
const mockLnd = EnvCanBeBoolean("MOCK_LND") const feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100)
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd } const mockLnd = EnvCanBeBoolean("MOCK_LND")
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, feeRateBps, mockLnd }
} }

View file

@ -0,0 +1,15 @@
import { InitWalletRequest } from "../../../proto/lnd/walletunlocker";
export const InitWalletReq = (walletPw: Buffer, cipherSeedMnemonic: string[]): InitWalletRequest => ({
aezeedPassphrase: Buffer.alloc(0),
walletPassword: walletPw,
cipherSeedMnemonic,
extendedMasterKey: "",
extendedMasterKeyBirthdayTimestamp: 0n,
macaroonRootKey: Buffer.alloc(0),
recoveryWindow: 0,
statelessInit: false,
channelBackups: undefined,
watchOnly: undefined,
})

View file

@ -1,252 +0,0 @@
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { decodeNprofile } from '../../custom-nip19.js'
import { getLogger } from '../helpers/logger.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { relayInit } from '../nostr/tools/relay.js'
import { InvoicePaidCb } from './settings.js'
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
export class LiquidityProvider {
client: ReturnType<typeof newNostrClient>
clientCbs: Record<string, nostrCallback<any>> = {}
clientId: string = ""
myPub: string = ""
log = getLogger({ component: 'liquidityProvider' })
nostrSend: NostrSend | null = null
ready = false
pubDestination: string
latestMaxWithdrawable: number | null = null
latestBalance: number | null = null
invoicePaidCb: InvoicePaidCb
connecting = false
readyInterval: NodeJS.Timeout
// make the sub process accept client
constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) {
if (!pubDestination) {
this.log("No pub provider to liquidity provider, will not be initialized")
return
}
this.log("connecting to liquidity provider", pubDestination)
this.pubDestination = pubDestination
this.invoicePaidCb = invoicePaidCb
this.client = newNostrClient({
pubDestination: this.pubDestination,
retrieveNostrUserAuth: async () => this.myPub,
}, this.clientSend, this.clientSub)
this.readyInterval = setInterval(() => {
if (this.ready) {
clearInterval(this.readyInterval)
this.Connect()
}
}, 1000)
}
Stop = () => {
clearInterval(this.readyInterval)
}
Connect = async () => {
await new Promise(res => setTimeout(res, 2000))
this.log("ready")
await this.CheckUserState()
if (this.latestMaxWithdrawable === null) {
return
}
this.log("subbing to user operations")
this.client.GetLiveUserOperations(res => {
console.log("got user operation", res)
if (res.status === 'ERROR') {
this.log("error getting user operations", res.reason)
return
}
this.log("got user operation", res.operation)
if (res.operation.type === Types.UserOperationType.INCOMING_INVOICE) {
this.log("invoice was paid", res.operation.identifier)
this.invoicePaidCb(res.operation.identifier, res.operation.amount, false)
}
})
}
GetLatestMaxWithdrawable = async (fetch = false) => {
if (this.latestMaxWithdrawable === null) {
this.log("liquidity provider is not ready yet")
return 0
}
if (fetch) {
await this.CheckUserState()
}
return this.latestMaxWithdrawable || 0
}
GetLatestBalance = async (fetch = false) => {
if (this.latestMaxWithdrawable === null) {
this.log("liquidity provider is not ready yet")
return 0
}
if (fetch) {
await this.CheckUserState()
}
return this.latestBalance || 0
}
CheckUserState = async () => {
const res = await this.client.GetUserInfo()
if (res.status === 'ERROR') {
this.log("error getting user info", res)
return
}
this.latestMaxWithdrawable = res.max_withdrawable
this.latestBalance = res.balance
this.log("latest provider balance:", res.balance, "latest max withdrawable:", res.max_withdrawable)
return res
}
CanProviderHandle = (req: LiquidityRequest) => {
if (this.latestMaxWithdrawable === null) {
return false
}
if (req.action === 'spend') {
return this.latestMaxWithdrawable > req.amount
}
return true
}
AddInvoice = async (amount: number, memo: string) => {
const res = await this.client.NewInvoice({ amountSats: amount, memo })
if (res.status === 'ERROR') {
this.log("error creating invoice", res.reason)
throw new Error(res.reason)
}
this.log("new invoice", res.invoice)
this.CheckUserState()
return res.invoice
}
PayInvoice = async (invoice: string) => {
const res = await this.client.PayInvoice({ invoice, amount: 0 })
if (res.status === 'ERROR') {
this.log("error paying invoice", res.reason)
throw new Error(res.reason)
}
this.log("paid invoice", res)
this.CheckUserState()
return res
}
setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => {
this.clientId = clientId
this.myPub = myPub
this.setSetIfReady()
}
attachNostrSend(f: NostrSend) {
this.nostrSend = f
this.setSetIfReady()
}
setSetIfReady = () => {
if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) {
this.ready = true
this.log("ready to send to ", this.pubDestination)
}
}
onEvent = async (res: { requestId: string }, fromPub: string) => {
if (fromPub !== this.pubDestination) {
this.log("got event from invalid pub", fromPub, this.pubDestination)
return false
}
if (this.clientCbs[res.requestId]) {
const cb = this.clientCbs[res.requestId]
cb.f(res)
if (cb.type === 'single') {
delete this.clientCbs[res.requestId]
this.log(this.getSingleSubs(), "single subs left")
}
return true
}
return false
}
clientSend = (to: string, message: NostrRequest): Promise<any> => {
if (!this.ready || !this.nostrSend) {
throw new Error("liquidity provider not initialized")
}
if (!message.requestId) {
message.requestId = makeId(16)
}
const reqId = message.requestId
if (this.clientCbs[reqId]) {
throw new Error("request was already sent")
}
this.nostrSend({ type: 'client', clientId: this.clientId }, {
type: 'content',
pub: to,
content: JSON.stringify(message)
})
//this.nostrSend(this.relays, to, JSON.stringify(message), this.settings)
this.log("subbing to single send", reqId, message.rpcName || 'no rpc name')
return new Promise(res => {
this.clientCbs[reqId] = {
startedAtMillis: Date.now(),
type: 'single',
f: (response: any) => { res(response) },
}
})
}
clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => {
if (!this.ready || !this.nostrSend) {
throw new Error("liquidity provider not initialized")
}
if (!message.requestId) {
message.requestId = message.rpcName
}
const reqId = message.requestId
if (!reqId) {
throw new Error("invalid sub")
}
if (this.clientCbs[reqId]) {
this.clientCbs[reqId] = {
startedAtMillis: Date.now(),
type: 'stream',
f: (response: any) => { cb(response) },
}
this.log("sub for", reqId, "was already registered, overriding")
return
}
this.nostrSend({ type: 'client', clientId: this.clientId }, {
type: 'content',
pub: to,
content: JSON.stringify(message)
})
this.log("subbing to stream", reqId)
this.clientCbs[reqId] = {
startedAtMillis: Date.now(),
type: 'stream',
f: (response: any) => { cb(response) }
}
}
getSingleSubs = () => {
return Object.entries(this.clientCbs).filter(([_, cb]) => cb.type === 'single')
}
}
export const makeId = (length: number) => {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

View file

@ -16,9 +16,12 @@ import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js'; import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js';
import { getLogger } from '../helpers/logger.js'; import { getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider, LiquidityRequest } from './liquidityProvider.js'; import { LiquidityProvider, LiquidityRequest } from '../main/liquidityProvider.js';
import { Utils } from '../helpers/utilsWrapper.js';
import { TxPointSettings } from '../storage/stateBundler.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 5 const deadLndRetrySeconds = 5
type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' }
export default class { export default class {
lightning: LightningClient lightning: LightningClient
invoices: InvoicesClient invoices: InvoicesClient
@ -36,9 +39,10 @@ export default class {
log = getLogger({ component: 'lndManager' }) log = getLogger({ component: 'lndManager' })
outgoingOpsLocked = false outgoingOpsLocked = false
liquidProvider: LiquidityProvider liquidProvider: LiquidityProvider
useOnlyLiquidityProvider = false utils: Utils
constructor(settings: LndSettings, provider: { liquidProvider: LiquidityProvider, useOnly?: boolean }, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) { constructor(settings: LndSettings, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
this.settings = settings this.settings = settings
this.utils = utils
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
this.newBlockCb = newBlockCb this.newBlockCb = newBlockCb
@ -63,8 +67,7 @@ export default class {
this.invoices = new InvoicesClient(transport) this.invoices = new InvoicesClient(transport)
this.router = new RouterClient(transport) this.router = new RouterClient(transport)
this.chainNotifier = new ChainNotifierClient(transport) this.chainNotifier = new ChainNotifierClient(transport)
this.liquidProvider = provider.liquidProvider this.liquidProvider = liquidProvider
this.useOnlyLiquidityProvider = !!provider.useOnly
} }
LockOutgoingOperations(): void { LockOutgoingOperations(): void {
@ -82,20 +85,6 @@ export default class {
this.liquidProvider.Stop() this.liquidProvider.Stop()
} }
async ShouldUseLiquidityProvider(req: LiquidityRequest): Promise<boolean> {
if (this.useOnlyLiquidityProvider) {
return true
}
if (!this.liquidProvider.CanProviderHandle(req)) {
return false
}
const channels = await this.ListChannels()
if (channels.channels.length === 0) {
this.log("no channels, will use liquidity provider")
return true
}
return false
}
async Warmup() { async Warmup() {
this.SubscribeAddressPaid() this.SubscribeAddressPaid()
this.SubscribeInvoicePaid() this.SubscribeInvoicePaid()
@ -212,8 +201,7 @@ export default class {
if (tx.numConfirmations === 0) { // only process pending transactions, confirmed transaction are processed by the newBlock CB if (tx.numConfirmations === 0) { // only process pending transactions, confirmed transaction are processed by the newBlock CB
tx.outputDetails.forEach(output => { tx.outputDetails.forEach(output => {
if (output.isOurAddress) { if (output.isOurAddress) {
this.log("received chan TX", Number(output.amount), "sats", "for", output.address) this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount), 'lnd')
this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount), false)
} }
}) })
} }
@ -233,9 +221,8 @@ export default class {
}, { abort: this.abortController.signal }) }, { abort: this.abortController.signal })
stream.responses.onMessage(invoice => { stream.responses.onMessage(invoice => {
if (invoice.state === Invoice_InvoiceState.SETTLED) { if (invoice.state === Invoice_InvoiceState.SETTLED) {
this.log("An invoice was paid for", Number(invoice.amtPaidSat), "sats", invoice.paymentRequest)
this.latestKnownSettleIndex = Number(invoice.settleIndex) this.latestKnownSettleIndex = Number(invoice.settleIndex)
this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), false) this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), 'lnd')
} }
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
@ -247,8 +234,8 @@ export default class {
}) })
} }
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> { async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> {
this.log("generating new address")
await this.Health() await this.Health()
let lndAddressType: AddressType let lndAddressType: AddressType
switch (addressType) { switch (addressType) {
@ -264,22 +251,34 @@ export default class {
default: default:
throw new Error("unknown address type " + addressType) throw new Error("unknown address type " + addressType)
} }
const res = await this.lightning.newAddress({ account: "", type: lndAddressType }, DeadLineMetadata()) if (useProvider) {
this.log("new address", res.response.address) throw new Error("provider payments not support chain payments yet")
return res.response }
try {
const res = await this.lightning.newAddress({ account: "", type: lndAddressType }, DeadLineMetadata())
this.utils.stateBundler.AddTxPoint('addedAddress', 1, { from, used: 'lnd' })
return res.response
} catch (err) {
this.utils.stateBundler.AddTxPointFailed('addedAddress', 1, { from, used: 'lnd' })
throw err
}
} }
async NewInvoice(value: number, memo: string, expiry: number, useProvider = false): Promise<Invoice> { async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions): Promise<Invoice> {
this.log("generating new invoice for", value, "sats")
await this.Health() await this.Health()
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'receive', amount: value }) if (useProvider) {
if (shouldUseLiquidityProvider || useProvider) { const invoice = await this.liquidProvider.AddInvoice(value, memo, from)
const invoice = await this.liquidProvider.AddInvoice(value, memo) const providerDst = this.liquidProvider.GetProviderDestination()
return { payRequest: invoice } return { payRequest: invoice, providerDst }
}
try {
const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, true, memo), DeadLineMetadata())
this.utils.stateBundler.AddTxPoint('addedInvoice', value, { from, used: 'lnd' })
return { payRequest: res.response.paymentRequest }
} catch (err) {
this.utils.stateBundler.AddTxPointFailed('addedInvoice', value, { from, used: 'lnd' })
throw err
} }
const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, false, memo), DeadLineMetadata())
this.log("new invoice", res.response.paymentRequest)
return { payRequest: res.response.paymentRequest }
} }
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> { async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
@ -300,39 +299,44 @@ export default class {
const r = res.response const r = res.response
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 } return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
} }
async PayInvoice(invoice: string, amount: number, feeLimit: number, useProvider = false): Promise<PaidInvoice> { async PayInvoice(invoice: string, amount: number, feeLimit: number, decodedAmount: number, { useProvider, from }: TxActionOptions): Promise<PaidInvoice> {
if (this.outgoingOpsLocked) { if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync") throw new Error("lnd node is currently out of sync")
} }
await this.Health() await this.Health()
this.log("paying invoice", invoice, "for", amount, "sats") if (useProvider) {
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'spend', amount }) const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from)
if (shouldUseLiquidityProvider || useProvider) { const providerDst = this.liquidProvider.GetProviderDestination()
const res = await this.liquidProvider.PayInvoice(invoice) return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst }
return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage }
} }
const abortController = new AbortController() try {
const req = PayInvoiceReq(invoice, amount, feeLimit) const abortController = new AbortController()
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) const req = PayInvoiceReq(invoice, amount, feeLimit)
return new Promise((res, rej) => { const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
stream.responses.onError(error => { return new Promise((res, rej) => {
this.log("invoice payment failed", error) stream.responses.onError(error => {
rej(error) this.log("invoice payment failed", error)
rej(error)
})
stream.responses.onMessage(payment => {
switch (payment.status) {
case Payment_PaymentStatus.FAILED:
this.log("invoice payment failed", payment.failureReason)
rej(PaymentFailureReason[payment.failureReason])
return
case Payment_PaymentStatus.SUCCEEDED:
this.utils.stateBundler.AddTxPoint('paidAnInvoice', Number(payment.valueSat), { from, used: 'lnd', timeDiscount: true })
res({ feeSat: Math.ceil(Number(payment.feeMsat) / 1000), valueSat: Number(payment.valueSat), paymentPreimage: payment.paymentPreimage })
return
}
})
}) })
stream.responses.onMessage(payment => { } catch (err) {
switch (payment.status) { this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', decodedAmount, { from, used: 'lnd' })
case Payment_PaymentStatus.FAILED: throw err
console.log(payment) }
this.log("invoice payment failed", payment.failureReason)
rej(PaymentFailureReason[payment.failureReason])
return
case Payment_PaymentStatus.SUCCEEDED:
this.log("invoice payment succeded", Number(payment.valueSat))
res({ feeSat: Math.ceil(Number(payment.feeMsat) / 1000), valueSat: Number(payment.valueSat), paymentPreimage: payment.paymentPreimage })
}
})
})
} }
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> { async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
@ -346,16 +350,23 @@ export default class {
return res.response return res.response
} }
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> { async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise<SendCoinsResponse> {
if (this.outgoingOpsLocked) { if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync") throw new Error("lnd node is currently out of sync")
} }
await this.Health() if (useProvider) {
this.log("sending chain TX for", amount, "sats", "to", address) throw new Error("provider payments not support chain payments yet")
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata()) }
this.log("sent chain TX for", amount, "sats", "to", address) try {
return res.response await this.Health()
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())
this.utils.stateBundler.AddTxPoint('paidAnAddress', amount, { from, used: 'lnd', timeDiscount: true })
return res.response
} catch (err) {
this.utils.stateBundler.AddTxPointFailed('paidAnAddress', amount, { from, used: 'lnd' })
throw err
}
} }
async GetTransactions(startHeight: number): Promise<TransactionDetails> { async GetTransactions(startHeight: number): Promise<TransactionDetails> {
@ -374,7 +385,20 @@ export default class {
return res.response return res.response
} }
async GetBalance(): Promise<BalanceInfo> { async GetTotalBalace() {
const walletBalance = await this.GetWalletBalance()
const confirmedWalletBalance = Number(walletBalance.confirmedBalance)
this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance)
const channelsBalance = await this.GetChannelBalance()
const totalLightningBalanceMsats = (channelsBalance.localBalance?.msat || 0n) + (channelsBalance.unsettledLocalBalance?.msat || 0n)
const totalLightningBalance = Math.ceil(Number(totalLightningBalanceMsats) / 1000)
this.utils.stateBundler.AddBalancePoint('channelBalance', totalLightningBalance)
const totalLndBalance = confirmedWalletBalance + totalLightningBalance
this.utils.stateBundler.AddBalancePoint('totalLndBalance', totalLndBalance)
return totalLndBalance
}
async GetBalance(): Promise<BalanceInfo> { // TODO: remove this
const wRes = await this.lightning.walletBalance({}, DeadLineMetadata()) const wRes = await this.lightning.walletBalance({}, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
const { response } = await this.lightning.listChannels({ const { response } = await this.lightning.listChannels({

View file

@ -1,5 +1,5 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityProvider } from "../main/liquidityProvider.js"
import { getLogger, PubLogger } from '../helpers/logger.js' import { getLogger, PubLogger } from '../helpers/logger.js'
import LND from "./lnd.js" import LND from "./lnd.js"
import { AddressType } from "../../../proto/autogenerated/ts/types.js" import { AddressType } from "../../../proto/autogenerated/ts/types.js"
@ -61,29 +61,6 @@ class LSP {
this.log = getLogger({ component: serviceName }) this.log = getLogger({ component: serviceName })
} }
shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => {
if (this.settings.channelThreshold === 0) {
this.log("channel threshold is 0")
return { shouldOpen: false }
}
const channels = await this.lnd.ListChannels()
if (channels.channels.length > 0) {
this.log("this node already has open channels")
return { shouldOpen: false }
}
const pendingChannels = await this.lnd.ListPendingChannels()
if (pendingChannels.pendingOpenChannels.length > 0) {
this.log("this node already has pending channels")
return { shouldOpen: false }
}
const userState = await this.liquidityProvider.CheckUserState()
if (!userState || userState.max_withdrawable < this.settings.channelThreshold) {
this.log("balance of", userState?.max_withdrawable || 0, "is lower than channel threshold of", this.settings.channelThreshold)
return { shouldOpen: false }
}
return { shouldOpen: true, maxSpendable: userState.max_withdrawable }
}
addPeer = async (pubKey: string, host: string) => { addPeer = async (pubKey: string, host: string) => {
const { peers } = await this.lnd.ListPeers() const { peers } = await this.lnd.ListPeers()
if (!peers.find(p => p.pubKey === pubKey)) { if (!peers.find(p => p.pubKey === pubKey)) {
@ -98,18 +75,14 @@ export class FlashsatsLSP extends LSP {
super("FlashsatsLSP", settings, lnd, liquidityProvider) super("FlashsatsLSP", settings, lnd, liquidityProvider)
} }
openChannelIfReady = async (): Promise<OrderResponse | null> => { requestChannel = async (maxSpendable: number): Promise<OrderResponse | null> => {
const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) {
return null
}
if (!this.settings.flashsatsServiceUrl) { if (!this.settings.flashsatsServiceUrl) {
this.log("no flashsats service url provided") this.log("no flashsats service url provided")
return null return null
} }
const serviceInfo = await this.getInfo() const serviceInfo = await this.getInfo()
if (+serviceInfo.options.min_initial_client_balance_sat > shouldOpen.maxSpendable) { if (+serviceInfo.options.min_initial_client_balance_sat > maxSpendable) {
this.log("balance of", shouldOpen.maxSpendable, "is lower than service minimum of", serviceInfo.options.min_initial_client_balance_sat) this.log("balance of", maxSpendable, "is lower than service minimum of", serviceInfo.options.min_initial_client_balance_sat)
return null return null
} }
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
@ -118,7 +91,8 @@ export class FlashsatsLSP extends LSP {
this.log("no uri found for this node,uri is required to use flashsats") this.log("no uri found for this node,uri is required to use flashsats")
return null return null
} }
const lspBalance = (this.settings.channelThreshold * 2).toString() const channelSize = Math.floor(maxSpendable * (1 - this.settings.maxRelativeFee)) * 2
const lspBalance = channelSize.toString()
const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks
const order = await this.createOrder({ nodeUri: myUri, lspBalance, clientBalance: "0", chanExpiryBlocks }) const order = await this.createOrder({ nodeUri: myUri, lspBalance, clientBalance: "0", chanExpiryBlocks })
if (order.payment.state !== 'EXPECT_PAYMENT') { if (order.payment.state !== 'EXPECT_PAYMENT') {
@ -130,18 +104,19 @@ export class FlashsatsLSP extends LSP {
this.log("invoice of amount", decoded.numSatoshis, "does not match order total of", order.payment.order_total_sat) this.log("invoice of amount", decoded.numSatoshis, "does not match order total of", order.payment.order_total_sat)
return null return null
} }
if (decoded.numSatoshis > shouldOpen.maxSpendable) { if (decoded.numSatoshis > maxSpendable) {
this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", shouldOpen.maxSpendable) this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", maxSpendable)
return null return null
} }
const relativeFee = +order.payment.fee_total_sat / this.settings.channelThreshold const relativeFee = +order.payment.fee_total_sat / channelSize
if (relativeFee > this.settings.maxRelativeFee) { if (relativeFee > this.settings.maxRelativeFee) {
this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice) const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system')
this.log("paid", res.amount_paid, "to open channel") const fees = +order.payment.fee_total_sat + res.network_fee + res.service_fee
return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees: +order.payment.fee_total_sat } this.log("paid", res.amount_paid, "to open channel, and a fee of", fees)
return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees }
} }
getInfo = async () => { getInfo = async () => {
@ -174,54 +149,52 @@ export class OlympusLSP extends LSP {
super("OlympusLSP", settings, lnd, liquidityProvider) super("OlympusLSP", settings, lnd, liquidityProvider)
} }
openChannelIfReady = async (): Promise<OrderResponse | null> => { requestChannel = async (maxSpendable: number): Promise<OrderResponse | null> => {
const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) {
return null
}
if (!this.settings.olympusServiceUrl) { if (!this.settings.olympusServiceUrl) {
this.log("no olympus service url provided") this.log("no olympus service url provided")
return null return null
} }
const serviceInfo = await this.getInfo() const serviceInfo = await this.getInfo()
if (+serviceInfo.options.min_initial_client_balance_sat > shouldOpen.maxSpendable) { if (+serviceInfo.min_initial_client_balance_sat > maxSpendable) {
this.log("balance of", shouldOpen.maxSpendable, "is lower than service minimum of", serviceInfo.options.min_initial_client_balance_sat) this.log("balance of", maxSpendable, "is lower than service minimum of", serviceInfo.min_initial_client_balance_sat)
return null return null
} }
const [servicePub, host] = serviceInfo.uris[0].split('@') const [servicePub, host] = serviceInfo.uris[0].split('@')
await this.addPeer(servicePub, host) await this.addPeer(servicePub, host)
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
const myPub = lndInfo.identityPubkey const myPub = lndInfo.identityPubkey
const refundAddr = await this.lnd.NewAddress(AddressType.WITNESS_PUBKEY_HASH) const refundAddr = await this.lnd.NewAddress(AddressType.WITNESS_PUBKEY_HASH, { useProvider: false, from: 'system' })
const lspBalance = (this.settings.channelThreshold * 2).toString() const channelSize = Math.floor(maxSpendable * (1 - this.settings.maxRelativeFee)) * 2
const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks const lspBalance = channelSize.toString()
const chanExpiryBlocks = serviceInfo.max_channel_expiry_blocks
const order = await this.createOrder({ pubKey: myPub, refundAddr: refundAddr.address, lspBalance, clientBalance: "0", chanExpiryBlocks }) const order = await this.createOrder({ pubKey: myPub, refundAddr: refundAddr.address, lspBalance, clientBalance: "0", chanExpiryBlocks })
if (order.payment.state !== 'EXPECT_PAYMENT') { if (order.payment.bolt11.state !== 'EXPECT_PAYMENT') {
this.log("order not in expect payment state") this.log("order not in expect payment state")
return null return null
} }
const decoded = await this.lnd.DecodeInvoice(order.payment.bolt11_invoice) const decoded = await this.lnd.DecodeInvoice(order.payment.bolt11.invoice)
if (decoded.numSatoshis !== +order.payment.order_total_sat) { if (decoded.numSatoshis !== +order.payment.bolt11.order_total_sat) {
this.log("invoice of amount", decoded.numSatoshis, "does not match order total of", order.payment.order_total_sat) this.log("invoice of amount", decoded.numSatoshis, "does not match order total of", order.payment.bolt11.order_total_sat)
return null return null
} }
if (decoded.numSatoshis > shouldOpen.maxSpendable) { if (decoded.numSatoshis > maxSpendable) {
this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", shouldOpen.maxSpendable) this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", maxSpendable)
return null return null
} }
const relativeFee = +order.payment.fee_total_sat / this.settings.channelThreshold const relativeFee = +order.payment.bolt11.fee_total_sat / channelSize
if (relativeFee > this.settings.maxRelativeFee) { if (relativeFee > this.settings.maxRelativeFee) {
this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice) const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system')
this.log("paid", res.amount_paid, "to open channel") const fees = +order.payment.bolt11.fee_total_sat + res.network_fee + res.service_fee
return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees: +order.payment.fee_total_sat } this.log("paid", res.amount_paid, "to open channel, and a fee of", fees)
return { orderId: order.order_id, invoice: order.payment.bolt11.invoice, totalSats: +order.payment.bolt11.order_total_sat, fees }
} }
getInfo = async () => { getInfo = async () => {
const res = await fetch(`${this.settings.olympusServiceUrl}/get_info`) const res = await fetch(`${this.settings.olympusServiceUrl}/get_info`)
const json = await res.json() as { options: { min_initial_client_balance_sat: string, max_channel_expiry_blocks: number }, uris: string[] } const json = await res.json() as { min_initial_client_balance_sat: string, max_channel_expiry_blocks: number, uris: string[] }
return json return json
} }
@ -241,7 +214,7 @@ export class OlympusLSP extends LSP {
body: JSON.stringify(req), body: JSON.stringify(req),
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
}) })
const json = await res.json() as { order_id: string, payment: { state: 'EXPECT_PAYMENT', bolt11_invoice: string, fee_total_sat: string, order_total_sat: string } } const json = await res.json() as { order_id: string, payment: { bolt11: { state: 'EXPECT_PAYMENT', invoice: string, fee_total_sat: string, order_total_sat: string } } }
return json return json
} }
@ -275,12 +248,7 @@ export class VoltageLSP extends LSP {
return json return json
} }
openChannelIfReady = async (): Promise<OrderResponse | null> => { requestChannel = async (maxSpendable: number): Promise<OrderResponse | null> => {
const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) {
return null
}
if (!this.settings.voltageServiceUrl) { if (!this.settings.voltageServiceUrl) {
this.log("no voltage service url provided") this.log("no voltage service url provided")
return null return null
@ -288,10 +256,11 @@ export class VoltageLSP extends LSP {
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
const myPub = lndInfo.identityPubkey const myPub = lndInfo.identityPubkey
const amtMsats = this.settings.channelThreshold * 1000 const amtSats = Math.floor(maxSpendable * 0.9)
const amtMsats = Math.floor(maxSpendable * 0.9) * 1000
const fee = await this.getFees(amtMsats, myPub) const fee = await this.getFees(amtMsats, myPub)
const feeSats = fee.fee_amount_msat / 1000 const feeSats = fee.fee_amount_msat / 1000
const relativeFee = feeSats / this.settings.channelThreshold const relativeFee = feeSats / (amtSats * 1.1)
if (relativeFee > this.settings.maxRelativeFee) { if (relativeFee > this.settings.maxRelativeFee) {
this.log("relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) this.log("relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
@ -306,17 +275,22 @@ export class VoltageLSP extends LSP {
} }
await this.addPeer(info.pubkey, `${ipv4.address}:${ipv4.port}`) await this.addPeer(info.pubkey, `${ipv4.address}:${ipv4.port}`)
const invoice = await this.lnd.NewInvoice(this.settings.channelThreshold, "open channel", 60 * 60) const invoice = await this.lnd.NewInvoice(amtSats, "open channel", 60 * 60, { from: 'system', useProvider: false })
const res = await this.proposal(invoice.payRequest, fee.id) const proposalRes = await this.proposal(invoice.payRequest, fee.id)
const decoded = await this.lnd.DecodeInvoice(res.jit_bolt11) this.log("proposal res", proposalRes, fee.id)
if (decoded.numSatoshis !== this.settings.channelThreshold + feeSats) { const decoded = await this.lnd.DecodeInvoice(proposalRes.jit_bolt11)
this.log("invoice of amount", decoded.numSatoshis, "does not match expected amount of", this.settings.channelThreshold + feeSats) if (decoded.numSatoshis !== amtSats + feeSats) {
this.log("invoice of amount", decoded.numSatoshis, "does not match expected amount of", amtSats + feeSats)
return null return null
} }
if (decoded.numSatoshis > maxSpendable) {
const invoiceRes = await this.liquidityProvider.PayInvoice(res.jit_bolt11) this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", maxSpendable)
this.log("paid", invoiceRes.amount_paid, "to open channel") return null
return { orderId: fee.id, invoice: res.jit_bolt11, totalSats: decoded.numSatoshis, fees: feeSats } }
const res = await this.liquidityProvider.PayInvoice(proposalRes.jit_bolt11, decoded.numSatoshis, 'system')
const fees = feeSats + res.network_fee + res.service_fee
this.log("paid", res.amount_paid, "to open channel, and a fee of", fees)
return { orderId: fee.id, invoice: proposalRes.jit_bolt11, totalSats: decoded.numSatoshis, fees }
} }
proposal = async (bolt11: string, feeId: string) => { proposal = async (bolt11: string, feeId: string) => {

View file

@ -1,30 +1,29 @@
import { OpenChannelRequest } from "../../../proto/lnd/lightning"; import { OpenChannelRequest } from "../../../proto/lnd/lightning";
import { SendPaymentRequest } from "../../../proto/lnd/router"; import { SendPaymentRequest } from "../../../proto/lnd/router";
export const PayInvoiceReq = (invoice: string, amount: number, feeLimit: number): SendPaymentRequest => ({ export const PayInvoiceReq = (invoice: string, amount: number, feeLimit: number): SendPaymentRequest => ({
amt: BigInt(amount), amt: BigInt(amount),
feeLimitSat: BigInt(feeLimit), feeLimitSat: BigInt(feeLimit),
noInflightUpdates: true, noInflightUpdates: false,
paymentRequest: invoice, paymentRequest: invoice,
maxParts: 3, maxParts: 3,
timeoutSeconds: 50, timeoutSeconds: 50,
allowSelfPayment: false, allowSelfPayment: false,
amp: false, amp: false,
amtMsat: 0n, amtMsat: 0n,
cltvLimit: 0, cltvLimit: 0,
dest: Buffer.alloc(0), dest: Buffer.alloc(0),
destCustomRecords: {}, destCustomRecords: {},
destFeatures: [], destFeatures: [],
feeLimitMsat: 0n, feeLimitMsat: 0n,
finalCltvDelta: 0, finalCltvDelta: 0,
lastHopPubkey: Buffer.alloc(0), lastHopPubkey: Buffer.alloc(0),
maxShardSizeMsat: 0n, maxShardSizeMsat: 0n,
outgoingChanIds: [], outgoingChanIds: [],
paymentAddr: Buffer.alloc(0), paymentAddr: Buffer.alloc(0),
paymentHash: Buffer.alloc(0), paymentHash: Buffer.alloc(0),
routeHints: [], routeHints: [],
timePref: 0, timePref: 0,
outgoingChanId: '0'
outgoingChanId: '0'
}) })

View file

@ -8,6 +8,7 @@ export type LndSettings = {
mainNode: NodeSettings mainNode: NodeSettings
feeRateLimit: number feeRateLimit: number
feeFixedLimit: number feeFixedLimit: number
feeRateBps: number
mockLnd: boolean mockLnd: boolean
otherNode?: NodeSettings otherNode?: NodeSettings
@ -30,8 +31,8 @@ export type BalanceInfo = {
channelsBalance: ChannelBalance[]; channelsBalance: ChannelBalance[];
} }
export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, internal: boolean) => void export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, used: 'lnd' | 'provider' | 'internal') => Promise<void>
export type InvoicePaidCb = (paymentRequest: string, amount: number, internal: boolean) => void export type InvoicePaidCb = (paymentRequest: string, amount: number, used: 'lnd' | 'provider' | 'internal') => Promise<void>
export type NewBlockCb = (height: number) => void export type NewBlockCb = (height: number) => void
export type HtlcCb = (event: HtlcEvent) => void export type HtlcCb = (event: HtlcEvent) => void
@ -46,6 +47,7 @@ export type NodeInfo = {
} }
export type Invoice = { export type Invoice = {
payRequest: string payRequest: string
providerDst?: string
} }
export type DecodedInvoice = { export type DecodedInvoice = {
numSatoshis: number numSatoshis: number
@ -55,4 +57,5 @@ export type PaidInvoice = {
feeSat: number feeSat: number
valueSat: number valueSat: number
paymentPreimage: string paymentPreimage: string
providerDst?: string
} }

View file

@ -0,0 +1,97 @@
import fs, { watchFile } from "fs";
import crypto from 'crypto'
import { getLogger } from "../helpers/logger.js";
import { MainSettings, getDataPath } from "./settings.js";
import Storage from "../storage/index.js";
export class AdminManager {
storage: Storage
log = getLogger({ component: "adminManager" })
adminNpub = ""
dataDir: string
adminNpubPath: string
adminEnrollTokenPath: string
interval: NodeJS.Timer
constructor(mainSettings: MainSettings, storage: Storage) {
this.storage = storage
this.dataDir = mainSettings.storageSettings.dataDir
this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub')
this.adminEnrollTokenPath = getDataPath(this.dataDir, '.admin_enroll')
this.start()
}
Stop = () => {
clearInterval(this.interval)
}
GenerateAdminEnrollToken = async () => {
const token = crypto.randomBytes(32).toString('hex')
fs.writeFileSync(this.adminEnrollTokenPath, token)
return token
}
start = () => {
const adminNpub = this.ReadAdminNpub()
if (adminNpub) {
this.adminNpub = adminNpub
} else if (!fs.existsSync(this.adminEnrollTokenPath)) {
this.GenerateAdminEnrollToken()
}
this.interval = setInterval(() => {
if (!this.adminNpub) {
return
}
const deleted = !fs.existsSync(this.adminNpubPath)
if (deleted) {
this.adminNpub = ""
this.log("admin npub file deleted")
this.GenerateAdminEnrollToken()
}
})
}
ReadAdminEnrollToken = () => {
try {
return fs.readFileSync(this.adminEnrollTokenPath, 'utf8').trim()
} catch (err: any) {
return ""
}
}
ReadAdminNpub = () => {
try {
return fs.readFileSync(this.adminNpubPath, 'utf8').trim()
} catch (err: any) {
return ""
}
}
GetAdminNpub = () => {
return this.adminNpub
}
ClearExistingAdmin = () => {
try {
fs.unlinkSync(this.adminNpubPath)
} catch (err: any) { }
}
PromoteUserToAdmin = async (appId: string, appUserId: string, token: string) => {
const app = await this.storage.applicationStorage.GetApplication(appId)
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, appUserId)
const npub = appUser.nostr_public_key
if (!npub) {
throw new Error("no npub found for user")
}
let actualToken
try {
actualToken = fs.readFileSync(this.adminEnrollTokenPath, 'utf8').trim()
} catch (err: any) {
throw new Error("invalid enroll token")
}
if (token !== actualToken) {
throw new Error("invalid enroll token")
}
fs.writeFileSync(this.adminNpubPath, npub)
fs.unlinkSync(this.adminEnrollTokenPath)
this.adminNpub = npub
}
}

View file

@ -56,7 +56,10 @@ export default class {
userId: ctx.user_id, userId: ctx.user_id,
balance: user.balance_sats, balance: user.balance_sats,
max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true), max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true),
user_identifier: appUser.identifier user_identifier: appUser.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
} }
} }

View file

@ -154,7 +154,11 @@ export default class {
userId: u.user.user_id, userId: u.user.user_id,
balance: u.user.balance_sats, balance: u.user.balance_sats,
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true), max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true),
user_identifier: u.identifier user_identifier: u.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
}, },
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
} }
@ -175,9 +179,7 @@ export default class {
const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier) const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier)
const { user: payer } = await this.storage.applicationStorage.GetOrCreateApplicationUser(app, req.payer_identifier, 0) const { user: payer } = await this.storage.applicationStorage.GetOrCreateApplicationUser(app, req.payer_identifier, 0)
const opts: InboundOptionals = { callbackUrl: req.http_callback_url, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app } const opts: InboundOptionals = { callbackUrl: req.http_callback_url, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app }
log("generating invoice...")
const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts) const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts)
log(receiver.identifier, "invoice created to be paid by", payer.identifier)
return { return {
invoice: appUserInvoice.invoice invoice: appUserInvoice.invoice
} }
@ -191,7 +193,10 @@ export default class {
max_withdrawable: max, identifier: req.user_identifier, info: { max_withdrawable: max, identifier: req.user_identifier, info: {
userId: user.user.user_id, balance: user.user.balance_sats, userId: user.user.user_id, balance: user.user.balance_sats,
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true), max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true),
user_identifier: user.identifier user_identifier: user.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
} }
} }
} }

View file

@ -15,8 +15,11 @@ import { UnsignedEvent } from '../nostr/tools/event.js'
import { NostrSend } from '../nostr/handler.js' import { NostrSend } from '../nostr/handler.js'
import MetricsManager from '../metrics/index.js' import MetricsManager from '../metrics/index.js'
import { LoggedEvent } from '../storage/eventsLog.js' import { LoggedEvent } from '../storage/eventsLog.js'
import { LiquidityProvider } from "../lnd/liquidityProvider.js" import { LiquidityProvider } from "./liquidityProvider.js"
import { LiquidityManager } from "./liquidityManager.js" import { LiquidityManager } from "./liquidityManager.js"
import { Utils } from "../helpers/utilsWrapper.js"
import { RugPullTracker } from "./rugPullTracker.js"
import { AdminManager } from "./adminManager.js"
type UserOperationsSub = { type UserOperationsSub = {
id: string id: string
@ -31,25 +34,31 @@ export default class {
lnd: LND lnd: LND
settings: MainSettings settings: MainSettings
userOperationsSub: UserOperationsSub | null = null userOperationsSub: UserOperationsSub | null = null
adminManager: AdminManager
productManager: ProductManager productManager: ProductManager
applicationManager: ApplicationManager applicationManager: ApplicationManager
appUserManager: AppUserManager appUserManager: AppUserManager
paymentManager: PaymentManager paymentManager: PaymentManager
paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {} paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {}
metricsManager: MetricsManager metricsManager: MetricsManager
liquidProvider: LiquidityProvider
liquidityManager: LiquidityManager liquidityManager: LiquidityManager
liquidityProvider: LiquidityProvider
utils: Utils
rugPullTracker: RugPullTracker
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
constructor(settings: MainSettings, storage: Storage) { constructor(settings: MainSettings, storage: Storage, adminManager: AdminManager, utils: Utils) {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
this.liquidProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.invoicePaidCb) this.utils = utils
const provider = { liquidProvider: this.liquidProvider, useOnly: settings.liquiditySettings.useOnlyLiquidityProvider } this.adminManager = adminManager
this.lnd = new LND(settings.lndSettings, provider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb) const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.liquiditySettings.liquidityProviderPub, b)
this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.liquidProvider, this.lnd) this.liquidityProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.utils, this.invoicePaidCb, updateProviderBalance)
this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider)
this.lnd = new LND(settings.lndSettings, this.liquidityProvider, this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker)
this.metricsManager = new MetricsManager(this.storage, this.lnd) this.metricsManager = new MetricsManager(this.storage, this.lnd)
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.liquidityManager, this.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.liquidityManager, this.utils, this.addressPaidCb, this.invoicePaidCb)
this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings) this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings)
this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager)
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
@ -69,7 +78,7 @@ export default class {
attachNostrSend(f: NostrSend) { attachNostrSend(f: NostrSend) {
this.nostrSend = f this.nostrSend = f
this.liquidProvider.attachNostrSend(f) this.liquidityProvider.attachNostrSend(f)
} }
htlcCb: HtlcCb = (e) => { htlcCb: HtlcCb = (e) => {
@ -88,7 +97,7 @@ export default class {
const balanceEvents = await this.paymentManager.GetLndBalance() const balanceEvents = await this.paymentManager.GetLndBalance()
await this.metricsManager.NewBlockCb(height, balanceEvents) await this.metricsManager.NewBlockCb(height, balanceEvents)
confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height) confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height)
this.liquidityManager.onNewBlock() await this.liquidityManager.onNewBlock()
} catch (err: any) { } catch (err: any) {
log(ERROR, "failed to check transactions after new block", err.message || err) log(ERROR, "failed to check transactions after new block", err.message || err)
return return
@ -125,11 +134,12 @@ export default class {
})) }))
} }
addressPaidCb: AddressPaidCb = (txOutput, address, amount, internal) => { addressPaidCb: AddressPaidCb = (txOutput, address, amount, used) => {
this.storage.StartTransaction(async tx => { return this.storage.StartTransaction(async tx => {
const { blockHeight } = await this.lnd.GetInfo() const { blockHeight } = await this.lnd.GetInfo()
const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx) const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx)
if (!userAddress) { return } if (!userAddress) { return }
const internal = used === 'internal'
let log = getLogger({}) let log = getLogger({})
if (!userAddress.linkedApplication) { if (!userAddress.linkedApplication) {
log(ERROR, "an address was paid, that has no linked application") log(ERROR, "an address was paid, that has no linked application")
@ -156,17 +166,20 @@ export default class {
const operationId = `${Types.UserOperationType.INCOMING_TX}-${addedTx.serial_id}` const operationId = `${Types.UserOperationType.INCOMING_TX}-${addedTx.serial_id}`
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: fee, confirmed: internal, tx_hash: txOutput.hash, internal: false } const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: fee, confirmed: internal, tx_hash: txOutput.hash, internal: false }
this.sendOperationToNostr(userAddress.linkedApplication, userAddress.user.user_id, op) this.sendOperationToNostr(userAddress.linkedApplication, userAddress.user.user_id, op)
} catch { this.utils.stateBundler.AddTxPoint('addressWasPaid', amount, { used, from: 'system', timeDiscount: true })
} catch (err: any) {
this.utils.stateBundler.AddTxPointFailed('addressWasPaid', amount, { used, from: 'system' })
log(ERROR, "cannot process address paid transaction, already registered") log(ERROR, "cannot process address paid transaction, already registered")
} }
}) })
} }
invoicePaidCb: InvoicePaidCb = (paymentRequest, amount, internal) => { invoicePaidCb: InvoicePaidCb = (paymentRequest, amount, used) => {
this.storage.StartTransaction(async tx => { return this.storage.StartTransaction(async tx => {
let log = getLogger({}) let log = getLogger({})
const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx) const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx)
if (!userInvoice) { return } if (!userInvoice) { return }
const internal = used === 'internal'
if (userInvoice.paid_at_unix > 0 && internal) { log("cannot pay internally, invoice already paid"); return } if (userInvoice.paid_at_unix > 0 && internal) { log("cannot pay internally, invoice already paid"); return }
if (userInvoice.paid_at_unix > 0 && !internal && userInvoice.paidByLnd) { log("invoice already paid by lnd"); return } if (userInvoice.paid_at_unix > 0 && !internal && userInvoice.paidByLnd) { log("invoice already paid by lnd"); return }
if (!userInvoice.linkedApplication) { if (!userInvoice.linkedApplication) {
@ -191,9 +204,10 @@ export default class {
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee, confirmed: true, tx_hash: "", internal } const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee, confirmed: true, tx_hash: "", internal }
this.sendOperationToNostr(userInvoice.linkedApplication, userInvoice.user.user_id, op) this.sendOperationToNostr(userInvoice.linkedApplication, userInvoice.user.user_id, op)
this.createZapReceipt(log, userInvoice) this.createZapReceipt(log, userInvoice)
log("paid invoice processed successfully")
this.liquidityManager.afterInInvoicePaid() this.liquidityManager.afterInInvoicePaid()
this.utils.stateBundler.AddTxPoint('invoiceWasPaid', amount, { used, from: 'system', timeDiscount: true })
} catch (err: any) { } catch (err: any) {
this.utils.stateBundler.AddTxPointFailed('invoiceWasPaid', amount, { used, from: 'system' })
log(ERROR, "cannot process paid invoice", err.message || "") log(ERROR, "cannot process paid invoice", err.message || "")
} }
}) })
@ -239,7 +253,6 @@ export default class {
async createZapReceipt(log: PubLogger, invoice: UserReceivingInvoice) { async createZapReceipt(log: PubLogger, invoice: UserReceivingInvoice) {
const zapInfo = invoice.zap_info const zapInfo = invoice.zap_info
if (!zapInfo || !invoice.linkedApplication || !invoice.linkedApplication.nostr_public_key) { if (!zapInfo || !invoice.linkedApplication || !invoice.linkedApplication.nostr_public_key) {
log(ERROR, "no zap info linked to payment")
return return
} }
const tags = [["p", zapInfo.pub], ["bolt11", invoice.invoice], ["description", zapInfo.description]] const tags = [["p", zapInfo.pub], ["bolt11", invoice.invoice], ["description", zapInfo.description]]

View file

@ -1,73 +1,92 @@
import { PubLogger, getLogger } from "../helpers/logger.js" import { PubLogger, getLogger } from "../helpers/logger.js"
import { LiquidityProvider } from "../lnd/liquidityProvider.js" import { LiquidityProvider } from "./liquidityProvider.js"
import Storage from "../storage/index.js" import { Unlocker } from "./unlocker.js"
import { TypeOrmMigrationRunner } from "../storage/migrations/runner.js" import Storage from "../storage/index.js"
import Main from "./index.js" import { TypeOrmMigrationRunner } from "../storage/migrations/runner.js"
import SanityChecker from "./sanityChecker.js" import Main from "./index.js"
import { MainSettings } from "./settings.js" import SanityChecker from "./sanityChecker.js"
export type AppData = { import { LoadMainSettingsFromEnv, MainSettings } from "./settings.js"
privateKey: string; import { Utils } from "../helpers/utilsWrapper.js"
publicKey: string; import { Wizard } from "../wizard/index.js"
appId: string; import { AdminManager } from "./adminManager.js"
name: string; import { encodeNprofile } from "../../custom-nip19.js"
} export type AppData = {
export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings) => { privateKey: string;
const storageManager = new Storage(mainSettings.storageSettings) publicKey: string;
const manualMigration = await TypeOrmMigrationRunner(log, storageManager, mainSettings.storageSettings.dbSettings, process.argv[2]) appId: string;
if (manualMigration) { name: string;
return }
} export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings) => {
const utils = new Utils(mainSettings)
const mainHandler = new Main(mainSettings, storageManager) const storageManager = new Storage(mainSettings.storageSettings)
await mainHandler.lnd.Warmup() const manualMigration = await TypeOrmMigrationRunner(log, storageManager, mainSettings.storageSettings.dbSettings, process.argv[2])
if (!mainSettings.skipSanityCheck) { if (manualMigration) {
const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd) return
await sanityChecker.VerifyEventsLog() }
} const unlocker = new Unlocker(mainSettings, storageManager)
await mainHandler.paymentManager.watchDog.Start() await unlocker.Unlock()
const appsData = await mainHandler.storage.applicationStorage.GetApplications() const adminManager = new AdminManager(mainSettings, storageManager)
const existingWalletApp = await appsData.find(app => app.name === 'wallet' || app.name === 'wallet-test') let reloadedSettings = mainSettings
if (!existingWalletApp) { let wizard: Wizard | null = null
log("no default wallet app found, creating one...") if (mainSettings.wizard) {
const newWalletApp = await mainHandler.storage.applicationStorage.AddApplication('wallet', true) wizard = new Wizard(mainSettings, storageManager, adminManager)
appsData.push(newWalletApp) const reload = await wizard.Configure()
} if (reload) {
const apps: AppData[] = await Promise.all(appsData.map(app => { reloadedSettings = LoadMainSettingsFromEnv()
if (!app.nostr_private_key || !app.nostr_public_key) { // TMP -- }
return mainHandler.storage.applicationStorage.GenerateApplicationKeys(app); }
} // --
else { const mainHandler = new Main(reloadedSettings, storageManager, adminManager, utils)
return { privateKey: app.nostr_private_key, publicKey: app.nostr_public_key, appId: app.app_id, name: app.name } await mainHandler.lnd.Warmup()
} if (!reloadedSettings.skipSanityCheck) {
})) const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd)
const liquidityProviderApp = apps.find(app => app.name === 'wallet' || app.name === 'wallet-test') await sanityChecker.VerifyEventsLog()
if (!liquidityProviderApp) { }
throw new Error("wallet app not initialized correctly") const appsData = await mainHandler.storage.applicationStorage.GetApplications()
} const defaultNames = ['wallet', 'wallet-test', reloadedSettings.defaultAppName]
const liquidityProviderInfo = { const existingWalletApp = await appsData.find(app => defaultNames.includes(app.name))
privateKey: liquidityProviderApp.privateKey, if (!existingWalletApp) {
publicKey: liquidityProviderApp.publicKey, log("no default wallet app found, creating one...")
name: "liquidity_provider", clientId: `client_${liquidityProviderApp.appId}` const newWalletApp = await mainHandler.storage.applicationStorage.AddApplication(reloadedSettings.defaultAppName, true)
} appsData.push(newWalletApp)
mainHandler.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) }
const stop = await processArgs(mainHandler) const apps: AppData[] = await Promise.all(appsData.map(app => {
if (stop) { if (!app.nostr_private_key || !app.nostr_public_key) { // TMP --
return return mainHandler.storage.applicationStorage.GenerateApplicationKeys(app);
} } // --
return { mainHandler, apps, liquidityProviderInfo, liquidityProviderApp } else {
} return { privateKey: app.nostr_private_key, publicKey: app.nostr_public_key, appId: app.app_id, name: app.name }
}
const processArgs = async (mainHandler: Main) => { }))
switch (process.argv[2]) { const liquidityProviderApp = apps.find(app => defaultNames.includes(app.name))
case 'updateUserBalance': if (!liquidityProviderApp) {
await mainHandler.storage.userStorage.UpdateUser(process.argv[3], { balance_sats: +process.argv[4] }) throw new Error("wallet app not initialized correctly")
getLogger({ userId: process.argv[3] })(`user balance updated correctly`) }
return false const liquidityProviderInfo = {
case 'unlock': privateKey: liquidityProviderApp.privateKey,
await mainHandler.storage.userStorage.UnbanUser(process.argv[3]) publicKey: liquidityProviderApp.publicKey,
getLogger({ userId: process.argv[3] })(`user unlocked`) name: "liquidity_provider", clientId: `client_${liquidityProviderApp.appId}`
return false }
default: mainHandler.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
return false const stop = await processArgs(mainHandler)
} if (stop) {
return
}
mainHandler.paymentManager.watchDog.Start()
return { mainHandler, apps, liquidityProviderInfo, liquidityProviderApp, wizard, adminManager }
}
const processArgs = async (mainHandler: Main) => {
switch (process.argv[2]) {
case 'updateUserBalance':
await mainHandler.storage.userStorage.UpdateUser(process.argv[3], { balance_sats: +process.argv[4] })
getLogger({ userId: process.argv[3] })(`user balance updated correctly`)
return false
case 'unlock':
await mainHandler.storage.userStorage.UnbanUser(process.argv[3])
getLogger({ userId: process.argv[3] })(`user unlocked`)
return false
default:
return false
}
} }

View file

@ -1,9 +1,11 @@
import { getLogger } from "../helpers/logger.js" import { getLogger } from "../helpers/logger.js"
import { LiquidityProvider } from "../lnd/liquidityProvider.js" import { Utils } from "../helpers/utilsWrapper.js"
import { LiquidityProvider } from "./liquidityProvider.js"
import LND from "../lnd/lnd.js" import LND from "../lnd/lnd.js"
import { FlashsatsLSP, LoadLSPSettingsFromEnv, LSPSettings, OlympusLSP, VoltageLSP } from "../lnd/lsp.js" import { FlashsatsLSP, LoadLSPSettingsFromEnv, LSPSettings, OlympusLSP, VoltageLSP } from "../lnd/lsp.js"
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js" import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"
import { RugPullTracker } from "./rugPullTracker.js"
export type LiquiditySettings = { export type LiquiditySettings = {
lspSettings: LSPSettings lspSettings: LSPSettings
liquidityProviderPub: string liquidityProviderPub: string
@ -18,6 +20,7 @@ export class LiquidityManager {
settings: LiquiditySettings settings: LiquiditySettings
storage: Storage storage: Storage
liquidityProvider: LiquidityProvider liquidityProvider: LiquidityProvider
rugPullTracker: RugPullTracker
lnd: LND lnd: LND
olympusLSP: OlympusLSP olympusLSP: OlympusLSP
voltageLSP: VoltageLSP voltageLSP: VoltageLSP
@ -25,82 +28,196 @@ export class LiquidityManager {
log = getLogger({ component: "liquidityManager" }) log = getLogger({ component: "liquidityManager" })
channelRequested = false channelRequested = false
channelRequesting = false channelRequesting = false
constructor(settings: LiquiditySettings, storage: Storage, liquidityProvider: LiquidityProvider, lnd: LND) { feesPaid = 0
utils: Utils
latestDrain: ({ success: true, amt: number } | { success: false, amt: number, attempt: number, at: Date }) = { success: true, amt: 0 }
drainsSkipped = 0
constructor(settings: LiquiditySettings, storage: Storage, utils: Utils, liquidityProvider: LiquidityProvider, lnd: LND, rugPullTracker: RugPullTracker) {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
this.liquidityProvider = liquidityProvider this.liquidityProvider = liquidityProvider
this.lnd = lnd this.lnd = lnd
this.rugPullTracker = rugPullTracker
this.utils = utils
this.olympusLSP = new OlympusLSP(settings.lspSettings, lnd, liquidityProvider) this.olympusLSP = new OlympusLSP(settings.lspSettings, lnd, liquidityProvider)
this.voltageLSP = new VoltageLSP(settings.lspSettings, lnd, liquidityProvider) this.voltageLSP = new VoltageLSP(settings.lspSettings, lnd, liquidityProvider)
this.flashsatsLSP = new FlashsatsLSP(settings.lspSettings, lnd, liquidityProvider) this.flashsatsLSP = new FlashsatsLSP(settings.lspSettings, lnd, liquidityProvider)
} }
GetPaidFees = () => {
this.utils.stateBundler.AddBalancePoint('feesPaidForLiquidity', this.feesPaid)
return this.feesPaid
}
onNewBlock = async () => { onNewBlock = async () => {
const balance = await this.liquidityProvider.GetLatestMaxWithdrawable() try {
const { remote } = await this.lnd.ChannelBalance() await this.shouldDrainProvider()
if (remote > balance) { } catch (err: any) {
this.log("draining provider balance to channel") this.log("error in onNewBlock", err.message || err)
const invoice = await this.lnd.NewInvoice(balance, "liqudity provider drain", defaultInvoiceExpiry)
const res = await this.liquidityProvider.PayInvoice(invoice.payRequest)
this.log("drained provider balance to channel", res.amount_paid)
} }
} }
beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => {
const { remote } = await this.lnd.ChannelBalance() if (this.settings.useOnlyLiquidityProvider) {
if (remote > amount) { return 'provider'
this.log("channel has enough balance for invoice") }
if (this.rugPullTracker.HasProviderRugPulled()) {
return 'lnd'
}
const { remote } = await this.lnd.ChannelBalance()
if (remote > amount) {
return 'lnd'
}
const providerCanHandle = this.liquidityProvider.CanProviderHandle({ action: 'receive', amount })
if (!providerCanHandle) {
return 'lnd' return 'lnd'
} }
this.log("channel does not have enough balance for invoice,suggesting provider")
return 'provider' return 'provider'
} }
afterInInvoicePaid = async () => { afterInInvoicePaid = async () => {
try {
await this.orderChannelIfNeeded()
} catch (e: any) {
this.log("error ordering channel", e)
}
}
beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => {
if (this.settings.useOnlyLiquidityProvider) {
return 'provider'
}
const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount })
if (canHandle) {
return 'provider'
}
return 'lnd'
}
afterOutInvoicePaid = async () => { }
shouldDrainProvider = async () => {
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable()
const { remote } = await this.lnd.ChannelBalance()
const drainable = Math.min(maxW, remote)
if (drainable < 500) {
return
}
if (this.latestDrain.success) {
if (this.latestDrain.amt === 0) {
await this.drainProvider(drainable)
} else {
await this.drainProvider(Math.min(drainable, this.latestDrain.amt * 2))
}
} else if (this.latestDrain.attempt * 10 < this.drainsSkipped) {
const drain = Math.min(drainable, Math.ceil(this.latestDrain.amt / 2))
this.drainsSkipped = 0
if (drain < 500) {
this.log("drain attempt went below 500 sats, will start again")
this.updateLatestDrain(true, 0)
} else {
await this.drainProvider(drain)
}
} else {
this.drainsSkipped += 1
}
}
drainProvider = async (amt: number) => {
try {
const invoice = await this.lnd.NewInvoice(amt, "liqudity provider drain", defaultInvoiceExpiry, { from: 'system', useProvider: false })
const res = await this.liquidityProvider.PayInvoice(invoice.payRequest, amt, 'system')
const fees = res.network_fee + res.service_fee
this.feesPaid += fees
this.updateLatestDrain(true, amt)
} catch (err: any) {
this.log("error draining provider balance", err.message || err)
this.updateLatestDrain(false, amt)
}
}
updateLatestDrain = (success: boolean, amt: number) => {
if (this.latestDrain.success) {
if (success) {
this.latestDrain = { success: true, amt }
} else {
this.latestDrain = { success: false, amt, attempt: 1, at: new Date() }
}
} else {
if (success) {
this.latestDrain = { success: true, amt }
} else {
this.latestDrain = { success: false, amt, attempt: this.latestDrain.attempt + 1, at: new Date() }
}
}
}
shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => {
const threshold = this.settings.lspSettings.channelThreshold
if (threshold === 0) {
return { shouldOpen: false }
}
const { remote } = await this.lnd.ChannelBalance()
if (remote > threshold) {
return { shouldOpen: false }
}
const pendingChannels = await this.lnd.ListPendingChannels()
if (pendingChannels.pendingOpenChannels.length > 0) {
return { shouldOpen: false }
}
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable()
if (maxW < threshold) {
return { shouldOpen: false }
}
return { shouldOpen: true, maxSpendable: maxW }
}
orderChannelIfNeeded = async () => {
const existingOrder = await this.storage.liquidityStorage.GetLatestLspOrder() const existingOrder = await this.storage.liquidityStorage.GetLatestLspOrder()
if (existingOrder) { if (existingOrder && existingOrder.created_at > new Date(Date.now() - 20 * 60 * 1000)) {
this.log("most recent lsp order is less than 20 minutes old")
return
}
const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) {
return return
} }
if (this.channelRequested || this.channelRequesting) { if (this.channelRequested || this.channelRequesting) {
return return
} }
this.channelRequesting = true this.channelRequesting = true
this.log("checking if channel should be requested") this.log("requesting channel from lsp")
const olympusOk = await this.olympusLSP.openChannelIfReady() const olympusOk = await this.olympusLSP.requestChannel(shouldOpen.maxSpendable)
if (olympusOk) { if (olympusOk) {
this.log("requested channel from olympus") this.log("requested channel from olympus")
this.channelRequested = true this.channelRequested = true
this.channelRequesting = false this.channelRequesting = false
this.feesPaid += olympusOk.fees
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'olympus', invoice: olympusOk.invoice, total_paid: olympusOk.totalSats, order_id: olympusOk.orderId, fees: olympusOk.fees }) await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'olympus', invoice: olympusOk.invoice, total_paid: olympusOk.totalSats, order_id: olympusOk.orderId, fees: olympusOk.fees })
return return
} }
const voltageOk = await this.voltageLSP.openChannelIfReady() const voltageOk = await this.voltageLSP.requestChannel(shouldOpen.maxSpendable)
if (voltageOk) { if (voltageOk) {
this.log("requested channel from voltage") this.log("requested channel from voltage")
this.channelRequested = true this.channelRequested = true
this.channelRequesting = false this.channelRequesting = false
this.feesPaid += voltageOk.fees
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'voltage', invoice: voltageOk.invoice, total_paid: voltageOk.totalSats, order_id: voltageOk.orderId, fees: voltageOk.fees }) await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'voltage', invoice: voltageOk.invoice, total_paid: voltageOk.totalSats, order_id: voltageOk.orderId, fees: voltageOk.fees })
return return
} }
const flashsatsOk = await this.flashsatsLSP.openChannelIfReady() const flashsatsOk = await this.flashsatsLSP.requestChannel(shouldOpen.maxSpendable)
if (flashsatsOk) { if (flashsatsOk) {
this.log("requested channel from flashsats") this.log("requested channel from flashsats")
this.channelRequested = true this.channelRequested = true
this.channelRequesting = false this.channelRequesting = false
this.feesPaid += flashsatsOk.fees
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'flashsats', invoice: flashsatsOk.invoice, total_paid: flashsatsOk.totalSats, order_id: flashsatsOk.orderId, fees: flashsatsOk.fees }) await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'flashsats', invoice: flashsatsOk.invoice, total_paid: flashsatsOk.totalSats, order_id: flashsatsOk.orderId, fees: flashsatsOk.fees })
return return
} }
this.channelRequesting = false this.channelRequesting = false
this.log("no channel requested") this.log("no channel requested")
} }
beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => {
const balance = await this.liquidityProvider.GetLatestMaxWithdrawable(true)
if (balance > amount) {
this.log("provider has enough balance for payment")
return 'provider'
}
this.log("provider does not have enough balance for payment, suggesting lnd")
return 'lnd'
}
afterOutInvoicePaid = async () => { }
} }

View file

@ -0,0 +1,338 @@
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { decodeNprofile } from '../../custom-nip19.js'
import { getLogger } from '../helpers/logger.js'
import { Utils } from '../helpers/utilsWrapper.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { relayInit } from '../nostr/tools/relay.js'
import { InvoicePaidCb } from '../lnd/settings.js'
import Storage from '../storage/index.js'
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
export class LiquidityProvider {
client: ReturnType<typeof newNostrClient>
clientCbs: Record<string, nostrCallback<any>> = {}
clientId: string = ""
myPub: string = ""
log = getLogger({ component: 'liquidityProvider' })
nostrSend: NostrSend | null = null
configured = false
pubDestination: string
ready: boolean
invoicePaidCb: InvoicePaidCb
connecting = false
configuredInterval: NodeJS.Timeout
queue: ((state: 'ready') => void)[] = []
utils: Utils
pendingPayments: Record<string, number> = {}
incrementProviderBalance: (balance: number) => Promise<void>
// make the sub process accept client
constructor(pubDestination: string, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) {
this.utils = utils
if (!pubDestination) {
this.log("No pub provider to liquidity provider, will not be initialized")
return
}
this.log("connecting to liquidity provider:", pubDestination)
this.pubDestination = pubDestination
this.invoicePaidCb = invoicePaidCb
this.incrementProviderBalance = incrementProviderBalance
this.client = newNostrClient({
pubDestination: this.pubDestination,
retrieveNostrUserAuth: async () => this.myPub,
retrieveNostrAdminAuth: async () => this.myPub,
retrieveNostrMetricsAuth: async () => this.myPub,
}, this.clientSend, this.clientSub)
this.configuredInterval = setInterval(() => {
if (this.configured) {
clearInterval(this.configuredInterval)
this.Connect()
}
}, 1000)
}
GetProviderDestination() {
return this.pubDestination
}
IsReady = () => {
return this.ready
}
AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => {
if (!this.pubDestination) {
return 'inactive'
}
if (this.ready) {
return 'ready'
}
return new Promise<'ready'>(res => {
this.queue.push(res)
})
}
Stop = () => {
clearInterval(this.configuredInterval)
}
Connect = async () => {
await new Promise(res => setTimeout(res, 2000))
const res = await this.GetUserState()
if (res.status === 'ERROR' && res.reason !== 'timeout') {
return
}
this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0)
this.ready = true
this.queue.forEach(q => q('ready'))
this.log("subbing to user operations")
this.client.GetLiveUserOperations(res => {
if (res.status === 'ERROR') {
this.log("error getting user operations", res.reason)
return
}
//this.log("got user operation", res.operation)
if (res.operation.type === Types.UserOperationType.INCOMING_INVOICE) {
this.incrementProviderBalance(res.operation.amount)
this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider')
}
})
}
GetUserState = async () => {
const res = await Promise.race([this.client.GetUserInfo(), new Promise<Types.ResultError>(res => setTimeout(() => res({ status: 'ERROR', reason: 'timeout' }), 10 * 1000))])
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return res
}
this.utils.stateBundler.AddBalancePoint('providerBalance', res.balance)
this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable)
return res
}
GetLatestMaxWithdrawable = async () => {
if (!this.ready) {
return 0
}
const res = await this.GetUserState()
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return 0
}
return res.max_withdrawable
}
GetLatestBalance = async () => {
if (!this.ready) {
return 0
}
const res = await this.GetUserState()
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return 0
}
return res.balance
}
GetPendingBalance = async () => {
return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0)
}
CalculateExpectedFeeLimit = (amount: number, info: Types.UserInfo) => {
const serviceFeeRate = info.service_fee_bps / 10000
const serviceFee = Math.ceil(serviceFeeRate * amount)
const networkMaxFeeRate = info.network_max_fee_bps / 10000
const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + info.network_max_fee_fixed)
return serviceFee + networkFeeLimit
}
CanProviderHandle = async (req: LiquidityRequest) => {
if (!this.ready) {
return false
}
const maxW = await this.GetLatestMaxWithdrawable()
if (req.action === 'spend') {
return maxW > req.amount
}
return true
}
AddInvoice = async (amount: number, memo: string, from: 'user' | 'system') => {
try {
if (!this.ready) {
throw new Error("liquidity provider is not ready yet")
}
const res = await this.client.NewInvoice({ amountSats: amount, memo })
if (res.status === 'ERROR') {
this.log("error creating invoice", res.reason)
throw new Error(res.reason)
}
this.utils.stateBundler.AddTxPoint('addedInvoice', amount, { used: 'provider', from })
return res.invoice
} catch (err) {
this.utils.stateBundler.AddTxPointFailed('addedInvoice', amount, { used: 'provider', from })
throw err
}
}
PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => {
try {
if (!this.ready) {
throw new Error("liquidity provider is not ready yet")
}
const userInfo = await this.GetUserState()
if (userInfo.status === 'ERROR') {
throw new Error(userInfo.reason)
}
this.pendingPayments[invoice] = decodedAmount + this.CalculateExpectedFeeLimit(decodedAmount, userInfo)
const res = await this.client.PayInvoice({ invoice, amount: 0 })
if (res.status === 'ERROR') {
this.log("error paying invoice", res.reason)
throw new Error(res.reason)
}
const totalPaid = res.amount_paid + res.network_fee + res.service_fee
this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] })
this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true })
return res
} catch (err) {
delete this.pendingPayments[invoice]
this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', decodedAmount, { used: 'provider', from })
throw err
}
}
GetOperations = async () => {
if (!this.ready) {
throw new Error("liquidity provider is not ready yet")
}
const res = await this.client.GetUserOperations({
latestIncomingInvoice: 0, latestOutgoingInvoice: 0,
latestIncomingTx: 0, latestOutgoingTx: 0, latestIncomingUserToUserPayment: 0,
latestOutgoingUserToUserPayment: 0, max_size: 200
})
if (res.status === 'ERROR') {
this.log("error getting operations", res.reason)
throw new Error(res.reason)
}
return res
}
setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => {
this.log("setting nostr info")
this.clientId = clientId
this.myPub = myPub
this.setSetIfConfigured()
}
attachNostrSend(f: NostrSend) {
this.log("attaching nostrSend action")
this.nostrSend = f
this.setSetIfConfigured()
}
setSetIfConfigured = () => {
if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) {
this.configured = true
this.log("configured to send to ", this.pubDestination)
}
}
onEvent = async (res: { requestId: string }, fromPub: string) => {
if (fromPub !== this.pubDestination) {
this.log("got event from invalid pub", fromPub, this.pubDestination)
return false
}
if (this.clientCbs[res.requestId]) {
const cb = this.clientCbs[res.requestId]
cb.f(res)
if (cb.type === 'single') {
delete this.clientCbs[res.requestId]
this.utils.stateBundler.AddMaxPoint('maxProviderRespTime', Date.now() - cb.startedAtMillis)
}
return true
}
return false
}
clientSend = (to: string, message: NostrRequest): Promise<any> => {
if (!this.configured || !this.nostrSend) {
throw new Error("liquidity provider not initialized")
}
if (!message.requestId) {
message.requestId = makeId(16)
}
const reqId = message.requestId
if (this.clientCbs[reqId]) {
throw new Error("request was already sent")
}
this.nostrSend({ type: 'client', clientId: this.clientId }, {
type: 'content',
pub: to,
content: JSON.stringify(message)
})
//this.nostrSend(this.relays, to, JSON.stringify(message), this.settings)
// this.log("subbing to single send", reqId, message.rpcName || 'no rpc name')
return new Promise(res => {
this.clientCbs[reqId] = {
startedAtMillis: Date.now(),
type: 'single',
f: (response: any) => { res(response) },
}
})
}
clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => {
if (!this.configured || !this.nostrSend) {
throw new Error("liquidity provider not initialized")
}
if (!message.requestId) {
message.requestId = message.rpcName
}
const reqId = message.requestId
if (!reqId) {
throw new Error("invalid sub")
}
if (this.clientCbs[reqId]) {
this.clientCbs[reqId] = {
startedAtMillis: Date.now(),
type: 'stream',
f: (response: any) => { cb(response) },
}
this.log("sub for", reqId, "was already registered, overriding")
return
}
this.nostrSend({ type: 'client', clientId: this.clientId }, {
type: 'content',
pub: to,
content: JSON.stringify(message)
})
this.log("subbing to stream", reqId)
this.clientCbs[reqId] = {
startedAtMillis: Date.now(),
type: 'stream',
f: (response: any) => { cb(response) }
}
}
getSingleSubs = () => {
return Object.entries(this.clientCbs).filter(([_, cb]) => cb.type === 'single')
}
}
export const makeId = (length: number) => {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

View file

@ -15,8 +15,10 @@ import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js' import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js'
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js' import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
import { Watchdog } from './watchdog.js' import { Watchdog } from './watchdog.js'
import { LiquidityProvider } from '../lnd/liquidityProvider.js' import { LiquidityProvider } from './liquidityProvider.js'
import { LiquidityManager } from './liquidityManager.js' import { LiquidityManager } from './liquidityManager.js'
import { Utils } from '../helpers/utilsWrapper.js'
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -49,12 +51,14 @@ export default class {
log = getLogger({ component: "PaymentManager" }) log = getLogger({ component: "PaymentManager" })
watchDog: Watchdog watchDog: Watchdog
liquidityManager: LiquidityManager liquidityManager: LiquidityManager
constructor(storage: Storage, lnd: LND, settings: MainSettings, liquidityManager: LiquidityManager, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { utils: Utils
constructor(storage: Storage, lnd: LND, settings: MainSettings, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.lnd = lnd this.lnd = lnd
this.watchDog = new Watchdog(settings.watchDogSettings, lnd, storage)
this.liquidityManager = liquidityManager this.liquidityManager = liquidityManager
this.utils = utils
this.watchDog = new Watchdog(settings.watchDogSettings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker)
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
} }
@ -113,7 +117,7 @@ export default class {
if (existingAddress) { if (existingAddress) {
return { address: existingAddress.address } return { address: existingAddress.address }
} }
const res = await this.lnd.NewAddress(req.addressType) const res = await this.lnd.NewAddress(req.addressType, { useProvider: false, from: 'user' })
const userAddress = await this.storage.paymentStorage.AddUserAddress(user, res.address, { linkedApplication: app }) const userAddress = await this.storage.paymentStorage.AddUserAddress(user, res.address, { linkedApplication: app })
this.storage.eventsLog.LogEvent({ type: 'new_address', userId: user.user_id, appUserId: "", appId: app.app_id, balance: user.balance_sats, data: res.address, amount: 0 }) this.storage.eventsLog.LogEvent({ type: 'new_address', userId: user.user_id, appUserId: "", appId: app.app_id, balance: user.balance_sats, data: res.address, amount: 0 })
return { address: userAddress.address } return { address: userAddress.address }
@ -125,8 +129,8 @@ export default class {
throw new Error("user is banned, cannot generate invoice") throw new Error("user is banned, cannot generate invoice")
} }
const use = await this.liquidityManager.beforeInvoiceCreation(req.amountSats) const use = await this.liquidityManager.beforeInvoiceCreation(req.amountSats)
const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry, use === 'provider') const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry, { useProvider: use === 'provider', from: 'user' })
const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options) const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options, res.providerDst)
const appId = options.linkedApplication ? options.linkedApplication.app_id : "" const appId = options.linkedApplication ? options.linkedApplication.app_id : ""
this.storage.eventsLog.LogEvent({ type: 'new_invoice', userId: user.user_id, appUserId: "", appId, balance: user.balance_sats, data: userInvoice.invoice, amount: req.amountSats }) this.storage.eventsLog.LogEvent({ type: 'new_invoice', userId: user.user_id, appUserId: "", appId, balance: user.balance_sats, data: userInvoice.invoice, amount: req.amountSats })
return { return {
@ -151,7 +155,6 @@ export default class {
} }
async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> { async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> {
this.log("paying invoice", req.invoice, "for user", userId, "with amount", req.amount)
await this.watchDog.PaymentRequested() await this.watchDog.PaymentRequested()
const maybeBanned = await this.storage.userStorage.GetUser(userId) const maybeBanned = await this.storage.userStorage.GetUser(userId)
if (maybeBanned.locked) { if (maybeBanned.locked) {
@ -191,7 +194,7 @@ export default class {
amount_paid: paymentInfo.amtPaid, amount_paid: paymentInfo.amtPaid,
operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`,
network_fee: paymentInfo.networkFee, network_fee: paymentInfo.networkFee,
service_fee: serviceFee service_fee: serviceFee,
} }
} }
@ -199,42 +202,60 @@ export default class {
if (this.settings.disableExternalPayments) { if (this.settings.disableExternalPayments) {
throw new Error("something went wrong sending payment, please try again later") throw new Error("something went wrong sending payment, please try again later")
} }
const existingPendingPayment = await this.storage.paymentStorage.GetPaymentOwner(invoice)
if (existingPendingPayment) {
if (existingPendingPayment.paid_at_unix > 0) {
throw new Error("this invoice was already paid")
} else if (existingPendingPayment.paid_at_unix < 0) {
throw new Error("this invoice was already paid and failed, try another invoice")
}
throw new Error("payment already in progress")
}
const { amountForLnd, payAmount, serviceFee } = amounts const { amountForLnd, payAmount, serviceFee } = amounts
const totalAmountToDecrement = payAmount + serviceFee const totalAmountToDecrement = payAmount + serviceFee
this.log("paying external invoice", invoice)
const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount)
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice)
const pendingPayment = await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, payAmount, linkedApplication) let pendingPayment: UserInvoicePayment | null = null
const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount)
try { try {
const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, use === 'provider') pendingPayment = await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, payAmount, linkedApplication)
const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount)
const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' })
if (routingFeeLimit - payment.feeSat > 0) { if (routingFeeLimit - payment.feeSat > 0) {
this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats") this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats")
await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice) await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice)
} }
await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true) await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerDst)
return { preimage: payment.paymentPreimage, amtPaid: payment.valueSat, networkFee: payment.feeSat, serialId: pendingPayment.serial_id } return { preimage: payment.paymentPreimage, amtPaid: payment.valueSat, networkFee: payment.feeSat, serialId: pendingPayment.serial_id }
} catch (err) { } catch (err) {
await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, "payment_refund:" + invoice) await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, "payment_refund:" + invoice)
await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, 0, 0, false) if (pendingPayment) {
await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, 0, 0, false)
}
throw err throw err
} }
} }
async PayInternalInvoice(userId: string, internalInvoice: UserReceivingInvoice, amounts: { payAmount: number, serviceFee: number }, linkedApplication: Application) { async PayInternalInvoice(userId: string, internalInvoice: UserReceivingInvoice, amounts: { payAmount: number, serviceFee: number }, linkedApplication: Application) {
this.log("paying internal invoice", internalInvoice.invoice)
if (internalInvoice.paid_at_unix > 0) { if (internalInvoice.paid_at_unix > 0) {
throw new Error("this invoice was already paid") throw new Error("this invoice was already paid")
} }
const { payAmount, serviceFee } = amounts const { payAmount, serviceFee } = amounts
const totalAmountToDecrement = payAmount + serviceFee const totalAmountToDecrement = payAmount + serviceFee
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, internalInvoice.invoice) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, internalInvoice.invoice)
this.invoicePaidCb(internalInvoice.invoice, payAmount, true) try {
const newPayment = await this.storage.paymentStorage.AddInternalPayment(userId, internalInvoice.invoice, payAmount, serviceFee, linkedApplication) await this.invoicePaidCb(internalInvoice.invoice, payAmount, 'internal')
return { preimage: "", amtPaid: payAmount, networkFee: 0, serialId: newPayment.serial_id } const newPayment = await this.storage.paymentStorage.AddInternalPayment(userId, internalInvoice.invoice, payAmount, serviceFee, linkedApplication)
this.utils.stateBundler.AddTxPoint('paidAnInvoice', payAmount, { used: 'internal', from: 'user' })
return { preimage: "", amtPaid: payAmount, networkFee: 0, serialId: newPayment.serial_id }
} catch (err) {
await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "internal_payment_refund:" + internalInvoice.invoice)
this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', payAmount, { used: 'internal', from: 'user' })
throw err
}
} }
@ -262,7 +283,7 @@ export default class {
// WARNING, before re-enabling this, make sure to add the tx_hash to the DecrementUserBalance "reason"!! // WARNING, before re-enabling this, make sure to add the tx_hash to the DecrementUserBalance "reason"!!
this.storage.userStorage.DecrementUserBalance(ctx.user_id, total + serviceFee, req.address) this.storage.userStorage.DecrementUserBalance(ctx.user_id, total + serviceFee, req.address)
try { try {
const payment = await this.lnd.PayAddress(req.address, req.amoutSats, req.satsPerVByte) const payment = await this.lnd.PayAddress(req.address, req.amoutSats, req.satsPerVByte, "", { useProvider: false, from: 'user' })
txId = payment.txid txId = payment.txid
} catch (err) { } catch (err) {
// WARNING, before re-enabling this, make sure to add the tx_hash to the IncrementUserBalance "reason"!! // WARNING, before re-enabling this, make sure to add the tx_hash to the IncrementUserBalance "reason"!!
@ -274,7 +295,7 @@ export default class {
txId = crypto.randomBytes(32).toString("hex") txId = crypto.randomBytes(32).toString("hex")
const addressData = `${req.address}:${txId}` const addressData = `${req.address}:${txId}`
await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData) await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData)
this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, true) this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, 'internal')
} }
if (isAppUserPayment && serviceFee > 0) { if (isAppUserPayment && serviceFee > 0) {
@ -360,11 +381,9 @@ export default class {
if (this.isDefaultServiceUrl()) { if (this.isDefaultServiceUrl()) {
throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable") throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable")
} }
getLogger({})("getting lnurl pay link")
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const key = await this.storage.paymentStorage.AddUserEphemeralKey(ctx.user_id, 'pay', app) const key = await this.storage.paymentStorage.AddUserEphemeralKey(ctx.user_id, 'pay', app)
const lnurl = this.encodeLnurl(this.lnurlPayUrl(key.key)) const lnurl = this.encodeLnurl(this.lnurlPayUrl(key.key))
getLogger({})("got lnurl pay link: ", lnurl)
return { return {
lnurl, lnurl,
k1: key.key k1: key.key

View file

@ -0,0 +1,68 @@
import FunctionQueue from "../helpers/functionQueue.js";
import { getLogger } from "../helpers/logger.js";
import { Utils } from "../helpers/utilsWrapper.js";
import { LiquidityProvider } from "./liquidityProvider.js";
import { TrackedProvider } from "../storage/entity/TrackedProvider.js";
import Storage from "../storage/index.js";
export class RugPullTracker {
liquidProvider: LiquidityProvider
storage: Storage
log = getLogger({ component: "rugPullTracker" })
rugPulled = false
constructor(storage: Storage, liquidProvider: LiquidityProvider) {
this.liquidProvider = liquidProvider
this.storage = storage
}
HasProviderRugPulled = () => {
return this.rugPulled
}
CheckProviderBalance = async (): Promise<{ balance: number, prevBalance?: number }> => {
const pubDst = this.liquidProvider.GetProviderDestination()
if (!pubDst) {
return { balance: 0 }
}
const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst)
const ready = this.liquidProvider.IsReady()
if (ready) {
const balance = await this.liquidProvider.GetLatestBalance()
const pendingBalance = await this.liquidProvider.GetPendingBalance()
const trackedBalance = balance + pendingBalance
this.log({ pendingBalance, balance, trackedBalance })
if (!providerTracker) {
this.log("starting to track provider", this.liquidProvider.GetProviderDestination())
await this.storage.liquidityStorage.CreateTrackedProvider('lnPub', pubDst, trackedBalance)
return { balance: trackedBalance }
}
return this.checkForDisruption(pubDst, trackedBalance, providerTracker)
} else {
return { balance: providerTracker?.latest_balance || 0 }
}
}
checkForDisruption = async (pubDst: string, trackedBalance: number, providerTracker: TrackedProvider) => {
const diff = trackedBalance - providerTracker.latest_balance
this.log({ latestBalance: providerTracker.latest_balance, diff })
if (diff < 0) {
this.rugPulled = true
if (providerTracker.latest_distruption_at_unix === 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnPub', pubDst, Math.floor(Date.now() / 1000))
getLogger({ component: 'rugPull' })("detected rugpull from: ", pubDst, "provider balance changed from", providerTracker.latest_balance, "to", trackedBalance, "losing", diff)
} else {
getLogger({ component: 'rugPull' })("ongoing rugpull from: ", pubDst, "provider balance changed from", providerTracker.latest_balance, "to", trackedBalance, "losing", diff)
}
} else {
this.rugPulled = false
if (providerTracker.latest_distruption_at_unix !== 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnPub', pubDst, 0)
getLogger({ component: 'rugPull' })("rugpull from: ", pubDst, "cleared after: ", Math.floor(Date.now() / 1000) - providerTracker.latest_distruption_at_unix, "seconds")
}
if (diff > 0) {
this.log("detected excees from: ", pubDst, "provider balance changed from", providerTracker.latest_balance, "to", trackedBalance, "gaining", diff)
}
}
return { balance: trackedBalance, prevBalance: providerTracker.latest_balance }
}
}

View file

@ -1,268 +1,270 @@
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import LND from "../lnd/lnd.js" import LND from "../lnd/lnd.js"
import { LoggedEvent } from '../storage/eventsLog.js' import { LoggedEvent } from '../storage/eventsLog.js'
import { Invoice, Payment } from '../../../proto/lnd/lightning'; import { Invoice, Payment } from '../../../proto/lnd/lightning';
import { getLogger } from '../helpers/logger.js'; import { getLogger } from '../helpers/logger.js';
const LN_INVOICE_REGEX = /^(lightning:)?(lnbc|lntb)[0-9a-zA-Z]+$/; import * as Types from '../../../proto/autogenerated/ts/types.js'
const BITCOIN_ADDRESS_REGEX = /^(bitcoin:)?([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-zA-HJ-NP-Z0-9]{39,59})$/; const LN_INVOICE_REGEX = /^(lightning:)?(lnbc|lntb)[0-9a-zA-Z]+$/;
type UniqueDecrementReasons = 'ban' const BITCOIN_ADDRESS_REGEX = /^(bitcoin:)?([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-zA-HJ-NP-Z0-9]{39,59})$/;
type UniqueIncrementReasons = 'fees' | 'routing_fee_refund' | 'payment_refund' type UniqueDecrementReasons = 'ban'
type CommonReasons = 'invoice' | 'address' | 'u2u' type UniqueIncrementReasons = 'fees' | 'routing_fee_refund' | 'payment_refund'
type Reason = UniqueDecrementReasons | UniqueIncrementReasons | CommonReasons type CommonReasons = 'invoice' | 'address' | 'u2u'
const incrementTwiceAllowed = ['fees', 'ban'] type Reason = UniqueDecrementReasons | UniqueIncrementReasons | CommonReasons
export default class SanityChecker { const incrementTwiceAllowed = ['fees', 'ban']
storage: Storage export default class SanityChecker {
lnd: LND storage: Storage
lnd: LND
events: LoggedEvent[] = []
invoices: Invoice[] = [] events: LoggedEvent[] = []
payments: Payment[] = [] invoices: Invoice[] = []
incrementSources: Record<string, boolean> = {} payments: Payment[] = []
decrementSources: Record<string, boolean> = {} incrementSources: Record<string, boolean> = {}
decrementEvents: Record<string, { userId: string, refund: number, failure: boolean }> = {} decrementSources: Record<string, boolean> = {}
log = getLogger({ component: "SanityChecker" }) decrementEvents: Record<string, { userId: string, refund: number, failure: boolean }> = {}
users: Record<string, { ts: number, updatedBalance: number }> = {} log = getLogger({ component: "SanityChecker" })
constructor(storage: Storage, lnd: LND) { users: Record<string, { ts: number, updatedBalance: number }> = {}
this.storage = storage constructor(storage: Storage, lnd: LND) {
this.lnd = lnd this.storage = storage
} this.lnd = lnd
}
parseDataField(data: string): { type: Reason, data: string, txHash?: string, serialId?: number } {
const parts = data.split(":") parseDataField(data: string): { type: Reason, data: string, txHash?: string, serialId?: number } {
if (parts.length === 1) { const parts = data.split(":")
const [fullData] = parts if (parts.length === 1) {
if (fullData === 'fees' || fullData === 'ban') { const [fullData] = parts
return { type: fullData, data: fullData } if (fullData === 'fees' || fullData === 'ban') {
} else if (LN_INVOICE_REGEX.test(fullData)) { return { type: fullData, data: fullData }
return { type: 'invoice', data: fullData } } else if (LN_INVOICE_REGEX.test(fullData)) {
} else if (BITCOIN_ADDRESS_REGEX.test(fullData)) { return { type: 'invoice', data: fullData }
return { type: 'address', data: fullData } } else if (BITCOIN_ADDRESS_REGEX.test(fullData)) {
} else { return { type: 'address', data: fullData }
return { type: 'u2u', data: fullData } } else {
} return { type: 'u2u', data: fullData }
} else if (parts.length === 2) { }
const [prefix, data] = parts } else if (parts.length === 2) {
if (prefix === 'routing_fee_refund' || prefix === 'payment_refund') { const [prefix, data] = parts
return { type: prefix, data } if (prefix === 'routing_fee_refund' || prefix === 'payment_refund') {
} else if (BITCOIN_ADDRESS_REGEX.test(prefix)) { return { type: prefix, data }
return { type: 'address', data: prefix, txHash: data } } else if (BITCOIN_ADDRESS_REGEX.test(prefix)) {
} else { return { type: 'address', data: prefix, txHash: data }
return { type: 'u2u', data: prefix, serialId: +data } } else {
} return { type: 'u2u', data: prefix, serialId: +data }
} }
throw new Error("unknown data format") }
} throw new Error("unknown data format")
}
async verifyDecrementEvent(e: LoggedEvent) {
if (this.decrementSources[e.data]) { async verifyDecrementEvent(e: LoggedEvent) {
throw new Error("entry decremented more that once " + e.data) if (this.decrementSources[e.data]) {
} throw new Error("entry decremented more that once " + e.data)
this.decrementSources[e.data] = !incrementTwiceAllowed.includes(e.data) }
this.users[e.userId] = this.checkUserEntry(e, this.users[e.userId]) this.decrementSources[e.data] = !incrementTwiceAllowed.includes(e.data)
const parsed = this.parseDataField(e.data) this.users[e.userId] = this.checkUserEntry(e, this.users[e.userId])
switch (parsed.type) { const parsed = this.parseDataField(e.data)
case 'ban': switch (parsed.type) {
return case 'ban':
case 'address': return
return this.validateUserTransactionPayment({ address: parsed.data, txHash: parsed.txHash, userId: e.userId }) case 'address':
case 'invoice': return this.validateUserTransactionPayment({ address: parsed.data, txHash: parsed.txHash, userId: e.userId })
return this.validateUserInvoicePayment({ invoice: parsed.data, userId: e.userId, amt: e.amount }) case 'invoice':
case 'u2u': return this.validateUserInvoicePayment({ invoice: parsed.data, userId: e.userId, amt: e.amount })
return this.validateUser2UserPayment({ fromUser: e.userId, toUser: parsed.data, serialId: parsed.serialId }) case 'u2u':
default: return this.validateUser2UserPayment({ fromUser: e.userId, toUser: parsed.data, serialId: parsed.serialId })
throw new Error("unknown decrement type " + parsed.type) default:
} throw new Error("unknown decrement type " + parsed.type)
} }
}
async validateUserTransactionPayment({ address, txHash, userId }: { userId: string, address: string, txHash?: string }) {
if (!txHash) { async validateUserTransactionPayment({ address, txHash, userId }: { userId: string, address: string, txHash?: string }) {
throw new Error("no tx hash provided to payment for address " + address) if (!txHash) {
} throw new Error("no tx hash provided to payment for address " + address)
const entry = await this.storage.paymentStorage.GetUserTransactionPaymentOwner(address, txHash) }
if (!entry) { const entry = await this.storage.paymentStorage.GetUserTransactionPaymentOwner(address, txHash)
throw new Error("no payment found for tx hash " + txHash) if (!entry) {
} throw new Error("no payment found for tx hash " + txHash)
if (entry.user.user_id !== userId) { }
throw new Error("payment user id mismatch for tx hash " + txHash) if (entry.user.user_id !== userId) {
} throw new Error("payment user id mismatch for tx hash " + txHash)
if (entry.paid_at_unix <= 0) { }
throw new Error("payment not paid for tx hash " + txHash) if (entry.paid_at_unix <= 0) {
} throw new Error("payment not paid for tx hash " + txHash)
} }
}
async validateUserInvoicePayment({ invoice, userId, amt }: { userId: string, invoice: string, amt: number }) {
const entry = await this.storage.paymentStorage.GetPaymentOwner(invoice) async validateUserInvoicePayment({ invoice, userId, amt }: { userId: string, invoice: string, amt: number }) {
if (!entry) { const entry = await this.storage.paymentStorage.GetPaymentOwner(invoice)
throw new Error("no payment found for invoice " + invoice) if (!entry) {
} throw new Error("no payment found for invoice " + invoice)
if (entry.user.user_id !== userId) { }
throw new Error("payment user id mismatch for invoice " + invoice) if (entry.user.user_id !== userId) {
} throw new Error("payment user id mismatch for invoice " + invoice)
if (entry.paid_at_unix === 0) { }
throw new Error("payment never settled for invoice " + invoice) // TODO: check if this is correct if (entry.paid_at_unix === 0) {
} throw new Error("payment never settled for invoice " + invoice) // TODO: check if this is correct
if (entry.paid_at_unix === -1) { }
this.decrementEvents[invoice] = { userId, refund: amt, failure: true } if (entry.paid_at_unix === -1) {
} else { this.decrementEvents[invoice] = { userId, refund: amt, failure: true }
const refund = amt - (entry.paid_amount + entry.routing_fees + entry.service_fees) return
this.decrementEvents[invoice] = { userId, refund, failure: false } }
} const refund = amt - (entry.paid_amount + entry.routing_fees + entry.service_fees)
if (!entry.internal) { this.decrementEvents[invoice] = { userId, refund, failure: false }
const lndEntry = this.payments.find(i => i.paymentRequest === invoice) if (!entry.internal && !entry.liquidityProvider) {
if (!lndEntry) { const lndEntry = this.payments.find(i => i.paymentRequest === invoice)
throw new Error("payment not found in lnd for invoice " + invoice) if (!lndEntry) {
} throw new Error("payment not found in lnd for invoice " + invoice)
} }
} }
}
async validateUser2UserPayment({ fromUser, toUser, serialId }: { fromUser: string, toUser: string, serialId?: number }) {
if (!serialId) { async validateUser2UserPayment({ fromUser, toUser, serialId }: { fromUser: string, toUser: string, serialId?: number }) {
throw new Error("no serial id provided to u2u payment") if (!serialId) {
} throw new Error("no serial id provided to u2u payment")
const entry = await this.storage.paymentStorage.GetUser2UserPayment(serialId) }
if (!entry) { const entry = await this.storage.paymentStorage.GetUser2UserPayment(serialId)
throw new Error("no payment u2u found for serial id " + serialId) if (!entry) {
} throw new Error("no payment u2u found for serial id " + serialId)
if (entry.from_user.user_id !== fromUser || entry.to_user.user_id !== toUser) { }
throw new Error("u2u payment user id mismatch for serial id " + serialId) if (entry.from_user.user_id !== fromUser || entry.to_user.user_id !== toUser) {
} throw new Error("u2u payment user id mismatch for serial id " + serialId)
if (entry.paid_at_unix <= 0) { }
throw new Error("payment not paid for serial id " + serialId) if (entry.paid_at_unix <= 0) {
} throw new Error("payment not paid for serial id " + serialId)
} }
}
async verifyIncrementEvent(e: LoggedEvent) {
if (this.incrementSources[e.data]) { async verifyIncrementEvent(e: LoggedEvent) {
throw new Error("entry incremented more that once " + e.data) if (this.incrementSources[e.data]) {
} throw new Error("entry incremented more that once " + e.data)
this.incrementSources[e.data] = !incrementTwiceAllowed.includes(e.data) }
this.users[e.userId] = this.checkUserEntry(e, this.users[e.userId]) this.incrementSources[e.data] = !incrementTwiceAllowed.includes(e.data)
const parsed = this.parseDataField(e.data) this.users[e.userId] = this.checkUserEntry(e, this.users[e.userId])
switch (parsed.type) { const parsed = this.parseDataField(e.data)
case 'fees': switch (parsed.type) {
return case 'fees':
case 'address': return
return this.validateAddressReceivingTransaction({ address: parsed.data, txHash: parsed.txHash, userId: e.userId }) case 'address':
case 'invoice': return this.validateAddressReceivingTransaction({ address: parsed.data, txHash: parsed.txHash, userId: e.userId })
return this.validateReceivingInvoice({ invoice: parsed.data, userId: e.userId }) case 'invoice':
case 'u2u': return this.validateReceivingInvoice({ invoice: parsed.data, userId: e.userId })
return this.validateUser2UserPayment({ fromUser: parsed.data, toUser: e.userId, serialId: parsed.serialId }) case 'u2u':
case 'routing_fee_refund': return this.validateUser2UserPayment({ fromUser: parsed.data, toUser: e.userId, serialId: parsed.serialId })
return this.validateRoutingFeeRefund({ amt: e.amount, invoice: parsed.data, userId: e.userId }) case 'routing_fee_refund':
case 'payment_refund': return this.validateRoutingFeeRefund({ amt: e.amount, invoice: parsed.data, userId: e.userId })
return this.validatePaymentRefund({ amt: e.amount, invoice: parsed.data, userId: e.userId }) case 'payment_refund':
default: return this.validatePaymentRefund({ amt: e.amount, invoice: parsed.data, userId: e.userId })
throw new Error("unknown increment type " + parsed.type) default:
} throw new Error("unknown increment type " + parsed.type)
} }
}
async validateAddressReceivingTransaction({ userId, address, txHash }: { userId: string, address: string, txHash?: string }) {
if (!txHash) { async validateAddressReceivingTransaction({ userId, address, txHash }: { userId: string, address: string, txHash?: string }) {
throw new Error("no tx hash provided to address " + address) if (!txHash) {
} throw new Error("no tx hash provided to address " + address)
const entry = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner(address, txHash) }
if (!entry) { const entry = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner(address, txHash)
throw new Error("no tx found for tx hash " + txHash) if (!entry) {
} throw new Error("no tx found for tx hash " + txHash)
if (entry.user_address.user.user_id !== userId) { }
throw new Error("tx user id mismatch for tx hash " + txHash) if (entry.user_address.user.user_id !== userId) {
} throw new Error("tx user id mismatch for tx hash " + txHash)
if (entry.paid_at_unix <= 0) { }
throw new Error("tx not paid for tx hash " + txHash) if (entry.paid_at_unix <= 0) {
} throw new Error("tx not paid for tx hash " + txHash)
} }
}
async validateReceivingInvoice({ userId, invoice }: { userId: string, invoice: string }) {
const entry = await this.storage.paymentStorage.GetInvoiceOwner(invoice) async validateReceivingInvoice({ userId, invoice }: { userId: string, invoice: string }) {
if (!entry) { const entry = await this.storage.paymentStorage.GetInvoiceOwner(invoice)
throw new Error("no invoice found for invoice " + invoice) if (!entry) {
} throw new Error("no invoice found for invoice " + invoice)
if (entry.user.user_id !== userId) { }
throw new Error("invoice user id mismatch for invoice " + invoice) if (entry.user.user_id !== userId) {
} throw new Error("invoice user id mismatch for invoice " + invoice)
if (entry.paid_at_unix <= 0) { }
throw new Error("invoice not paid for invoice " + invoice) if (entry.paid_at_unix <= 0) {
} throw new Error("invoice not paid for invoice " + invoice)
if (!entry.internal) { }
const entry = this.invoices.find(i => i.paymentRequest === invoice) if (!entry.internal && !entry.liquidityProvider) {
if (!entry) { const entry = this.invoices.find(i => i.paymentRequest === invoice)
throw new Error("invoice not found in lnd " + invoice) if (!entry) {
} throw new Error("invoice not found in lnd " + invoice)
} }
} }
}
async validateRoutingFeeRefund({ amt, invoice, userId }: { userId: string, invoice: string, amt: number }) {
const entry = this.decrementEvents[invoice] async validateRoutingFeeRefund({ amt, invoice, userId }: { userId: string, invoice: string, amt: number }) {
if (!entry) { const entry = this.decrementEvents[invoice]
throw new Error("no decrement event found for invoice routing fee refound " + invoice) if (!entry) {
} throw new Error("no decrement event found for invoice routing fee refound " + invoice)
if (entry.userId !== userId) { }
throw new Error("user id mismatch for routing fee refund " + invoice) if (entry.userId !== userId) {
} throw new Error("user id mismatch for routing fee refund " + invoice)
if (entry.failure) { }
throw new Error("payment failled, should not refund routing fees " + invoice) if (entry.failure) {
} throw new Error("payment failled, should not refund routing fees " + invoice)
if (entry.refund !== amt) { }
throw new Error("refund amount mismatch for routing fee refund " + invoice) if (entry.refund !== amt) {
} throw new Error("refund amount mismatch for routing fee refund " + invoice)
} }
}
async validatePaymentRefund({ amt, invoice, userId }: { userId: string, invoice: string, amt: number }) {
const entry = this.decrementEvents[invoice] async validatePaymentRefund({ amt, invoice, userId }: { userId: string, invoice: string, amt: number }) {
if (!entry) { const entry = this.decrementEvents[invoice]
throw new Error("no decrement event found for invoice payment refund " + invoice) if (!entry) {
} throw new Error("no decrement event found for invoice payment refund " + invoice)
if (entry.userId !== userId) { }
throw new Error("user id mismatch for payment refund " + invoice) if (entry.userId !== userId) {
} throw new Error("user id mismatch for payment refund " + invoice)
if (!entry.failure) { }
throw new Error("payment did not fail, should not refund payment " + invoice) if (!entry.failure) {
} throw new Error("payment did not fail, should not refund payment " + invoice)
if (entry.refund !== amt) { }
throw new Error("refund amount mismatch for payment refund " + invoice) if (entry.refund !== amt) {
} throw new Error("refund amount mismatch for payment refund " + invoice)
} }
}
async VerifyEventsLog() {
this.events = await this.storage.eventsLog.GetAllLogs() async VerifyEventsLog() {
this.invoices = (await this.lnd.GetAllPaidInvoices(1000)).invoices this.events = await this.storage.eventsLog.GetAllLogs()
this.payments = (await this.lnd.GetAllPayments(1000)).payments this.invoices = (await this.lnd.GetAllPaidInvoices(1000)).invoices
this.incrementSources = {} this.payments = (await this.lnd.GetAllPayments(1000)).payments
this.decrementSources = {}
this.users = {} this.incrementSources = {}
this.users = {} this.decrementSources = {}
this.decrementEvents = {} this.users = {}
for (let i = 0; i < this.events.length; i++) { this.users = {}
const e = this.events[i] this.decrementEvents = {}
if (e.type === 'balance_decrement') { for (let i = 0; i < this.events.length; i++) {
await this.verifyDecrementEvent(e) const e = this.events[i]
} else if (e.type === 'balance_increment') { if (e.type === 'balance_decrement') {
await this.verifyIncrementEvent(e) await this.verifyDecrementEvent(e)
} else { } else if (e.type === 'balance_increment') {
await this.storage.paymentStorage.VerifyDbEvent(e) await this.verifyIncrementEvent(e)
} } else {
} await this.storage.paymentStorage.VerifyDbEvent(e)
await Promise.all(Object.entries(this.users).map(async ([userId, u]) => { }
const user = await this.storage.userStorage.GetUser(userId) }
if (user.balance_sats !== u.updatedBalance) { await Promise.all(Object.entries(this.users).map(async ([userId, u]) => {
throw new Error("sanity check on balance failed, expected: " + u.updatedBalance + " found: " + user.balance_sats) const user = await this.storage.userStorage.GetUser(userId)
} if (user.balance_sats !== u.updatedBalance) {
})) throw new Error("sanity check on balance failed, expected: " + u.updatedBalance + " found: " + user.balance_sats)
} }
}))
checkUserEntry(e: LoggedEvent, u: { ts: number, updatedBalance: number } | undefined) { }
const newEntry = { ts: e.timestampMs, updatedBalance: e.balance + e.amount * (e.type === 'balance_decrement' ? -1 : 1) }
if (!u) { checkUserEntry(e: LoggedEvent, u: { ts: number, updatedBalance: number } | undefined) {
this.log(e.userId, "balance starts at", e.balance, "sats and moves by", e.amount * (e.type === 'balance_decrement' ? -1 : 1), "sats, resulting in", newEntry.updatedBalance, "sats") const newEntry = { ts: e.timestampMs, updatedBalance: e.balance + e.amount * (e.type === 'balance_decrement' ? -1 : 1) }
return newEntry if (!u) {
} this.log(e.userId, "balance starts at", e.balance, "sats and moves by", e.amount * (e.type === 'balance_decrement' ? -1 : 1), "sats, resulting in", newEntry.updatedBalance, "sats")
if (e.timestampMs < u.ts) { return newEntry
throw new Error("entry out of order " + e.timestampMs + " " + u.ts) }
} if (e.timestampMs < u.ts) {
if (e.balance !== u.updatedBalance) { throw new Error("entry out of order " + e.timestampMs + " " + u.ts)
throw new Error("inconsistent balance update got: " + e.balance + " expected " + u.updatedBalance) }
} if (e.balance !== u.updatedBalance) {
this.log(e.userId, "balance updates from", e.balance, "sats and moves by", e.amount * (e.type === 'balance_decrement' ? -1 : 1), "sats, resulting in", newEntry.updatedBalance, "sats") throw new Error("inconsistent balance update got: " + e.balance + " expected " + u.updatedBalance)
return newEntry }
} this.log(e.userId, "balance updates from", e.balance, "sats and moves by", e.amount * (e.type === 'balance_decrement' ? -1 : 1), "sats, resulting in", newEntry.updatedBalance, "sats")
return newEntry
}
} }

View file

@ -7,18 +7,22 @@ import { getLogger } from '../helpers/logger.js'
import fs from 'fs' import fs from 'fs'
import crypto from 'crypto'; import crypto from 'crypto';
import { LiquiditySettings, LoadLiquiditySettingsFromEnv } from './liquidityManager.js' import { LiquiditySettings, LoadLiquiditySettingsFromEnv } from './liquidityManager.js'
export type MainSettings = { export type MainSettings = {
storageSettings: StorageSettings, storageSettings: StorageSettings,
lndSettings: LndSettings, lndSettings: LndSettings,
watchDogSettings: WatchdogSettings, watchDogSettings: WatchdogSettings,
liquiditySettings: LiquiditySettings, liquiditySettings: LiquiditySettings,
jwtSecret: string jwtSecret: string
walletPasswordPath: string
walletSecretPath: string
incomingTxFee: number incomingTxFee: number
outgoingTxFee: number outgoingTxFee: number
incomingAppInvoiceFee: number incomingAppInvoiceFee: number
incomingAppUserInvoiceFee: number incomingAppUserInvoiceFee: number
outgoingAppInvoiceFee: number outgoingAppInvoiceFee: number
outgoingAppUserInvoiceFee: number outgoingAppUserInvoiceFee: number
outgoingAppUserInvoiceFeeBps: number
userToUserFee: number userToUserFee: number
appToUserFee: number appToUserFee: number
serviceUrl: string serviceUrl: string
@ -26,27 +30,37 @@ export type MainSettings = {
recordPerformance: boolean recordPerformance: boolean
skipSanityCheck: boolean skipSanityCheck: boolean
disableExternalPayments: boolean disableExternalPayments: boolean
wizard: boolean
defaultAppName: string
pushBackupsToNostr: boolean
} }
export type BitcoinCoreSettings = { export type BitcoinCoreSettings = {
port: number port: number
user: string user: string
pass: string pass: string
} }
export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings } export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings }
export const LoadMainSettingsFromEnv = (): MainSettings => { export const LoadMainSettingsFromEnv = (): MainSettings => {
const storageSettings = LoadStorageSettingsFromEnv() const storageSettings = LoadStorageSettingsFromEnv()
const outgoingAppUserInvoiceFeeBps = EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0)
return { return {
watchDogSettings: LoadWatchdogSettingsFromEnv(), watchDogSettings: LoadWatchdogSettingsFromEnv(),
lndSettings: LoadLndSettingsFromEnv(), lndSettings: LoadLndSettingsFromEnv(),
storageSettings: storageSettings, storageSettings: storageSettings,
liquiditySettings: LoadLiquiditySettingsFromEnv(), liquiditySettings: LoadLiquiditySettingsFromEnv(),
jwtSecret: loadJwtSecret(storageSettings.dataDir), jwtSecret: loadJwtSecret(storageSettings.dataDir),
walletSecretPath: process.env.WALLET_SECRET_PATH || getDataPath(storageSettings.dataDir, ".wallet_secret"),
walletPasswordPath: process.env.WALLET_PASSWORD_PATH || getDataPath(storageSettings.dataDir, ".wallet_password"),
incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000, incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000,
outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000, outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000,
incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000, incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000,
outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000, outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000,
incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000, incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000,
outgoingAppUserInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0) / 10000, outgoingAppUserInvoiceFeeBps,
outgoingAppUserInvoiceFee: outgoingAppUserInvoiceFeeBps / 10000,
userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000, userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000,
appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000, appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000,
serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvCanBeInteger("PORT", 1776)}`, serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvCanBeInteger("PORT", 1776)}`,
@ -54,11 +68,14 @@ export const LoadMainSettingsFromEnv = (): MainSettings => {
recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false, recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false,
skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false, skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false,
disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false, disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false,
wizard: process.env.WIZARD === 'true' || false,
defaultAppName: process.env.DEFAULT_APP_NAME || "wallet",
pushBackupsToNostr: process.env.PUSH_BACKUPS_TO_NOSTR === 'true' || false
} }
} }
export const LoadTestSettingsFromEnv = (): TestSettings => { export const LoadTestSettingsFromEnv = (): TestSettings => {
const eventLogPath = `logs/eventLogV2Test${Date.now()}.csv` const eventLogPath = `logs/eventLogV3Test${Date.now()}.csv`
const settings = LoadMainSettingsFromEnv() const settings = LoadMainSettingsFromEnv()
return { return {
...settings, ...settings,
@ -101,7 +118,7 @@ export const loadJwtSecret = (dataDir: string): string => {
return secret return secret
} }
log("JWT_SECRET not set in env, checking .jwt_secret file") log("JWT_SECRET not set in env, checking .jwt_secret file")
const secretPath = dataDir !== "" ? `${dataDir}/.jwt_secret` : ".jwt_secret" const secretPath = getDataPath(dataDir, ".jwt_secret")
try { try {
const fileContent = fs.readFileSync(secretPath, "utf-8") const fileContent = fs.readFileSync(secretPath, "utf-8")
return fileContent.trim() return fileContent.trim()
@ -111,4 +128,8 @@ export const loadJwtSecret = (dataDir: string): string => {
fs.writeFileSync(secretPath, secret) fs.writeFileSync(secretPath, secret)
return secret return secret
} }
}
export const getDataPath = (dataDir: string, dataPath: string) => {
return dataDir !== "" ? `${dataDir}/${dataPath}` : dataPath
} }

View file

@ -0,0 +1,275 @@
import fs from 'fs'
import crypto from 'crypto'
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import { credentials, Metadata } from '@grpc/grpc-js'
import { getLogger } from '../helpers/logger.js';
import { WalletUnlockerClient } from '../../../proto/lnd/walletunlocker.client.js';
import { MainSettings } from '../main/settings.js';
import { InitWalletReq } from '../lnd/initWalletReq.js';
import Storage from '../storage/index.js'
import { LightningClient } from '../../../proto/lnd/lightning.client.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
type EncryptedData = { iv: string, encrypted: string }
type Seed = { plaintextSeed: string[], encryptedSeed: EncryptedData }
export class Unlocker {
settings: MainSettings
storage: Storage
abortController = new AbortController()
subbedToBackups = false
log = getLogger({ component: "unlocker" })
constructor(settings: MainSettings, storage: Storage) {
this.settings = settings
this.storage = storage
}
Stop = () => {
this.abortController.abort()
}
getCreds = () => {
const macroonPath = this.settings.lndSettings.mainNode.lndMacaroonPath
const certPath = this.settings.lndSettings.mainNode.lndCertPath
let macaroon = ""
let lndCert: Buffer
try {
lndCert = fs.readFileSync(certPath)
} catch (err: any) {
throw new Error("failed to access lnd cert, make sure to set LND_CERT_PATH in .env, that the path is correct, and that lnd is running")
}
try {
macaroon = fs.readFileSync(macroonPath).toString('hex');
} catch (err: any) {
if (err.code !== 'ENOENT') {
throw err
}
}
return { lndCert, macaroon }
}
IsInitialized = () => {
const { macaroon } = this.getCreds()
return macaroon !== ''
}
Unlock = async (): Promise<'created' | 'unlocked' | 'noaction'> => {
const { lndCert, macaroon } = this.getCreds()
if (macaroon === "") {
const { ln, pub } = await this.InitFlow(lndCert)
this.subscribeToBackups(ln, pub)
return 'created'
}
const { ln, pub, action } = await this.UnlockFlow(lndCert, macaroon)
this.subscribeToBackups(ln, pub)
return action
}
UnlockFlow = async (lndCert: Buffer, macaroon: string): Promise<{ ln: LightningClient, pub: string, action: 'unlocked' | 'noaction' }> => {
const ln = this.GetLightningClient(lndCert, macaroon)
const info = await this.GetLndInfo(ln)
if (info.ok) {
this.log("the wallet is already unlocked with pub:", info.pub)
return { ln, pub: info.pub, action: 'noaction' }
}
if (info.failure !== 'locked') {
throw new Error("failed to get lnd info for reason: " + info.failure)
}
this.log("wallet is locked, unlocking...")
const unlocker = this.GetUnlockerClient(lndCert)
const walletPassword = this.GetWalletPassword()
await unlocker.unlockWallet({ walletPassword, recoveryWindow: 0, statelessInit: false, channelBackups: undefined }, DeadLineMetadata())
const infoAfter = await this.GetLndInfo(ln)
if (!infoAfter.ok) {
throw new Error("failed to unlock lnd wallet " + infoAfter.failure)
}
this.log("unlocked wallet with pub:", infoAfter.pub)
return { ln, pub: infoAfter.pub, action: 'unlocked' }
}
InitFlow = async (lndCert: Buffer) => {
this.log("macaroon not found, creating wallet...")
const unlocker = this.GetUnlockerClient(lndCert)
const { plaintextSeed, encryptedSeed } = await this.genSeed(unlocker)
return this.initWallet(lndCert, unlocker, { plaintextSeed, encryptedSeed })
}
genSeed = async (unlocker: WalletUnlockerClient): Promise<Seed> => {
const entropy = crypto.randomBytes(16)
const seedRes = await unlocker.genSeed({
aezeedPassphrase: Buffer.alloc(0),
seedEntropy: entropy
}, DeadLineMetadata())
this.log("seed created")
const { encryptedData } = this.EncryptWalletSeed(seedRes.response.cipherSeedMnemonic)
return { plaintextSeed: seedRes.response.cipherSeedMnemonic, encryptedSeed: encryptedData }
}
initWallet = async (lndCert: Buffer, unlocker: WalletUnlockerClient, seed: Seed) => {
const walletPw = this.GetWalletPassword()
const req = InitWalletReq(walletPw, seed.plaintextSeed)
const initRes = await unlocker.initWallet(req, DeadLineMetadata(60 * 1000))
const adminMacaroon = Buffer.from(initRes.response.adminMacaroon).toString('hex')
const ln = this.GetLightningClient(lndCert, adminMacaroon)
// Retry mechanism to ensure LND is ready
let info;
for (let i = 0; i < 10; i++) {
info = await this.GetLndInfo(ln);
if (info.ok) break;
this.log("LND not ready, retrying in 5 seconds...");
await new Promise(res => setTimeout(res, 5000));
}
if (!info || !info.ok) {
throw new Error("failed to init lnd wallet " + (info ? info.failure : "unknown error"))
}
await this.storage.liquidityStorage.SaveNodeSeed(info.pub, JSON.stringify(seed.encryptedSeed))
this.log("created wallet with pub:", info.pub)
return { ln, pub: info.pub }
}
GetLndInfo = async (ln: LightningClient): Promise<{ ok: false, failure: 'locked' | 'unknown' } | { ok: true, pub: string }> => {
while (true) {
try {
const info = await ln.getInfo({}, DeadLineMetadata())
return { ok: true, pub: info.response.identityPubkey }
} catch (err: any) {
if (err.message === '2 UNKNOWN: wallet locked, unlock it to enable full RPC access') {
this.log("wallet is locked")
return { ok: false, failure: 'locked' }
} else if (err.message === '2 UNKNOWN: the RPC server is in the process of starting up, but not yet ready to accept calls') {
this.log("lnd is not ready yet, waiting...")
await new Promise((res) => setTimeout(res, 1000))
} else {
this.log("failed to get lnd info", err.message)
return { ok: false, failure: 'unknown' }
}
}
}
}
EncryptWalletSeed = (seed: string[]) => {
return this.encrypt(seed.join('+'), true)
}
DecryptWalletSeed = (data: { iv: string, encrypted: string }) => {
return this.decrypt(data).split('+')
}
EncryptBackup = (backup: Buffer) => {
return this.encrypt(backup.toString('hex'))
}
DecryptBackup = (data: { iv: string, encrypted: string }) => {
return Buffer.from(this.decrypt(data), 'hex')
}
encrypt = (text: string, must = false) => {
const sec = this.GetWalletSecret(must)
if (!sec) {
throw new Error("wallet secret not found to encrypt")
}
const secret = Buffer.from(sec, 'hex')
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-cbc', secret, iv)
const rawData = Buffer.from(text, 'utf-8')
const cyData = cipher.update(rawData)
const encrypted = Buffer.concat([cyData, cipher.final()])
const encryptedData = { iv: iv.toString('hex'), encrypted: encrypted.toString('hex') }
return { encryptedData }
}
decrypt = (data: { iv: string, encrypted: string }) => {
const sec = this.GetWalletSecret(false)
if (!sec) {
throw new Error("wallet secret not found to decrypt")
}
const secret = Buffer.from(sec, 'hex')
const iv = Buffer.from(data.iv, 'hex')
const encrypted = Buffer.from(data.encrypted, 'hex')
const decipher = crypto.createDecipheriv('aes-256-cbc', secret, iv)
const decrypted = decipher.update(encrypted)
const raw = Buffer.concat([decrypted, decipher.final()])
return raw.toString('utf-8')
}
GetWalletSecret = (create: boolean) => {
const path = this.settings.walletSecretPath
let secret = ""
try {
secret = fs.readFileSync(path, 'utf-8')
} catch {
this.log("the wallet secret file was not found")
}
if (secret === "" && create) {
this.log("creating wallet secret file")
secret = crypto.randomBytes(32).toString('hex')
fs.writeFileSync(path, secret)
}
return secret
}
GetWalletPassword = () => {
const path = this.settings.walletPasswordPath
let password = Buffer.alloc(0)
try {
password = fs.readFileSync(path)
} catch {
}
if (password.length === 0) {
this.log("no wallet password configured, using wallet secret")
const secret = this.GetWalletSecret(false)
if (secret === "") {
throw new Error("no usable password found")
}
password = Buffer.from(secret, 'hex')
}
return password
}
subscribeToBackups = async (ln: LightningClient, pub: string) => {
if (this.subbedToBackups) {
return
}
this.subbedToBackups = true
this.log("subscribing to channel backups for: ", pub)
const stream = ln.subscribeChannelBackups({}, { abort: this.abortController.signal })
stream.responses.onMessage(async (msg) => {
if (msg.multiChanBackup) {
this.log("received backup, saving")
try {
const { encryptedData } = this.EncryptBackup(Buffer.from(msg.multiChanBackup.multiChanBackup))
await this.storage.liquidityStorage.SaveNodeBackup(pub, JSON.stringify(encryptedData))
} catch (err: any) {
this.log("failed to save backup", err.message)
}
}
})
}
GetUnlockerClient = (cert: Buffer) => {
const host = this.settings.lndSettings.mainNode.lndAddr
const channelCredentials = credentials.createSsl(cert)
const transport = new GrpcTransport({ host, channelCredentials })
const client = new WalletUnlockerClient(transport)
return client
}
GetLightningClient = (cert: Buffer, macaroon: string) => {
const host = this.settings.lndSettings.mainNode.lndAddr
const sslCreds = credentials.createSsl(cert)
const macaroonCreds = credentials.createFromMetadataGenerator(
function (args: any, callback: any) {
let metadata = new Metadata();
metadata.add('macaroon', macaroon);
callback(null, metadata);
},
);
const channelCredentials = credentials.combineChannelCredentials(
sslCreds,
macaroonCreds,
);
const transport = new GrpcTransport({ host, channelCredentials })
const client = new LightningClient(transport)
return client
}
}

View file

@ -1,10 +1,13 @@
import { EnvCanBeInteger } from "../helpers/envParser.js"; import { EnvCanBeInteger } from "../helpers/envParser.js";
import FunctionQueue from "../helpers/functionQueue.js"; import FunctionQueue from "../helpers/functionQueue.js";
import { getLogger } from "../helpers/logger.js"; import { getLogger } from "../helpers/logger.js";
import { LiquidityProvider } from "../lnd/liquidityProvider.js"; import { Utils } from "../helpers/utilsWrapper.js";
import { LiquidityProvider } from "./liquidityProvider.js";
import LND from "../lnd/lnd.js"; import LND from "../lnd/lnd.js";
import { ChannelBalance } from "../lnd/settings.js"; import { ChannelBalance } from "../lnd/settings.js";
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { LiquidityManager } from "./liquidityManager.js";
import { RugPullTracker } from "./rugPullTracker.js";
export type WatchdogSettings = { export type WatchdogSettings = {
maxDiffSats: number maxDiffSats: number
} }
@ -22,17 +25,24 @@ export class Watchdog {
accumulatedHtlcFees: number; accumulatedHtlcFees: number;
lnd: LND; lnd: LND;
liquidProvider: LiquidityProvider; liquidProvider: LiquidityProvider;
liquidityManager: LiquidityManager;
settings: WatchdogSettings; settings: WatchdogSettings;
storage: Storage; storage: Storage;
rugPullTracker: RugPullTracker
utils: Utils
latestCheckStart = 0 latestCheckStart = 0
log = getLogger({ component: "watchdog" }) log = getLogger({ component: "watchdog" })
ready = false ready = false
interval: NodeJS.Timer; interval: NodeJS.Timer;
constructor(settings: WatchdogSettings, lnd: LND, storage: Storage) { lndPubKey: string;
constructor(settings: WatchdogSettings, liquidityManager: LiquidityManager, lnd: LND, storage: Storage, utils: Utils, rugPullTracker: RugPullTracker) {
this.lnd = lnd; this.lnd = lnd;
this.settings = settings; this.settings = settings;
this.storage = storage; this.storage = storage;
this.liquidProvider = lnd.liquidProvider this.liquidProvider = lnd.liquidProvider
this.liquidityManager = liquidityManager
this.utils = utils
this.rugPullTracker = rugPullTracker
this.queue = new FunctionQueue("watchdog_queue", () => this.StartCheck()) this.queue = new FunctionQueue("watchdog_queue", () => this.StartCheck())
} }
@ -41,19 +51,30 @@ export class Watchdog {
clearInterval(this.interval) clearInterval(this.interval)
} }
} }
Start = async () => { Start = async () => {
try {
await this.StartWatching()
} catch (err: any) {
this.log("Failed to start watchdog", err.message || err)
throw err
}
}
StartWatching = async () => {
this.log("Starting watchdog")
this.startedAtUnix = Math.floor(Date.now() / 1000) this.startedAtUnix = Math.floor(Date.now() / 1000)
const info = await this.lnd.GetInfo()
this.lndPubKey = info.identityPubkey
await this.getTracker()
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.initialLndBalance = await this.getTotalLndBalance(totalUsersBalance) this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
this.initialLndBalance = await this.getAggregatedExternalBalance()
this.initialUsersBalance = totalUsersBalance this.initialUsersBalance = totalUsersBalance
const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix) const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix)
this.latestIndexOffset = fwEvents.lastOffsetIndex this.latestIndexOffset = fwEvents.lastOffsetIndex
this.accumulatedHtlcFees = 0 this.accumulatedHtlcFees = 0
this.interval = setInterval(() => { this.interval = setInterval(() => {
if (this.latestCheckStart + (1000 * 60) < Date.now()) { if (this.latestCheckStart + (1000 * 58) < Date.now()) {
this.log("No balance check was made in the last minute, checking now")
this.PaymentRequested() this.PaymentRequested()
} }
}, 1000 * 60) }, 1000 * 60)
@ -70,49 +91,42 @@ export class Watchdog {
} }
getAggregatedExternalBalance = async () => {
const totalLndBalance = await this.lnd.GetTotalBalace()
getTotalLndBalance = async (usersTotal: number) => { const feesPaidForLiquidity = this.liquidityManager.GetPaidFees()
const walletBalance = await this.lnd.GetWalletBalance() const pb = await this.rugPullTracker.CheckProviderBalance()
this.log(Number(walletBalance.confirmedBalance), "sats in chain wallet") const providerBalance = pb.prevBalance || pb.balance
const channelsBalance = await this.lnd.GetChannelBalance() return totalLndBalance + providerBalance + feesPaidForLiquidity
getLogger({ component: "debugLndBalancev3" })({ w: walletBalance, c: channelsBalance, u: usersTotal, f: this.accumulatedHtlcFees })
const totalLightningBalanceMsats = (channelsBalance.localBalance?.msat || 0n) + (channelsBalance.unsettledLocalBalance?.msat || 0n)
const totalLightningBalance = Math.ceil(Number(totalLightningBalanceMsats) / 1000)
const providerBalance = await this.liquidProvider.GetLatestBalance()
return Number(walletBalance.confirmedBalance) + totalLightningBalance + providerBalance
} }
checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => { checkBalanceUpdate = async (deltaLnd: number, deltaUsers: number) => {
this.log("LND balance update:", deltaLnd, "sats since app startup") this.utils.stateBundler.AddBalancePoint('deltaExternal', deltaLnd)
this.log("Users balance update:", deltaUsers, "sats since app startup") this.utils.stateBundler.AddBalancePoint('deltaUsers', deltaUsers)
const result = this.checkDeltas(deltaLnd, deltaUsers) const result = this.checkDeltas(deltaLnd, deltaUsers)
switch (result.type) { switch (result.type) {
case 'mismatch': case 'mismatch':
if (deltaLnd < 0) { if (deltaLnd < 0) {
this.log("WARNING! LND balance decreased while users balance increased creating a difference of", result.absoluteDiff, "sats")
if (result.absoluteDiff > this.settings.maxDiffSats) { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations") await this.updateDisruption(true, result.absoluteDiff)
return true return true
} }
} else { } else {
this.log("LND balance increased while users balance decreased creating a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection") this.updateDisruption(false, result.absoluteDiff)
return false return false
} }
break break
case 'negative': case 'negative':
if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) { if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) {
this.log("WARNING! LND balance decreased more than users balance with a difference of", result.absoluteDiff, "sats")
if (result.absoluteDiff > this.settings.maxDiffSats) { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations") await this.updateDisruption(true, result.absoluteDiff)
return true return true
} }
} else if (deltaLnd === deltaUsers) { } else if (deltaLnd === deltaUsers) {
this.log("LND and users balance went both DOWN consistently") await this.updateDisruption(false, 0)
return false return false
} else { } else {
this.log("LND balance decreased less than users balance with a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection") await this.updateDisruption(false, result.absoluteDiff)
return false return false
} }
break break
@ -120,28 +134,49 @@ export class Watchdog {
if (deltaLnd < deltaUsers) { if (deltaLnd < deltaUsers) {
this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats") this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats")
if (result.absoluteDiff > this.settings.maxDiffSats) { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations") await this.updateDisruption(true, result.absoluteDiff)
return true return true
} }
} else if (deltaLnd === deltaUsers) { } else if (deltaLnd === deltaUsers) {
this.log("LND and users balance went both UP consistently") await this.updateDisruption(false, 0)
return false return false
} else { } else {
this.log("LND balance increased more than users balance with a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection") await this.updateDisruption(false, result.absoluteDiff)
return false return false
} }
} }
return false return false
} }
updateDisruption = async (isDisrupted: boolean, absoluteDiff: number) => {
const tracker = await this.getTracker()
if (isDisrupted) {
if (tracker.latest_distruption_at_unix === 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnd', this.lndPubKey, Math.floor(Date.now() / 1000))
getLogger({ component: 'bark' })("detected lnd loss of", absoluteDiff, "sats,", absoluteDiff - this.settings.maxDiffSats, "above the max allowed")
} else {
getLogger({ component: 'bark' })("ongoing lnd loss of", absoluteDiff, "sats,", absoluteDiff - this.settings.maxDiffSats, "above the max allowed")
}
} else {
if (tracker.latest_distruption_at_unix !== 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnd', this.lndPubKey, 0)
getLogger({ component: 'bark' })("loss cleared after: ", Math.floor(Date.now() / 1000) - tracker.latest_distruption_at_unix, "seconds")
} else if (absoluteDiff > 0) {
this.log("lnd balance increased more than users balance by", absoluteDiff)
}
}
}
StartCheck = async () => { StartCheck = async () => {
this.latestCheckStart = Date.now() this.latestCheckStart = Date.now()
await this.updateAccumulatedHtlcFees() await this.updateAccumulatedHtlcFees()
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance) this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
const totalLndBalance = await this.getAggregatedExternalBalance()
this.utils.stateBundler.AddBalancePoint('accumulatedHtlcFees', this.accumulatedHtlcFees)
const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees) const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees)
const deltaUsers = totalUsersBalance - this.initialUsersBalance const deltaUsers = totalUsersBalance - this.initialUsersBalance
const deny = this.checkBalanceUpdate(deltaLnd, deltaUsers) const deny = await this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (deny) { if (deny) {
this.log("Balance mismatch detected in absolute update, locking outgoing operations") this.log("Balance mismatch detected in absolute update, locking outgoing operations")
this.lnd.LockOutgoingOperations() this.lnd.LockOutgoingOperations()
@ -151,7 +186,6 @@ export class Watchdog {
} }
PaymentRequested = async () => { PaymentRequested = async () => {
this.log("Payment requested, checking balance")
if (!this.ready) { if (!this.ready) {
throw new Error("Watchdog not ready") throw new Error("Watchdog not ready")
} }
@ -179,5 +213,13 @@ export class Watchdog {
} }
} }
} }
getTracker = async () => {
const tracker = await this.storage.liquidityStorage.GetTrackedProvider('lnd', this.lndPubKey)
if (!tracker) {
return this.storage.liquidityStorage.CreateTrackedProvider('lnd', this.lndPubKey, 0)
}
return tracker
}
} }
type DeltaCheckResult = { type: 'negative' | 'positive', absoluteDiff: number, relativeDiff: number } | { type: 'mismatch', absoluteDiff: number } type DeltaCheckResult = { type: 'negative' | 'positive', absoluteDiff: number, relativeDiff: number } | { type: 'mismatch', absoluteDiff: number }

View file

@ -18,10 +18,9 @@ export default class HtlcTracker {
} }
log = getLogger({ component: 'htlcTracker' }) log = getLogger({ component: 'htlcTracker' })
onHtlcEvent = async (htlc: HtlcEvent) => { onHtlcEvent = async (htlc: HtlcEvent) => {
getLogger({ component: 'debugHtlcs' })(htlc) //getLogger({ component: 'debugHtlcs' })(htlc)
const htlcEvent = htlc.event const htlcEvent = htlc.event
if (htlcEvent.oneofKind === 'subscribedEvent') { if (htlcEvent.oneofKind === 'subscribedEvent') {
this.log("htlc subscribed")
return return
} }
const outgoingHtlcId = Number(htlc.outgoingHtlcId) const outgoingHtlcId = Number(htlc.outgoingHtlcId)
@ -45,12 +44,11 @@ export default class HtlcTracker {
case 'settleEvent': case 'settleEvent':
return this.handleSuccess(info) return this.handleSuccess(info)
default: default:
this.log("unknown htlc event type") //this.log("unknown htlc event type")
} }
} }
handleForward = (fwe: ForwardEvent, { eventType, outgoingHtlcId, incomingHtlcId }: EventInfo) => { handleForward = (fwe: ForwardEvent, { eventType, outgoingHtlcId, incomingHtlcId }: EventInfo) => {
this.log("new forward event, currently tracked htlcs: (s,r,f)", this.pendingSendHtlcs.size, this.pendingReceiveHtlcs.size, this.pendingForwardHtlcs.size)
const { info } = fwe const { info } = fwe
const incomingAmtMsat = info ? Number(info.incomingAmtMsat) : 0 const incomingAmtMsat = info ? Number(info.incomingAmtMsat) : 0
const outgoingAmtMsat = info ? Number(info.outgoingAmtMsat) : 0 const outgoingAmtMsat = info ? Number(info.outgoingAmtMsat) : 0
@ -60,8 +58,6 @@ export default class HtlcTracker {
this.pendingReceiveHtlcs.set(incomingHtlcId, incomingAmtMsat - outgoingAmtMsat) this.pendingReceiveHtlcs.set(incomingHtlcId, incomingAmtMsat - outgoingAmtMsat)
} else if (eventType === HtlcEvent_EventType.FORWARD) { } else if (eventType === HtlcEvent_EventType.FORWARD) {
this.pendingForwardHtlcs.set(outgoingHtlcId, outgoingAmtMsat - incomingAmtMsat) this.pendingForwardHtlcs.set(outgoingHtlcId, outgoingAmtMsat - incomingAmtMsat)
} else {
this.log("unknown htlc event type for forward event")
} }
} }
@ -90,7 +86,6 @@ export default class HtlcTracker {
return this.incrementReceiveFailures(incomingChannelId) return this.incrementReceiveFailures(incomingChannelId)
} }
} }
this.log("unknown htlc event type for failure event", eventType)
} }
handleSuccess = ({ eventType, outgoingHtlcId, incomingHtlcId }: EventInfo) => { handleSuccess = ({ eventType, outgoingHtlcId, incomingHtlcId }: EventInfo) => {
@ -104,8 +99,6 @@ export default class HtlcTracker {
if (this.deleteMapEntry(outgoingHtlcId, this.pendingSendHtlcs) !== null) return if (this.deleteMapEntry(outgoingHtlcId, this.pendingSendHtlcs) !== null) return
if (this.deleteMapEntry(incomingHtlcId, this.pendingReceiveHtlcs) !== null) return if (this.deleteMapEntry(incomingHtlcId, this.pendingReceiveHtlcs) !== null) return
if (this.deleteMapEntry(outgoingHtlcId, this.pendingForwardHtlcs) !== null) return if (this.deleteMapEntry(outgoingHtlcId, this.pendingForwardHtlcs) !== null) return
} else {
this.log("unknown htlc event type for success event", eventType)
} }
} }

View file

@ -1,226 +1,224 @@
//import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools' //import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools'
import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, finishEvent, relayInit } from './tools/index.js' import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, finishEvent, relayInit } from './tools/index.js'
import { encryptData, decryptData, getSharedSecret, decodePayload, encodePayload } from './nip44.js' import { encryptData, decryptData, getSharedSecret, decodePayload, encodePayload } from './nip44.js'
import { ERROR, getLogger } from '../helpers/logger.js' import { ERROR, getLogger } from '../helpers/logger.js'
import { encodeNprofile } from '../../custom-nip19.js' import { encodeNprofile } from '../../custom-nip19.js'
const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string } type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string }
type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string } type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string }
export type SendData = { type: "content", content: string, pub: string } | { type: "event", event: UnsignedEvent } export type SendData = { type: "content", content: string, pub: string } | { type: "event", event: UnsignedEvent }
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string } export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void
export type NostrSettings = { export type NostrSettings = {
apps: AppInfo[] apps: AppInfo[]
relays: string[] relays: string[]
clients: ClientInfo[] clients: ClientInfo[]
} }
export type NostrEvent = { export type NostrEvent = {
id: string id: string
pub: string pub: string
content: string content: string
appId: string appId: string
startAtNano: string startAtNano: string
startAtMs: number startAtMs: number
} }
type SettingsRequest = { type SettingsRequest = {
type: 'settings' type: 'settings'
settings: NostrSettings settings: NostrSettings
} }
type SendRequest = { type SendRequest = {
type: 'send' type: 'send'
initiator: SendInitiator initiator: SendInitiator
data: SendData data: SendData
relays?: string[] relays?: string[]
} }
type ReadyResponse = { type ReadyResponse = {
type: 'ready' type: 'ready'
} }
type EventResponse = { type EventResponse = {
type: 'event' type: 'event'
event: NostrEvent event: NostrEvent
} }
export type ChildProcessRequest = SettingsRequest | SendRequest export type ChildProcessRequest = SettingsRequest | SendRequest
export type ChildProcessResponse = ReadyResponse | EventResponse export type ChildProcessResponse = ReadyResponse | EventResponse
const send = (message: ChildProcessResponse) => { const send = (message: ChildProcessResponse) => {
if (process.send) { if (process.send) {
process.send(message) process.send(message, undefined, undefined, err => {
} if (err) {
} getLogger({ component: "nostrMiddleware" })(ERROR, "failed to send message to parent process", err, "message:", message)
let subProcessHandler: Handler | undefined process.exit(1)
process.on("message", (message: ChildProcessRequest) => { }
switch (message.type) { })
case 'settings': }
initSubprocessHandler(message.settings) }
break let subProcessHandler: Handler | undefined
case 'send': process.on("message", (message: ChildProcessRequest) => {
sendToNostr(message.initiator, message.data, message.relays) switch (message.type) {
break case 'settings':
default: initSubprocessHandler(message.settings)
getLogger({ component: "nostrMiddleware" })(ERROR, "unknown nostr request", message) break
break case 'send':
} sendToNostr(message.initiator, message.data, message.relays)
}) break
const initSubprocessHandler = (settings: NostrSettings) => { default:
if (subProcessHandler) { getLogger({ component: "nostrMiddleware" })(ERROR, "unknown nostr request", message)
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr settings ignored since handler already exists") break
return }
} })
subProcessHandler = new Handler(settings, event => { const initSubprocessHandler = (settings: NostrSettings) => {
send({ if (subProcessHandler) {
type: 'event', getLogger({ component: "nostrMiddleware" })(ERROR, "nostr settings ignored since handler already exists")
event: event return
}) }
}) subProcessHandler = new Handler(settings, event => {
} send({
const sendToNostr: NostrSend = (initiator, data, relays) => { type: 'event',
if (!subProcessHandler) { event: event
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized") })
return })
} }
subProcessHandler.Send(initiator, data, relays) const sendToNostr: NostrSend = (initiator, data, relays) => {
} if (!subProcessHandler) {
send({ type: 'ready' }) getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized")
return
export default class Handler { }
pool = new SimplePool() subProcessHandler.Send(initiator, data, relays)
settings: NostrSettings }
subs: Sub[] = [] send({ type: 'ready' })
apps: Record<string, AppInfo> = {}
eventCallback: (event: NostrEvent) => void export default class Handler {
log = getLogger({ component: "nostrMiddleware" }) pool = new SimplePool()
constructor(settings: NostrSettings, eventCallback: (event: NostrEvent) => void) { settings: NostrSettings
this.settings = settings subs: Sub[] = []
this.log( apps: Record<string, AppInfo> = {}
{ eventCallback: (event: NostrEvent) => void
...settings, log = getLogger({ component: "nostrMiddleware" })
apps: settings.apps.map(app => { constructor(settings: NostrSettings, eventCallback: (event: NostrEvent) => void) {
const { privateKey, ...rest } = app; this.settings = settings
return { this.log("connecting to relays:", settings.relays)
...rest, this.settings.apps.forEach(app => {
nprofile: encodeNprofile({ pubkey: rest.publicKey, relays: settings.relays }) this.log("appId:", app.appId, "pubkey:", app.publicKey, "nprofile:", encodeNprofile({ pubkey: app.publicKey, relays: settings.relays }))
} })
}) this.eventCallback = eventCallback
} this.settings.apps.forEach(app => {
) this.apps[app.publicKey] = app
this.eventCallback = eventCallback })
this.settings.apps.forEach(app => { this.Connect()
this.apps[app.publicKey] = app }
})
this.Connect() async Connect() {
} const log = getLogger({})
log("conneting to relay...", this.settings.relays[0])
async Connect() { const relay = relayInit(this.settings.relays[0]) // TODO: create multiple conns for multiple relays
const log = getLogger({}) try {
log("conneting to relay...", this.settings.relays[0]) await relay.connect()
const relay = relayInit(this.settings.relays[0]) // TODO: create multiple conns for multiple relays } catch (err) {
try { log("failed to connect to relay, will try again in 2 seconds")
await relay.connect() setTimeout(() => {
} catch (err) { this.Connect()
log("failed to connect to relay, will try again in 2 seconds") }, 2000)
setTimeout(() => { return
this.Connect() }
}, 2000) log("connected, subbing...")
return relay.on('disconnect', () => {
} log("relay disconnected, will try to reconnect")
log("connected, subbing...") relay.close()
relay.on('disconnect', () => { this.Connect()
log("relay disconnected, will try to reconnect") })
relay.close() const sub = relay.sub([
this.Connect() {
}) since: Math.ceil(Date.now() / 1000),
const sub = relay.sub([ kinds: [21000],
{ '#p': Object.keys(this.apps),
since: Math.ceil(Date.now() / 1000), }
kinds: [21000], ])
'#p': Object.keys(this.apps), sub.on('eose', () => {
} log("up to date with nostr events")
]) })
sub.on('eose', () => { sub.on('event', async (e) => {
log("up to date with nostr events") if (e.kind !== 21000 || !e.pubkey) {
}) return
sub.on('event', async (e) => { }
if (e.kind !== 21000 || !e.pubkey) { const pubTags = e.tags.find(tags => tags && tags.length > 1 && tags[0] === 'p')
return if (!pubTags) {
} return
const pubTags = e.tags.find(tags => tags && tags.length > 1 && tags[0] === 'p') }
if (!pubTags) { const app = this.apps[pubTags[1]]
return if (app) {
} await this.processEvent(e, app)
const app = this.apps[pubTags[1]] return
if (app) { }
await this.processEvent(e, app) })
return }
}
}) async processEvent(e: Event<21000>, app: AppInfo) {
} const eventId = e.id
if (handledEvents.includes(eventId)) {
async processEvent(e: Event<21000>, app: AppInfo) { this.log("event already handled")
const eventId = e.id return
if (handledEvents.includes(eventId)) { }
this.log("event already handled") handledEvents.push(eventId)
return const startAtMs = Date.now()
} const startAtNano = process.hrtime.bigint().toString()
handledEvents.push(eventId) const decoded = decodePayload(e.content)
const startAtMs = Date.now() const content = await decryptData(decoded, getSharedSecret(app.privateKey, e.pubkey))
const startAtNano = process.hrtime.bigint().toString() this.eventCallback({ id: eventId, content, pub: e.pubkey, appId: app.appId, startAtNano, startAtMs })
const decoded = decodePayload(e.content) }
const content = await decryptData(decoded, getSharedSecret(app.privateKey, e.pubkey))
this.eventCallback({ id: eventId, content, pub: e.pubkey, appId: app.appId, startAtNano, startAtMs }) async Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
} const keys = this.GetSendKeys(initiator)
let toSign: UnsignedEvent
async Send(initiator: SendInitiator, data: SendData, relays?: string[]) { if (data.type === 'content') {
const keys = this.GetSendKeys(initiator) const decoded = await encryptData(data.content, getSharedSecret(keys.privateKey, data.pub))
let toSign: UnsignedEvent const content = encodePayload(decoded)
if (data.type === 'content') { toSign = {
const decoded = await encryptData(data.content, getSharedSecret(keys.privateKey, data.pub)) content,
const content = encodePayload(decoded) created_at: Math.floor(Date.now() / 1000),
toSign = { kind: 21000,
content, pubkey: keys.publicKey,
created_at: Math.floor(Date.now() / 1000), tags: [['p', data.pub]],
kind: 21000, }
pubkey: keys.publicKey, } else {
tags: [['p', data.pub]], toSign = data.event
} }
} else {
toSign = data.event const signed = finishEvent(toSign, keys.privateKey)
} let sent = false
const log = getLogger({ appName: keys.name })
const signed = finishEvent(toSign, keys.privateKey) await Promise.all(this.pool.publish(relays || this.settings.relays, signed).map(async p => {
let sent = false try {
const log = getLogger({ appName: keys.name }) await p
await Promise.all(this.pool.publish(relays || this.settings.relays, signed).map(async p => { sent = true
try { } catch (e: any) {
await p console.log(e)
sent = true log(e)
} catch (e: any) { }
log(e) }))
} if (!sent) {
})) log("failed to send event")
if (!sent) { }
log("failed to send event") }
}
} GetSendKeys(initiator: SendInitiator) {
if (initiator.type === 'app') {
GetSendKeys(initiator: SendInitiator) { const { appId } = initiator
if (initiator.type === 'app') { const found = this.settings.apps.find((info: AppInfo) => info.appId === appId)
const { appId } = initiator if (!found) {
const found = this.settings.apps.find((info: AppInfo) => info.appId === appId) throw new Error("unkown app")
if (!found) { }
throw new Error("unkown app") return found
} } else if (initiator.type === 'client') {
return found const { clientId } = initiator
} else if (initiator.type === 'client') { const found = this.settings.clients.find((info: ClientInfo) => info.clientId === clientId)
const { clientId } = initiator if (!found) {
const found = this.settings.clients.find((info: ClientInfo) => info.clientId === clientId) throw new Error("unkown client")
if (!found) { }
throw new Error("unkown client") return found
} }
return found throw new Error("unkown initiator type")
} }
throw new Error("unkown initiator type")
}
} }

View file

@ -214,6 +214,13 @@ export default (mainHandler: Main): Types.ServerMethods => {
}) })
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.applicationManager.LinkNpubThroughToken(ctx, req) return mainHandler.applicationManager.LinkNpubThroughToken(ctx, req)
} },
EnrollAdminToken: async ({ ctx, req }) => {
const err = Types.EnrollAdminTokenRequestValidate(req, {
admin_token_CustomCheck: token => token !== ''
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.PromoteUserToAdmin(ctx.app_id, ctx.app_user_id, req.admin_token)
},
} }
} }

View file

@ -9,7 +9,6 @@ import { EnvMustBeNonEmptyString } from "../helpers/envParser.js"
import { UserTransactionPayment } from "./entity/UserTransactionPayment.js" import { UserTransactionPayment } from "./entity/UserTransactionPayment.js"
import { UserBasicAuth } from "./entity/UserBasicAuth.js" import { UserBasicAuth } from "./entity/UserBasicAuth.js"
import { UserEphemeralKey } from "./entity/UserEphemeralKey.js" import { UserEphemeralKey } from "./entity/UserEphemeralKey.js"
import { Product } from "./entity/Product.js"
import { UserToUserPayment } from "./entity/UserToUserPayment.js" import { UserToUserPayment } from "./entity/UserToUserPayment.js"
import { Application } from "./entity/Application.js" import { Application } from "./entity/Application.js"
import { ApplicationUser } from "./entity/ApplicationUser.js" import { ApplicationUser } from "./entity/ApplicationUser.js"
@ -18,6 +17,9 @@ import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js"
import { getLogger } from "../helpers/logger.js" import { getLogger } from "../helpers/logger.js"
import { ChannelRouting } from "./entity/ChannelRouting.js" import { ChannelRouting } from "./entity/ChannelRouting.js"
import { LspOrder } from "./entity/LspOrder.js" import { LspOrder } from "./entity/LspOrder.js"
import { Product } from "./entity/Product.js"
import { LndNodeInfo } from "./entity/LndNodeInfo.js"
import { TrackedProvider } from "./entity/TrackedProvider.js"
export type DbSettings = { export type DbSettings = {
@ -57,7 +59,7 @@ export default async (settings: DbSettings, migrations: Function[]): Promise<{ s
database: settings.databaseFile, database: settings.databaseFile,
// logging: true, // logging: true,
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder], UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider],
//synchronize: true, //synchronize: true,
migrations migrations
}).initialize() }).initialize()

View file

@ -0,0 +1,24 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm"
import { User } from "./User.js"
@Entity()
export class LndNodeInfo {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
pubkey: string
@Column({ nullable: true })
seed?: string
@Column({ nullable: true })
backup?: string
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -0,0 +1,26 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from "typeorm"
@Entity()
@Index("tracked_provider_unique", ["provider_type", "provider_pubkey"], { unique: true })
export class TrackedProvider {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
provider_type: 'lnd' | 'lnPub'
@Column()
provider_pubkey: string
@Column()
latest_balance: number
@Column({ default: 0 })
latest_distruption_at_unix: number
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -1,42 +1,47 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"
import { User } from "./User.js" import { User } from "./User.js"
import { Application } from "./Application.js" import { Application } from "./Application.js"
@Entity() @Entity()
export class UserInvoicePayment { export class UserInvoicePayment {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
serial_id: number serial_id: number
@ManyToOne(type => User, { eager: true }) @ManyToOne(type => User, { eager: true })
@JoinColumn() @JoinColumn()
user: User user: User
@Column() @Column()
@Index({ unique: true }) @Index({ unique: true })
invoice: string invoice: string
@Column() @Column()
paid_amount: number paid_amount: number
@Column() @Column()
routing_fees: number routing_fees: number
@Column() @Column()
service_fees: number service_fees: number
@Column() @Column()
paid_at_unix: number paid_at_unix: number
@Column({ default: false }) @Column({ default: false })
internal: boolean internal: boolean
@ManyToOne(type => Application, { eager: true }) @ManyToOne(type => Application, { eager: true })
linkedApplication: Application | null linkedApplication: Application | null
@CreateDateColumn() @Column({
created_at: Date nullable: true,
})
@UpdateDateColumn() liquidityProvider?: string
updated_at: Date
} @CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -1,66 +1,71 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"
import { Product } from "./Product.js" import { Product } from "./Product.js"
import { User } from "./User.js" import { User } from "./User.js"
import { Application } from "./Application.js" import { Application } from "./Application.js"
export type ZapInfo = { export type ZapInfo = {
pub: string pub: string
eventId: string eventId: string
relays: string[] relays: string[]
description: string description: string
} }
@Entity() @Entity()
export class UserReceivingInvoice { export class UserReceivingInvoice {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
serial_id: number serial_id: number
@ManyToOne(type => User, { eager: true }) @ManyToOne(type => User, { eager: true })
@JoinColumn() @JoinColumn()
user: User user: User
@Column() @Column()
@Index({ unique: true }) @Index({ unique: true })
invoice: string invoice: string
@Column() @Column()
expires_at_unix: number expires_at_unix: number
@Column({ default: 0 }) @Column({ default: 0 })
paid_at_unix: number paid_at_unix: number
@Column({ default: false }) @Column({ default: false })
internal: boolean internal: boolean
@Column({ default: false }) @Column({ default: false })
paidByLnd: boolean paidByLnd: boolean
@Column({ default: "" }) @Column({ default: "" })
callbackUrl: string callbackUrl: string
@Column({ default: 0 }) @Column({ default: 0 })
paid_amount: number paid_amount: number
@Column({ default: 0 }) @Column({ default: 0 })
service_fee: number service_fee: number
@ManyToOne(type => Product, { eager: true }) @ManyToOne(type => Product, { eager: true })
product: Product | null product: Product | null
@ManyToOne(type => User, { eager: true }) @ManyToOne(type => User, { eager: true })
payer: User | null payer: User | null
@ManyToOne(type => Application, { eager: true }) @ManyToOne(type => Application, { eager: true })
linkedApplication: Application | null linkedApplication: Application | null
@Column({ @Column({
nullable: true, nullable: true,
type: 'simple-json' type: 'simple-json'
}) })
zap_info?: ZapInfo zap_info?: ZapInfo
@CreateDateColumn() @Column({
created_at: Date nullable: true,
})
@UpdateDateColumn() liquidityProvider?: string
updated_at: Date
} @CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -1,125 +1,125 @@
import fs from 'fs' import fs from 'fs'
import { parse, stringify } from 'csv' import { parse, stringify } from 'csv'
import { getLogger } from '../helpers/logger.js' import { getLogger } from '../helpers/logger.js'
//const eventLogPath = "logs/eventLogV2.csv" //const eventLogPath = "logs/eventLogV3.csv"
type LoggedEventType = 'new_invoice' | 'new_address' | 'address_paid' | 'invoice_paid' | 'invoice_payment' | 'address_payment' | 'u2u_receiver' | 'u2u_sender' | 'balance_increment' | 'balance_decrement' type LoggedEventType = 'new_invoice' | 'new_address' | 'address_paid' | 'invoice_paid' | 'invoice_payment' | 'address_payment' | 'u2u_receiver' | 'u2u_sender' | 'balance_increment' | 'balance_decrement'
export type LoggedEvent = { export type LoggedEvent = {
timestampMs: number timestampMs: number
userId: string userId: string
appUserId: string appUserId: string
appId: string appId: string
balance: number balance: number
type: LoggedEventType type: LoggedEventType
data: string data: string
amount: number amount: number
} }
type TimeEntry = { type TimeEntry = {
timestamp: number timestamp: number
amount: number amount: number
balance: number balance: number
userId: string userId: string
} }
const columns = ["timestampMs", "userId", "appUserId", "appId", "balance", "type", "data", "amount"] const columns = ["timestampMs", "userId", "appUserId", "appId", "balance", "type", "data", "amount"]
type StringerWrite = (chunk: any, cb: (error: Error | null | undefined) => void) => boolean type StringerWrite = (chunk: any, cb: (error: Error | null | undefined) => void) => boolean
export default class EventsLogManager { export default class EventsLogManager {
eventLogPath: string eventLogPath: string
log = getLogger({ component: "EventsLogManager" }) log = getLogger({ component: "EventsLogManager" })
stringerWrite: StringerWrite stringerWrite: StringerWrite
constructor(eventLogPath: string) { constructor(eventLogPath: string) {
this.eventLogPath = eventLogPath this.eventLogPath = eventLogPath
const exists = fs.existsSync(eventLogPath) const exists = fs.existsSync(eventLogPath)
if (!exists) { if (!exists) {
const stringer = stringify({ header: true, columns }) const stringer = stringify({ header: true, columns })
stringer.pipe(fs.createWriteStream(eventLogPath, { flags: 'a' })) stringer.pipe(fs.createWriteStream(eventLogPath, { flags: 'a' }))
this.stringerWrite = (chunk, cb) => stringer.write(chunk, cb) this.stringerWrite = (chunk, cb) => stringer.write(chunk, cb)
} else { } else {
const stringer = stringify({}) const stringer = stringify({})
stringer.pipe(fs.createWriteStream(eventLogPath, { flags: 'a' })) stringer.pipe(fs.createWriteStream(eventLogPath, { flags: 'a' }))
this.stringerWrite = (chunk, cb) => stringer.write(chunk, cb) this.stringerWrite = (chunk, cb) => stringer.write(chunk, cb)
} }
} }
LogEvent = (e: Omit<LoggedEvent, 'timestampMs'>) => { LogEvent = (e: Omit<LoggedEvent, 'timestampMs'>) => {
this.log(e.type, "->", e.userId, "->", e.appId, "->", e.appUserId, "->", e.balance, "->", e.data, "->", e.amount) //this.log(e.type, "->", e.userId, "->", e.appId, "->", e.appUserId, "->", e.balance, "->", e.data, "->", e.amount)
this.write([Date.now(), e.userId, e.appUserId, e.appId, e.balance, e.type, e.data, e.amount]) this.write([Date.now(), e.userId, e.appUserId, e.appId, e.balance, e.type, e.data, e.amount])
} }
GetAllLogs = async (path?: string): Promise<LoggedEvent[]> => { GetAllLogs = async (path?: string): Promise<LoggedEvent[]> => {
const logs = await this.Read(path) const logs = await this.Read(path)
this.log("found", logs.length, "event logs") this.log("found", logs.length, "event logs")
return logs return logs
} }
Read = async (path?: string): Promise<LoggedEvent[]> => { Read = async (path?: string): Promise<LoggedEvent[]> => {
const filePath = path ? path : this.eventLogPath const filePath = path ? path : this.eventLogPath
const exists = fs.existsSync(filePath) const exists = fs.existsSync(filePath)
if (!exists) { if (!exists) {
return [] return []
} }
return new Promise<LoggedEvent[]>((res, rej) => { return new Promise<LoggedEvent[]>((res, rej) => {
const result: LoggedEvent[] = [] const result: LoggedEvent[] = []
fs.createReadStream(filePath) fs.createReadStream(filePath)
.pipe(parse({ delimiter: ",", from_line: 2 })) .pipe(parse({ delimiter: ",", from_line: 2 }))
.on('data', data => { result.push(this.parseEvent(data)) }) .on('data', data => { result.push(this.parseEvent(data)) })
.on('error', err => { rej(err) }) .on('error', err => { rej(err) })
.on('end', () => { res(result) }) .on('end', () => { res(result) })
}) })
} }
parseEvent = (args: string[]): LoggedEvent => { parseEvent = (args: string[]): LoggedEvent => {
const [timestampMs, userId, appUserId, appId, balance, type, data, amount] = args const [timestampMs, userId, appUserId, appId, balance, type, data, amount] = args
return { timestampMs: +timestampMs, userId, appUserId, appId, balance: +balance, type: type as LoggedEventType, data, amount: +amount } return { timestampMs: +timestampMs, userId, appUserId, appId, balance: +balance, type: type as LoggedEventType, data, amount: +amount }
} }
write = async (args: (string | number)[]) => { write = async (args: (string | number)[]) => {
return new Promise<void>((res, rej) => { return new Promise<void>((res, rej) => {
this.stringerWrite(args, err => { this.stringerWrite(args, err => {
if (err) { if (err) {
rej(err) rej(err)
} else { res() } } else { res() }
}) })
}) })
} }
ignoredKeys = ['fees', "bc1qkafgye62h2zhzlwtrga6jytz2p7af4lg8fwqt6", "6eb1d279f95377b8514aad3b79ff1cddbe9f5d3b95653b55719850df9df63821", "b11585413bfa7bf65a5f1263e3100e53b4c9afe6b5d8c94c6b85017dfcbf3d49"] ignoredKeys = ['fees', "bc1qkafgye62h2zhzlwtrga6jytz2p7af4lg8fwqt6", "6eb1d279f95377b8514aad3b79ff1cddbe9f5d3b95653b55719850df9df63821", "b11585413bfa7bf65a5f1263e3100e53b4c9afe6b5d8c94c6b85017dfcbf3d49"]
createTimeSeries = (events: LoggedEvent[]) => { createTimeSeries = (events: LoggedEvent[]) => {
const dataAppIds: Record<string, string> = {} const dataAppIds: Record<string, string> = {}
const order: { timestamp: number, data: string, type: 'inc' | 'dec' }[] = [] const order: { timestamp: number, data: string, type: 'inc' | 'dec' }[] = []
const incrementEntries: Record<string, TimeEntry> = {} const incrementEntries: Record<string, TimeEntry> = {}
const decrementEntries: Record<string, TimeEntry> = {} const decrementEntries: Record<string, TimeEntry> = {}
events.forEach(e => { events.forEach(e => {
if (this.ignoredKeys.includes(e.data)) { if (this.ignoredKeys.includes(e.data)) {
return return
} }
if (e.type === 'balance_increment') { if (e.type === 'balance_increment') {
if (incrementEntries[e.data]) { if (incrementEntries[e.data]) {
throw new Error("increment duplicate! " + e.data) throw new Error("increment duplicate! " + e.data)
} }
incrementEntries[e.data] = { timestamp: e.timestampMs, balance: e.balance, amount: e.amount, userId: e.userId } incrementEntries[e.data] = { timestamp: e.timestampMs, balance: e.balance, amount: e.amount, userId: e.userId }
order.push({ timestamp: e.timestampMs, data: e.data, type: 'inc' }) order.push({ timestamp: e.timestampMs, data: e.data, type: 'inc' })
} else if (e.type === 'balance_decrement') { } else if (e.type === 'balance_decrement') {
if (decrementEntries[e.data]) { if (decrementEntries[e.data]) {
throw new Error("decrement duplicate! " + e.data) throw new Error("decrement duplicate! " + e.data)
} }
decrementEntries[e.data] = { timestamp: e.timestampMs, balance: e.balance, amount: e.amount, userId: e.userId } decrementEntries[e.data] = { timestamp: e.timestampMs, balance: e.balance, amount: e.amount, userId: e.userId }
order.push({ timestamp: e.timestampMs, data: e.data, type: 'dec' }) order.push({ timestamp: e.timestampMs, data: e.data, type: 'dec' })
} else if (e.appId) { } else if (e.appId) {
dataAppIds[e.data] = e.appId dataAppIds[e.data] = e.appId
} }
}) })
const full = order.map(o => { const full = order.map(o => {
const { type } = o const { type } = o
if (type === 'inc') { if (type === 'inc') {
const entry = incrementEntries[o.data] const entry = incrementEntries[o.data]
return { timestamp: entry.timestamp, amount: entry.amount, balance: entry.balance, userId: entry.userId, appId: dataAppIds[o.data], internal: !!decrementEntries[o.data] } return { timestamp: entry.timestamp, amount: entry.amount, balance: entry.balance, userId: entry.userId, appId: dataAppIds[o.data], internal: !!decrementEntries[o.data] }
} else { } else {
const entry = decrementEntries[o.data] const entry = decrementEntries[o.data]
return { timestamp: entry.timestamp, amount: -entry.amount, balance: entry.balance, userId: entry.userId, appId: dataAppIds[o.data], internal: !!incrementEntries[o.data] } return { timestamp: entry.timestamp, amount: -entry.amount, balance: entry.balance, userId: entry.userId, appId: dataAppIds[o.data], internal: !!incrementEntries[o.data] }
} }
}) })
full.sort((a, b) => a.timestamp - b.timestamp) full.sort((a, b) => a.timestamp - b.timestamp)
fs.writeFileSync("timeSeries.json", JSON.stringify(full, null, 2)) fs.writeFileSync("timeSeries.json", JSON.stringify(full, null, 2))
} }
} }

View file

@ -1,4 +1,5 @@
import { DataSource, EntityManager } from "typeorm" import { DataSource, EntityManager } from "typeorm"
import fs from 'fs'
import NewDB, { DbSettings, LoadDbSettingsFromEnv } from "./db.js" import NewDB, { DbSettings, LoadDbSettingsFromEnv } from "./db.js"
import ProductStorage from './productStorage.js' import ProductStorage from './productStorage.js'
import ApplicationStorage from './applicationStorage.js' import ApplicationStorage from './applicationStorage.js'
@ -8,13 +9,14 @@ import MetricsStorage from "./metricsStorage.js";
import TransactionsQueue, { TX } from "./transactionsQueue.js"; import TransactionsQueue, { TX } from "./transactionsQueue.js";
import EventsLogManager from "./eventsLog.js"; import EventsLogManager from "./eventsLog.js";
import { LiquidityStorage } from "./liquidityStorage.js"; import { LiquidityStorage } from "./liquidityStorage.js";
import { StateBundler } from "./stateBundler.js";
export type StorageSettings = { export type StorageSettings = {
dbSettings: DbSettings dbSettings: DbSettings
eventLogPath: string eventLogPath: string
dataDir: string dataDir: string
} }
export const LoadStorageSettingsFromEnv = (): StorageSettings => { export const LoadStorageSettingsFromEnv = (): StorageSettings => {
return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV2.csv", dataDir: process.env.DATA_DIR || "" } return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV3.csv", dataDir: process.env.DATA_DIR || "" }
} }
export default class { export default class {
DB: DataSource | EntityManager DB: DataSource | EntityManager
@ -27,6 +29,7 @@ export default class {
metricsStorage: MetricsStorage metricsStorage: MetricsStorage
liquidityStorage: LiquidityStorage liquidityStorage: LiquidityStorage
eventsLog: EventsLogManager eventsLog: EventsLogManager
stateBundler: StateBundler
constructor(settings: StorageSettings) { constructor(settings: StorageSettings) {
this.settings = settings this.settings = settings
this.eventsLog = new EventsLogManager(settings.eventLogPath) this.eventsLog = new EventsLogManager(settings.eventLogPath)
@ -41,6 +44,7 @@ export default class {
this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue) this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue)
this.metricsStorage = new MetricsStorage(this.settings) this.metricsStorage = new MetricsStorage(this.settings)
this.liquidityStorage = new LiquidityStorage(this.DB, this.txQueue) this.liquidityStorage = new LiquidityStorage(this.DB, this.txQueue)
try { if (this.settings.dataDir) fs.mkdirSync(this.settings.dataDir) } catch (e) { }
const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations) const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations)
return { executedMigrations, executedMetricsMigrations }; return { executedMigrations, executedMetricsMigrations };
} }

View file

@ -1,6 +1,8 @@
import { DataSource, EntityManager, MoreThan } from "typeorm" import { DataSource, EntityManager, MoreThan } from "typeorm"
import { LspOrder } from "./entity/LspOrder.js"; import { LspOrder } from "./entity/LspOrder.js";
import TransactionsQueue, { TX } from "./transactionsQueue.js"; import TransactionsQueue, { TX } from "./transactionsQueue.js";
import { LndNodeInfo } from "./entity/LndNodeInfo.js";
import { TrackedProvider } from "./entity/TrackedProvider.js";
export class LiquidityStorage { export class LiquidityStorage {
DB: DataSource | EntityManager DB: DataSource | EntityManager
txQueue: TransactionsQueue txQueue: TransactionsQueue
@ -17,4 +19,46 @@ export class LiquidityStorage {
const entry = this.DB.getRepository(LspOrder).create(order) const entry = this.DB.getRepository(LspOrder).create(order)
return this.txQueue.PushToQueue<LspOrder>({ exec: async db => db.getRepository(LspOrder).save(entry), dbTx: false }) return this.txQueue.PushToQueue<LspOrder>({ exec: async db => db.getRepository(LspOrder).save(entry), dbTx: false })
} }
async SaveNodeSeed(pubkey: string, seed: string) {
const existing = await this.DB.getRepository(LndNodeInfo).findOne({ where: { pubkey } })
if (existing) {
throw new Error("A seed already exists for this pub key")
}
const entry = this.DB.getRepository(LndNodeInfo).create({ pubkey, seed })
return this.txQueue.PushToQueue<LndNodeInfo>({ exec: async db => db.getRepository(LndNodeInfo).save(entry), dbTx: false })
}
async SaveNodeBackup(pubkey: string, backup: string) {
const existing = await this.DB.getRepository(LndNodeInfo).findOne({ where: { pubkey } })
if (existing) {
await this.DB.getRepository(LndNodeInfo).update(existing.serial_id, { backup })
return
}
const entry = this.DB.getRepository(LndNodeInfo).create({ pubkey, backup })
await this.txQueue.PushToQueue<LndNodeInfo>({ exec: async db => db.getRepository(LndNodeInfo).save(entry), dbTx: false })
}
async GetTrackedProvider(providerType: 'lnd' | 'lnPub', pub: string) {
return this.DB.getRepository(TrackedProvider).findOne({ where: { provider_pubkey: pub, provider_type: providerType } })
}
async CreateTrackedProvider(providerType: 'lnd' | 'lnPub', pub: string, latestBalance = 0) {
const entry = this.DB.getRepository(TrackedProvider).create({ provider_pubkey: pub, provider_type: providerType, latest_balance: latestBalance })
return this.txQueue.PushToQueue<TrackedProvider>({ exec: async db => db.getRepository(TrackedProvider).save(entry), dbTx: false })
}
async UpdateTrackedProviderBalance(providerType: 'lnd' | 'lnPub', pub: string, latestBalance: number) {
console.log("updating tracked balance:", latestBalance)
return this.DB.getRepository(TrackedProvider).update({ provider_pubkey: pub, provider_type: providerType }, { latest_balance: latestBalance })
}
async IncrementTrackedProviderBalance(providerType: 'lnd' | 'lnPub', pub: string, amount: number) {
if (amount < 0) {
return this.DB.getRepository(TrackedProvider).increment({ provider_pubkey: pub, provider_type: providerType }, "latest_balance", amount)
} else {
return this.DB.getRepository(TrackedProvider).decrement({ provider_pubkey: pub, provider_type: providerType }, "latest_balance", -amount)
}
}
async UpdateTrackedProviderDisruption(providerType: 'lnd' | 'lnPub', pub: string, latestDisruptionAtUnix: number) {
return this.DB.getRepository(TrackedProvider).update({ provider_pubkey: pub, provider_type: providerType }, { latest_distruption_at_unix: latestDisruptionAtUnix })
}
} }

View file

@ -0,0 +1,36 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class LiquidityProvider1719335699480 implements MigrationInterface {
name = 'LiquidityProvider1719335699480'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_a131e6b58f084f1340538681b5"`);
await queryRunner.query(`CREATE TABLE "temporary_user_receiving_invoice" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "expires_at_unix" integer NOT NULL, "paid_at_unix" integer NOT NULL DEFAULT (0), "internal" boolean NOT NULL DEFAULT (0), "paidByLnd" boolean NOT NULL DEFAULT (0), "callbackUrl" varchar NOT NULL DEFAULT (''), "paid_amount" integer NOT NULL DEFAULT (0), "service_fee" integer NOT NULL DEFAULT (0), "zap_info" text, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "productProductId" varchar, "payerSerialId" integer, "linkedApplicationSerialId" integer, "liquidityProvider" varchar, CONSTRAINT "FK_714a8b7d4f89f8a802ca181b789" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_d4bb1e4c60e8a869f1f43ca2e31" FOREIGN KEY ("payerSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_5263bde2a519db9ea608b702ec8" FOREIGN KEY ("productProductId") REFERENCES "product" ("product_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_2c0dfb3483f3e5e7e3cdd5dc71f" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_user_receiving_invoice"("serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId") SELECT "serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId" FROM "user_receiving_invoice"`);
await queryRunner.query(`DROP TABLE "user_receiving_invoice"`);
await queryRunner.query(`ALTER TABLE "temporary_user_receiving_invoice" RENAME TO "user_receiving_invoice"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a131e6b58f084f1340538681b5" ON "user_receiving_invoice" ("invoice") `);
await queryRunner.query(`DROP INDEX "IDX_a609a4d3d8d9b07b90692a3c45"`);
await queryRunner.query(`CREATE TABLE "temporary_user_invoice_payment" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "paid_amount" integer NOT NULL, "routing_fees" integer NOT NULL, "service_fees" integer NOT NULL, "paid_at_unix" integer NOT NULL, "internal" boolean NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "linkedApplicationSerialId" integer, "liquidityProvider" varchar, CONSTRAINT "FK_6bcac90887eea1dc61d37db2994" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ef2aa6761ab681bbbd5f94e0fcb" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_user_invoice_payment"("serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId") SELECT "serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId" FROM "user_invoice_payment"`);
await queryRunner.query(`DROP TABLE "user_invoice_payment"`);
await queryRunner.query(`ALTER TABLE "temporary_user_invoice_payment" RENAME TO "user_invoice_payment"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a609a4d3d8d9b07b90692a3c45" ON "user_invoice_payment" ("invoice") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_a609a4d3d8d9b07b90692a3c45"`);
await queryRunner.query(`ALTER TABLE "user_invoice_payment" RENAME TO "temporary_user_invoice_payment"`);
await queryRunner.query(`CREATE TABLE "user_invoice_payment" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "paid_amount" integer NOT NULL, "routing_fees" integer NOT NULL, "service_fees" integer NOT NULL, "paid_at_unix" integer NOT NULL, "internal" boolean NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "linkedApplicationSerialId" integer, CONSTRAINT "FK_6bcac90887eea1dc61d37db2994" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ef2aa6761ab681bbbd5f94e0fcb" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "user_invoice_payment"("serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId") SELECT "serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId" FROM "temporary_user_invoice_payment"`);
await queryRunner.query(`DROP TABLE "temporary_user_invoice_payment"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a609a4d3d8d9b07b90692a3c45" ON "user_invoice_payment" ("invoice") `);
await queryRunner.query(`DROP INDEX "IDX_a131e6b58f084f1340538681b5"`);
await queryRunner.query(`ALTER TABLE "user_receiving_invoice" RENAME TO "temporary_user_receiving_invoice"`);
await queryRunner.query(`CREATE TABLE "user_receiving_invoice" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "expires_at_unix" integer NOT NULL, "paid_at_unix" integer NOT NULL DEFAULT (0), "internal" boolean NOT NULL DEFAULT (0), "paidByLnd" boolean NOT NULL DEFAULT (0), "callbackUrl" varchar NOT NULL DEFAULT (''), "paid_amount" integer NOT NULL DEFAULT (0), "service_fee" integer NOT NULL DEFAULT (0), "zap_info" text, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "productProductId" varchar, "payerSerialId" integer, "linkedApplicationSerialId" integer, CONSTRAINT "FK_714a8b7d4f89f8a802ca181b789" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_d4bb1e4c60e8a869f1f43ca2e31" FOREIGN KEY ("payerSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_5263bde2a519db9ea608b702ec8" FOREIGN KEY ("productProductId") REFERENCES "product" ("product_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_2c0dfb3483f3e5e7e3cdd5dc71f" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "user_receiving_invoice"("serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId") SELECT "serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId" FROM "temporary_user_receiving_invoice"`);
await queryRunner.query(`DROP TABLE "temporary_user_receiving_invoice"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a131e6b58f084f1340538681b5" ON "user_receiving_invoice" ("invoice") `);
}
}

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class LndNodeInfo1720187506189 implements MigrationInterface {
name = 'LndNodeInfo1720187506189'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "lnd_node_info" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "pubkey" varchar NOT NULL, "seed" varchar, "backup" varchar, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "lnd_node_info"`);
}
}

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TrackedProvider1720814323679 implements MigrationInterface {
name = 'TrackedProvider1720814323679'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "tracked_provider" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "provider_type" varchar NOT NULL, "provider_pubkey" varchar NOT NULL, "latest_balance" integer NOT NULL, "latest_distruption_at_unix" integer NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
await queryRunner.query(`CREATE UNIQUE INDEX "tracked_provider_unique" ON "tracked_provider" ("provider_type", "provider_pubkey") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "tracked_provider_unique"`);
await queryRunner.query(`DROP TABLE "tracked_provider"`);
}
}

View file

@ -5,7 +5,10 @@ import { Initial1703170309875 } from './1703170309875-initial.js'
import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js' import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js'
import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js' import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js'
import { LspOrder1718387847693 } from './1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './1718387847693-lsp_order.js'
const allMigrations = [Initial1703170309875, LspOrder1718387847693] import { LiquidityProvider1719335699480 } from './1719335699480-liquidity_provider.js'
import { LndNodeInfo1720187506189 } from './1720187506189-lnd_node_info.js'
import { TrackedProvider1720814323679 } from './1720814323679-tracked_provider.js'
const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679]
const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538] const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538]
export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => { export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
if (arg === 'fake_initial_migration') { if (arg === 'fake_initial_migration') {

View file

@ -1,417 +1,419 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { Between, DataSource, EntityManager, FindOperator, IsNull, LessThanOrEqual, MoreThan, MoreThanOrEqual } from "typeorm" import { Between, DataSource, EntityManager, FindOperator, IsNull, LessThanOrEqual, MoreThan, MoreThanOrEqual } from "typeorm"
import { User } from './entity/User.js'; import { User } from './entity/User.js';
import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js';
import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js';
import { UserReceivingInvoice, ZapInfo } from './entity/UserReceivingInvoice.js'; import { UserReceivingInvoice, ZapInfo } from './entity/UserReceivingInvoice.js';
import { UserReceivingAddress } from './entity/UserReceivingAddress.js'; import { UserReceivingAddress } from './entity/UserReceivingAddress.js';
import { Product } from './entity/Product.js'; import { Product } from './entity/Product.js';
import UserStorage from './userStorage.js'; import UserStorage from './userStorage.js';
import { AddressReceivingTransaction } from './entity/AddressReceivingTransaction.js'; import { AddressReceivingTransaction } from './entity/AddressReceivingTransaction.js';
import { UserInvoicePayment } from './entity/UserInvoicePayment.js'; import { UserInvoicePayment } from './entity/UserInvoicePayment.js';
import { UserToUserPayment } from './entity/UserToUserPayment.js'; import { UserToUserPayment } from './entity/UserToUserPayment.js';
import { Application } from './entity/Application.js'; import { Application } from './entity/Application.js';
import TransactionsQueue from "./transactionsQueue.js"; import TransactionsQueue from "./transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js'; import { LoggedEvent } from './eventsLog.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo } export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo }
export const defaultInvoiceExpiry = 60 * 60 export const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
DB: DataSource | EntityManager DB: DataSource | EntityManager
userStorage: UserStorage userStorage: UserStorage
txQueue: TransactionsQueue txQueue: TransactionsQueue
constructor(DB: DataSource | EntityManager, userStorage: UserStorage, txQueue: TransactionsQueue) { constructor(DB: DataSource | EntityManager, userStorage: UserStorage, txQueue: TransactionsQueue) {
this.DB = DB this.DB = DB
this.userStorage = userStorage this.userStorage = userStorage
this.txQueue = txQueue this.txQueue = txQueue
} }
async AddAddressReceivingTransaction(address: UserReceivingAddress, txHash: string, outputIndex: number, amount: number, serviceFee: number, internal: boolean, height: number, dbTx: EntityManager | DataSource) { async AddAddressReceivingTransaction(address: UserReceivingAddress, txHash: string, outputIndex: number, amount: number, serviceFee: number, internal: boolean, height: number, dbTx: EntityManager | DataSource) {
const newAddressTransaction = dbTx.getRepository(AddressReceivingTransaction).create({ const newAddressTransaction = dbTx.getRepository(AddressReceivingTransaction).create({
user_address: address, user_address: address,
tx_hash: txHash, tx_hash: txHash,
output_index: outputIndex, output_index: outputIndex,
paid_amount: amount, paid_amount: amount,
service_fee: serviceFee, service_fee: serviceFee,
paid_at_unix: Math.floor(Date.now() / 1000), paid_at_unix: Math.floor(Date.now() / 1000),
internal, internal,
broadcast_height: height, broadcast_height: height,
confs: internal ? 10 : 0 confs: internal ? 10 : 0
}) })
return dbTx.getRepository(AddressReceivingTransaction).save(newAddressTransaction) return dbTx.getRepository(AddressReceivingTransaction).save(newAddressTransaction)
} }
GetUserReceivingTransactions(userId: string, fromIndex: number, take = 50, entityManager = this.DB): Promise<AddressReceivingTransaction[]> { GetUserReceivingTransactions(userId: string, fromIndex: number, take = 50, entityManager = this.DB): Promise<AddressReceivingTransaction[]> {
return entityManager.getRepository(AddressReceivingTransaction).find({ return entityManager.getRepository(AddressReceivingTransaction).find({
where: { where: {
user_address: { user: { user_id: userId } }, user_address: { user: { user_id: userId } },
serial_id: MoreThanOrEqual(fromIndex), serial_id: MoreThanOrEqual(fromIndex),
paid_at_unix: MoreThan(0), paid_at_unix: MoreThan(0),
}, },
order: { order: {
paid_at_unix: 'DESC' paid_at_unix: 'DESC'
}, },
take take
}) })
} }
async GetExistingUserAddress(userId: string, linkedApplication: Application, entityManager = this.DB) { async GetExistingUserAddress(userId: string, linkedApplication: Application, entityManager = this.DB) {
return entityManager.getRepository(UserReceivingAddress).findOne({ where: { user: { user_id: userId }, linkedApplication: { app_id: linkedApplication.app_id } } }) return entityManager.getRepository(UserReceivingAddress).findOne({ where: { user: { user_id: userId }, linkedApplication: { app_id: linkedApplication.app_id } } })
} }
async AddUserAddress(user: User, address: string, opts: { callbackUrl?: string, linkedApplication?: Application } = {}): Promise<UserReceivingAddress> { async AddUserAddress(user: User, address: string, opts: { callbackUrl?: string, linkedApplication?: Application } = {}): Promise<UserReceivingAddress> {
const newUserAddress = this.DB.getRepository(UserReceivingAddress).create({ const newUserAddress = this.DB.getRepository(UserReceivingAddress).create({
address, address,
callbackUrl: opts.callbackUrl || "", callbackUrl: opts.callbackUrl || "",
linkedApplication: opts.linkedApplication, linkedApplication: opts.linkedApplication,
user user
}) })
return this.txQueue.PushToQueue<UserReceivingAddress>({ exec: async db => db.getRepository(UserReceivingAddress).save(newUserAddress), dbTx: false, description: `add address for ${user.user_id} linked to ${opts.linkedApplication?.app_id}: ${address} ` }) return this.txQueue.PushToQueue<UserReceivingAddress>({ exec: async db => db.getRepository(UserReceivingAddress).save(newUserAddress), dbTx: false, description: `add address for ${user.user_id} linked to ${opts.linkedApplication?.app_id}: ${address} ` })
} }
async FlagInvoiceAsPaid(invoice: UserReceivingInvoice, amount: number, serviceFee: number, internal: boolean, dbTx: EntityManager | DataSource) { async FlagInvoiceAsPaid(invoice: UserReceivingInvoice, amount: number, serviceFee: number, internal: boolean, dbTx: EntityManager | DataSource) {
const i: Partial<UserReceivingInvoice> = { paid_at_unix: Math.floor(Date.now() / 1000), paid_amount: amount, service_fee: serviceFee, internal } const i: Partial<UserReceivingInvoice> = { paid_at_unix: Math.floor(Date.now() / 1000), paid_amount: amount, service_fee: serviceFee, internal }
if (!internal) { if (!internal) {
i.paidByLnd = true i.paidByLnd = true
} }
return dbTx.getRepository(UserReceivingInvoice).update(invoice.serial_id, i) return dbTx.getRepository(UserReceivingInvoice).update(invoice.serial_id, i)
} }
GetUserInvoicesFlaggedAsPaid(userId: string, fromIndex: number, take = 50, entityManager = this.DB): Promise<UserReceivingInvoice[]> { GetUserInvoicesFlaggedAsPaid(userId: string, fromIndex: number, take = 50, entityManager = this.DB): Promise<UserReceivingInvoice[]> {
return entityManager.getRepository(UserReceivingInvoice).find({ return entityManager.getRepository(UserReceivingInvoice).find({
where: { where: {
user: { user: {
user_id: userId user_id: userId
}, },
serial_id: MoreThanOrEqual(fromIndex), serial_id: MoreThanOrEqual(fromIndex),
paid_at_unix: MoreThan(0), paid_at_unix: MoreThan(0),
}, },
order: { order: {
paid_at_unix: 'DESC' paid_at_unix: 'DESC'
}, },
take take
}) })
} }
async AddUserInvoice(user: User, invoice: string, options: InboundOptionals = { expiry: defaultInvoiceExpiry }): Promise<UserReceivingInvoice> { async AddUserInvoice(user: User, invoice: string, options: InboundOptionals = { expiry: defaultInvoiceExpiry }, providerDestination?: string): Promise<UserReceivingInvoice> {
const newUserInvoice = this.DB.getRepository(UserReceivingInvoice).create({ const newUserInvoice = this.DB.getRepository(UserReceivingInvoice).create({
invoice: invoice, invoice: invoice,
callbackUrl: options.callbackUrl, callbackUrl: options.callbackUrl,
user: user, user: user,
product: options.product, product: options.product,
expires_at_unix: Math.floor(Date.now() / 1000) + options.expiry, expires_at_unix: Math.floor(Date.now() / 1000) + options.expiry,
payer: options.expectedPayer, payer: options.expectedPayer,
linkedApplication: options.linkedApplication, linkedApplication: options.linkedApplication,
zap_info: options.zapInfo zap_info: options.zapInfo,
}) liquidityProvider: providerDestination
return this.txQueue.PushToQueue<UserReceivingInvoice>({ exec: async db => db.getRepository(UserReceivingInvoice).save(newUserInvoice), dbTx: false, description: `add invoice for ${user.user_id} linked to ${options.linkedApplication?.app_id}: ${invoice} ` }) })
} return this.txQueue.PushToQueue<UserReceivingInvoice>({ exec: async db => db.getRepository(UserReceivingInvoice).save(newUserInvoice), dbTx: false, description: `add invoice for ${user.user_id} linked to ${options.linkedApplication?.app_id}: ${invoice} ` })
}
async GetAddressOwner(address: string, entityManager = this.DB): Promise<UserReceivingAddress | null> {
return entityManager.getRepository(UserReceivingAddress).findOne({ async GetAddressOwner(address: string, entityManager = this.DB): Promise<UserReceivingAddress | null> {
where: { return entityManager.getRepository(UserReceivingAddress).findOne({
address where: {
} address
}) }
} })
}
async GetAddressReceivingTransactionOwner(address: string, txHash: string, entityManager = this.DB): Promise<AddressReceivingTransaction | null> {
return entityManager.getRepository(AddressReceivingTransaction).findOne({ async GetAddressReceivingTransactionOwner(address: string, txHash: string, entityManager = this.DB): Promise<AddressReceivingTransaction | null> {
where: { return entityManager.getRepository(AddressReceivingTransaction).findOne({
user_address: { address }, where: {
tx_hash: txHash user_address: { address },
} tx_hash: txHash
}) }
} })
async GetUserTransactionPaymentOwner(address: string, txHash: string, entityManager = this.DB): Promise<UserTransactionPayment | null> { }
return entityManager.getRepository(UserTransactionPayment).findOne({ async GetUserTransactionPaymentOwner(address: string, txHash: string, entityManager = this.DB): Promise<UserTransactionPayment | null> {
where: { return entityManager.getRepository(UserTransactionPayment).findOne({
address, where: {
tx_hash: txHash address,
} tx_hash: txHash
}) }
} })
}
async GetInvoiceOwner(paymentRequest: string, entityManager = this.DB): Promise<UserReceivingInvoice | null> {
return entityManager.getRepository(UserReceivingInvoice).findOne({ async GetInvoiceOwner(paymentRequest: string, entityManager = this.DB): Promise<UserReceivingInvoice | null> {
where: { return entityManager.getRepository(UserReceivingInvoice).findOne({
invoice: paymentRequest where: {
} invoice: paymentRequest
}) }
} })
async GetPaymentOwner(paymentRequest: string, entityManager = this.DB): Promise<UserInvoicePayment | null> { }
return entityManager.getRepository(UserInvoicePayment).findOne({ async GetPaymentOwner(paymentRequest: string, entityManager = this.DB): Promise<UserInvoicePayment | null> {
where: { return entityManager.getRepository(UserInvoicePayment).findOne({
invoice: paymentRequest where: {
} invoice: paymentRequest
}) }
} })
async GetUser2UserPayment(serialId: number, entityManager = this.DB): Promise<UserToUserPayment | null> { }
return entityManager.getRepository(UserToUserPayment).findOne({ async GetUser2UserPayment(serialId: number, entityManager = this.DB): Promise<UserToUserPayment | null> {
where: { return entityManager.getRepository(UserToUserPayment).findOne({
serial_id: serialId where: {
} serial_id: serialId
}) }
} })
}
async AddPendingExternalPayment(userId: string, invoice: string, amount: number, linkedApplication: Application): Promise<UserInvoicePayment> {
const newPayment = this.DB.getRepository(UserInvoicePayment).create({ async AddPendingExternalPayment(userId: string, invoice: string, amount: number, linkedApplication: Application): Promise<UserInvoicePayment> {
user: await this.userStorage.GetUser(userId), const newPayment = this.DB.getRepository(UserInvoicePayment).create({
paid_amount: amount, user: await this.userStorage.GetUser(userId),
invoice, paid_amount: amount,
routing_fees: 0, invoice,
service_fees: 0, routing_fees: 0,
paid_at_unix: 0, service_fees: 0,
internal: false, paid_at_unix: 0,
linkedApplication internal: false,
}) linkedApplication
return this.txQueue.PushToQueue<UserInvoicePayment>({ exec: async db => db.getRepository(UserInvoicePayment).save(newPayment), dbTx: false, description: `add pending invoice payment for ${userId} linked to ${linkedApplication.app_id}: ${invoice}, amt: ${amount} ` }) })
} return this.txQueue.PushToQueue<UserInvoicePayment>({ exec: async db => db.getRepository(UserInvoicePayment).save(newPayment), dbTx: false, description: `add pending invoice payment for ${userId} linked to ${linkedApplication.app_id}: ${invoice}, amt: ${amount} ` })
}
async UpdateExternalPayment(invoicePaymentSerialId: number, routingFees: number, serviceFees: number, success: boolean) {
return this.DB.getRepository(UserInvoicePayment).update(invoicePaymentSerialId, { async UpdateExternalPayment(invoicePaymentSerialId: number, routingFees: number, serviceFees: number, success: boolean, providerDestination?: string) {
routing_fees: routingFees, return this.DB.getRepository(UserInvoicePayment).update(invoicePaymentSerialId, {
service_fees: serviceFees, routing_fees: routingFees,
paid_at_unix: success ? Math.floor(Date.now() / 1000) : -1 service_fees: serviceFees,
}) paid_at_unix: success ? Math.floor(Date.now() / 1000) : -1,
} liquidityProvider: providerDestination
})
async AddInternalPayment(userId: string, invoice: string, amount: number, serviceFees: number, linkedApplication: Application): Promise<UserInvoicePayment> { }
const newPayment = this.DB.getRepository(UserInvoicePayment).create({
user: await this.userStorage.GetUser(userId), async AddInternalPayment(userId: string, invoice: string, amount: number, serviceFees: number, linkedApplication: Application): Promise<UserInvoicePayment> {
paid_amount: amount, const newPayment = this.DB.getRepository(UserInvoicePayment).create({
invoice, user: await this.userStorage.GetUser(userId),
routing_fees: 0, paid_amount: amount,
service_fees: serviceFees, invoice,
paid_at_unix: Math.floor(Date.now() / 1000), routing_fees: 0,
internal: true, service_fees: serviceFees,
linkedApplication paid_at_unix: Math.floor(Date.now() / 1000),
}) internal: true,
return this.txQueue.PushToQueue<UserInvoicePayment>({ exec: async db => db.getRepository(UserInvoicePayment).save(newPayment), dbTx: false, description: `add internal invoice payment for ${userId} linked to ${linkedApplication.app_id}: ${invoice}, amt: ${amount} ` }) linkedApplication
} })
return this.txQueue.PushToQueue<UserInvoicePayment>({ exec: async db => db.getRepository(UserInvoicePayment).save(newPayment), dbTx: false, description: `add internal invoice payment for ${userId} linked to ${linkedApplication.app_id}: ${invoice}, amt: ${amount} ` })
GetUserInvoicePayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB): Promise<UserInvoicePayment[]> { }
return entityManager.getRepository(UserInvoicePayment).find({
where: { GetUserInvoicePayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB): Promise<UserInvoicePayment[]> {
user: { return entityManager.getRepository(UserInvoicePayment).find({
user_id: userId where: {
}, user: {
serial_id: MoreThanOrEqual(fromIndex), user_id: userId
paid_at_unix: MoreThan(0), },
}, serial_id: MoreThanOrEqual(fromIndex),
order: { paid_at_unix: MoreThan(0),
paid_at_unix: 'DESC' },
}, order: {
take paid_at_unix: 'DESC'
}) },
} take
})
async AddUserTransactionPayment(userId: string, address: string, txHash: string, txOutput: number, amount: number, chainFees: number, serviceFees: number, internal: boolean, height: number, linkedApplication: Application): Promise<UserTransactionPayment> { }
const newTx = this.DB.getRepository(UserTransactionPayment).create({
user: await this.userStorage.GetUser(userId), async AddUserTransactionPayment(userId: string, address: string, txHash: string, txOutput: number, amount: number, chainFees: number, serviceFees: number, internal: boolean, height: number, linkedApplication: Application): Promise<UserTransactionPayment> {
address, const newTx = this.DB.getRepository(UserTransactionPayment).create({
paid_amount: amount, user: await this.userStorage.GetUser(userId),
chain_fees: chainFees, address,
output_index: txOutput, paid_amount: amount,
tx_hash: txHash, chain_fees: chainFees,
service_fees: serviceFees, output_index: txOutput,
paid_at_unix: Math.floor(Date.now() / 1000), tx_hash: txHash,
internal, service_fees: serviceFees,
broadcast_height: height, paid_at_unix: Math.floor(Date.now() / 1000),
confs: internal ? 10 : 0, internal,
linkedApplication broadcast_height: height,
}) confs: internal ? 10 : 0,
return this.txQueue.PushToQueue<UserTransactionPayment>({ exec: async db => db.getRepository(UserTransactionPayment).save(newTx), dbTx: false, description: `add tx payment for ${userId} linked to ${linkedApplication.app_id}: ${address}, amt: ${amount} ` }) linkedApplication
} })
return this.txQueue.PushToQueue<UserTransactionPayment>({ exec: async db => db.getRepository(UserTransactionPayment).save(newTx), dbTx: false, description: `add tx payment for ${userId} linked to ${linkedApplication.app_id}: ${address}, amt: ${amount} ` })
GetUserTransactionPayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB): Promise<UserTransactionPayment[]> { }
return entityManager.getRepository(UserTransactionPayment).find({
where: { GetUserTransactionPayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB): Promise<UserTransactionPayment[]> {
user: { return entityManager.getRepository(UserTransactionPayment).find({
user_id: userId where: {
}, user: {
serial_id: MoreThanOrEqual(fromIndex), user_id: userId
paid_at_unix: MoreThan(0), },
}, serial_id: MoreThanOrEqual(fromIndex),
order: { paid_at_unix: MoreThan(0),
paid_at_unix: 'DESC' },
}, order: {
take paid_at_unix: 'DESC'
}) },
} take
})
async GetPendingTransactions(entityManager = this.DB) { }
const incoming = await entityManager.getRepository(AddressReceivingTransaction).find({ where: { confs: 0 } })
const outgoing = await entityManager.getRepository(UserTransactionPayment).find({ where: { confs: 0 } }) async GetPendingTransactions(entityManager = this.DB) {
return { incoming, outgoing } const incoming = await entityManager.getRepository(AddressReceivingTransaction).find({ where: { confs: 0 } })
} const outgoing = await entityManager.getRepository(UserTransactionPayment).find({ where: { confs: 0 } })
return { incoming, outgoing }
async UpdateAddressReceivingTransaction(serialId: number, update: Partial<AddressReceivingTransaction>, entityManager = this.DB) { }
return entityManager.getRepository(AddressReceivingTransaction).update(serialId, update)
} async UpdateAddressReceivingTransaction(serialId: number, update: Partial<AddressReceivingTransaction>, entityManager = this.DB) {
async UpdateUserTransactionPayment(serialId: number, update: Partial<UserTransactionPayment>, entityManager = this.DB) { return entityManager.getRepository(AddressReceivingTransaction).update(serialId, update)
await entityManager.getRepository(UserTransactionPayment).update(serialId, update) }
} async UpdateUserTransactionPayment(serialId: number, update: Partial<UserTransactionPayment>, entityManager = this.DB) {
await entityManager.getRepository(UserTransactionPayment).update(serialId, update)
}
async AddUserEphemeralKey(userId: string, keyType: EphemeralKeyType, linkedApplication: Application): Promise<UserEphemeralKey> {
const found = await this.DB.getRepository(UserEphemeralKey).findOne({ where: { type: keyType, user: { user_id: userId }, linkedApplication: { app_id: linkedApplication.app_id } } })
if (found) { async AddUserEphemeralKey(userId: string, keyType: EphemeralKeyType, linkedApplication: Application): Promise<UserEphemeralKey> {
return found const found = await this.DB.getRepository(UserEphemeralKey).findOne({ where: { type: keyType, user: { user_id: userId }, linkedApplication: { app_id: linkedApplication.app_id } } })
} if (found) {
const newKey = this.DB.getRepository(UserEphemeralKey).create({ return found
user: await this.userStorage.GetUser(userId), }
key: crypto.randomBytes(31).toString('hex'), const newKey = this.DB.getRepository(UserEphemeralKey).create({
type: keyType, user: await this.userStorage.GetUser(userId),
linkedApplication key: crypto.randomBytes(31).toString('hex'),
}) type: keyType,
return this.txQueue.PushToQueue<UserEphemeralKey>({ exec: async db => db.getRepository(UserEphemeralKey).save(newKey), dbTx: false }) linkedApplication
} })
return this.txQueue.PushToQueue<UserEphemeralKey>({ exec: async db => db.getRepository(UserEphemeralKey).save(newKey), dbTx: false })
async UseUserEphemeralKey(key: string, keyType: EphemeralKeyType, persist = false, entityManager = this.DB): Promise<UserEphemeralKey> { }
const found = await entityManager.getRepository(UserEphemeralKey).findOne({
where: { async UseUserEphemeralKey(key: string, keyType: EphemeralKeyType, persist = false, entityManager = this.DB): Promise<UserEphemeralKey> {
key: key, const found = await entityManager.getRepository(UserEphemeralKey).findOne({
type: keyType where: {
} key: key,
}) type: keyType
if (!found) { }
throw new Error("the provided ephemeral key is invalid") })
} if (!found) {
if (!persist) { throw new Error("the provided ephemeral key is invalid")
await entityManager.getRepository(UserEphemeralKey).delete(found.serial_id) }
} if (!persist) {
return found await entityManager.getRepository(UserEphemeralKey).delete(found.serial_id)
} }
return found
async AddPendingUserToUserPayment(fromUserId: string, toUserId: string, amount: number, fee: number, linkedApplication: Application, dbTx: DataSource | EntityManager) { }
const entry = dbTx.getRepository(UserToUserPayment).create({
from_user: await this.userStorage.GetUser(fromUserId, dbTx), async AddPendingUserToUserPayment(fromUserId: string, toUserId: string, amount: number, fee: number, linkedApplication: Application, dbTx: DataSource | EntityManager) {
to_user: await this.userStorage.GetUser(toUserId, dbTx), const entry = dbTx.getRepository(UserToUserPayment).create({
paid_at_unix: 0, from_user: await this.userStorage.GetUser(fromUserId, dbTx),
paid_amount: amount, to_user: await this.userStorage.GetUser(toUserId, dbTx),
service_fees: fee, paid_at_unix: 0,
linkedApplication paid_amount: amount,
}) service_fees: fee,
return dbTx.getRepository(UserToUserPayment).save(entry) linkedApplication
} })
async SetPendingUserToUserPaymentAsPaid(serialId: number, dbTx: DataSource | EntityManager) { return dbTx.getRepository(UserToUserPayment).save(entry)
dbTx.getRepository(UserToUserPayment).update(serialId, { paid_at_unix: Math.floor(Date.now() / 1000) }) }
} async SetPendingUserToUserPaymentAsPaid(serialId: number, dbTx: DataSource | EntityManager) {
dbTx.getRepository(UserToUserPayment).update(serialId, { paid_at_unix: Math.floor(Date.now() / 1000) })
GetUserToUserReceivedPayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB) { }
return entityManager.getRepository(UserToUserPayment).find({
where: { GetUserToUserReceivedPayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB) {
to_user: { return entityManager.getRepository(UserToUserPayment).find({
user_id: userId where: {
}, to_user: {
serial_id: MoreThanOrEqual(fromIndex), user_id: userId
paid_at_unix: MoreThan(0), },
}, serial_id: MoreThanOrEqual(fromIndex),
order: { paid_at_unix: MoreThan(0),
paid_at_unix: 'DESC' },
}, order: {
take paid_at_unix: 'DESC'
}) },
} take
})
GetUserToUserSentPayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB) { }
return entityManager.getRepository(UserToUserPayment).find({
where: { GetUserToUserSentPayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB) {
from_user: { return entityManager.getRepository(UserToUserPayment).find({
user_id: userId where: {
}, from_user: {
serial_id: MoreThanOrEqual(fromIndex), user_id: userId
paid_at_unix: MoreThan(0), },
}, serial_id: MoreThanOrEqual(fromIndex),
order: { paid_at_unix: MoreThan(0),
paid_at_unix: 'DESC' },
}, order: {
take paid_at_unix: 'DESC'
}) },
} take
})
async GetTotalFeesPaidInApp(app: Application | null, entityManager = this.DB) { }
if (!app) {
return 0 async GetTotalFeesPaidInApp(app: Application | null, entityManager = this.DB) {
} if (!app) {
const entries = await Promise.all([ return 0
entityManager.getRepository(UserReceivingInvoice).sum("service_fee", { linkedApplication: { app_id: app.app_id } }), }
entityManager.getRepository(AddressReceivingTransaction).sum("service_fee", { user_address: { linkedApplication: { app_id: app.app_id } } }), const entries = await Promise.all([
entityManager.getRepository(UserInvoicePayment).sum("service_fees", { linkedApplication: { app_id: app.app_id } }), entityManager.getRepository(UserReceivingInvoice).sum("service_fee", { linkedApplication: { app_id: app.app_id } }),
entityManager.getRepository(UserTransactionPayment).sum("service_fees", { linkedApplication: { app_id: app.app_id } }), entityManager.getRepository(AddressReceivingTransaction).sum("service_fee", { user_address: { linkedApplication: { app_id: app.app_id } } }),
entityManager.getRepository(UserToUserPayment).sum("service_fees", { linkedApplication: { app_id: app.app_id } }) entityManager.getRepository(UserInvoicePayment).sum("service_fees", { linkedApplication: { app_id: app.app_id } }),
]) entityManager.getRepository(UserTransactionPayment).sum("service_fees", { linkedApplication: { app_id: app.app_id } }),
let total = 0 entityManager.getRepository(UserToUserPayment).sum("service_fees", { linkedApplication: { app_id: app.app_id } })
entries.forEach(e => { ])
if (e) { let total = 0
total += e entries.forEach(e => {
} if (e) {
}) total += e
return total }
} })
return total
async GetAppOperations(application: Application | null, { from, to }: { from?: number, to?: number }, entityManager = this.DB) { }
const q = application ? { app_id: application.app_id } : IsNull()
let time: { created_at?: FindOperator<Date> } = {} async GetAppOperations(application: Application | null, { from, to }: { from?: number, to?: number }, entityManager = this.DB) {
if (!!from && !!to) { const q = application ? { app_id: application.app_id } : IsNull()
time.created_at = Between<Date>(new Date(from * 1000), new Date(to * 1000)) let time: { created_at?: FindOperator<Date> } = {}
} else if (!!from) { if (!!from && !!to) {
time.created_at = MoreThanOrEqual<Date>(new Date(from * 1000)) time.created_at = Between<Date>(new Date(from * 1000), new Date(to * 1000))
} else if (!!to) { } else if (!!from) {
time.created_at = LessThanOrEqual<Date>(new Date(to * 1000)) time.created_at = MoreThanOrEqual<Date>(new Date(from * 1000))
} } else if (!!to) {
time.created_at = LessThanOrEqual<Date>(new Date(to * 1000))
const [receivingInvoices, receivingAddresses, outgoingInvoices, outgoingTransactions, userToUser] = await Promise.all([ }
entityManager.getRepository(UserReceivingInvoice).find({ where: { linkedApplication: q, ...time } }),
entityManager.getRepository(UserReceivingAddress).find({ where: { linkedApplication: q, ...time } }), const [receivingInvoices, receivingAddresses, outgoingInvoices, outgoingTransactions, userToUser] = await Promise.all([
entityManager.getRepository(UserInvoicePayment).find({ where: { linkedApplication: q, ...time } }), entityManager.getRepository(UserReceivingInvoice).find({ where: { linkedApplication: q, ...time } }),
entityManager.getRepository(UserTransactionPayment).find({ where: { linkedApplication: q, ...time } }), entityManager.getRepository(UserReceivingAddress).find({ where: { linkedApplication: q, ...time } }),
entityManager.getRepository(UserToUserPayment).find({ where: { linkedApplication: q, ...time } }) entityManager.getRepository(UserInvoicePayment).find({ where: { linkedApplication: q, ...time } }),
]) entityManager.getRepository(UserTransactionPayment).find({ where: { linkedApplication: q, ...time } }),
const receivingTransactions = await Promise.all(receivingAddresses.map(addr => entityManager.getRepository(AddressReceivingTransaction).find({ where: { user_address: { serial_id: addr.serial_id }, ...time } }))) entityManager.getRepository(UserToUserPayment).find({ where: { linkedApplication: q, ...time } })
return { ])
receivingInvoices, receivingAddresses, receivingTransactions, const receivingTransactions = await Promise.all(receivingAddresses.map(addr => entityManager.getRepository(AddressReceivingTransaction).find({ where: { user_address: { serial_id: addr.serial_id }, ...time } })))
outgoingInvoices, outgoingTransactions, return {
userToUser receivingInvoices, receivingAddresses, receivingTransactions,
} outgoingInvoices, outgoingTransactions,
} userToUser
}
async UserHasOutgoingOperation(userId: string, entityManager = this.DB) { }
const [i, tx, u2u] = await Promise.all([
entityManager.getRepository(UserInvoicePayment).findOne({ where: { user: { user_id: userId } } }), async UserHasOutgoingOperation(userId: string, entityManager = this.DB) {
entityManager.getRepository(UserTransactionPayment).findOne({ where: { user: { user_id: userId } } }), const [i, tx, u2u] = await Promise.all([
entityManager.getRepository(UserToUserPayment).findOne({ where: { from_user: { user_id: userId } } }), entityManager.getRepository(UserInvoicePayment).findOne({ where: { user: { user_id: userId } } }),
]) entityManager.getRepository(UserTransactionPayment).findOne({ where: { user: { user_id: userId } } }),
return !!i || !!tx || !!u2u entityManager.getRepository(UserToUserPayment).findOne({ where: { from_user: { user_id: userId } } }),
} ])
return !!i || !!tx || !!u2u
async VerifyDbEvent(e: LoggedEvent) { }
switch (e.type) {
case "new_invoice": async VerifyDbEvent(e: LoggedEvent) {
return this.DB.getRepository(UserReceivingInvoice).findOneOrFail({ where: { invoice: e.data, user: { user_id: e.userId } } }) switch (e.type) {
case 'new_address': case "new_invoice":
return this.DB.getRepository(UserReceivingAddress).findOneOrFail({ where: { address: e.data, user: { user_id: e.userId } } }) return this.DB.getRepository(UserReceivingInvoice).findOneOrFail({ where: { invoice: e.data, user: { user_id: e.userId } } })
case 'invoice_paid': case 'new_address':
return this.DB.getRepository(UserReceivingInvoice).findOneOrFail({ where: { invoice: e.data, user: { user_id: e.userId }, paid_at_unix: MoreThan(0) } }) return this.DB.getRepository(UserReceivingAddress).findOneOrFail({ where: { address: e.data, user: { user_id: e.userId } } })
case 'invoice_payment': case 'invoice_paid':
return this.DB.getRepository(UserInvoicePayment).findOneOrFail({ where: { invoice: e.data, user: { user_id: e.userId } } }) return this.DB.getRepository(UserReceivingInvoice).findOneOrFail({ where: { invoice: e.data, user: { user_id: e.userId }, paid_at_unix: MoreThan(0) } })
case 'address_paid': case 'invoice_payment':
const [receivingAddress, receivedHash] = e.data.split(":") return this.DB.getRepository(UserInvoicePayment).findOneOrFail({ where: { invoice: e.data, user: { user_id: e.userId } } })
return this.DB.getRepository(AddressReceivingTransaction).findOneOrFail({ where: { user_address: { address: receivingAddress }, tx_hash: receivedHash, confs: MoreThan(0) } }) case 'address_paid':
case 'address_payment': const [receivingAddress, receivedHash] = e.data.split(":")
const [sentAddress, sentHash] = e.data.split(":") return this.DB.getRepository(AddressReceivingTransaction).findOneOrFail({ where: { user_address: { address: receivingAddress }, tx_hash: receivedHash, confs: MoreThan(0) } })
return this.DB.getRepository(UserTransactionPayment).findOneOrFail({ where: { address: sentAddress, tx_hash: sentHash, user: { user_id: e.userId } } }) case 'address_payment':
case 'u2u_receiver': const [sentAddress, sentHash] = e.data.split(":")
return this.DB.getRepository(UserToUserPayment).findOneOrFail({ where: { from_user: { user_id: e.data }, to_user: { user_id: e.userId } } }) return this.DB.getRepository(UserTransactionPayment).findOneOrFail({ where: { address: sentAddress, tx_hash: sentHash, user: { user_id: e.userId } } })
case 'u2u_sender': case 'u2u_receiver':
return this.DB.getRepository(UserToUserPayment).findOneOrFail({ where: { to_user: { user_id: e.data }, from_user: { user_id: e.userId } } }) return this.DB.getRepository(UserToUserPayment).findOneOrFail({ where: { from_user: { user_id: e.data }, to_user: { user_id: e.userId } } })
default: case 'u2u_sender':
break; return this.DB.getRepository(UserToUserPayment).findOneOrFail({ where: { to_user: { user_id: e.data }, from_user: { user_id: e.userId } } })
} default:
} break;
}
async GetTotalUsersBalance(entityManager = this.DB) { }
const total = await entityManager.getRepository(User).sum("balance_sats")
return total || 0 async GetTotalUsersBalance(entityManager = this.DB) {
} const total = await entityManager.getRepository(User).sum("balance_sats")
return total || 0
}
} }

View file

@ -0,0 +1,142 @@
import { getLogger } from "../helpers/logger.js"
const transactionStatePointTypes = ['addedInvoice', 'invoiceWasPaid', 'paidAnInvoice', 'addedAddress', 'addressWasPaid', 'paidAnAddress', 'user2user'] as const
const balanceStatePointTypes = ['providerBalance', 'providerMaxWithdrawable', 'walletBalance', 'channelBalance', 'usersBalance', 'feesPaidForLiquidity', 'totalLndBalance', 'accumulatedHtlcFees', 'deltaUsers', 'deltaExternal'] as const
const maxStatePointTypes = ['maxProviderRespTime'] as const
export type TransactionStatePointType = typeof transactionStatePointTypes[number]
export type BalanceStatePointType = typeof balanceStatePointTypes[number]
export type MaxStatePointType = typeof maxStatePointTypes[number]
/*export type TransactionStatePoint = {
type: typeof TransactionStatePointTypes[number]
with: 'lnd' | 'internal' | 'provider'
by: 'user' | 'system'
amount: number
success: boolean
networkFee?: number
serviceFee?: number
liquidtyFee?: number
}*/
type StateBundle = Record<string, number>
export type TxPointSettings = {
used: 'lnd' | 'internal' | 'provider' | 'unknown'
from: 'user' | 'system'
meta?: string[]
timeDiscount?: true
}
export class StateBundler {
sinceStart: StateBundle = {}
lastReport: StateBundle = {}
sinceLatestReport: StateBundle = {}
reportPeriod = 1000 * 60 * 60 * 12 //12h
satsPer1SecondDiscount = 1
totalSatsForDiscount = 0
latestReport = Date.now()
reportLog = getLogger({ component: 'stateBundlerReport' })
constructor() {
process.on('exit', () => {
this.Report()
});
// catch ctrl+c event and exit normally
process.on('SIGINT', () => {
console.log('Ctrl-C...');
process.exit(2);
});
//catch uncaught exceptions, trace, then exit normally
process.on('uncaughtException', (e) => {
console.log('Uncaught Exception...');
console.log(e.stack);
process.exit(99);
});
}
increment = (key: string, value: number) => {
this.sinceStart[key] = (this.sinceStart[key] || 0) + value
this.sinceLatestReport[key] = (this.sinceLatestReport[key] || 0) + value
this.triggerReportCheck()
}
set = (key: string, value: number) => {
this.sinceStart[key] = value
this.sinceLatestReport[key] = value
this.triggerReportCheck()
}
max = (key: string, value: number) => {
this.sinceStart[key] = Math.max(this.sinceStart[key] || 0, value)
this.sinceLatestReport[key] = Math.max(this.sinceLatestReport[key] || 0, value)
this.triggerReportCheck()
}
AddTxPoint = (actionName: TransactionStatePointType, v: number, settings: TxPointSettings) => {
const { used, from, timeDiscount } = settings
const meta = settings.meta || []
const key = [actionName, from, used, ...meta].join('_')
if (timeDiscount) {
this.totalSatsForDiscount += v
}
this.increment(key, v)
//this.smallLogEvent(actionName, from)
}
AddTxPointFailed = (actionName: TransactionStatePointType, v: number, settings: TxPointSettings) => {
const { used, from } = settings
const meta = settings.meta || []
const key = [actionName, from, used, ...meta, 'failed'].join('_')
this.increment(key, v)
}
AddBalancePoint = (actionName: BalanceStatePointType, v: number, meta = []) => {
const key = [actionName, ...meta].join('_')
this.set(key, v)
}
AddMaxPoint = (actionName: MaxStatePointType, v: number, meta = []) => {
const key = [actionName, ...meta].join('_')
this.max(key, v)
}
triggerReportCheck = () => {
const discountSeconds = Math.floor(this.totalSatsForDiscount / this.satsPer1SecondDiscount)
const totalElapsed = Date.now() - this.latestReport
const elapsedWithDiscount = totalElapsed + discountSeconds * 1000
if (elapsedWithDiscount > this.reportPeriod) {
this.Report()
}
}
smallLogEvent(event: TransactionStatePointType, from: 'user' | 'system') {
const char = from === 'user' ? 'U' : 'S'
switch (event) {
case 'addedAddress':
case 'addedInvoice':
process.stdout.write(`${char}+,`)
return
case 'addressWasPaid':
case 'invoiceWasPaid':
process.stdout.write(`${char}>,`)
return
case 'paidAnAddress':
case 'paidAnInvoice':
process.stdout.write(`${char}<,`)
return
case 'user2user':
process.stdout.write(`UU`)
}
}
Report = () => {
this.totalSatsForDiscount = 0
this.latestReport = Date.now()
this.reportLog("+++++ since last report:")
Object.entries(this.sinceLatestReport).forEach(([key, value]) => {
this.reportLog(key, value)
})
this.reportLog("+++++ since start:")
Object.entries(this.sinceStart).forEach(([key, value]) => {
this.reportLog(key, value)
})
this.lastReport = { ...this.sinceLatestReport }
this.sinceLatestReport = {}
}
}

View file

@ -20,21 +20,17 @@ export default class {
PushToQueue<T>(op: TxOperation<T>) { PushToQueue<T>(op: TxOperation<T>) {
if (!this.pendingTx) { if (!this.pendingTx) {
this.log("queue empty, starting transaction", this.transactionsQueue.length)
return this.execQueueItem(op) return this.execQueueItem(op)
} }
this.log("queue not empty, possibly stuck")
return new Promise<T>((res, rej) => { return new Promise<T>((res, rej) => {
this.transactionsQueue.push({ op, res, rej }) this.transactionsQueue.push({ op, res, rej })
}) })
} }
async execNextInQueue() { async execNextInQueue() {
this.log("executing next in queue")
this.pendingTx = false this.pendingTx = false
const next = this.transactionsQueue.pop() const next = this.transactionsQueue.pop()
if (!next) { if (!next) {
this.log("queue is clear")
return return
} }
try { try {
@ -51,7 +47,6 @@ export default class {
throw new Error("cannot start DB transaction") throw new Error("cannot start DB transaction")
} }
this.pendingTx = true this.pendingTx = true
this.log("starting", op.dbTx ? "db transaction" : "operation", op.description || "")
if (op.dbTx) { if (op.dbTx) {
return this.doTransaction(op.exec) return this.doTransaction(op.exec)
} }

View file

@ -0,0 +1,171 @@
import fs from 'fs'
import path from 'path';
import { config as loadEnvFile } from 'dotenv'
import { getLogger } from "../helpers/logger.js"
import NewWizardServer from "../../../proto/wizard_service/autogenerated/ts/express_server.js"
import * as WizardTypes from "../../../proto/wizard_service/autogenerated/ts/types.js"
import { MainSettings } from "../main/settings.js"
import Storage from '../storage/index.js'
import { Unlocker } from "../main/unlocker.js"
import { AdminManager } from '../main/adminManager.js';
export type WizardSettings = {
sourceName: string
relayUrl: string
automateLiquidity: boolean
pushBackupsToNostr: boolean
}
const defaultProviderPub = ""
export class Wizard {
log = getLogger({ component: "wizard" })
settings: MainSettings
adminManager: AdminManager
storage: Storage
configQueue: { res: (reload: boolean) => void }[] = []
pendingConfig: WizardSettings | null = null
awaitingNprofile: { res: (nprofile: string) => void }[] = []
nprofile = ""
relays: string[] = []
constructor(mainSettings: MainSettings, storage: Storage, adminManager: AdminManager) {
this.settings = mainSettings
this.adminManager = adminManager
this.storage = storage
this.log('Starting wizard...')
const wizardServer = NewWizardServer({
WizardState: async () => { return this.WizardState() },
WizardConfig: async ({ req }) => { return this.wizardConfig(req) },
GetAdminConnectInfo: async () => { return this.GetAdminConnectInfo() },
GetServiceState: async () => { return this.GetServiceState() }
}, { GuestAuthGuard: async () => "", metricsCallback: () => { }, staticFiles: 'static' })
wizardServer.Listen(mainSettings.servicePort + 1)
}
GetServiceState = async (): Promise<WizardTypes.ServiceStateResponse> => {
const apps = await this.storage.applicationStorage.GetApplications()
const appNamesList = apps.map(app => app.name).join(', ')
return {
admin_npub: this.adminManager.GetAdminNpub(),
http_url: this.settings.serviceUrl,
lnd_state: WizardTypes.LndState.OFFLINE,
nprofile: this.nprofile,
provider_name: appNamesList,
relay_connected: false,
relays: this.relays,
watchdog_ok: false
}
}
WizardState = async (): Promise<WizardTypes.StateResponse> => {
return {
config_sent: this.pendingConfig !== null,
admin_linked: this.adminManager.GetAdminNpub() !== "",
}
}
IsInitialized = () => {
return !!this.adminManager.GetAdminNpub()
}
GetAdminConnectInfo = async (): Promise<WizardTypes.AdminConnectInfoResponse> => {
const nprofile = await this.getNprofile()
const enrolledAdmin = this.adminManager.GetAdminNpub()
if (enrolledAdmin !== "") {
return {
nprofile, connect_info: {
type: WizardTypes.AdminConnectInfoResponse_connect_info_type.ENROLLED_NPUB,
enrolled_npub: enrolledAdmin
}
}
}
const adminEnroll = this.adminManager.ReadAdminEnrollToken()
if (adminEnroll !== "") {
return {
nprofile, connect_info: {
type: WizardTypes.AdminConnectInfoResponse_connect_info_type.ADMIN_TOKEN,
admin_token: adminEnroll
}
}
}
throw new Error("something went wrong initializing admin creds")
}
getNprofile = async (): Promise<string> => {
if (this.nprofile !== "") {
return this.nprofile
}
console.log("waiting for nprofile")
return new Promise((res) => {
this.awaitingNprofile.push({ res })
})
}
AddConnectInfo = (nprofile: string, relays: string[]) => {
this.nprofile = nprofile
this.awaitingNprofile.forEach(q => q.res(nprofile))
this.awaitingNprofile = []
}
Configure = async (): Promise<boolean> => {
if (this.IsInitialized() || this.pendingConfig !== null) {
return false
}
return new Promise((res) => {
this.configQueue.push({ res })
})
}
wizardConfig = async (req: WizardTypes.ConfigRequest): Promise<void> => {
const err = WizardTypes.ConfigRequestValidate(req, {
source_name_CustomCheck: source => source !== '',
relay_url_CustomCheck: relay => relay !== '',
})
if (err != null) { throw new Error(err.message) }
if (this.IsInitialized() || this.pendingConfig !== null) {
throw new Error("already initialized")
}
const pendingConfig = { sourceName: req.source_name, relayUrl: req.relay_url, automateLiquidity: req.automate_liquidity, pushBackupsToNostr: req.push_backups_to_nostr }
this.updateEnvFile(pendingConfig)
this.configQueue.forEach(q => q.res(true))
this.configQueue = []
return
}
updateEnvFile = (pendingConfig: WizardSettings) => {
let envFileContent: string[] = []
try {
envFileContent = fs.readFileSync('.env', 'utf-8').split('\n')
} catch (err: any) {
if (err.code !== 'ENOENT') {
throw err
}
}
const toMerge: string[] = []
const sourceNameIndex = envFileContent.findIndex(line => line.startsWith('DEFAULT_APP_NAME'))
if (sourceNameIndex === -1) {
toMerge.push(`DEFAULT_APP_NAME=${pendingConfig.sourceName}`)
} else {
envFileContent[sourceNameIndex] = `DEFAULT_APP_NAME=${pendingConfig.sourceName}`
}
const relayUrlIndex = envFileContent.findIndex(line => line.startsWith('RELAY_URL'))
if (relayUrlIndex === -1) {
toMerge.push(`RELAY_URL=${pendingConfig.relayUrl}`)
} else {
envFileContent[relayUrlIndex] = `RELAY_URL=${pendingConfig.relayUrl}`
}
const automateLiquidityIndex = envFileContent.findIndex(line => line.startsWith('LIQUIDITY_PROVIDER_PUB'))
if (automateLiquidityIndex === -1) {
toMerge.push(`LIQUIDITY_PROVIDER_PUB=${pendingConfig.automateLiquidity ? defaultProviderPub : ""}`)
} else {
envFileContent[automateLiquidityIndex] = `LIQUIDITY_PROVIDER_PUB=`
}
const pushBackupsToNostrIndex = envFileContent.findIndex(line => line.startsWith('PUSH_BACKUPS_TO_NOSTR'))
if (pushBackupsToNostrIndex === -1) {
toMerge.push(`PUSH_BACKUPS_TO_NOSTR=${pendingConfig.pushBackupsToNostr ? 'true' : 'false'}`)
} else {
envFileContent[pushBackupsToNostrIndex] = `PUSH_BACKUPS_TO_NOSTR=${pendingConfig.pushBackupsToNostr ? 'true' : 'false'}`
}
const merged = [...envFileContent, ...toMerge].join('\n')
fs.writeFileSync('.env', merged)
loadEnvFile()
}
}

View file

@ -13,7 +13,7 @@ export default async (T: TestBase) => {
const testSuccessfulExternalPayment = async (T: TestBase) => { const testSuccessfulExternalPayment = async (T: TestBase) => {
T.d("starting testSuccessfulExternalPayment") T.d("starting testSuccessfulExternalPayment")
const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry) const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })
expect(invoice.payRequest).to.startWith("lnbcrt5u") expect(invoice.payRequest).to.startWith("lnbcrt5u")
T.d("generated 500 sats invoice for external node") T.d("generated 500 sats invoice for external node")
@ -32,7 +32,7 @@ const testSuccessfulExternalPayment = async (T: TestBase) => {
const testFailedExternalPayment = async (T: TestBase) => { const testFailedExternalPayment = async (T: TestBase) => {
T.d("starting testFailedExternalPayment") T.d("starting testFailedExternalPayment")
const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const invoice = await T.externalAccessToOtherLnd.NewInvoice(1500, "test", defaultInvoiceExpiry) const invoice = await T.externalAccessToOtherLnd.NewInvoice(1500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })
expect(invoice.payRequest).to.startWith("lnbcrt15u") expect(invoice.payRequest).to.startWith("lnbcrt15u")
T.d("generated 1500 sats invoice for external node") T.d("generated 1500 sats invoice for external node")

View file

@ -23,16 +23,13 @@ const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, b
T.d("starting testInboundPaymentFromProvider") T.d("starting testInboundPaymentFromProvider")
const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" }) const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" })
await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100) await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100, 2000, { from: 'system', useProvider: false })
await new Promise((resolve) => setTimeout(resolve, 200)) await new Promise((resolve) => setTimeout(resolve, 200))
const userBalance = await bootstrapped.appUserManager.GetUserInfo({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }) const userBalance = await bootstrapped.appUserManager.GetUserInfo({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier })
T.expect(userBalance.balance).to.equal(2000) T.expect(userBalance.balance).to.equal(2000)
T.d("user balance is 2000") T.d("user balance is 2000")
const providerBalance = await bootstrapped.liquidProvider.CheckUserState() const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance()
if (!providerBalance) { T.expect(providerBalance).to.equal(2000)
throw new Error("provider balance not found")
}
T.expect(providerBalance.balance).to.equal(2000)
T.d("provider balance is 2000") T.d("provider balance is 2000")
T.d("testInboundPaymentFromProvider done") T.d("testInboundPaymentFromProvider done")
} }
@ -40,17 +37,14 @@ const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, b
const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => { const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => {
T.d("starting testOutboundPaymentFromProvider") T.d("starting testOutboundPaymentFromProvider")
const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60) const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60, { from: 'system', useProvider: false })
const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier } const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier }
const res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 }) const res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 })
const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx) const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx)
T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2) T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2)
const providerBalance = await bootstrapped.liquidProvider.CheckUserState() const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance()
if (!providerBalance) { T.expect(providerBalance).to.equal(992) // 2000 - (1000 + 6 +2)
throw new Error("provider balance not found")
}
T.expect(providerBalance.balance).to.equal(992) // 2000 - (1000 + 6 +2)
T.d("testOutboundPaymentFromProvider done") T.d("testOutboundPaymentFromProvider done")
} }

View file

@ -1,15 +1,17 @@
import { LoadTestSettingsFromEnv } from "../services/main/settings.js" import { LoadTestSettingsFromEnv } from "../services/main/settings.js"
import { BitcoinCoreWrapper } from "./bitcoinCore.js" import { BitcoinCoreWrapper } from "./bitcoinCore.js"
import LND from '../services/lnd/lnd.js' import LND from '../services/lnd/lnd.js'
import { LiquidityProvider } from "../services/lnd/liquidityProvider.js" import { LiquidityProvider } from "../services/main/liquidityProvider.js"
import { Utils } from "../services/helpers/utilsWrapper.js"
export const setupNetwork = async () => { export const setupNetwork = async () => {
const settings = LoadTestSettingsFromEnv() const settings = LoadTestSettingsFromEnv()
const core = new BitcoinCoreWrapper(settings) const core = new BitcoinCoreWrapper(settings)
await core.InitAddress() await core.InitAddress()
await core.Mine(1) await core.Mine(1)
const alice = new LND(settings.lndSettings, { liquidProvider: new LiquidityProvider("", () => { }) }, () => { }, () => { }, () => { }, () => { }) const setupUtils = new Utils(settings)
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, { liquidProvider: new LiquidityProvider("", () => { }) }, () => { }, () => { }, () => { }, () => { }) const alice = new LND(settings.lndSettings, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { })
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { })
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const peers = await alice.ListPeers() const peers = await alice.ListPeers()
if (peers.peers.length > 0) { if (peers.peers.length > 0) {

View file

@ -24,9 +24,9 @@ export const initBootstrappedInstance = async (T: TestBase) => {
} }
const j = JSON.parse(data.content) as { requestId: string } const j = JSON.parse(data.content) as { requestId: string }
console.log("sending new operation to provider") console.log("sending new operation to provider")
bootstrapped.liquidProvider.onEvent(j, T.app.publicKey) bootstrapped.liquidityProvider.onEvent(j, T.app.publicKey)
}) })
bootstrapped.liquidProvider.attachNostrSend(async (_, data, r) => { bootstrapped.liquidityProvider.attachNostrSend(async (_, data, r) => {
const res = await handleSend(T, data) const res = await handleSend(T, data)
if (data.type === 'event') { if (data.type === 'event') {
throw new Error("unsupported event type") throw new Error("unsupported event type")
@ -34,12 +34,13 @@ export const initBootstrappedInstance = async (T: TestBase) => {
if (!res) { if (!res) {
return return
} }
bootstrapped.liquidProvider.onEvent(res, data.pub) bootstrapped.liquidityProvider.onEvent(res, data.pub)
}) })
bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) bootstrapped.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
await new Promise<void>(res => { await new Promise<void>(res => {
const interval = setInterval(() => { const interval = setInterval(async () => {
if (bootstrapped.liquidProvider.CanProviderHandle({ action: 'receive', amount: 2000 })) { const canHandle = await bootstrapped.liquidityProvider.CanProviderHandle({ action: 'receive', amount: 2000 })
if (canHandle) {
clearInterval(interval) clearInterval(interval)
res() res()
} else { } else {

View file

@ -14,7 +14,7 @@ export default async (T: TestBase) => {
const testSpamExternalPayment = async (T: TestBase) => { const testSpamExternalPayment = async (T: TestBase) => {
T.d("starting testSpamExternalPayment") T.d("starting testSpamExternalPayment")
const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const invoices = await Promise.all(new Array(10).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry))) const invoices = await Promise.all(new Array(10).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })))
T.d("generated 10 500 sats invoices for external node") T.d("generated 10 500 sats invoices for external node")
const res = await Promise.all(invoices.map(async (invoice, i) => { const res = await Promise.all(invoices.map(async (invoice, i) => {
try { try {

View file

@ -15,7 +15,7 @@ export default async (T: TestBase) => {
const testSpamExternalPayment = async (T: TestBase) => { const testSpamExternalPayment = async (T: TestBase) => {
T.d("starting testSpamExternalPayment") T.d("starting testSpamExternalPayment")
const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const invoicesForExternal = await Promise.all(new Array(5).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry))) const invoicesForExternal = await Promise.all(new Array(5).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })))
const invoicesForUser2 = await Promise.all(new Array(5).fill(0).map(() => T.main.paymentManager.NewInvoice(T.user2.userId, { amountSats: 500, memo: "test" }, { linkedApplication: application, expiry: defaultInvoiceExpiry }))) const invoicesForUser2 = await Promise.all(new Array(5).fill(0).map(() => T.main.paymentManager.NewInvoice(T.user2.userId, { amountSats: 500, memo: "test" }, { linkedApplication: application, expiry: defaultInvoiceExpiry })))
const invoices = invoicesForExternal.map(i => i.payRequest).concat(invoicesForUser2.map(i => i.invoice)) const invoices = invoicesForExternal.map(i => i.payRequest).concat(invoicesForUser2.map(i => i.invoice))
T.d("generated 10 500 sats mixed invoices between external node and user 2") T.d("generated 10 500 sats mixed invoices between external node and user 2")

View file

@ -10,7 +10,9 @@ import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
import SanityChecker from '../services/main/sanityChecker.js' import SanityChecker from '../services/main/sanityChecker.js'
import LND from '../services/lnd/lnd.js' import LND from '../services/lnd/lnd.js'
import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js' import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js'
import { LiquidityProvider } from '../services/lnd/liquidityProvider.js' import { LiquidityProvider } from '../services/main/liquidityProvider.js'
import { Utils } from '../services/helpers/utilsWrapper.js'
import { AdminManager } from '../services/main/adminManager.js'
chai.use(chaiString) chai.use(chaiString)
export const expect = chai.expect export const expect = chai.expect
export type Describe = (message: string, failure?: boolean) => void export type Describe = (message: string, failure?: boolean) => void
@ -29,6 +31,7 @@ export type TestBase = {
externalAccessToMainLnd: LND externalAccessToMainLnd: LND
externalAccessToOtherLnd: LND externalAccessToOtherLnd: LND
externalAccessToThirdLnd: LND externalAccessToThirdLnd: LND
adminManager: AdminManager
d: Describe d: Describe
} }
@ -45,16 +48,16 @@ export const SetupTest = async (d: Describe): Promise<TestBase> => {
const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId } const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId }
const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId } const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId }
const extermnalUtils = new Utils(settings)
const externalAccessToMainLnd = new LND(settings.lndSettings, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { }) const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToMainLnd.Warmup() await externalAccessToMainLnd.Warmup()
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode } const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }
const externalAccessToOtherLnd = new LND(otherLndSetting, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { }) const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToOtherLnd.Warmup() await externalAccessToOtherLnd.Warmup()
const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode } const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode }
const externalAccessToThirdLnd = new LND(thirdLndSetting, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { }) const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToThirdLnd.Warmup() await externalAccessToThirdLnd.Warmup()
@ -62,7 +65,8 @@ export const SetupTest = async (d: Describe): Promise<TestBase> => {
expect, main, app, expect, main, app,
user1, user2, user1, user2,
externalAccessToMainLnd, externalAccessToOtherLnd, externalAccessToThirdLnd, externalAccessToMainLnd, externalAccessToOtherLnd, externalAccessToThirdLnd,
d d,
adminManager: initialized.adminManager
} }
} }
@ -71,6 +75,7 @@ export const teardown = async (T: TestBase) => {
T.externalAccessToMainLnd.Stop() T.externalAccessToMainLnd.Stop()
T.externalAccessToOtherLnd.Stop() T.externalAccessToOtherLnd.Stop()
T.externalAccessToThirdLnd.Stop() T.externalAccessToThirdLnd.Stop()
T.adminManager.Stop()
resetDisabledLoggers() resetDisabledLoggers()
console.log("teardown") console.log("teardown")
} }
@ -78,7 +83,7 @@ export const teardown = async (T: TestBase) => {
export const safelySetUserBalance = async (T: TestBase, user: TestUserData, amount: number) => { export const safelySetUserBalance = async (T: TestBase, user: TestUserData, amount: number) => {
const app = await T.main.storage.applicationStorage.GetApplication(user.appId) const app = await T.main.storage.applicationStorage.GetApplication(user.appId)
const invoice = await T.main.paymentManager.NewInvoice(user.userId, { amountSats: amount, memo: "test" }, { linkedApplication: app, expiry: defaultInvoiceExpiry }) const invoice = await T.main.paymentManager.NewInvoice(user.userId, { amountSats: amount, memo: "test" }, { linkedApplication: app, expiry: defaultInvoiceExpiry })
await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, 100) await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, 100, amount, { from: 'system', useProvider: false })
const u = await T.main.storage.userStorage.GetUser(user.userId) const u = await T.main.storage.userStorage.GetUser(user.userId)
expect(u.balance_sats).to.be.equal(amount) expect(u.balance_sats).to.be.equal(amount)
T.d(`user ${user.appUserIdentifier} balance is now ${amount}`) T.d(`user ${user.appUserIdentifier} balance is now ${amount}`)

View file

@ -12,7 +12,7 @@ export default async (T: TestBase) => {
const testSuccessfulUserPaymentToExternalNode = async (T: TestBase) => { const testSuccessfulUserPaymentToExternalNode = async (T: TestBase) => {
T.d("starting testSuccessfulUserPaymentToExternalNode") T.d("starting testSuccessfulUserPaymentToExternalNode")
const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry) const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })
const payment = await T.main.appUserManager.PayInvoice({ app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, { invoice: invoice.payRequest, amount: 0 }) const payment = await T.main.appUserManager.PayInvoice({ app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, { invoice: invoice.payRequest, amount: 0 })
T.d("paid 500 sats invoice from user1 to external node") T.d("paid 500 sats invoice from user1 to external node")
} }

View file

@ -1,102 +1,142 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Montserrat"
/>
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/backup.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<header>
<img
src="img/pub_logo.png"
width="38px"
height="auto"
alt="Lightning Pub logo"
/>
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main> <head>
<section class="setup-header"> <meta charset="UTF-8" />
<button class="icon-button back-button" onclick="history.back()"> <title></title>
<img src="img/back.svg" alt="" /> <meta charset="UTF-8" />
</button> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<h2>Choose a Recovery Method</h2> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<p class="header-title"> <link rel="stylesheet" href="css/styles.css" />
<span style="font-weight: bold">New Node! 🎉</span> It's important <link rel="stylesheet" href="css/backup.css" />
to backup your keys. <!-- HTML Meta Tags -->
</p> <title>Lightning.Pub</title>
</section> <meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<div class="line"></div> <body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<section class="setup-content"> <main>
<div class="description-box"> <section class="setup-header">
<div class="description"> <button class="icon-button back-button" onclick="history.back()">
In addition to your seed phrase, you also need channel details to recover funds should your node experience a hardware failure. <img src="img/back.svg" alt="" />
</div> </button>
<br /> <h2>Choose a Recovery Method</h2>
<div class="description"> <p class="header-title">
It's important always to have the latest version of this file. Fortunately, it's small enough to automatically store on the Nostr relay. <span style="font-weight: bold">New Node! 🎉</span> It's important
</div> to backup your keys.
</div> </p>
<div class="warning-text"> </section>
If you did not choose the developers relay, be sure your relay has
adequate storage policies to hold NIP78 events.
</div>
<div class="checkbox-container">
<div class="checkbox" style="margin-top: 12px">
<input type="checkbox" id="backup" />
<div class="checkbox-shape"></div>
<label for="backup">
Encrypted Backup to Nostr Relay
</label>
</div>
</div>
<div class="checkbox-container">
<div class="checkbox manual-checkbox" style="margin-top: 12px">
<input type="checkbox" id="manual-backup" />
<div class="checkbox-shape"></div>
<label for="manual-backup" >
DO NOT store on relay (Manual Backups)
</label>
</div>
</div>
<button
class="push-button hidden-button"
onclick="location.href='seed.html'"
style="margin-top: 60px;"
id="next-button"
>
Next
</button>
</section>
</main>
<footer> <div class="line"></div>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div> <section class="setup-content">
<div>bleeding-edge software, and agree to the providers</div> <div class="description-box">
<div> <div class="description">
<span style="color: #c434e0">terms</span> regarding any services In addition to your seed phrase, you also need channel details to recover funds should your node experience a
herein. hardware failure.
</div>
<br />
<div class="description">
It's important always to have the latest version of this file. Fortunately, it's small enough to automatically
store on the Nostr relay.
</div> </div>
</div> </div>
<div class="line"></div> <div class="warning-text">
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a> If you did not choose the developers relay, be sure your relay has
</footer> adequate storage policies to hold NIP78 events.
</div>
<div class="checkbox-container">
<div class="checkbox" style="margin-top: 12px">
<input type="checkbox" id="backup" />
<div class="checkbox-shape"></div>
<label for="backup">
Encrypted Backup to Nostr Relay
</label>
</div>
</div>
<div class="checkbox-container">
<div class="checkbox manual-checkbox" style="margin-top: 12px">
<input type="checkbox" id="manual-backup" />
<div class="checkbox-shape"></div>
<label for="manual-backup">
DO NOT store on relay (Manual Backups)
</label>
</div>
</div>
<div>
<p id="errorText" style="color:red"></p>
</div>
<button class="push-button hidden-button" style="margin-top: 60px;" id="next-button">
Next
</button>
</section>
</main>
<script src="js/backup.js"></script> <footer>
</body> <div class="footer-text">
</html> <div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div>
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script src="js/backup.js"></script>
<script>
const sendConfig = async () => {
const req = {
source_name: localStorage.getItem("wizard/nodeName"),
relay_url: localStorage.getItem("wizard/relayUrl"),
automate_liquidity: localStorage.getItem("wizard/liquidity") === 'automate',
push_backups_to_nostr: localStorage.getItem("wizard/backup") === 'backup',
}
const res = await fetch("/wizard/config", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(req)
})
if (res.status !== 200) {
document.getElementById('errorText').innerText = "failed to start service"
return
}
const j = await res.json()
if (j.status !== 'OK') {
document.getElementById('errorText').innerText = "failed to start service" + j.reason
return
}
location.href = 'connect.html'
}
document.getElementById("next-button").onclick = (e) => {
const backup = document.getElementById('backup').checked
const manual = document.getElementById('manual-backup').checked
if (!backup && !manual) {
document.getElementById('errorText').innerText = 'Please select an option'
return
}
if (backup && manual) {
document.getElementById('errorText').innerText = 'Please select only one option'
return
}
if (backup) {
localStorage.setItem('wizard/backup', 'backup')
} else {
localStorage.setItem('wizard/backup', 'manual')
}
sendConfig()
}
</script>
</body>
</html>

View file

@ -1,80 +1,116 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Montserrat"
/>
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/connect.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<header>
<img
src="img/pub_logo.png"
width="38px"
height="auto"
alt="Lightning Pub logo"
/>
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main> <head>
<section class="setup-header"> <meta charset="UTF-8" />
<button class="icon-button back-button" onclick="history.back()"> <title></title>
<img src="img/back.svg" alt="" /> <meta charset="UTF-8" />
</button> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<h2>Connect</h2> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<p class="header-title"> <link rel="stylesheet" href="css/styles.css" />
You can now manage your node remotely <link rel="stylesheet" href="css/connect.css" />
</p> <!-- HTML Meta Tags -->
</section> <title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<div class="line"></div> <body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<section class="setup-content"> <main>
<div>For dashboard access, use <a href="https://preview.uxpin.com/ae6e6372ab26cd13438d486d4d1ac9d184ec8e82#/pages/164889267" style="color: #2aabe9;" target="_blank">ShockWallet</a> and tap the logo 3 times.</div> <section class="setup-header">
<div style="font-size: 13px; margin-top: 5px;">Scan the QR or Copy-Paste the string to establish the connection.</div> <button class="icon-button back-button" onclick="history.back()">
<div style="display: flex; justify-content: center;"> <img src="img/back.svg" alt="" />
<div class="qrcode-box" id="codebox"> </button>
<div style="font-size: 11px;"> <h2>Connect</h2>
<div style="text-align: center; color: #a3a3a3;">Code contains a one-time pairing secret</div> <p class="header-title">
<div style="text-align: center; color: #c434e0;" id="click-text">Click to reveal</div> You can now manage your node remotely
</div> </p>
<div id="qrcode"></div> </section>
<div style="color: #a3a3a3; font-size: 11px;">
<div>npub123abcdefghhhhhhhhhhhhhhh</div> <div class="line"></div>
<div>relay.lightning.pub</div>
</div> <section class="setup-content">
<div>For dashboard access, use <a
href="https://preview.uxpin.com/ae6e6372ab26cd13438d486d4d1ac9d184ec8e82#/pages/164889267"
style="color: #2aabe9;" target="_blank">ShockWallet</a> and tap the logo 3 times.</div>
<div style="font-size: 13px; margin-top: 5px;">Scan the QR or Copy-Paste the string to establish the connection.
</div>
<div style="display: flex; justify-content: center;">
<div class="qrcode-box" id="codebox">
<div style="font-size: 11px;">
<div style="text-align: center; color: #a3a3a3;">Code contains a one-time pairing secret</div>
<div style="text-align: center; color: #c434e0;" id="click-text">Click to reveal</div>
</div>
<div id="qrcode"></div>
<div style="color: #a3a3a3; font-size: 11px;">
<div id="connectString"></div>
</div> </div>
</div> </div>
</section>
</main>
<footer>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div> </div>
<div class="line"></div> </section>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a> </main>
</footer> <p class="errorText" style="color:red"></p>
<footer>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div>
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script> <script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>
<script src="js/script.js"></script> <script src="js/connect.js"></script>
<script src="js/connect.js"></script> <script>
</body> const fetchInfo = async () => {
</html> console.log("fewtching...")
const res = await fetch("/wizard/admin_connect_info")
console.log(res)
if (res.status !== 200) {
document.getElementById('errorText').innerText = "failed to get connection info"
return
}
const j = await res.json()
console.log(j)
if (j.status !== 'OK') {
document.getElementById('errorText').innerText = "failed to get connection info" + j.reason
return
}
if (j.connect_info.enrolled_npub) {
location.href = 'status.html'
} else {
const connectString = j.nprofile + ":" + j.connect_info.admin_token
console.log({ connectString })
const qrElement = document.getElementById("qrcode")
qrElement.onclick = () => {
document.navigator.clipboard.writeText(connectString)
}
const qrcode = new QRCode(qrElement, {
text: connectString,
colorDark: "#000000",
colorLight: "#ffffff",
width: 157,
height: 157,
// correctLevel : QRCode.CorrectLevel.H
});
document.getElementById('connectString').innerHTML = connectString
}
}
try {
fetchInfo()
} catch (e) { console.log({ e }) }
</script>
</body>
</html>

View file

@ -1,89 +1,112 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Montserrat"
/>
<link rel="stylesheet" href="css/styles.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
</head>
<body>
<header>
<img
src="img/pub_logo.png"
width="38px"
height="auto"
alt="Lightning Pub logo"
/>
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main> <head>
<section class="setup-header"> <meta charset="UTF-8" />
<h2>Setup your Pub</h2> <title></title>
<p class="header-title"> <meta charset="UTF-8" />
</p> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</section> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<link rel="stylesheet" href="css/styles.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
</head>
<div class="line"></div> <body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<section class="setup-content"> <main>
<div class="input-group"> <section class="setup-header">
<span>Give this node a name that wallet users will see:</span> <h2>Setup your Pub</h2>
<input <p class="header-title">
type="text" </p>
placeholder="Nodey McNodeFace" </section>
value=""
style="width: 100%"
/>
</div>
<div class="input-group" style="margin-top: 38px"> <div class="line"></div>
<span>If you want to use a specific Nostr relay, enter it now:</span>
<input
type="text"
placeholder="wss://relay.lightning.pub"
style="width: 100%"
/>
</div>
<div class="checkbox" style="margin-top: 12px"> <section class="setup-content">
<input type="checkbox" id="customCheckbox" /> <div class="input-group">
<div class="checkbox-shape"></div> <span>Give this node a name that wallet users will see:</span>
<label for="customCheckbox"> <input type="text" placeholder="Nodey McNodeFace" value="" style="width: 100%" id="nodeName" />
Use the default managed relay service and auto-pay 1000 sats
per month to support developers
</label>
</div>
<button
class="push-button"
onclick="location.href='liquidity.html'"
style="margin-top: 60px"
>
Next
</button>
</section>
</main>
<footer>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div> </div>
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a> <div class="input-group" style="margin-top: 38px">
</footer> <span>If you want to use a specific Nostr relay, enter it now:</span>
</body> <input type="text" placeholder="wss://relay.lightning.pub" style="width: 100%" id="relayUrl" />
</html> </div>
<div class="checkbox" style="margin-top: 12px">
<input type="checkbox" id="customCheckbox" />
<div class="checkbox-shape"></div>
<label for="customCheckbox">
Use the default managed relay service and auto-pay 1000 sats
per month to support developers
</label>
</div>
<div>
<p id="errorText" style="color:red"></p>
</div>
<button class="push-button" style="margin-top: 60px" id="liquidityBtn">
Next
</button>
</section>
</main>
<footer>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div>
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script>
document.getElementById("liquidityBtn").onclick = (e) => {
const nodeName = document.getElementById("nodeName").value;
const relayUrl = document.getElementById("relayUrl").value;
const checked = document.getElementById("customCheckbox").checked;
if (!nodeName) {
document.getElementById("errorText").innerText = "Please enter a node name";
return;
}
if (!checked && !relayUrl) {
document.getElementById("errorText").innerText = "Please enter a relay URL or check the default relay box";
return;
}
localStorage.setItem("wizard/nodeName", nodeName);
if (checked) {
localStorage.setItem("wizard/relayUrl", "wss://relay.lightning.pub");
} else {
localStorage.setItem("wizard/relayUrl", relayUrl);
}
location.href = 'liquidity.html'
}
fetch("/wizard/state").then((res) => {
if (res.status === 200) {
res.json().then((data) => {
if (data.admin_linked) {
location.href = 'status.html'
} else if (data.config_sent) {
location.href = 'connect.html'
} else {
console.log("ready to initialize")
}
});
}
});
</script>
</body>
</html>

View file

@ -1,93 +1,108 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Montserrat"
/>
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/liquidity.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<header>
<img
src="img/pub_logo.png"
width="38px"
height="auto"
alt="Lightning Pub logo"
/>
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main> <head>
<section class="setup-header"> <meta charset="UTF-8" />
<button class="icon-button back-button" onclick="history.back()"> <title></title>
<img src="img/back.svg" alt="" /> <meta charset="UTF-8" />
</button> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<h2>Manage Node Liquidity</h2> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<p class="header-title"> <link rel="stylesheet" href="css/styles.css" />
How do you want to manage Lightning channels? <link rel="stylesheet" href="css/liquidity.css" />
</p> <!-- HTML Meta Tags -->
</section> <title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<div class="line"></div> <body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<section class="setup-content"> <main>
<div class="checkbox" style="margin-top: 60px"> <section class="setup-header">
<input type="checkbox" id="automate" data-group="service" /> <button class="icon-button back-button" onclick="history.back()">
<div class="checkbox-shape"></div> <img src="img/back.svg" alt="" />
<label for="automate" class="automate"> </button>
Use Automation Service <h2>Manage Node Liquidity</h2>
<div class="question-box"> <p class="header-title">
<button class="icon-button" id="show-question"> How do you want to manage Lightning channels?
<img src="img/question.svg" /> </p>
</button> </section>
</div>
<div class="question-content" id="question-content">
Automation helps reduce the fees you pay by trusting peers temporarily until your node balance is sufficient to open a balanced Lightning channel.
<button class="icon-button close-button" id="close-question">
<img src="img/close.svg" alt="" />
</button>
<a href="https://docs.shock.network/" target="_blank" class="marked question-more">Learn More</a>
</div>
</label>
</div>
<div class="checkbox" style="margin-top: 30px">
<input type="checkbox" id="manual" data-group="service" />
<div class="checkbox-shape"></div>
<label for="manual">Manage my channels manually</label>
</div>
<button
class="push-button"
onclick="location.href='backup.html'"
style="margin-top: 60px"
>
Next
</button>
</section>
</main>
<footer> <div class="line"></div>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div> <section class="setup-content">
<div>bleeding-edge software, and agree to the providers</div> <div class="checkbox" style="margin-top: 60px">
<div> <input type="checkbox" id="automate" data-group="service" />
<span style="color: #c434e0">terms</span> regarding any services <div class="checkbox-shape"></div>
herein. <label for="automate" class="automate">
</div> Use Automation Service
<div class="question-box">
<button class="icon-button" id="show-question">
<img src="img/question.svg" />
</button>
</div>
<div class="question-content" id="question-content">
Automation helps reduce the fees you pay by trusting peers temporarily until your node balance is sufficient
to open a balanced Lightning channel.
<button class="icon-button close-button" id="close-question">
<img src="img/close.svg" alt="" />
</button>
<a href="https://docs.shock.network/" target="_blank" class="marked question-more">Learn More</a>
</div>
</label>
</div> </div>
<div class="line"></div> <div class="checkbox" style="margin-top: 30px">
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a> <input type="checkbox" id="manual" data-group="service" />
</footer> <div class="checkbox-shape"></div>
<label for="manual">Manage my channels manually</label>
</div>
<div>
<p id="errorText" style="color:red"></p>
</div>
<button class="push-button" style="margin-top: 60px" id="backupBtn">
Next
</button>
</section>
</main>
<script src="js/liquidity.js"></script> <footer>
</body> <div class="footer-text">
</html> <div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div>
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script src="js/liquidity.js"></script>
<script>
document.getElementById("backupBtn").onclick = (e) => {
const automate = document.getElementById('automate').checked
const manual = document.getElementById('manual').checked
if (!automate && !manual) {
document.getElementById('errorText').innerText = 'Please select an option'
return
}
if (automate && manual) {
document.getElementById('errorText').innerText = 'Please select only one option'
return
}
if (automate) {
localStorage.setItem('wizard/liquidity', 'automate')
} else {
localStorage.setItem('wizard/liquidity', 'manual')
}
location.href = 'backup.html'
}
</script>
</body>
</html>

View file

@ -1,180 +1,74 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Montserrat"
/>
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/seed.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<header>
<img
src="img/pub_logo.png"
width="38px"
height="auto"
alt="Lightning Pub logo"
/>
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main> <head>
<section class="setup-header"> <meta charset="UTF-8" />
<button class="icon-button back-button" onclick="history.back()"> <title></title>
<img src="img/back.svg" alt="" /> <meta charset="UTF-8" />
</button> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<h2>Seed Phrase</h2> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<p class="header-title"> <link rel="stylesheet" href="css/styles.css" />
Store your seed phrase somewhere safe, you will need it if something ever goes wrong with your node hard drive. <link rel="stylesheet" href="css/seed.css" />
</p> <!-- HTML Meta Tags -->
</section> <title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<div class="line"></div> <body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<section class="setup-content"> <main>
<div class="seed-box-container blur-filter" id="seed-box-container"> <section class="setup-header">
<div class="seed-box"> <button class="icon-button back-button" onclick="history.back()">
<span>1</span> <img src="img/back.svg" alt="" />
<span>albert</span> </button>
</div> <h2>Seed Phrase</h2>
<div class="seed-box"> <p class="header-title">
<span>1</span> Store your seed phrase somewhere safe, you will need it if something ever goes wrong with your node hard drive.
<span>albert</span> </p>
</div> </section>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
<div class="seed-box">
<span>1</span>
<span>albert</span>
</div>
</div>
<button class="reveal-button" id="reveal-button">Click To Reveal</button> <div class="line"></div>
<div class="checkbox-container"> <section class="setup-content">
<div class="checkbox" style="margin-top: 12px"> <div class="seed-box-container blur-filter" id="seed-box-container">
<input type="checkbox" id="copied" /> </div>
<div class="checkbox-shape"></div>
<label for="copied">
I have copied this somewhere safe
</label>
</div>
</div>
<button
id="next-button"
class="push-button hidden-button"
onclick="location.href='connect.html'"
style="margin-top: 60px"
>
Next
</button>
</section>
</main>
<footer> <button class="reveal-button" id="reveal-button">Click To Reveal</button>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div> <div class="checkbox-container">
<div>bleeding-edge software, and agree to the providers</div> <div class="checkbox" style="margin-top: 12px">
<div> <input type="checkbox" id="copied" />
<span style="color: #c434e0">terms</span> regarding any services <div class="checkbox-shape"></div>
herein. <label for="copied">
I have copied this somewhere safe
</label>
</div> </div>
</div> </div>
<div class="line"></div> <button id="next-button" class="push-button hidden-button" style="margin-top: 60px">
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a> Next
</footer> </button>
</section>
</main>
<script src="js/seed.js"></script> <footer>
</body> <div class="footer-text">
</html> <div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div>
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script src="js/seed.js"></script>
</body>
</html>

174
static/status.html Normal file
View file

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/status.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main>
<section class="setup-header">
<h2>Node Status</h2>
<p class="header-title"></p>
</section>
<div class="line" style="width: 100%;"></div>
<section class="node-status">
<p id="errorText" style="color:red"></p>
<div>
<div class="status-element" style="margin-top: 15px;">
<div style="text-align: left;">Public Node Name:</div>
<div class="fc-grey editable-content">
<div class="show-nodey" style="display: flex; flex-direction: column; display: none;">
<input type="text" value="" name="show-nodey" placeholder="Nodey McNodeFace" />
<div style="display: flex;justify-content: end;">
<button class="small-btn" id="cancel-show-nodey">Cancel</button>
<button class="small-btn" id="save-show-nodey">Save</button>
</div>
</div>
<div id="show-nodey-text">Nodey McNodeFace</div>
<div class="question-box">
<button class="icon-button" id="show-nodey">
<img src="img/pencil.svg" style="cursor: pointer;" />
</button>
</div>
</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div style="text-align: left;">Nostr Relay:</div>
<div class="fc-grey editable-content">
<div class="show-nostr" style="display: flex; flex-direction: column; display: none;">
<input type="text" value="" name="show-nostr" placeholder="wss://relay.lightning.pub" />
<div style="display: flex;justify-content: end;">
<button class="small-btn" id="cancel-show-nostr">Cancel</button>
<button class="small-btn" id="save-show-nostr">Save</button>
</div>
</div>
<div id="show-nostr-text">wss://relay.lightning.pub</div>
<div class="question-box">
<button class="icon-button" id="show-nostr">
<img src="img/pencil.svg" style="cursor: pointer;" />
</button>
</div>
</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div>Administrator:</div>
<div id="adminNpub" style="line-break: anywhere;">
Loading...
</div>
</div>
</div>
<div style="display: flex; justify-content: end;padding-right: 12px;">
<div class="marked" id="show-reset" style="text-decoration: underline; margin-top: 5px;position: relative;">
Reset
<div class="watchdog-status">
<button class="icon-button" id="show-question">
<img src="img/question.svg" />
</button>
</div>
</div>
</div>
<div id="reset-box">
<div style="width: 100%;height: 100%;position: relative;">
<button class="icon-button close-button" id="close-reset-box">
<img src="img/close.svg" alt="">
</button>
<div class="reset-box-content" id="reset-content">
</div>
<div class="continue-button-container">
<div class="continue-button" id="">Continue</div>
</div>
</div>
</div>
<div style="margin-top: 40px;">
<div class="status-element">
<div>Relay Status:</div>
<div id="relayStatus">
<span class="yellow-dot">&#9679;</span> Loading...
</div>
</div>
<div class="status-element">
<div>Lightning Status:</div>
<div id="lndStatus">
<span class="yellow-dot">&#9679;</span> Loading...
</div>
</div>
<div class="status-element">
<div style="position: relative;">
Watchdog Status:
<div class="watchdog-status">
<button class="icon-button" id="show-question">
<img src="img/question.svg" />
</button>
</div>
</div>
<div id="watchdog-status">
<span class="green-dot">&#9679;</span> Loading...
</div>
</div>
</div>
<div style="margin-top: 20px;">
<div style="font-size: 13px; text-align: left;">Guest Invitation Link:</div>
<a href="https://my.shockwallet.app/invite/nprofile12345678899988" target="_blank"
style="font-size: 11px;line-break: anywhere;" id="inviteLinkHttp" class="invite-link">
https://my.shockwallet.app/invite/nprofile12345678899988
</div>
</div>
</section>
</main>
<footer>
<div class="footer-text" style="width: 80%">
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script src="js/status.js"></script>
<script>
const fetchInfo = async () => {
console.log("fewtching...")
const res = await fetch("/wizard/service_state")
console.log(res)
if (res.status !== 200) {
document.getElementById('errorText').innerText = "failed to get state info"
return
}
const j = await res.json()
console.log(j)
if (j.status !== 'OK') {
document.getElementById('errorText').innerText = "failed to get state info" + j.reason
return
}
document.getElementById("show-nodey-text").innerHTML = j.provider_name
document.getElementById("show-nostr-text").innerHTML = j.relays[0]
document.getElementById("adminNpub").innerText = j.admin_npub
document.getElementById("relayStatus").innerHTML = `<span class="${j.relay_connected ? 'green-dot' : 'red-dot'}">&#9679;</span> ${j.relay_connected ? 'Connected' : 'Disconnected'}`
document.getElementById("lndStatus").innerHTML = `<span class="${j.lnd_state === 'ONLINE' ? 'green-dot' : 'red-dot'}">&#9679;</span> ${j.lnd_state}`
document.getElementById("watchdog-status").innerHTML = `<span class="${j.watchdog_ok ? 'green-dot' : 'red-dot'}">&#9679;</span> ${j.watchdog_ok ? 'No Alerts' : 'ALERT!!'}`
document.getElementById("inviteLinkHttp").href = `https://my.shockwallet.app/invite/${j.nprofile}`
document.getElementById("inviteLinkHttp").innerHTML = `https://my.shockwallet.app/invite/${j.nprofile}`
}
try {
fetchInfo()
} catch (e) { console.log({ e }) }
</script>
</body>
</html>

View file

@ -1,148 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Montserrat"
/>
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/status.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<header>
<img
src="img/pub_logo.png"
width="38px"
height="auto"
alt="Lightning Pub logo"
/>
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main>
<section class="setup-header">
<h2>Node Status</h2>
<p class="header-title"></p>
</section>
<div class="line" style="width: 100%;"></div>
<section class="node-status">
<div>
<div class="status-element" style="margin-top: 15px;">
<div style="text-align: left;">Public Node Name:</div>
<div class="fc-grey editable-content">
<div class="show-nodey" style="display: flex; flex-direction: column; display: none;">
<input type="text" value="" name="show-nodey" placeholder="Nodey McNodeFace" />
<div style="display: flex;justify-content: end;">
<button class="small-btn" id="cancel-show-nodey">Cancel</button>
<button class="small-btn" id="save-show-nodey">Save</button>
</div>
</div>
<div id="show-nodey-text">Nodey McNodeFace</div>
<div class="question-box">
<button class="icon-button" id="show-nodey">
<img src="img/pencil.svg" style="cursor: pointer;" />
</button>
</div>
</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div style="text-align: left;">Nostr Relay:</div>
<div class="fc-grey editable-content">
<div class="show-nostr" style="display: flex; flex-direction: column; display: none;">
<input type="text" value="" name="show-nostr" placeholder="wss://relay.lightning.pub" />
<div style="display: flex;justify-content: end;">
<button class="small-btn" id="cancel-show-nostr">Cancel</button>
<button class="small-btn" id="save-show-nostr">Save</button>
</div>
</div>
<div id="show-nostr-text">wss://relay.lightning.pub</div>
<div class="question-box">
<button class="icon-button" id="show-nostr">
<img src="img/pencil.svg" style="cursor: pointer;" />
</button>
</div>
</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div>Administrator:</div>
<div>
npub12334556677889990
</div>
</div>
</div>
<div style="display: flex; justify-content: end;padding-right: 12px;">
<div class="marked" id="show-reset" style="text-decoration: underline; margin-top: 5px;position: relative;">Reset
<div class="watchdog-status">
<button class="icon-button" id="show-question">
<img src="img/question.svg" />
</button>
</div>
</div>
</div>
<div id="reset-box">
<div style="width: 100%;height: 100%;position: relative;">
<button class="icon-button close-button" id="close-reset-box">
<img src="img/close.svg" alt="">
</button>
<div class="reset-box-content" id="reset-content">
</div>
<div class="continue-button-container">
<div class="continue-button" id="">Continue</div>
</div>
</div>
</div>
<div style="margin-top: 40px;">
<div class="status-element">
<div>Relay Status:</div>
<div>
<span class="green-dot">&#9679;</span> Connected
</div>
</div>
<div class="status-element">
<div>Lightning Status:</div>
<div>
<span class="yellow-dot">&#9679;</span> Syncing
</div>
</div>
<div class="status-element">
<div style="position: relative;">
Watchdog Status:
<div class="watchdog-status">
<button class="icon-button" id="show-question">
<img src="img/question.svg" />
</button>
</div>
</div>
<div>
<span class="green-dot">&#9679;</span> No Alarms
</div>
</div>
</div>
<div style="margin-top: 20px;">
<div style="font-size: 13px; text-align: left;">Guest Invitation Link:</div>
<a href="https://my.shockwallet.app/invite/nprofile12345678899988" target="_blank" style="font-size: 11px;" class="invite-link">
https://my.shockwallet.app/invite/nprofile12345678899988
</div>
</div>
</section>
</main>
<footer>
<div class="footer-text" style="width: 80%">
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script src="js/status.js"></script>
</body>
</html>