Merge pull request #858 from shocknet/swaps-test

Reverse Swaps + provider fixes + fees fixes
This commit is contained in:
Justin (shocknet) 2026-01-19 13:13:23 -05:00 committed by GitHub
commit 4c58cab1d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 4306 additions and 770 deletions

View file

@ -21,6 +21,7 @@ import { ManagementGrant } from "./build/src/services/storage/entity/ManagementG
import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js" import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js"
import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js" import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js"
import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js" import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js"
import { TransactionSwap } from "./build/src/services/storage/entity/TransactionSwap.js"
import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js' import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
@ -41,6 +42,11 @@ import { AppUserDevice1753285173175 } from './build/src/services/storage/migrati
import { UserAccess1759426050669 } from './build/src/services/storage/migrations/1759426050669-user_access.js' import { UserAccess1759426050669 } from './build/src/services/storage/migrations/1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './build/src/services/storage/migrations/1760000000000-add_blind_to_user_offer.js' import { AddBlindToUserOffer1760000000000 } from './build/src/services/storage/migrations/1760000000000-add_blind_to_user_offer.js'
import { ApplicationAvatarUrl1761000001000 } from './build/src/services/storage/migrations/1761000001000-application_avatar_url.js' import { ApplicationAvatarUrl1761000001000 } from './build/src/services/storage/migrations/1761000001000-application_avatar_url.js'
import { AdminSettings1761683639419 } from './build/src/services/storage/migrations/1761683639419-admin_settings.js'
import { TxSwap1762890527098 } from './build/src/services/storage/migrations/1762890527098-tx_swap.js'
import { TxSwapAddress1764779178945 } from './build/src/services/storage/migrations/1764779178945-tx_swap_address.js'
import { ClinkRequester1765497600000 } from './build/src/services/storage/migrations/1765497600000-clink_requester.js'
import { TrackedProviderHeight1766504040000 } from './build/src/services/storage/migrations/1766504040000-tracked_provider_height.js'
export default new DataSource({ export default new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
@ -49,10 +55,12 @@ export default new DataSource({
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878,
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611,
AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000], AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings], TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap],
// synchronize: true, // synchronize: true,
}) })
//npx typeorm migration:generate ./src/services/storage/migrations/admin_settings -d ./datasource.js //npx typeorm migration:generate ./src/services/storage/migrations/swaps_service_url -d ./datasource.js

73
decode.js Executable file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env node
import { decodeBech32, nip19 } from '@shocknet/clink-sdk'
const nip19String = process.argv[2]
if (!nip19String) {
console.error('Usage: node decode.js <nip19_string>')
console.error('Example: node decode.js nprofile1qyd8wumn8ghj7um5wfn8y7fwwd5x7cmt9ehx2arhdaexkqpqwmk5tuqvafa6ckwc6zmaypyy3af3n4aeds2ql7m0ew42kzsn638q9s9z8p')
process.exit(1)
}
try {
// Check prefix to determine which decoder to use
const prefix = nip19String.split('1')[0]
const decoded = (prefix === 'noffer' || prefix === 'ndebit' || prefix === 'nmanage')
? decodeBech32(nip19String)
: nip19.decode(nip19String)
console.log('\nDecoded:')
console.log('Type:', decoded.type)
console.log('\nData:')
console.log(JSON.stringify(decoded.data, null, 2))
if (decoded.type === 'nprofile') {
console.log('\nDetails:')
console.log(' Pubkey:', decoded.data.pubkey)
if (decoded.data.relays) {
console.log(' Relays:')
decoded.data.relays.forEach((relay, i) => {
console.log(` ${i + 1}. ${relay}`)
})
}
} else if (decoded.type === 'npub') {
console.log('\nDetails:')
console.log(' Pubkey:', decoded.data)
} else if (decoded.type === 'nsec') {
console.log('\nDetails:')
console.log(' Private Key:', decoded.data)
} else if (decoded.type === 'note') {
console.log('\nDetails:')
console.log(' Event ID:', decoded.data)
} else if (decoded.type === 'noffer') {
console.log('\nDetails:')
console.log(' Pubkey:', decoded.data.pubkey)
console.log(' Offer:', decoded.data.offer)
if (decoded.data.relay) {
console.log(' Relay:', decoded.data.relay)
}
if (decoded.data.priceType) {
console.log(' Price Type:', decoded.data.priceType)
}
} else if (decoded.type === 'ndebit') {
console.log('\nDetails:')
console.log(' Pubkey:', decoded.data.pubkey)
console.log(' Pointer:', decoded.data.pointer)
if (decoded.data.relay) {
console.log(' Relay:', decoded.data.relay)
}
} else if (decoded.type === 'nmanage') {
console.log('\nDetails:')
console.log(' Pubkey:', decoded.data.pubkey)
console.log(' Pointer:', decoded.data.pointer)
if (decoded.data.relay) {
console.log(' Relay:', decoded.data.relay)
}
}
} catch (error) {
console.error('Error decoding bech32 string:', error.message)
process.exit(1)
}

View file

@ -2,6 +2,7 @@
# Copy this file as .env in the Pub folder and uncomment the desired settings to override defaults # Copy this file as .env in the Pub folder and uncomment the desired settings to override defaults
# Alternatively, these settings can be passed as environment variables at startup # Alternatively, these settings can be passed as environment variables at startup
#LND_CONNECTION #LND_CONNECTION
# Defaults typical for straight Linux # Defaults typical for straight Linux
# Containers, Mac and Windows may need more detailed paths # Containers, Mac and Windows may need more detailed paths
@ -9,15 +10,22 @@
#LND_CERT_PATH=~/.lnd/tls.cert #LND_CERT_PATH=~/.lnd/tls.cert
#LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon #LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
#LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log #LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log
#BTC_NETWORK=mainnet
# Bypass LND entirely and daisychain off the bootstrap provider (testing only) # Bypass LND entirely and daisychain off the bootstrap provider (testing only)
#USE_ONLY_LIQUIDITY_PROVIDER=false #USE_ONLY_LIQUIDITY_PROVIDER=false
#BOOTSTRAP_PEER #BOOTSTRAP_PEER
# A trusted peer that will hold a node-level account until channel automation becomes affordable # A trusted peer that will hold a node-level account until channel automation becomes affordable
# The developer is used by default or you may specify your own # The provider pubkey is extracted from PROVIDER_NPROFILE (nprofile contains both pubkey and relay URL)
# To disable this feature entirely overwrite the env with "null" # The developers node is used by default, or you may specify another.
#LIQUIDITY_PROVIDER_PUB=null # To disable this feature entirely set DISABLE_LIQUIDITY_PROVIDER=true
#DISABLE_LIQUIDITY_PROVIDER=false #DISABLE_LIQUIDITY_PROVIDER=false
#PROVIDER_NPROFILE=nprofile1qyd8wumn8ghj7um5wfn8y7fwwd5x7cmt9ehx2arhdaexkqpqwmk5tuqvafa6ckwc6zmaypyy3af3n4aeds2ql7m0ew42kzsn638q9s9z8p
#SWAPS
#BOLTZ_HTTP_URL=
#BOLTZ_WEBSOCKET_URL=
#ENABLE_SWAPS=false
#DB #DB
#DATABASE_FILE=db.sqlite #DATABASE_FILE=db.sqlite
@ -33,48 +41,44 @@
#LOCALHOST #LOCALHOST
# For REST Management
#ADMIN_TOKEN= #ADMIN_TOKEN=
#PORT=1776 #PORT=1776
#JWT_SECRET= #JWT_SECRET=
#PUSH_SERVICE
# For Wallet notifs via FCM
#SHOCK_PUSH_URL= #SHOCK_PUSH_URL=
#Lightning Address Bridge #LNA Bridge
# Tell wallets where they can get a noffer-based Lightning Address
#BRIDGE_URL=https://shockwallet.app #BRIDGE_URL=https://shockwallet.app
#LIGHTNING #LIGHTNING
# Maximum amount in network fees passed to LND when it pays an external invoice # Service Fee: What Pub charges users (max of BPS*amount or floor)
# Routing Fee Limit: What Pub allows LND to spend (max of BPS*amount or floor)
# Routing fee limit must be <= service fee (validated at startup to ensure spread)
# BPS are basis points, 100 BPS = 1% # BPS are basis points, 100 BPS = 1%
#OUTBOUND_MAX_FEE_BPS=60 #SERVICE_FEE_BPS=60
#OUTBOUND_MAX_FEE_EXTRA_SATS=100 #ROUTING_FEE_LIMIT_BPS=50
# If the back-end doesn't have adequate channel capacity, buy one from an LSP #SERVICE_FEE_FLOOR_SATS=10
# Will execute when it costs less than 1% of balance and uses a trusted peer #ROUTING_FEE_FLOOR_SATS=5
#BOOTSTRAP=1
#LSP
OLYMPUS_LSP_URL=https://lsps1.lnolymp.us/api/v1
VOLTAGE_LSP_URL=https://lsp.voltageapi.com/api/v1
FLASHSATS_LSP_URL=https://lsp.flashsats.xyz/lsp/channel
LSP_CHANNEL_THRESHOLD=1000000
LSP_MAX_FEE_BPS=100
#ROOT_FEES #ROOT_FEES
# Applied to either debits or credits and sent to an admin account # Internal transaction fees (placeholder, bugged, do not use)
# BPS are basis points, 100 BPS = 1% #TX_FEE_INTERNAL_ROOT_BPS=0
#INCOMING_CHAIN_FEE_ROOT_BPS=0
#INCOMING_INVOICE_FEE_ROOT_BPS=0
# Chain spends are currently unstable and thus disabled, do not use until further notice
#OUTGOING_CHAIN_FEE_ROOT_BPS=60
# Outgoing Invoice Fee must be >= Lightning Outbound Max Fee so admins don't incur losses on spends
#OUTGOING_INVOICE_FEE_ROOT_BPS=60
# Internal user fees bugged, do not use until further notice
#TX_FEE_INTERNAL_ROOT_BPS=0 #applied to inter-application txns
#APP_FEES #LSP
# An extra fee applied at the app level and sent to the application owner # If the back-end doesn't have adequate channel capacity, buy one from an LSP
#INCOMING_INVOICE_FEE_USER_BPS=0 #OLYMPUS_LSP_URL=https://lsps1.lnolymp.us/api/v1
#OUTGOING_INVOICE_FEE_USER_BPS=0 #FLASHSATS_LSP_URL=https://lsp.flashsats.xyz/lsp/channel
#TX_FEE_INTERNAL_USER_BPS=0 #LSP_CHANNEL_THRESHOLD=1000000
#LSP_MAX_FEE_BPS=100
# Use trusted peer until LSP costs less than ~1% of balance or threshold
#BOOTSTRAP=1
#NOSTR #NOSTR
# Default relay may become rate-limited without a paid subscription # Default relay may become rate-limited without a paid subscription
@ -84,9 +88,9 @@ LSP_MAX_FEE_BPS=100
#LNURL #LNURL
# Optional # Optional
# If undefined, LNURLs (including Lightning Address) will be disabled # If undefined, LNURLs will be disabled but wallets may still use bridge for Lightning Addresses
# To enable, add a reachable https endpoint for requests (or purchase a subscription) # To enable, add a reachable https endpoint for requests
# You also need an SSL reverse proxy from the domain to this local host # You also need an SSL reverse proxy from the domain to the Pub
# Read more at https://docs.shock.network # Read more at https://docs.shock.network
#SERVICE_URL=https://yourdomainhere.xyz #SERVICE_URL=https://yourdomainhere.xyz
#LNURL_META_TEXT=LNURL via Lightning.Pub #LNURL_META_TEXT=LNURL via Lightning.Pub
@ -97,7 +101,7 @@ LSP_MAX_FEE_BPS=100
# Read more at https://docs.shock.network # Read more at https://docs.shock.network
#SUBSCRIBER=1 #SUBSCRIBER=1
#DEV_OPTS #DEV
#MOCK_LND=false #MOCK_LND=false
#ALLOW_BALANCE_MIGRATION=false #ALLOW_BALANCE_MIGRATION=false
#MIGRATE_DB=false #MIGRATE_DB=false
@ -109,10 +113,8 @@ LSP_MAX_FEE_BPS=100
#SKIP_SANITY_CHECK=false #SKIP_SANITY_CHECK=false
# A read-only token that can be used with dashboard to view reports # A read-only token that can be used with dashboard to view reports
#METRICS_TOKEN= #METRICS_TOKEN=
# Disable outbound payments aka honeypot mode
#DISABLE_EXTERNAL_PAYMENTS=false
#ALLOW_RESET_METRICS_STORAGES=false #ALLOW_RESET_METRICS_STORAGES=false
ALLOW_HTTP_UPGRADE=false #ALLOW_HTTP_UPGRADE=false
#WATCHDOG SECURITY #WATCHDOG SECURITY
# A last line of defense against 0-day drainage attacks # A last line of defense against 0-day drainage attacks
@ -121,3 +123,5 @@ ALLOW_HTTP_UPGRADE=false
# Increase this values to add a spending buffer for non-Pub services sharing LND # Increase this values to add a spending buffer for non-Pub services sharing LND
# Max difference between users balance and LND balance at Pub startup # Max difference between users balance and LND balance at Pub startup
#WATCHDOG_MAX_DIFF_SATS=0 #WATCHDOG_MAX_DIFF_SATS=0
# Disable outbound payments aka honeypot mode
#DISABLE_EXTERNAL_PAYMENTS=false

480
package-lock.json generated
View file

@ -18,10 +18,13 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^17.0.31", "@types/node": "^17.0.31",
"@types/secp256k1": "^4.0.3", "@types/secp256k1": "^4.0.3",
"@vulpemventures/secp256k1-zkp": "^3.2.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^12.2.0",
"bitcoin-core": "^4.2.0", "bitcoin-core": "^4.2.0",
"bitcoinjs-lib": "^6.1.7",
"boltz-core": "^3.0.0",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0", "chai-string": "^1.5.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
@ -29,6 +32,7 @@
"csv": "^6.3.8", "csv": "^6.3.8",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"eccrypto": "^1.1.6", "eccrypto": "^1.1.6",
"ecpair": "^3.0.0",
"express": "^4.21.2", "express": "^4.21.2",
"globby": "^13.1.2", "globby": "^13.1.2",
"grpc-tools": "^1.12.4", "grpc-tools": "^1.12.4",
@ -42,6 +46,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
"secp256k1": "^5.0.1", "secp256k1": "^5.0.1",
"tiny-secp256k1": "^2.2.4",
"ts-node": "^10.7.0", "ts-node": "^10.7.0",
"ts-proto": "^1.131.2", "ts-proto": "^1.131.2",
"typeorm": "^0.3.26", "typeorm": "^0.3.26",
@ -70,6 +75,12 @@
"typescript": "5.5.4" "typescript": "5.5.4"
} }
}, },
"node_modules/@boltz/bitcoin-ops": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@boltz/bitcoin-ops/-/bitcoin-ops-2.0.0.tgz",
"integrity": "sha512-AM7vFNwSD7B4XI6yeRKccWbbD/lvwoFr8U3pqhzryBQo4uMkYe5V3/kMVnml4SNuxzyqdIFu4ur3TId02sC33A==",
"license": "MIT"
},
"node_modules/@bufbuild/protobuf": { "node_modules/@bufbuild/protobuf": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.7.0.tgz", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.7.0.tgz",
@ -443,6 +454,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@openzeppelin/contracts": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.4.0.tgz",
"integrity": "sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A==",
"license": "MIT"
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -1014,6 +1031,16 @@
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/randombytes": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/randombytes/-/randombytes-2.0.3.tgz",
"integrity": "sha512-+NRgihTfuURllWCiIAhm1wsJqzsocnqXM77V/CalsdJIYSRGEHMnritxh+6EsBklshC+clo1KgnN14qgSGeQdw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/range-parser": { "node_modules/@types/range-parser": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
@ -1114,6 +1141,18 @@
"uuid": "bin/uuid" "uuid": "bin/uuid"
} }
}, },
"node_modules/@vulpemventures/secp256k1-zkp": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@vulpemventures/secp256k1-zkp/-/secp256k1-zkp-3.2.1.tgz",
"integrity": "sha512-2U4nuNbXuUgMmxhuoILbRMoD2DE7KND3udk5cYilIS1MHvMtje9ywUm/zsI0g7d7x8g2A57xri+wvqCC/fCnJg==",
"license": "MIT",
"dependencies": {
"long": "^5.2.3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -1686,6 +1725,12 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true "optional": true
}, },
"node_modules/base-x": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
"license": "MIT"
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -1766,12 +1811,54 @@
"file-uri-to-path": "1.0.0" "file-uri-to-path": "1.0.0"
} }
}, },
"node_modules/bip174": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz",
"integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/bip174-liquid": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bip174-liquid/-/bip174-liquid-1.0.3.tgz",
"integrity": "sha512-e69sC0Cq2tBJuhG2+wieXv40DN13YBR/wbIjZp4Mqwpar5vQm8Ldqijdd6N33XG7LtfvQi/zKm5fSzdPY/9mgw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/bip32": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/bip32/-/bip32-4.0.0.tgz",
"integrity": "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.2.0",
"@scure/base": "^1.1.1",
"typeforce": "^1.11.5",
"wif": "^2.0.6"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/bip65": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bip65/-/bip65-1.0.3.tgz",
"integrity": "sha512-RQ1nc7xtnLa5XltnCqkoR2zmhuz498RjMJwrLKQzOE049D1HUqnYfon7cVSbwS5UGm0/EQlC2CH+NY3MyITA4Q==",
"license": "ISC",
"engines": {
"node": ">=4.5.0"
}
},
"node_modules/bip66": { "node_modules/bip66": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz",
"integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
@ -1794,6 +1881,71 @@
"node": ">=7" "node": ">=7"
} }
}, },
"node_modules/bitcoinjs-lib": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz",
"integrity": "sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.2.0",
"bech32": "^2.0.0",
"bip174": "^2.1.1",
"bs58check": "^3.0.1",
"typeforce": "^1.11.3",
"varuint-bitcoin": "^1.1.2"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/bitcoinjs-lib/node_modules/base-x": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
"integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==",
"license": "MIT"
},
"node_modules/bitcoinjs-lib/node_modules/bs58": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
"license": "MIT",
"dependencies": {
"base-x": "^4.0.0"
}
},
"node_modules/bitcoinjs-lib/node_modules/bs58check": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
"integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.2.0",
"bs58": "^5.0.0"
}
},
"node_modules/bitcoinjs-lib/node_modules/varuint-bitcoin": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz",
"integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.1"
}
},
"node_modules/bitset": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bitset/-/bitset-5.2.3.tgz",
"integrity": "sha512-uZ7++Z60MC9cZ+7YzQ1v9yPDydcjhmcMjGx2yoGTjjSXBoVMmTr2LCRbkpI19S9P/C75hhP7Bsakj+gVzVUDbQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/bl": { "node_modules/bl": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@ -1828,6 +1980,26 @@
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
}, },
"node_modules/blech32": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/blech32/-/blech32-1.1.2.tgz",
"integrity": "sha512-C5qxzoF9KyX88X8Zz18cZ6BOeL0n5/Eg/cDot1frntkArRMwg1djNim5wA6QFWwu0lJ1LN8iiRMN4Lp2kZzdfA==",
"license": "MIT",
"peer": true,
"dependencies": {
"long": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/blech32/node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
"license": "Apache-2.0",
"peer": true
},
"node_modules/bn.js": { "node_modules/bn.js": {
"version": "4.12.2", "version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
@ -1873,6 +2045,42 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/boltz-core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/boltz-core/-/boltz-core-3.0.0.tgz",
"integrity": "sha512-L0jzUnTIb/vir9k6yVlCkV8I29ZeBK7LWq3rrDcp/KP7bB0VvKR5JTceua8g2KBCBM5HOMVDEuPW3c9/82/Nmg==",
"license": "AGPL-3.0",
"dependencies": {
"@boltz/bitcoin-ops": "^2.0.0",
"@openzeppelin/contracts": "^5.2.0",
"@vulpemventures/secp256k1-zkp": "^3.2.1",
"bip32": "^4.0.0",
"bip65": "^1.0.3",
"bip66": "^2.0.0",
"bitcoinjs-lib": "^6.1.7",
"bn.js": "^5.2.1",
"ecpair": "^3.0.0",
"varuint-bitcoin": "^2.0.0"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"liquidjs-lib": "^6.0.2-liquid.37"
}
},
"node_modules/boltz-core/node_modules/bip66": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bip66/-/bip66-2.0.0.tgz",
"integrity": "sha512-kBG+hSpgvZBrkIm9dt5T1Hd/7xGCPEX2npoxAWZfsK1FvjgaxySEh2WizjyIstWXriKo9K9uJ4u0OnsyLDUPXQ==",
"license": "MIT"
},
"node_modules/boltz-core/node_modules/bn.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
"license": "MIT"
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -1916,6 +2124,25 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"node_modules/bs58": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
"license": "MIT",
"dependencies": {
"base-x": "^5.0.0"
}
},
"node_modules/bs58check": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz",
"integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.2.0",
"bs58": "^6.0.0"
}
},
"node_modules/buffer": { "node_modules/buffer": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@ -2206,7 +2433,6 @@
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
"integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"inherits": "^2.0.4", "inherits": "^2.0.4",
"safe-buffer": "^5.2.1" "safe-buffer": "^5.2.1"
@ -2547,7 +2773,6 @@
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"cipher-base": "^1.0.1", "cipher-base": "^1.0.1",
"inherits": "^2.0.1", "inherits": "^2.0.1",
@ -2561,7 +2786,6 @@
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"cipher-base": "^1.0.3", "cipher-base": "^1.0.3",
"create-hash": "^1.1.0", "create-hash": "^1.1.0",
@ -2982,6 +3206,52 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"node_modules/ecpair": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ecpair/-/ecpair-3.0.0.tgz",
"integrity": "sha512-kf4JxjsRQoD4EBzpYjGAcR0t9i/4oAeRPtyCpKvSwyotgkc6oA4E4M0/e+kep7cXe+mgxAvoeh/jdgH9h5+Wxw==",
"license": "MIT",
"dependencies": {
"uint8array-tools": "^0.0.8",
"valibot": "^0.37.0",
"wif": "^5.0.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/ecpair/node_modules/uint8array-tools": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz",
"integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/ecpair/node_modules/valibot": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-0.37.0.tgz",
"integrity": "sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==",
"license": "MIT",
"peerDependencies": {
"typescript": ">=5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ecpair/node_modules/wif": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/wif/-/wif-5.0.0.tgz",
"integrity": "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==",
"license": "MIT",
"dependencies": {
"bs58check": "^4.0.0"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -3777,7 +4047,6 @@
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"inherits": "^2.0.4", "inherits": "^2.0.4",
"readable-stream": "^3.6.0", "readable-stream": "^3.6.0",
@ -3792,7 +4061,6 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
@ -3807,7 +4075,6 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
@ -4355,6 +4622,88 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/liquidjs-lib": {
"version": "6.0.2-liquid.37",
"resolved": "https://registry.npmjs.org/liquidjs-lib/-/liquidjs-lib-6.0.2-liquid.37.tgz",
"integrity": "sha512-AOPwqg9wtLMNxfqOf8afC+bYYrs3VBsTq/Mq/iOe0F2X3CcbgiLeSAcvOY34jbnUre6U+zmuXKP9bWmxnOG08A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/randombytes": "^2.0.0",
"bech32": "^2.0.0",
"bip174-liquid": "^1.0.3",
"bip66": "^1.1.0",
"bitcoinjs-lib": "^6.0.2",
"bitset": "^5.1.1",
"blech32": "^1.0.1",
"bs58check": "^2.0.0",
"create-hash": "^1.2.0",
"ecpair": "^2.1.0",
"slip77": "^0.2.0",
"typeforce": "^1.11.3",
"varuint-bitcoin": "^1.1.2"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/liquidjs-lib/node_modules/base-x": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
"integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/liquidjs-lib/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"peer": true,
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/liquidjs-lib/node_modules/bs58check": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz",
"integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==",
"license": "MIT",
"peer": true,
"dependencies": {
"bs58": "^4.0.0",
"create-hash": "^1.1.0",
"safe-buffer": "^5.1.2"
}
},
"node_modules/liquidjs-lib/node_modules/ecpair": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.1.0.tgz",
"integrity": "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==",
"license": "MIT",
"peer": true,
"dependencies": {
"randombytes": "^2.1.0",
"typeforce": "^1.18.0",
"wif": "^2.0.6"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/liquidjs-lib/node_modules/varuint-bitcoin": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz",
"integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==",
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "^5.1.1"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -4535,7 +4884,6 @@
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"hash-base": "^3.0.0", "hash-base": "^3.0.0",
"inherits": "^2.0.1", "inherits": "^2.0.1",
@ -5796,6 +6144,16 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -6012,7 +6370,6 @@
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"hash-base": "^3.0.0", "hash-base": "^3.0.0",
"inherits": "^2.0.1" "inherits": "^2.0.1"
@ -6413,6 +6770,28 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/slip77": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/slip77/-/slip77-0.2.0.tgz",
"integrity": "sha512-LQaxb1Hef10kU36qvk71tlSt5BWph7GM0j+t2n5zs169X4QfnNbb6xqKZ38X3ETzGmCQaVdBwr925HDagHra/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "^13.9.1",
"create-hmac": "^1.1.7",
"typeforce": "^1.18.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/slip77/node_modules/@types/node": {
"version": "13.13.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz",
"integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==",
"license": "MIT",
"peer": true
},
"node_modules/smart-buffer": { "node_modules/smart-buffer": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -6813,6 +7192,27 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/tiny-secp256k1": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz",
"integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==",
"license": "MIT",
"dependencies": {
"uint8array-tools": "0.0.7"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tiny-secp256k1/node_modules/uint8array-tools": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz",
"integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/to-buffer": { "node_modules/to-buffer": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
@ -7034,6 +7434,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/typeforce": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==",
"license": "MIT"
},
"node_modules/typeorm": { "node_modules/typeorm": {
"version": "0.3.26", "version": "0.3.26",
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz",
@ -7387,6 +7793,24 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/varuint-bitcoin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz",
"integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==",
"license": "MIT",
"dependencies": {
"uint8array-tools": "^0.0.8"
}
},
"node_modules/varuint-bitcoin/node_modules/uint8array-tools": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz",
"integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -7489,6 +7913,44 @@
"string-width": "^1.0.2 || 2 || 3 || 4" "string-width": "^1.0.2 || 2 || 3 || 4"
} }
}, },
"node_modules/wif": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz",
"integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==",
"license": "MIT",
"dependencies": {
"bs58check": "<3.0.0"
}
},
"node_modules/wif/node_modules/base-x": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
"integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/wif/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/wif/node_modules/bs58check": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz",
"integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==",
"license": "MIT",
"dependencies": {
"bs58": "^4.0.0",
"create-hash": "^1.1.0",
"safe-buffer": "^5.1.2"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View file

@ -36,10 +36,13 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^17.0.31", "@types/node": "^17.0.31",
"@types/secp256k1": "^4.0.3", "@types/secp256k1": "^4.0.3",
"@vulpemventures/secp256k1-zkp": "^3.2.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^12.2.0",
"bitcoin-core": "^4.2.0", "bitcoin-core": "^4.2.0",
"bitcoinjs-lib": "^6.1.7",
"boltz-core": "^3.0.0",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0", "chai-string": "^1.5.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
@ -47,6 +50,7 @@
"csv": "^6.3.8", "csv": "^6.3.8",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"eccrypto": "^1.1.6", "eccrypto": "^1.1.6",
"ecpair": "^3.0.0",
"express": "^4.21.2", "express": "^4.21.2",
"globby": "^13.1.2", "globby": "^13.1.2",
"grpc-tools": "^1.12.4", "grpc-tools": "^1.12.4",
@ -60,6 +64,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
"secp256k1": "^5.0.1", "secp256k1": "^5.0.1",
"tiny-secp256k1": "^2.2.4",
"ts-node": "^10.7.0", "ts-node": "^10.7.0",
"ts-proto": "^1.131.2", "ts-proto": "^1.131.2",
"typeorm": "^0.3.26", "typeorm": "^0.3.26",
@ -88,4 +93,4 @@
"typescript": "5.5.4" "typescript": "5.5.4"
}, },
"overrides": {} "overrides": {}
} }

View file

@ -93,6 +93,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [MessagingToken](#MessagingToken) - input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- GetAdminTransactionSwapQuotes
- auth type: __Admin__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuoteList](#TransactionSwapQuoteList)
- GetAppsMetrics - GetAppsMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- input: [AppsMetricsRequest](#AppsMetricsRequest) - input: [AppsMetricsRequest](#AppsMetricsRequest)
@ -198,6 +203,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [SingleMetricReq](#SingleMetricReq) - input: [SingleMetricReq](#SingleMetricReq)
- output: [UsageMetricTlv](#UsageMetricTlv) - output: [UsageMetricTlv](#UsageMetricTlv)
- GetTransactionSwapQuotes
- auth type: __User__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuoteList](#TransactionSwapQuoteList)
- GetUsageMetrics - GetUsageMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- input: [LatestUsageMetricReq](#LatestUsageMetricReq) - input: [LatestUsageMetricReq](#LatestUsageMetricReq)
@ -233,11 +243,21 @@ The nostr server will send back a message response, and inside the body there wi
- input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest) - input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- ListAdminSwaps
- auth type: __Admin__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- ListChannels - ListChannels
- auth type: __Admin__ - auth type: __Admin__
- This methods has an __empty__ __request__ body - This methods has an __empty__ __request__ body
- output: [LndChannels](#LndChannels) - output: [LndChannels](#LndChannels)
- ListSwaps
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- LndGetInfo - LndGetInfo
- auth type: __Admin__ - auth type: __Admin__
- input: [LndGetInfoRequest](#LndGetInfoRequest) - input: [LndGetInfoRequest](#LndGetInfoRequest)
@ -270,6 +290,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayAddressRequest](#PayAddressRequest) - input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse) - output: [PayAddressResponse](#PayAddressResponse)
- PayAdminTransactionSwap
- auth type: __Admin__
- input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest)
- output: [AdminSwapResponse](#AdminSwapResponse)
- PayInvoice - PayInvoice
- auth type: __User__ - auth type: __User__
- input: [PayInvoiceRequest](#PayInvoiceRequest) - input: [PayInvoiceRequest](#PayInvoiceRequest)
@ -515,6 +540,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [MessagingToken](#MessagingToken) - input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- GetAdminTransactionSwapQuotes
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/transaction/quote__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuoteList](#TransactionSwapQuoteList)
- GetApp - GetApp
- auth type: __App__ - auth type: __App__
- http method: __post__ - http method: __post__
@ -708,6 +740,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [SingleMetricReq](#SingleMetricReq) - input: [SingleMetricReq](#SingleMetricReq)
- output: [UsageMetricTlv](#UsageMetricTlv) - output: [UsageMetricTlv](#UsageMetricTlv)
- GetTransactionSwapQuotes
- auth type: __User__
- http method: __post__
- http route: __/api/user/swap/quote__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuoteList](#TransactionSwapQuoteList)
- GetUsageMetrics - GetUsageMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- http method: __post__ - http method: __post__
@ -795,6 +834,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest) - input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- ListAdminSwaps
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/list__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- ListChannels - ListChannels
- auth type: __Admin__ - auth type: __Admin__
- http method: __get__ - http method: __get__
@ -802,6 +848,13 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body - This methods has an __empty__ __request__ body
- output: [LndChannels](#LndChannels) - output: [LndChannels](#LndChannels)
- ListSwaps
- auth type: __User__
- http method: __post__
- http route: __/api/user/swap/list__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- LndGetInfo - LndGetInfo
- auth type: __Admin__ - auth type: __Admin__
- http method: __post__ - http method: __post__
@ -846,6 +899,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayAddressRequest](#PayAddressRequest) - input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse) - output: [PayAddressResponse](#PayAddressResponse)
- PayAdminTransactionSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/transaction/pay__
- input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest)
- output: [AdminSwapResponse](#AdminSwapResponse)
- PayAppUserInvoice - PayAppUserInvoice
- auth type: __App__ - auth type: __App__
- http method: __post__ - http method: __post__
@ -1038,6 +1098,10 @@ The nostr server will send back a message response, and inside the body there wi
- __name__: _string_ - __name__: _string_
- __price_sats__: _number_ - __price_sats__: _number_
### AdminSwapResponse
- __network_fee__: _number_
- __tx_id__: _string_
### AppMetrics ### AppMetrics
- __app__: _[Application](#Application)_ - __app__: _[Application](#Application)_
- __available__: _number_ - __available__: _number_
@ -1092,6 +1156,13 @@ The nostr server will send back a message response, and inside the body there wi
- __nostr_pub__: _string_ - __nostr_pub__: _string_
- __user_identifier__: _string_ - __user_identifier__: _string_
### BeaconData
- __avatarUrl__: _string_ *this field is optional
- __fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional
- __name__: _string_
- __nextRelay__: _string_ *this field is optional
- __type__: _string_
### BundleData ### BundleData
- __available_chunks__: ARRAY of: _number_ - __available_chunks__: ARRAY of: _number_
- __base_64_data__: ARRAY of: _string_ - __base_64_data__: ARRAY of: _string_
@ -1137,6 +1208,10 @@ The nostr server will send back a message response, and inside the body there wi
### CreateOneTimeInviteLinkResponse ### CreateOneTimeInviteLinkResponse
- __invitation_link__: _string_ - __invitation_link__: _string_
### CumulativeFees
- __serviceFeeBps__: _number_
- __serviceFeeFloor__: _number_
### DebitAuthorization ### DebitAuthorization
- __authorized__: _boolean_ - __authorized__: _boolean_
- __debit_id__: _string_ - __debit_id__: _string_
@ -1275,6 +1350,7 @@ The nostr server will send back a message response, and inside the body there wi
- __request_id__: _string_ - __request_id__: _string_
### LiveUserOperation ### LiveUserOperation
- __latest_balance__: _number_
- __operation__: _[UserOperation](#UserOperation)_ - __operation__: _[UserOperation](#UserOperation)_
### LndChannels ### LndChannels
@ -1448,8 +1524,9 @@ The nostr server will send back a message response, and inside the body there wi
### PayAddressRequest ### PayAddressRequest
- __address__: _string_ - __address__: _string_
- __amoutSats__: _number_ - __amountSats__: _number_
- __satsPerVByte__: _number_ - __satsPerVByte__: _number_
- __swap_operation_id__: _string_ *this field is optional
### PayAddressResponse ### PayAddressResponse
- __network_fee__: _number_ - __network_fee__: _number_
@ -1457,19 +1534,26 @@ The nostr server will send back a message response, and inside the body there wi
- __service_fee__: _number_ - __service_fee__: _number_
- __txId__: _string_ - __txId__: _string_
### PayAdminTransactionSwapRequest
- __address__: _string_
- __swap_operation_id__: _string_
### PayAppUserInvoiceRequest ### PayAppUserInvoiceRequest
- __amount__: _number_ - __amount__: _number_
- __debit_npub__: _string_ *this field is optional - __debit_npub__: _string_ *this field is optional
- __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional
- __invoice__: _string_ - __invoice__: _string_
- __user_identifier__: _string_ - __user_identifier__: _string_
### PayInvoiceRequest ### PayInvoiceRequest
- __amount__: _number_ - __amount__: _number_
- __debit_npub__: _string_ *this field is optional - __debit_npub__: _string_ *this field is optional
- __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional
- __invoice__: _string_ - __invoice__: _string_
### PayInvoiceResponse ### PayInvoiceResponse
- __amount_paid__: _number_ - __amount_paid__: _number_
- __latest_balance__: _number_
- __network_fee__: _number_ - __network_fee__: _number_
- __operation_id__: _string_ - __operation_id__: _string_
- __preimage__: _string_ - __preimage__: _string_
@ -1480,7 +1564,9 @@ The nostr server will send back a message response, and inside the body there wi
### PaymentState ### PaymentState
- __amount__: _number_ - __amount__: _number_
- __internal__: _boolean_
- __network_fee__: _number_ - __network_fee__: _number_
- __operation_id__: _string_
- __paid_at_unix__: _number_ - __paid_at_unix__: _number_
- __service_fee__: _number_ - __service_fee__: _number_
@ -1554,6 +1640,31 @@ The nostr server will send back a message response, and inside the body there wi
- __page__: _number_ - __page__: _number_
- __request_id__: _number_ *this field is optional - __request_id__: _number_ *this field is optional
### SwapOperation
- __address_paid__: _string_
- __failure_reason__: _string_ *this field is optional
- __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional
- __swap_operation_id__: _string_
### SwapsList
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_
- __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_
### TransactionSwapQuote
- __chain_fee_sats__: _number_
- __invoice_amount_sats__: _number_
- __service_fee_sats__: _number_
- __service_url__: _string_
- __swap_fee_sats__: _number_
- __swap_operation_id__: _string_
- __transaction_amount_sats__: _number_
### TransactionSwapQuoteList
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_
### TransactionSwapRequest
- __transaction_amount_sats__: _number_
### UpdateChannelPolicyRequest ### UpdateChannelPolicyRequest
- __policy__: _[ChannelPolicy](#ChannelPolicy)_ - __policy__: _[ChannelPolicy](#ChannelPolicy)_
- __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_ - __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_

View file

@ -66,81 +66,86 @@ type Client struct {
BanDebit func(req DebitOperation) error BanDebit func(req DebitOperation) error
BanUser func(req BanUserRequest) (*BanUserResponse, error) BanUser func(req BanUserRequest) (*BanUserResponse, error)
// batching method: BatchUser not implemented // batching method: BatchUser not implemented
CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error) CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error)
CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error) CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error)
DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error) DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error)
DeleteUserOffer func(req OfferId) error DeleteUserOffer func(req OfferId) error
EditDebit func(req DebitAuthorizationRequest) error EditDebit func(req DebitAuthorizationRequest) error
EncryptionExchange func(req EncryptionExchangeRequest) error EncryptionExchange func(req EncryptionExchangeRequest) error
EnrollAdminToken func(req EnrollAdminTokenRequest) error EnrollAdminToken func(req EnrollAdminTokenRequest) error
EnrollMessagingToken func(req MessagingToken) error EnrollMessagingToken func(req MessagingToken) error
GetApp func() (*Application, error) GetAdminTransactionSwapQuotes func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error)
GetAppUser func(req GetAppUserRequest) (*AppUser, error) GetApp func() (*Application, error)
GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) GetAppUser func(req GetAppUserRequest) (*AppUser, error)
GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error)
GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error)
GetDebitAuthorizations func() (*DebitAuthorizations, error) GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error)
GetErrorStats func() (*ErrorStats, error) GetDebitAuthorizations func() (*DebitAuthorizations, error)
GetHttpCreds func() (*HttpCreds, error) GetErrorStats func() (*ErrorStats, error)
GetInviteLinkState func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error) GetHttpCreds func() (*HttpCreds, error)
GetLNURLChannelLink func() (*LnurlLinkResponse, error) GetInviteLinkState func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error)
GetLiveDebitRequests func() (*LiveDebitRequest, error) GetLNURLChannelLink func() (*LnurlLinkResponse, error)
GetLiveManageRequests func() (*LiveManageRequest, error) GetLiveDebitRequests func() (*LiveDebitRequest, error)
GetLiveUserOperations func() (*LiveUserOperation, error) GetLiveManageRequests func() (*LiveManageRequest, error)
GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error) GetLiveUserOperations func() (*LiveUserOperation, error)
GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error) GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error)
GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error) GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error)
GetLnurlPayLink func() (*LnurlLinkResponse, error) GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error)
GetLnurlWithdrawInfo func(query GetLnurlWithdrawInfo_Query) (*LnurlWithdrawInfoResponse, error) GetLnurlPayLink func() (*LnurlLinkResponse, error)
GetLnurlWithdrawLink func() (*LnurlLinkResponse, error) GetLnurlWithdrawInfo func(query GetLnurlWithdrawInfo_Query) (*LnurlWithdrawInfoResponse, error)
GetManageAuthorizations func() (*ManageAuthorizations, error) GetLnurlWithdrawLink func() (*LnurlLinkResponse, error)
GetMigrationUpdate func() (*MigrationUpdate, error) GetManageAuthorizations func() (*ManageAuthorizations, error)
GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error) GetMigrationUpdate func() (*MigrationUpdate, error)
GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error) GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error)
GetProvidersDisruption func() (*ProvidersDisruption, error) GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error)
GetSeed func() (*LndSeed, error) GetProvidersDisruption func() (*ProvidersDisruption, error)
GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error) GetSeed func() (*LndSeed, error)
GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error) GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error)
GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error) GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error)
GetUserInfo func() (*UserInfo, error) GetTransactionSwapQuotes func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error)
GetUserOffer func(req OfferId) (*OfferConfig, error) GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error)
GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error) GetUserInfo func() (*UserInfo, error)
GetUserOffers func() (*UserOffers, error) GetUserOffer func(req OfferId) (*OfferConfig, error)
GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error)
HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) GetUserOffers func() (*UserOffers, error)
HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error)
HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error)
Health func() error HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error)
LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error
ListChannels func() (*LndChannels, error) Health func() error
LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error) LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error
NewAddress func(req NewAddressRequest) (*NewAddressResponse, error) ListAdminSwaps func() (*SwapsList, error)
NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error) ListChannels func() (*LndChannels, error)
NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error) ListSwaps func() (*SwapsList, error)
OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error) LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error)
PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) NewAddress func(req NewAddressRequest) (*NewAddressResponse, error)
PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error)
PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error)
PingSubProcesses func() error OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error)
RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) PayAddress func(req PayAddressRequest) (*PayAddressResponse, error)
ResetDebit func(req DebitOperation) error PayAdminTransactionSwap func(req PayAdminTransactionSwapRequest) (*AdminSwapResponse, error)
ResetManage func(req ManageOperation) error PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error)
ResetMetricsStorages func() error PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error)
ResetNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) PingSubProcesses func() error
RespondToDebit func(req DebitResponse) error RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
SendAppUserToAppPayment func(req SendAppUserToAppPaymentRequest) error ResetDebit func(req DebitOperation) error
SendAppUserToAppUserPayment func(req SendAppUserToAppUserPaymentRequest) error ResetManage func(req ManageOperation) error
SetMockAppBalance func(req SetMockAppBalanceRequest) error ResetMetricsStorages func() error
SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error ResetNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error RespondToDebit func(req DebitResponse) error
SubToWebRtcCandidates func() (*WebRtcCandidate, error) SendAppUserToAppPayment func(req SendAppUserToAppPaymentRequest) error
SubmitWebRtcMessage func(req WebRtcMessage) (*WebRtcAnswer, error) SendAppUserToAppUserPayment func(req SendAppUserToAppUserPaymentRequest) error
UpdateCallbackUrl func(req CallbackUrl) (*CallbackUrl, error) SetMockAppBalance func(req SetMockAppBalanceRequest) error
UpdateChannelPolicy func(req UpdateChannelPolicyRequest) error SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error
UpdateUserOffer func(req OfferConfig) error SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error
UseInviteLink func(req UseInviteLinkRequest) error SubToWebRtcCandidates func() (*WebRtcCandidate, error)
UserHealth func() (*UserHealthState, error) SubmitWebRtcMessage func(req WebRtcMessage) (*WebRtcAnswer, error)
ZipMetricsStorages func() (*ZippedMetrics, error) UpdateCallbackUrl func(req CallbackUrl) (*CallbackUrl, error)
UpdateChannelPolicy func(req UpdateChannelPolicyRequest) error
UpdateUserOffer func(req OfferConfig) error
UseInviteLink func(req UseInviteLinkRequest) error
UserHealth func() (*UserHealthState, error)
ZipMetricsStorages func() (*ZippedMetrics, error)
} }
func NewClient(params ClientParams) *Client { func NewClient(params ClientParams) *Client {
@ -662,6 +667,35 @@ func NewClient(params ClientParams) *Client {
} }
return nil return nil
}, },
GetAdminTransactionSwapQuotes: func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/transaction/quote"
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := TransactionSwapQuoteList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetApp: func() (*Application, error) { GetApp: func() (*Application, error) {
auth, err := params.RetrieveAppAuth() auth, err := params.RetrieveAppAuth()
if err != nil { if err != nil {
@ -1285,6 +1319,35 @@ func NewClient(params ClientParams) *Client {
} }
return &res, nil return &res, nil
}, },
GetTransactionSwapQuotes: func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error) {
auth, err := params.RetrieveUserAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/user/swap/quote"
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := TransactionSwapQuoteList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetUsageMetrics: func(req LatestUsageMetricReq) (*UsageMetrics, error) { GetUsageMetrics: func(req LatestUsageMetricReq) (*UsageMetrics, error) {
auth, err := params.RetrieveMetricsAuth() auth, err := params.RetrieveMetricsAuth()
if err != nil { if err != nil {
@ -1580,6 +1643,32 @@ func NewClient(params ClientParams) *Client {
} }
return nil return nil
}, },
ListAdminSwaps: func() (*SwapsList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/list"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := SwapsList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
ListChannels: func() (*LndChannels, error) { ListChannels: func() (*LndChannels, error) {
auth, err := params.RetrieveAdminAuth() auth, err := params.RetrieveAdminAuth()
if err != nil { if err != nil {
@ -1602,6 +1691,32 @@ func NewClient(params ClientParams) *Client {
} }
return &res, nil return &res, nil
}, },
ListSwaps: func() (*SwapsList, error) {
auth, err := params.RetrieveUserAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/user/swap/list"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := SwapsList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
LndGetInfo: func(req LndGetInfoRequest) (*LndGetInfoResponse, error) { LndGetInfo: func(req LndGetInfoRequest) (*LndGetInfoResponse, error) {
auth, err := params.RetrieveAdminAuth() auth, err := params.RetrieveAdminAuth()
if err != nil { if err != nil {
@ -1777,6 +1892,35 @@ func NewClient(params ClientParams) *Client {
} }
return &res, nil return &res, nil
}, },
PayAdminTransactionSwap: func(req PayAdminTransactionSwapRequest) (*AdminSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/transaction/pay"
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := AdminSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
PayAppUserInvoice: func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) { PayAppUserInvoice: func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) {
auth, err := params.RetrieveAppAuth() auth, err := params.RetrieveAppAuth()
if err != nil { if err != nil {

View file

@ -123,6 +123,10 @@ type AddProductRequest struct {
Name string `json:"name"` Name string `json:"name"`
Price_sats int64 `json:"price_sats"` Price_sats int64 `json:"price_sats"`
} }
type AdminSwapResponse struct {
Network_fee int64 `json:"network_fee"`
Tx_id string `json:"tx_id"`
}
type AppMetrics struct { type AppMetrics struct {
App *Application `json:"app"` App *Application `json:"app"`
Available int64 `json:"available"` Available int64 `json:"available"`
@ -177,6 +181,13 @@ type BannedAppUser struct {
Nostr_pub string `json:"nostr_pub"` Nostr_pub string `json:"nostr_pub"`
User_identifier string `json:"user_identifier"` User_identifier string `json:"user_identifier"`
} }
type BeaconData struct {
Avatarurl string `json:"avatarUrl"`
Fees *CumulativeFees `json:"fees"`
Name string `json:"name"`
Nextrelay string `json:"nextRelay"`
Type string `json:"type"`
}
type BundleData struct { type BundleData struct {
Available_chunks []int64 `json:"available_chunks"` Available_chunks []int64 `json:"available_chunks"`
Base_64_data []string `json:"base_64_data"` Base_64_data []string `json:"base_64_data"`
@ -222,6 +233,10 @@ type CreateOneTimeInviteLinkRequest struct {
type CreateOneTimeInviteLinkResponse struct { type CreateOneTimeInviteLinkResponse struct {
Invitation_link string `json:"invitation_link"` Invitation_link string `json:"invitation_link"`
} }
type CumulativeFees struct {
Servicefeebps int64 `json:"serviceFeeBps"`
Servicefeefloor int64 `json:"serviceFeeFloor"`
}
type DebitAuthorization struct { type DebitAuthorization struct {
Authorized bool `json:"authorized"` Authorized bool `json:"authorized"`
Debit_id string `json:"debit_id"` Debit_id string `json:"debit_id"`
@ -360,7 +375,8 @@ type LiveManageRequest struct {
Request_id string `json:"request_id"` Request_id string `json:"request_id"`
} }
type LiveUserOperation struct { type LiveUserOperation struct {
Operation *UserOperation `json:"operation"` Latest_balance int64 `json:"latest_balance"`
Operation *UserOperation `json:"operation"`
} }
type LndChannels struct { type LndChannels struct {
Open_channels []OpenChannel `json:"open_channels"` Open_channels []OpenChannel `json:"open_channels"`
@ -532,9 +548,10 @@ type OperationsCursor struct {
Ts int64 `json:"ts"` Ts int64 `json:"ts"`
} }
type PayAddressRequest struct { type PayAddressRequest struct {
Address string `json:"address"` Address string `json:"address"`
Amoutsats int64 `json:"amoutSats"` Amountsats int64 `json:"amountSats"`
Satspervbyte int64 `json:"satsPerVByte"` Satspervbyte int64 `json:"satsPerVByte"`
Swap_operation_id string `json:"swap_operation_id"`
} }
type PayAddressResponse struct { type PayAddressResponse struct {
Network_fee int64 `json:"network_fee"` Network_fee int64 `json:"network_fee"`
@ -542,32 +559,41 @@ type PayAddressResponse struct {
Service_fee int64 `json:"service_fee"` Service_fee int64 `json:"service_fee"`
Txid string `json:"txId"` Txid string `json:"txId"`
} }
type PayAdminTransactionSwapRequest struct {
Address string `json:"address"`
Swap_operation_id string `json:"swap_operation_id"`
}
type PayAppUserInvoiceRequest struct { type PayAppUserInvoiceRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Debit_npub string `json:"debit_npub"` Debit_npub string `json:"debit_npub"`
Invoice string `json:"invoice"` Expected_fees *CumulativeFees `json:"expected_fees"`
User_identifier string `json:"user_identifier"` Invoice string `json:"invoice"`
User_identifier string `json:"user_identifier"`
} }
type PayInvoiceRequest struct { type PayInvoiceRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Debit_npub string `json:"debit_npub"` Debit_npub string `json:"debit_npub"`
Invoice string `json:"invoice"` Expected_fees *CumulativeFees `json:"expected_fees"`
Invoice string `json:"invoice"`
} }
type PayInvoiceResponse struct { type PayInvoiceResponse struct {
Amount_paid int64 `json:"amount_paid"` Amount_paid int64 `json:"amount_paid"`
Network_fee int64 `json:"network_fee"` Latest_balance int64 `json:"latest_balance"`
Operation_id string `json:"operation_id"` Network_fee int64 `json:"network_fee"`
Preimage string `json:"preimage"` Operation_id string `json:"operation_id"`
Service_fee int64 `json:"service_fee"` Preimage string `json:"preimage"`
Service_fee int64 `json:"service_fee"`
} }
type PayerData struct { type PayerData struct {
Data map[string]string `json:"data"` Data map[string]string `json:"data"`
} }
type PaymentState struct { type PaymentState struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Network_fee int64 `json:"network_fee"` Internal bool `json:"internal"`
Paid_at_unix int64 `json:"paid_at_unix"` Network_fee int64 `json:"network_fee"`
Service_fee int64 `json:"service_fee"` Operation_id string `json:"operation_id"`
Paid_at_unix int64 `json:"paid_at_unix"`
Service_fee int64 `json:"service_fee"`
} }
type Product struct { type Product struct {
Id string `json:"id"` Id string `json:"id"`
@ -639,6 +665,31 @@ type SingleMetricReq struct {
Page int64 `json:"page"` Page int64 `json:"page"`
Request_id int64 `json:"request_id"` Request_id int64 `json:"request_id"`
} }
type SwapOperation struct {
Address_paid string `json:"address_paid"`
Failure_reason string `json:"failure_reason"`
Operation_payment *UserOperation `json:"operation_payment"`
Swap_operation_id string `json:"swap_operation_id"`
}
type SwapsList struct {
Quotes []TransactionSwapQuote `json:"quotes"`
Swaps []SwapOperation `json:"swaps"`
}
type TransactionSwapQuote struct {
Chain_fee_sats int64 `json:"chain_fee_sats"`
Invoice_amount_sats int64 `json:"invoice_amount_sats"`
Service_fee_sats int64 `json:"service_fee_sats"`
Service_url string `json:"service_url"`
Swap_fee_sats int64 `json:"swap_fee_sats"`
Swap_operation_id string `json:"swap_operation_id"`
Transaction_amount_sats int64 `json:"transaction_amount_sats"`
}
type TransactionSwapQuoteList struct {
Quotes []TransactionSwapQuote `json:"quotes"`
}
type TransactionSwapRequest struct {
Transaction_amount_sats int64 `json:"transaction_amount_sats"`
}
type UpdateChannelPolicyRequest struct { type UpdateChannelPolicyRequest struct {
Policy *ChannelPolicy `json:"policy"` Policy *ChannelPolicy `json:"policy"`
Update *UpdateChannelPolicyRequest_update `json:"update"` Update *UpdateChannelPolicyRequest_update `json:"update"`

View file

@ -477,6 +477,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'GetTransactionSwapQuotes':
if (!methods.GetTransactionSwapQuotes) {
throw new Error('method GetTransactionSwapQuotes not found' )
} else {
const error = Types.TransactionSwapRequestValidate(operation.req)
opStats.validate = process.hrtime.bigint()
if (error !== null) throw error
const res = await methods.GetTransactionSwapQuotes({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'GetUserInfo': case 'GetUserInfo':
if (!methods.GetUserInfo) { if (!methods.GetUserInfo) {
throw new Error('method GetUserInfo not found' ) throw new Error('method GetUserInfo not found' )
@ -533,6 +545,16 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'ListSwaps':
if (!methods.ListSwaps) {
throw new Error('method ListSwaps not found' )
} else {
opStats.validate = opStats.guard
const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'NewAddress': case 'NewAddress':
if (!methods.NewAddress) { if (!methods.NewAddress) {
throw new Error('method NewAddress not found' ) throw new Error('method NewAddress not found' )
@ -847,6 +869,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } } 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.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented')
app.post('/api/admin/swap/transaction/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminTransactionSwapQuotes', 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.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.TransactionSwapRequestValidate(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
const response = await methods.GetAdminTransactionSwapQuotes({rpcName:'GetAdminTransactionSwapQuotes', ctx:authContext , req: request})
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.GetApp) throw new Error('method: GetApp is not implemented') if (!opts.allowNotImplementedMethods && !methods.GetApp) throw new Error('method: GetApp is not implemented')
app.post('/api/app/get', async (req, res) => { app.post('/api/app/get', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0}
@ -1317,6 +1361,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } } 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.GetTransactionSwapQuotes) throw new Error('method: GetTransactionSwapQuotes is not implemented')
app.post('/api/user/swap/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetTransactionSwapQuotes', 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.GetTransactionSwapQuotes) throw new Error('method: GetTransactionSwapQuotes is not implemented')
const authContext = await opts.UserAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.TransactionSwapRequestValidate(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
const response = await methods.GetTransactionSwapQuotes({rpcName:'GetTransactionSwapQuotes', ctx:authContext , req: request})
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.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented') if (!opts.allowNotImplementedMethods && !methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented')
app.post('/api/reports/usage', async (req, res) => { app.post('/api/reports/usage', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetUsageMetrics', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'GetUsageMetrics', batch: false, nostr: false, batchSize: 0}
@ -1541,6 +1607,25 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } } 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.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented')
app.post('/api/admin/swap/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListAdminSwaps', 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.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented')
const authContext = await opts.AdminAuthGuard(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.ListAdminSwaps({rpcName:'ListAdminSwaps', 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.ListChannels) throw new Error('method: ListChannels is not implemented') if (!opts.allowNotImplementedMethods && !methods.ListChannels) throw new Error('method: ListChannels is not implemented')
app.get('/api/admin/channels', async (req, res) => { app.get('/api/admin/channels', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListChannels', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'ListChannels', batch: false, nostr: false, batchSize: 0}
@ -1560,6 +1645,25 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } } 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.ListSwaps) throw new Error('method: ListSwaps is not implemented')
app.post('/api/user/swap/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListSwaps', 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.ListSwaps) throw new Error('method: ListSwaps is not implemented')
const authContext = await opts.UserAuthGuard(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.ListSwaps({rpcName:'ListSwaps', 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.LndGetInfo) throw new Error('method: LndGetInfo is not implemented') if (!opts.allowNotImplementedMethods && !methods.LndGetInfo) throw new Error('method: LndGetInfo is not implemented')
app.post('/api/admin/lnd/getinfo', async (req, res) => { app.post('/api/admin/lnd/getinfo', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'LndGetInfo', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'LndGetInfo', batch: false, nostr: false, batchSize: 0}
@ -1689,6 +1793,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } } 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.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
app.post('/api/admin/swap/transaction/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAdminTransactionSwap', 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.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.PayAdminTransactionSwapRequestValidate(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
const response = await methods.PayAdminTransactionSwap({rpcName:'PayAdminTransactionSwap', ctx:authContext , req: request})
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.PayAppUserInvoice) throw new Error('method: PayAppUserInvoice is not implemented') if (!opts.allowNotImplementedMethods && !methods.PayAppUserInvoice) throw new Error('method: PayAppUserInvoice is not implemented')
app.post('/api/app/invoice/pay', async (req, res) => { app.post('/api/app/invoice/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAppUserInvoice', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'PayAppUserInvoice', batch: false, nostr: false, batchSize: 0}

View file

@ -273,6 +273,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/quote'
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') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.TransactionSwapQuoteListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => { GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => {
const auth = await params.retrieveAppAuth() const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null') if (auth === null) throw new Error('retrieveAppAuth() returned null')
@ -603,6 +617,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/swap/quote'
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') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.TransactionSwapQuoteListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetUsageMetrics: async (request: Types.LatestUsageMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => { GetUsageMetrics: async (request: Types.LatestUsageMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => {
const auth = await params.retrieveMetricsAuth() const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null') if (auth === null) throw new Error('retrieveMetricsAuth() returned null')
@ -753,6 +781,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
ListAdminSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/list'
const { data } = await axios.post(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.SwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListChannels: async (): Promise<ResultError | ({ status: 'OK' }& Types.LndChannels)> => { ListChannels: async (): Promise<ResultError | ({ status: 'OK' }& Types.LndChannels)> => {
const auth = await params.retrieveAdminAuth() const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') if (auth === null) throw new Error('retrieveAdminAuth() returned null')
@ -767,6 +809,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
ListSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/swap/list'
const { data } = await axios.post(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.SwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
LndGetInfo: async (request: Types.LndGetInfoRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndGetInfoResponse)> => { LndGetInfo: async (request: Types.LndGetInfoRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndGetInfoResponse)> => {
const auth = await params.retrieveAdminAuth() const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') if (auth === null) throw new Error('retrieveAdminAuth() returned null')
@ -853,6 +909,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/pay'
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') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAppUserInvoice: async (request: Types.PayAppUserInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => { PayAppUserInvoice: async (request: Types.PayAppUserInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => {
const auth = await params.retrieveAppAuth() const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null') if (auth === null) throw new Error('retrieveAppAuth() returned null')

View file

@ -230,6 +230,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'GetAdminTransactionSwapQuotes',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.TransactionSwapQuoteListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => { GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth() const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
@ -536,6 +551,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
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:'GetTransactionSwapQuotes',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.TransactionSwapQuoteListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetUsageMetrics: async (request: Types.LatestUsageMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => { GetUsageMetrics: async (request: Types.LatestUsageMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth() const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
@ -636,6 +666,20 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
ListAdminSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'ListAdminSwaps',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.SwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListChannels: async (): Promise<ResultError | ({ status: 'OK' }& Types.LndChannels)> => { ListChannels: async (): Promise<ResultError | ({ status: 'OK' }& Types.LndChannels)> => {
const auth = await params.retrieveNostrAdminAuth() const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
@ -650,6 +694,20 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
ListSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'ListSwaps',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.SwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
LndGetInfo: async (request: Types.LndGetInfoRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndGetInfoResponse)> => { LndGetInfo: async (request: Types.LndGetInfoRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndGetInfoResponse)> => {
const auth = await params.retrieveNostrAdminAuth() const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
@ -740,6 +798,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'PayAdminTransactionSwap',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.AdminSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayInvoice: async (request: Types.PayInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => { 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')

View file

@ -359,6 +359,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'GetTransactionSwapQuotes':
if (!methods.GetTransactionSwapQuotes) {
throw new Error('method not defined: GetTransactionSwapQuotes')
} else {
const error = Types.TransactionSwapRequestValidate(operation.req)
opStats.validate = process.hrtime.bigint()
if (error !== null) throw error
const res = await methods.GetTransactionSwapQuotes({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'GetUserInfo': case 'GetUserInfo':
if (!methods.GetUserInfo) { if (!methods.GetUserInfo) {
throw new Error('method not defined: GetUserInfo') throw new Error('method not defined: GetUserInfo')
@ -415,6 +427,16 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'ListSwaps':
if (!methods.ListSwaps) {
throw new Error('method not defined: ListSwaps')
} else {
opStats.validate = opStats.guard
const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'NewAddress': case 'NewAddress':
if (!methods.NewAddress) { if (!methods.NewAddress) {
throw new Error('method not defined: NewAddress') throw new Error('method not defined: NewAddress')
@ -665,6 +687,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'GetAdminTransactionSwapQuotes':
try {
if (!methods.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented')
const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.TransactionSwapRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.GetAdminTransactionSwapQuotes({rpcName:'GetAdminTransactionSwapQuotes', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({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 }
break
case 'GetAppsMetrics': case 'GetAppsMetrics':
try { try {
if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented') if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented')
@ -962,6 +1000,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'GetTransactionSwapQuotes':
try {
if (!methods.GetTransactionSwapQuotes) throw new Error('method: GetTransactionSwapQuotes is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.TransactionSwapRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.GetTransactionSwapQuotes({rpcName:'GetTransactionSwapQuotes', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({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 }
break
case 'GetUsageMetrics': case 'GetUsageMetrics':
try { try {
if (!methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented') if (!methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented')
@ -1068,6 +1122,19 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'ListAdminSwaps':
try {
if (!methods.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented')
const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
stats.validate = stats.guard
const response = await methods.ListAdminSwaps({rpcName:'ListAdminSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({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 }
break
case 'ListChannels': case 'ListChannels':
try { try {
if (!methods.ListChannels) throw new Error('method: ListChannels is not implemented') if (!methods.ListChannels) throw new Error('method: ListChannels is not implemented')
@ -1081,6 +1148,19 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'ListSwaps':
try {
if (!methods.ListSwaps) throw new Error('method: ListSwaps is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
stats.validate = stats.guard
const response = await methods.ListSwaps({rpcName:'ListSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({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 }
break
case 'LndGetInfo': case 'LndGetInfo':
try { try {
if (!methods.LndGetInfo) throw new Error('method: LndGetInfo is not implemented') if (!methods.LndGetInfo) throw new Error('method: LndGetInfo is not implemented')
@ -1174,6 +1254,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'PayAdminTransactionSwap':
try {
if (!methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.PayAdminTransactionSwapRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.PayAdminTransactionSwap({rpcName:'PayAdminTransactionSwap', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({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 }
break
case 'PayInvoice': case 'PayInvoice':
try { try {
if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented') if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented')

View file

@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?:
export type AdminContext = { export type AdminContext = {
admin_id: string admin_id: string
} }
export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetInviteLinkState_Input | GetSeed_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | UpdateChannelPolicy_Input export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminTransactionSwapQuotes_Input | GetInviteLinkState_Input | GetSeed_Input | ListAdminSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminTransactionSwap_Input | UpdateChannelPolicy_Input
export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetInviteLinkState_Output | GetSeed_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | UpdateChannelPolicy_Output export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminTransactionSwapQuotes_Output | GetInviteLinkState_Output | GetSeed_Output | ListAdminSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminTransactionSwap_Output | UpdateChannelPolicy_Output
export type AppContext = { export type AppContext = {
app_id: string app_id: string
} }
@ -35,8 +35,8 @@ export type UserContext = {
app_user_id: string app_user_id: string
user_id: string user_id: string
} }
export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | EnrollMessagingToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | EnrollMessagingToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetTransactionSwapQuotes_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | ListSwaps_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input
export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | EnrollMessagingToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | EnrollMessagingToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetTransactionSwapQuotes_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | ListSwaps_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output
export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext
export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest}
@ -99,6 +99,9 @@ export type EnrollAdminToken_Output = ResultError | { status: 'OK' }
export type EnrollMessagingToken_Input = {rpcName:'EnrollMessagingToken', req: MessagingToken} export type EnrollMessagingToken_Input = {rpcName:'EnrollMessagingToken', req: MessagingToken}
export type EnrollMessagingToken_Output = ResultError | { status: 'OK' } export type EnrollMessagingToken_Output = ResultError | { status: 'OK' }
export type GetAdminTransactionSwapQuotes_Input = {rpcName:'GetAdminTransactionSwapQuotes', req: TransactionSwapRequest}
export type GetAdminTransactionSwapQuotes_Output = ResultError | ({ status: 'OK' } & TransactionSwapQuoteList)
export type GetApp_Input = {rpcName:'GetApp'} export type GetApp_Input = {rpcName:'GetApp'}
export type GetApp_Output = ResultError | ({ status: 'OK' } & Application) export type GetApp_Output = ResultError | ({ status: 'OK' } & Application)
@ -186,6 +189,9 @@ export type GetSingleBundleMetrics_Output = ResultError | ({ status: 'OK' } & Bu
export type GetSingleUsageMetrics_Input = {rpcName:'GetSingleUsageMetrics', req: SingleMetricReq} export type GetSingleUsageMetrics_Input = {rpcName:'GetSingleUsageMetrics', req: SingleMetricReq}
export type GetSingleUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetricTlv) export type GetSingleUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetricTlv)
export type GetTransactionSwapQuotes_Input = {rpcName:'GetTransactionSwapQuotes', req: TransactionSwapRequest}
export type GetTransactionSwapQuotes_Output = ResultError | ({ status: 'OK' } & TransactionSwapQuoteList)
export type GetUsageMetrics_Input = {rpcName:'GetUsageMetrics', req: LatestUsageMetricReq} export type GetUsageMetrics_Input = {rpcName:'GetUsageMetrics', req: LatestUsageMetricReq}
export type GetUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetrics) export type GetUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetrics)
@ -232,9 +238,15 @@ export type Health_Output = ResultError | { status: 'OK' }
export type LinkNPubThroughToken_Input = {rpcName:'LinkNPubThroughToken', req: LinkNPubThroughTokenRequest} export type LinkNPubThroughToken_Input = {rpcName:'LinkNPubThroughToken', req: LinkNPubThroughTokenRequest}
export type LinkNPubThroughToken_Output = ResultError | { status: 'OK' } export type LinkNPubThroughToken_Output = ResultError | { status: 'OK' }
export type ListAdminSwaps_Input = {rpcName:'ListAdminSwaps'}
export type ListAdminSwaps_Output = ResultError | ({ status: 'OK' } & SwapsList)
export type ListChannels_Input = {rpcName:'ListChannels'} export type ListChannels_Input = {rpcName:'ListChannels'}
export type ListChannels_Output = ResultError | ({ status: 'OK' } & LndChannels) export type ListChannels_Output = ResultError | ({ status: 'OK' } & LndChannels)
export type ListSwaps_Input = {rpcName:'ListSwaps'}
export type ListSwaps_Output = ResultError | ({ status: 'OK' } & SwapsList)
export type LndGetInfo_Input = {rpcName:'LndGetInfo', req: LndGetInfoRequest} export type LndGetInfo_Input = {rpcName:'LndGetInfo', req: LndGetInfoRequest}
export type LndGetInfo_Output = ResultError | ({ status: 'OK' } & LndGetInfoResponse) export type LndGetInfo_Output = ResultError | ({ status: 'OK' } & LndGetInfoResponse)
@ -256,6 +268,9 @@ export type OpenChannel_Output = ResultError | ({ status: 'OK' } & OpenChannelRe
export type PayAddress_Input = {rpcName:'PayAddress', req: PayAddressRequest} export type PayAddress_Input = {rpcName:'PayAddress', req: PayAddressRequest}
export type PayAddress_Output = ResultError | ({ status: 'OK' } & PayAddressResponse) export type PayAddress_Output = ResultError | ({ status: 'OK' } & PayAddressResponse)
export type PayAdminTransactionSwap_Input = {rpcName:'PayAdminTransactionSwap', req: PayAdminTransactionSwapRequest}
export type PayAdminTransactionSwap_Output = ResultError | ({ status: 'OK' } & AdminSwapResponse)
export type PayAppUserInvoice_Input = {rpcName:'PayAppUserInvoice', req: PayAppUserInvoiceRequest} export type PayAppUserInvoice_Input = {rpcName:'PayAppUserInvoice', req: PayAppUserInvoiceRequest}
export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse)
@ -342,6 +357,7 @@ export type ServerMethods = {
EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void> EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void>
EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void> EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void>
EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise<void> EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise<void>
GetAdminTransactionSwapQuotes?: (req: GetAdminTransactionSwapQuotes_Input & {ctx: AdminContext }) => Promise<TransactionSwapQuoteList>
GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application> GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application>
GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser> GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser>
GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise<LnurlPayInfoResponse> GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise<LnurlPayInfoResponse>
@ -369,6 +385,7 @@ export type ServerMethods = {
GetSeed?: (req: GetSeed_Input & {ctx: AdminContext }) => Promise<LndSeed> GetSeed?: (req: GetSeed_Input & {ctx: AdminContext }) => Promise<LndSeed>
GetSingleBundleMetrics?: (req: GetSingleBundleMetrics_Input & {ctx: MetricsContext }) => Promise<BundleData> GetSingleBundleMetrics?: (req: GetSingleBundleMetrics_Input & {ctx: MetricsContext }) => Promise<BundleData>
GetSingleUsageMetrics?: (req: GetSingleUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetricTlv> GetSingleUsageMetrics?: (req: GetSingleUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetricTlv>
GetTransactionSwapQuotes?: (req: GetTransactionSwapQuotes_Input & {ctx: UserContext }) => Promise<TransactionSwapQuoteList>
GetUsageMetrics?: (req: GetUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetrics> GetUsageMetrics?: (req: GetUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetrics>
GetUserInfo?: (req: GetUserInfo_Input & {ctx: UserContext }) => Promise<UserInfo> GetUserInfo?: (req: GetUserInfo_Input & {ctx: UserContext }) => Promise<UserInfo>
GetUserOffer?: (req: GetUserOffer_Input & {ctx: UserContext }) => Promise<OfferConfig> GetUserOffer?: (req: GetUserOffer_Input & {ctx: UserContext }) => Promise<OfferConfig>
@ -380,13 +397,16 @@ export type ServerMethods = {
HandleLnurlWithdraw?: (req: HandleLnurlWithdraw_Input & {ctx: GuestContext }) => Promise<void> HandleLnurlWithdraw?: (req: HandleLnurlWithdraw_Input & {ctx: GuestContext }) => Promise<void>
Health?: (req: Health_Input & {ctx: GuestContext }) => Promise<void> Health?: (req: Health_Input & {ctx: GuestContext }) => Promise<void>
LinkNPubThroughToken?: (req: LinkNPubThroughToken_Input & {ctx: GuestWithPubContext }) => Promise<void> LinkNPubThroughToken?: (req: LinkNPubThroughToken_Input & {ctx: GuestWithPubContext }) => Promise<void>
ListAdminSwaps?: (req: ListAdminSwaps_Input & {ctx: AdminContext }) => Promise<SwapsList>
ListChannels?: (req: ListChannels_Input & {ctx: AdminContext }) => Promise<LndChannels> ListChannels?: (req: ListChannels_Input & {ctx: AdminContext }) => Promise<LndChannels>
ListSwaps?: (req: ListSwaps_Input & {ctx: UserContext }) => Promise<SwapsList>
LndGetInfo?: (req: LndGetInfo_Input & {ctx: AdminContext }) => Promise<LndGetInfoResponse> LndGetInfo?: (req: LndGetInfo_Input & {ctx: AdminContext }) => Promise<LndGetInfoResponse>
NewAddress?: (req: NewAddress_Input & {ctx: UserContext }) => Promise<NewAddressResponse> NewAddress?: (req: NewAddress_Input & {ctx: UserContext }) => Promise<NewAddressResponse>
NewInvoice?: (req: NewInvoice_Input & {ctx: UserContext }) => Promise<NewInvoiceResponse> NewInvoice?: (req: NewInvoice_Input & {ctx: UserContext }) => Promise<NewInvoiceResponse>
NewProductInvoice?: (req: NewProductInvoice_Input & {ctx: UserContext }) => Promise<NewInvoiceResponse> NewProductInvoice?: (req: NewProductInvoice_Input & {ctx: UserContext }) => Promise<NewInvoiceResponse>
OpenChannel?: (req: OpenChannel_Input & {ctx: AdminContext }) => Promise<OpenChannelResponse> OpenChannel?: (req: OpenChannel_Input & {ctx: AdminContext }) => Promise<OpenChannelResponse>
PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise<PayAddressResponse> PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise<PayAddressResponse>
PayAdminTransactionSwap?: (req: PayAdminTransactionSwap_Input & {ctx: AdminContext }) => Promise<AdminSwapResponse>
PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise<PayInvoiceResponse> PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise<PayInvoiceResponse>
PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise<PayInvoiceResponse> PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise<PayInvoiceResponse>
PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise<void> PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise<void>
@ -651,6 +671,29 @@ export const AddProductRequestValidate = (o?: AddProductRequest, opts: AddProduc
return null return null
} }
export type AdminSwapResponse = {
network_fee: number
tx_id: string
}
export const AdminSwapResponseOptionalFields: [] = []
export type AdminSwapResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
network_fee_CustomCheck?: (v: number) => boolean
tx_id_CustomCheck?: (v: string) => boolean
}
export const AdminSwapResponseValidate = (o?: AdminSwapResponse, opts: AdminSwapResponseOptions = {}, path: string = 'AdminSwapResponse::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.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`)
if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`)
if (typeof o.tx_id !== 'string') return new Error(`${path}.tx_id: is not a string`)
if (opts.tx_id_CustomCheck && !opts.tx_id_CustomCheck(o.tx_id)) return new Error(`${path}.tx_id: custom check failed`)
return null
}
export type AppMetrics = { export type AppMetrics = {
app: Application app: Application
available: number available: number
@ -979,6 +1022,48 @@ export const BannedAppUserValidate = (o?: BannedAppUser, opts: BannedAppUserOpti
return null return null
} }
export type BeaconData = {
avatarUrl?: string
fees?: CumulativeFees
name: string
nextRelay?: string
type: string
}
export type BeaconDataOptionalField = 'avatarUrl' | 'fees' | 'nextRelay'
export const BeaconDataOptionalFields: BeaconDataOptionalField[] = ['avatarUrl', 'fees', 'nextRelay']
export type BeaconDataOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: BeaconDataOptionalField[]
avatarUrl_CustomCheck?: (v?: string) => boolean
fees_Options?: CumulativeFeesOptions
name_CustomCheck?: (v: string) => boolean
nextRelay_CustomCheck?: (v?: string) => boolean
type_CustomCheck?: (v: string) => boolean
}
export const BeaconDataValidate = (o?: BeaconData, opts: BeaconDataOptions = {}, path: string = 'BeaconData::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 ((o.avatarUrl || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('avatarUrl')) && typeof o.avatarUrl !== 'string') return new Error(`${path}.avatarUrl: is not a string`)
if (opts.avatarUrl_CustomCheck && !opts.avatarUrl_CustomCheck(o.avatarUrl)) return new Error(`${path}.avatarUrl: custom check failed`)
if (typeof o.fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('fees')) {
const feesErr = CumulativeFeesValidate(o.fees, opts.fees_Options, `${path}.fees`)
if (feesErr !== null) return feesErr
}
if (typeof o.name !== 'string') return new Error(`${path}.name: is not a string`)
if (opts.name_CustomCheck && !opts.name_CustomCheck(o.name)) return new Error(`${path}.name: custom check failed`)
if ((o.nextRelay || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('nextRelay')) && typeof o.nextRelay !== 'string') return new Error(`${path}.nextRelay: is not a string`)
if (opts.nextRelay_CustomCheck && !opts.nextRelay_CustomCheck(o.nextRelay)) return new Error(`${path}.nextRelay: custom check failed`)
if (typeof o.type !== 'string') return new Error(`${path}.type: is not a string`)
if (opts.type_CustomCheck && !opts.type_CustomCheck(o.type)) return new Error(`${path}.type: custom check failed`)
return null
}
export type BundleData = { export type BundleData = {
available_chunks: number[] available_chunks: number[]
base_64_data: string[] base_64_data: string[]
@ -1252,6 +1337,29 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL
return null return null
} }
export type CumulativeFees = {
serviceFeeBps: number
serviceFeeFloor: number
}
export const CumulativeFeesOptionalFields: [] = []
export type CumulativeFeesOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
serviceFeeBps_CustomCheck?: (v: number) => boolean
serviceFeeFloor_CustomCheck?: (v: number) => boolean
}
export const CumulativeFeesValidate = (o?: CumulativeFees, opts: CumulativeFeesOptions = {}, path: string = 'CumulativeFees::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.serviceFeeBps !== 'number') return new Error(`${path}.serviceFeeBps: is not a number`)
if (opts.serviceFeeBps_CustomCheck && !opts.serviceFeeBps_CustomCheck(o.serviceFeeBps)) return new Error(`${path}.serviceFeeBps: custom check failed`)
if (typeof o.serviceFeeFloor !== 'number') return new Error(`${path}.serviceFeeFloor: is not a number`)
if (opts.serviceFeeFloor_CustomCheck && !opts.serviceFeeFloor_CustomCheck(o.serviceFeeFloor)) return new Error(`${path}.serviceFeeFloor: custom check failed`)
return null
}
export type DebitAuthorization = { export type DebitAuthorization = {
authorized: boolean authorized: boolean
debit_id: string debit_id: string
@ -2089,17 +2197,22 @@ export const LiveManageRequestValidate = (o?: LiveManageRequest, opts: LiveManag
} }
export type LiveUserOperation = { export type LiveUserOperation = {
latest_balance: number
operation: UserOperation operation: UserOperation
} }
export const LiveUserOperationOptionalFields: [] = [] export const LiveUserOperationOptionalFields: [] = []
export type LiveUserOperationOptions = OptionsBaseMessage & { export type LiveUserOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
latest_balance_CustomCheck?: (v: number) => boolean
operation_Options?: UserOperationOptions operation_Options?: UserOperationOptions
} }
export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserOperationOptions = {}, path: string = 'LiveUserOperation::root.'): Error | null => { export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserOperationOptions = {}, path: string = 'LiveUserOperation::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 (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 !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.latest_balance !== 'number') return new Error(`${path}.latest_balance: is not a number`)
if (opts.latest_balance_CustomCheck && !opts.latest_balance_CustomCheck(o.latest_balance)) return new Error(`${path}.latest_balance: custom check failed`)
const operationErr = UserOperationValidate(o.operation, opts.operation_Options, `${path}.operation`) const operationErr = UserOperationValidate(o.operation, opts.operation_Options, `${path}.operation`)
if (operationErr !== null) return operationErr if (operationErr !== null) return operationErr
@ -3130,15 +3243,18 @@ export const OperationsCursorValidate = (o?: OperationsCursor, opts: OperationsC
export type PayAddressRequest = { export type PayAddressRequest = {
address: string address: string
amoutSats: number amountSats: number
satsPerVByte: number satsPerVByte: number
swap_operation_id?: string
} }
export const PayAddressRequestOptionalFields: [] = [] export type PayAddressRequestOptionalField = 'swap_operation_id'
export const PayAddressRequestOptionalFields: PayAddressRequestOptionalField[] = ['swap_operation_id']
export type PayAddressRequestOptions = OptionsBaseMessage & { export type PayAddressRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: PayAddressRequestOptionalField[]
address_CustomCheck?: (v: string) => boolean address_CustomCheck?: (v: string) => boolean
amoutSats_CustomCheck?: (v: number) => boolean amountSats_CustomCheck?: (v: number) => boolean
satsPerVByte_CustomCheck?: (v: number) => boolean satsPerVByte_CustomCheck?: (v: number) => boolean
swap_operation_id_CustomCheck?: (v?: string) => boolean
} }
export const PayAddressRequestValidate = (o?: PayAddressRequest, opts: PayAddressRequestOptions = {}, path: string = 'PayAddressRequest::root.'): Error | null => { export const PayAddressRequestValidate = (o?: PayAddressRequest, opts: PayAddressRequestOptions = {}, path: string = 'PayAddressRequest::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 (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
@ -3147,12 +3263,15 @@ export const PayAddressRequestValidate = (o?: PayAddressRequest, opts: PayAddres
if (typeof o.address !== 'string') return new Error(`${path}.address: is not a string`) if (typeof o.address !== 'string') return new Error(`${path}.address: is not a string`)
if (opts.address_CustomCheck && !opts.address_CustomCheck(o.address)) return new Error(`${path}.address: custom check failed`) if (opts.address_CustomCheck && !opts.address_CustomCheck(o.address)) return new Error(`${path}.address: custom check failed`)
if (typeof o.amoutSats !== 'number') return new Error(`${path}.amoutSats: is not a number`) if (typeof o.amountSats !== 'number') return new Error(`${path}.amountSats: is not a number`)
if (opts.amoutSats_CustomCheck && !opts.amoutSats_CustomCheck(o.amoutSats)) return new Error(`${path}.amoutSats: custom check failed`) if (opts.amountSats_CustomCheck && !opts.amountSats_CustomCheck(o.amountSats)) return new Error(`${path}.amountSats: custom check failed`)
if (typeof o.satsPerVByte !== 'number') return new Error(`${path}.satsPerVByte: is not a number`) if (typeof o.satsPerVByte !== 'number') return new Error(`${path}.satsPerVByte: is not a number`)
if (opts.satsPerVByte_CustomCheck && !opts.satsPerVByte_CustomCheck(o.satsPerVByte)) return new Error(`${path}.satsPerVByte: custom check failed`) if (opts.satsPerVByte_CustomCheck && !opts.satsPerVByte_CustomCheck(o.satsPerVByte)) return new Error(`${path}.satsPerVByte: custom check failed`)
if ((o.swap_operation_id || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('swap_operation_id')) && typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
return null return null
} }
@ -3189,18 +3308,43 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr
return null return null
} }
export type PayAdminTransactionSwapRequest = {
address: string
swap_operation_id: string
}
export const PayAdminTransactionSwapRequestOptionalFields: [] = []
export type PayAdminTransactionSwapRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
address_CustomCheck?: (v: string) => boolean
swap_operation_id_CustomCheck?: (v: string) => boolean
}
export const PayAdminTransactionSwapRequestValidate = (o?: PayAdminTransactionSwapRequest, opts: PayAdminTransactionSwapRequestOptions = {}, path: string = 'PayAdminTransactionSwapRequest::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.address !== 'string') return new Error(`${path}.address: is not a string`)
if (opts.address_CustomCheck && !opts.address_CustomCheck(o.address)) return new Error(`${path}.address: custom check failed`)
if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
return null
}
export type PayAppUserInvoiceRequest = { export type PayAppUserInvoiceRequest = {
amount: number amount: number
debit_npub?: string debit_npub?: string
expected_fees?: CumulativeFees
invoice: string invoice: string
user_identifier: string user_identifier: string
} }
export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees'
export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub'] export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees']
export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & { export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[] checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[]
amount_CustomCheck?: (v: number) => boolean amount_CustomCheck?: (v: number) => boolean
debit_npub_CustomCheck?: (v?: string) => boolean debit_npub_CustomCheck?: (v?: string) => boolean
expected_fees_Options?: CumulativeFeesOptions
invoice_CustomCheck?: (v: string) => boolean invoice_CustomCheck?: (v: string) => boolean
user_identifier_CustomCheck?: (v: string) => boolean user_identifier_CustomCheck?: (v: string) => boolean
} }
@ -3214,6 +3358,12 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o
if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`)
if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`)
if (typeof o.expected_fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('expected_fees')) {
const expected_feesErr = CumulativeFeesValidate(o.expected_fees, opts.expected_fees_Options, `${path}.expected_fees`)
if (expected_feesErr !== null) return expected_feesErr
}
if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`)
if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`)
@ -3226,14 +3376,16 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o
export type PayInvoiceRequest = { export type PayInvoiceRequest = {
amount: number amount: number
debit_npub?: string debit_npub?: string
expected_fees?: CumulativeFees
invoice: string invoice: string
} }
export type PayInvoiceRequestOptionalField = 'debit_npub' export type PayInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees'
export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub'] export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees']
export type PayInvoiceRequestOptions = OptionsBaseMessage & { export type PayInvoiceRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: PayInvoiceRequestOptionalField[] checkOptionalsAreSet?: PayInvoiceRequestOptionalField[]
amount_CustomCheck?: (v: number) => boolean amount_CustomCheck?: (v: number) => boolean
debit_npub_CustomCheck?: (v?: string) => boolean debit_npub_CustomCheck?: (v?: string) => boolean
expected_fees_Options?: CumulativeFeesOptions
invoice_CustomCheck?: (v: string) => boolean invoice_CustomCheck?: (v: string) => boolean
} }
export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoiceRequestOptions = {}, path: string = 'PayInvoiceRequest::root.'): Error | null => { export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoiceRequestOptions = {}, path: string = 'PayInvoiceRequest::root.'): Error | null => {
@ -3246,6 +3398,12 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic
if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`)
if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`)
if (typeof o.expected_fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('expected_fees')) {
const expected_feesErr = CumulativeFeesValidate(o.expected_fees, opts.expected_fees_Options, `${path}.expected_fees`)
if (expected_feesErr !== null) return expected_feesErr
}
if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`)
if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`)
@ -3254,6 +3412,7 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic
export type PayInvoiceResponse = { export type PayInvoiceResponse = {
amount_paid: number amount_paid: number
latest_balance: number
network_fee: number network_fee: number
operation_id: string operation_id: string
preimage: string preimage: string
@ -3263,6 +3422,7 @@ export const PayInvoiceResponseOptionalFields: [] = []
export type PayInvoiceResponseOptions = OptionsBaseMessage & { export type PayInvoiceResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
amount_paid_CustomCheck?: (v: number) => boolean amount_paid_CustomCheck?: (v: number) => boolean
latest_balance_CustomCheck?: (v: number) => boolean
network_fee_CustomCheck?: (v: number) => boolean network_fee_CustomCheck?: (v: number) => boolean
operation_id_CustomCheck?: (v: string) => boolean operation_id_CustomCheck?: (v: string) => boolean
preimage_CustomCheck?: (v: string) => boolean preimage_CustomCheck?: (v: string) => boolean
@ -3275,6 +3435,9 @@ export const PayInvoiceResponseValidate = (o?: PayInvoiceResponse, opts: PayInvo
if (typeof o.amount_paid !== 'number') return new Error(`${path}.amount_paid: is not a number`) if (typeof o.amount_paid !== 'number') return new Error(`${path}.amount_paid: is not a number`)
if (opts.amount_paid_CustomCheck && !opts.amount_paid_CustomCheck(o.amount_paid)) return new Error(`${path}.amount_paid: custom check failed`) if (opts.amount_paid_CustomCheck && !opts.amount_paid_CustomCheck(o.amount_paid)) return new Error(`${path}.amount_paid: custom check failed`)
if (typeof o.latest_balance !== 'number') return new Error(`${path}.latest_balance: is not a number`)
if (opts.latest_balance_CustomCheck && !opts.latest_balance_CustomCheck(o.latest_balance)) return new Error(`${path}.latest_balance: custom check failed`)
if (typeof o.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`) if (typeof o.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`)
if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`) if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`)
@ -3312,7 +3475,9 @@ export const PayerDataValidate = (o?: PayerData, opts: PayerDataOptions = {}, pa
export type PaymentState = { export type PaymentState = {
amount: number amount: number
internal: boolean
network_fee: number network_fee: number
operation_id: string
paid_at_unix: number paid_at_unix: number
service_fee: number service_fee: number
} }
@ -3320,7 +3485,9 @@ export const PaymentStateOptionalFields: [] = []
export type PaymentStateOptions = OptionsBaseMessage & { export type PaymentStateOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
amount_CustomCheck?: (v: number) => boolean amount_CustomCheck?: (v: number) => boolean
internal_CustomCheck?: (v: boolean) => boolean
network_fee_CustomCheck?: (v: number) => boolean network_fee_CustomCheck?: (v: number) => boolean
operation_id_CustomCheck?: (v: string) => boolean
paid_at_unix_CustomCheck?: (v: number) => boolean paid_at_unix_CustomCheck?: (v: number) => boolean
service_fee_CustomCheck?: (v: number) => boolean service_fee_CustomCheck?: (v: number) => boolean
} }
@ -3331,9 +3498,15 @@ export const PaymentStateValidate = (o?: PaymentState, opts: PaymentStateOptions
if (typeof o.amount !== 'number') return new Error(`${path}.amount: is not a number`) if (typeof o.amount !== 'number') return new Error(`${path}.amount: is not a number`)
if (opts.amount_CustomCheck && !opts.amount_CustomCheck(o.amount)) return new Error(`${path}.amount: custom check failed`) if (opts.amount_CustomCheck && !opts.amount_CustomCheck(o.amount)) return new Error(`${path}.amount: custom check failed`)
if (typeof o.internal !== 'boolean') return new Error(`${path}.internal: is not a boolean`)
if (opts.internal_CustomCheck && !opts.internal_CustomCheck(o.internal)) return new Error(`${path}.internal: custom check failed`)
if (typeof o.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`) if (typeof o.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`)
if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`) if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`)
if (typeof o.operation_id !== 'string') return new Error(`${path}.operation_id: is not a string`)
if (opts.operation_id_CustomCheck && !opts.operation_id_CustomCheck(o.operation_id)) return new Error(`${path}.operation_id: custom check failed`)
if (typeof o.paid_at_unix !== 'number') return new Error(`${path}.paid_at_unix: is not a number`) if (typeof o.paid_at_unix !== 'number') return new Error(`${path}.paid_at_unix: is not a number`)
if (opts.paid_at_unix_CustomCheck && !opts.paid_at_unix_CustomCheck(o.paid_at_unix)) return new Error(`${path}.paid_at_unix: custom check failed`) if (opts.paid_at_unix_CustomCheck && !opts.paid_at_unix_CustomCheck(o.paid_at_unix)) return new Error(`${path}.paid_at_unix: custom check failed`)
@ -3744,6 +3917,165 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR
return null return null
} }
export type SwapOperation = {
address_paid: string
failure_reason?: string
operation_payment?: UserOperation
swap_operation_id: string
}
export type SwapOperationOptionalField = 'failure_reason' | 'operation_payment'
export const SwapOperationOptionalFields: SwapOperationOptionalField[] = ['failure_reason', 'operation_payment']
export type SwapOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: SwapOperationOptionalField[]
address_paid_CustomCheck?: (v: string) => boolean
failure_reason_CustomCheck?: (v?: string) => boolean
operation_payment_Options?: UserOperationOptions
swap_operation_id_CustomCheck?: (v: string) => boolean
}
export const SwapOperationValidate = (o?: SwapOperation, opts: SwapOperationOptions = {}, path: string = 'SwapOperation::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.address_paid !== 'string') return new Error(`${path}.address_paid: is not a string`)
if (opts.address_paid_CustomCheck && !opts.address_paid_CustomCheck(o.address_paid)) return new Error(`${path}.address_paid: custom check failed`)
if ((o.failure_reason || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('failure_reason')) && typeof o.failure_reason !== 'string') return new Error(`${path}.failure_reason: is not a string`)
if (opts.failure_reason_CustomCheck && !opts.failure_reason_CustomCheck(o.failure_reason)) return new Error(`${path}.failure_reason: custom check failed`)
if (typeof o.operation_payment === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('operation_payment')) {
const operation_paymentErr = UserOperationValidate(o.operation_payment, opts.operation_payment_Options, `${path}.operation_payment`)
if (operation_paymentErr !== null) return operation_paymentErr
}
if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
return null
}
export type SwapsList = {
quotes: TransactionSwapQuote[]
swaps: SwapOperation[]
}
export const SwapsListOptionalFields: [] = []
export type SwapsListOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
quotes_ItemOptions?: TransactionSwapQuoteOptions
quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean
swaps_ItemOptions?: SwapOperationOptions
swaps_CustomCheck?: (v: SwapOperation[]) => boolean
}
export const SwapsListValidate = (o?: SwapsList, opts: SwapsListOptions = {}, path: string = 'SwapsList::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.quotes)) return new Error(`${path}.quotes: is not an array`)
for (let index = 0; index < o.quotes.length; index++) {
const quotesErr = TransactionSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`)
if (quotesErr !== null) return quotesErr
}
if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`)
if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`)
for (let index = 0; index < o.swaps.length; index++) {
const swapsErr = SwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`)
if (swapsErr !== null) return swapsErr
}
if (opts.swaps_CustomCheck && !opts.swaps_CustomCheck(o.swaps)) return new Error(`${path}.swaps: custom check failed`)
return null
}
export type TransactionSwapQuote = {
chain_fee_sats: number
invoice_amount_sats: number
service_fee_sats: number
service_url: string
swap_fee_sats: number
swap_operation_id: string
transaction_amount_sats: number
}
export const TransactionSwapQuoteOptionalFields: [] = []
export type TransactionSwapQuoteOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
chain_fee_sats_CustomCheck?: (v: number) => boolean
invoice_amount_sats_CustomCheck?: (v: number) => boolean
service_fee_sats_CustomCheck?: (v: number) => boolean
service_url_CustomCheck?: (v: string) => boolean
swap_fee_sats_CustomCheck?: (v: number) => boolean
swap_operation_id_CustomCheck?: (v: string) => boolean
transaction_amount_sats_CustomCheck?: (v: number) => boolean
}
export const TransactionSwapQuoteValidate = (o?: TransactionSwapQuote, opts: TransactionSwapQuoteOptions = {}, path: string = 'TransactionSwapQuote::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.chain_fee_sats !== 'number') return new Error(`${path}.chain_fee_sats: is not a number`)
if (opts.chain_fee_sats_CustomCheck && !opts.chain_fee_sats_CustomCheck(o.chain_fee_sats)) return new Error(`${path}.chain_fee_sats: custom check failed`)
if (typeof o.invoice_amount_sats !== 'number') return new Error(`${path}.invoice_amount_sats: is not a number`)
if (opts.invoice_amount_sats_CustomCheck && !opts.invoice_amount_sats_CustomCheck(o.invoice_amount_sats)) return new Error(`${path}.invoice_amount_sats: custom check failed`)
if (typeof o.service_fee_sats !== 'number') return new Error(`${path}.service_fee_sats: is not a number`)
if (opts.service_fee_sats_CustomCheck && !opts.service_fee_sats_CustomCheck(o.service_fee_sats)) return new Error(`${path}.service_fee_sats: custom check failed`)
if (typeof o.service_url !== 'string') return new Error(`${path}.service_url: is not a string`)
if (opts.service_url_CustomCheck && !opts.service_url_CustomCheck(o.service_url)) return new Error(`${path}.service_url: custom check failed`)
if (typeof o.swap_fee_sats !== 'number') return new Error(`${path}.swap_fee_sats: is not a number`)
if (opts.swap_fee_sats_CustomCheck && !opts.swap_fee_sats_CustomCheck(o.swap_fee_sats)) return new Error(`${path}.swap_fee_sats: custom check failed`)
if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
if (typeof o.transaction_amount_sats !== 'number') return new Error(`${path}.transaction_amount_sats: is not a number`)
if (opts.transaction_amount_sats_CustomCheck && !opts.transaction_amount_sats_CustomCheck(o.transaction_amount_sats)) return new Error(`${path}.transaction_amount_sats: custom check failed`)
return null
}
export type TransactionSwapQuoteList = {
quotes: TransactionSwapQuote[]
}
export const TransactionSwapQuoteListOptionalFields: [] = []
export type TransactionSwapQuoteListOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
quotes_ItemOptions?: TransactionSwapQuoteOptions
quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean
}
export const TransactionSwapQuoteListValidate = (o?: TransactionSwapQuoteList, opts: TransactionSwapQuoteListOptions = {}, path: string = 'TransactionSwapQuoteList::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.quotes)) return new Error(`${path}.quotes: is not an array`)
for (let index = 0; index < o.quotes.length; index++) {
const quotesErr = TransactionSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`)
if (quotesErr !== null) return quotesErr
}
if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`)
return null
}
export type TransactionSwapRequest = {
transaction_amount_sats: number
}
export const TransactionSwapRequestOptionalFields: [] = []
export type TransactionSwapRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
transaction_amount_sats_CustomCheck?: (v: number) => boolean
}
export const TransactionSwapRequestValidate = (o?: TransactionSwapRequest, opts: TransactionSwapRequestOptions = {}, path: string = 'TransactionSwapRequest::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.transaction_amount_sats !== 'number') return new Error(`${path}.transaction_amount_sats: is not a number`)
if (opts.transaction_amount_sats_CustomCheck && !opts.transaction_amount_sats_CustomCheck(o.transaction_amount_sats)) return new Error(`${path}.transaction_amount_sats: custom check failed`)
return null
}
export type UpdateChannelPolicyRequest = { export type UpdateChannelPolicyRequest = {
policy: ChannelPolicy policy: ChannelPolicy
update: UpdateChannelPolicyRequest_update update: UpdateChannelPolicyRequest_update

View file

@ -175,6 +175,27 @@ service LightningPub {
option (nostr) = true; option (nostr) = true;
} }
rpc GetAdminTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/transaction/quote";
option (nostr) = true;
}
rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminSwapResponse) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/transaction/pay";
option (nostr) = true;
}
rpc ListAdminSwaps(structs.Empty) returns (structs.SwapsList) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/list";
option (nostr) = true;
}
rpc GetUsageMetrics(structs.LatestUsageMetricReq) returns (structs.UsageMetrics) { rpc GetUsageMetrics(structs.LatestUsageMetricReq) returns (structs.UsageMetrics) {
option (auth_type) = "Metrics"; option (auth_type) = "Metrics";
option (http_method) = "post"; option (http_method) = "post";
@ -480,7 +501,7 @@ service LightningPub {
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/user/operations"; option (http_route) = "/api/user/operations";
option (nostr) = true; option (nostr) = true;
} }
rpc NewAddress(structs.NewAddressRequest) returns (structs.NewAddressResponse) { rpc NewAddress(structs.NewAddressRequest) returns (structs.NewAddressResponse) {
option (auth_type) = "User"; option (auth_type) = "User";
@ -496,6 +517,20 @@ service LightningPub {
option (nostr) = true; option (nostr) = true;
} }
rpc GetTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/swap/quote";
option (nostr) = true;
}
rpc ListSwaps(structs.Empty) returns (structs.SwapsList){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/swap/list";
option (nostr) = true;
}
rpc NewInvoice(structs.NewInvoiceRequest) returns (structs.NewInvoiceResponse){ rpc NewInvoice(structs.NewInvoiceRequest) returns (structs.NewInvoiceResponse){
option (auth_type) = "User"; option (auth_type) = "User";
option (http_method) = "post"; option (http_method) = "post";

View file

@ -389,7 +389,8 @@ message PayAppUserInvoiceRequest {
string user_identifier = 1; string user_identifier = 1;
string invoice = 2; string invoice = 2;
int64 amount = 3; int64 amount = 3;
optional string debit_npub = 4; optional string debit_npub = 4;
optional CumulativeFees expected_fees = 5;
} }
message SendAppUserToAppUserPaymentRequest { message SendAppUserToAppUserPaymentRequest {
@ -430,8 +431,9 @@ message NewAddressResponse{
} }
message PayAddressRequest{ message PayAddressRequest{
string address = 1; string address = 1;
int64 amoutSats = 2; int64 amountSats = 2;
int64 satsPerVByte = 3; int64 satsPerVByte = 3;
optional string swap_operation_id = 4;
} }
message PayAddressResponse{ message PayAddressResponse{
@ -465,7 +467,8 @@ message DecodeInvoiceResponse{
message PayInvoiceRequest{ message PayInvoiceRequest{
string invoice = 1; string invoice = 1;
int64 amount = 2; int64 amount = 2;
optional string debit_npub = 3; optional string debit_npub = 3;
optional CumulativeFees expected_fees = 4;
} }
message PayInvoiceResponse{ message PayInvoiceResponse{
@ -474,6 +477,7 @@ message PayInvoiceResponse{
string operation_id = 3; string operation_id = 3;
int64 service_fee = 4; int64 service_fee = 4;
int64 network_fee = 5; int64 network_fee = 5;
int64 latest_balance = 6;
} }
message GetPaymentStateRequest{ message GetPaymentStateRequest{
@ -486,6 +490,9 @@ message PaymentState{
int64 amount = 2; int64 amount = 2;
int64 service_fee = 3; int64 service_fee = 3;
int64 network_fee = 4; int64 network_fee = 4;
bool internal = 5;
string operation_id = 6;
} }
message LnurlLinkResponse{ message LnurlLinkResponse{
@ -604,6 +611,7 @@ message GetProductBuyLinkResponse {
message LiveUserOperation { message LiveUserOperation {
UserOperation operation = 1; UserOperation operation = 1;
int64 latest_balance = 2;
} }
message MigrationUpdate { message MigrationUpdate {
optional ClosureMigration closure = 1; optional ClosureMigration closure = 1;
@ -824,3 +832,57 @@ message MessagingToken {
string device_id = 1; string device_id = 1;
string firebase_messaging_token = 2; string firebase_messaging_token = 2;
} }
message TransactionSwapRequest {
int64 transaction_amount_sats = 2;
}
message PayAdminTransactionSwapRequest {
string address = 1;
string swap_operation_id = 2;
}
message TransactionSwapQuote {
string swap_operation_id = 1;
int64 invoice_amount_sats = 2;
int64 transaction_amount_sats = 3;
int64 swap_fee_sats = 4;
int64 chain_fee_sats = 5;
int64 service_fee_sats = 7;
string service_url = 8;
}
message TransactionSwapQuoteList {
repeated TransactionSwapQuote quotes = 1;
}
message AdminSwapResponse {
string tx_id = 1;
int64 network_fee = 2;
}
message SwapOperation {
string swap_operation_id = 1;
optional UserOperation operation_payment = 2;
optional string failure_reason = 3;
string address_paid = 4;
}
message SwapsList {
repeated SwapOperation swaps = 1;
repeated TransactionSwapQuote quotes = 2;
}
message CumulativeFees {
int64 serviceFeeFloor = 2;
int64 serviceFeeBps = 3;
}
message BeaconData {
string type = 1;
string name = 2;
optional string avatarUrl = 3;
optional string nextRelay = 4;
optional CumulativeFees fees = 5;
}

View file

@ -7,6 +7,7 @@ import { getLogger } from './services/helpers/logger.js';
import { initMainHandler, initSettings } from './services/main/init.js'; import { initMainHandler, initSettings } from './services/main/init.js';
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { LoadStorageSettingsFromEnv } from './services/storage/index.js'; import { LoadStorageSettingsFromEnv } from './services/storage/index.js';
import { AppInfo } from './services/nostr/nostrPool.js';
//@ts-ignore //@ts-ignore
const { nprofileEncode } = nip19 const { nprofileEncode } = nip19
@ -20,18 +21,35 @@ const start = async () => {
return return
} }
const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const { mainHandler, localProviderClient, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler) const serverMethods = GetServerMethods(mainHandler)
const nostrSettings = settingsManager.getSettings().nostrRelaySettings const nostrSettings = settingsManager.getSettings().nostrRelaySettings
log("initializing nostr middleware") log("initializing nostr middleware")
const relays = settingsManager.getSettings().nostrRelaySettings.relays
const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength
const apps: AppInfo[] = keepOn.apps.map(app => {
return {
appId: app.appId,
privateKey: app.privateKey,
publicKey: app.publicKey,
name: app.name,
provider: app.publicKey === localProviderClient.publicKey ? {
clientId: `client_${localProviderClient.appId}`,
pubkey: settingsManager.getSettings().liquiditySettings.liquidityProviderPub,
relayUrl: settingsManager.getSettings().liquiditySettings.providerRelayUrl
} : undefined
}
})
const { Send } = nostrMiddleware(serverMethods, mainHandler, const { Send } = nostrMiddleware(serverMethods, mainHandler,
{ ...nostrSettings, apps, clients: [liquidityProviderInfo] }, {
relays, maxEventContentLength, apps
},
(e, p) => mainHandler.liquidityProvider.onEvent(e, p) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
) )
log("starting server") log("starting server")
mainHandler.attachNostrSend(Send) mainHandler.attachNostrSend(Send)
mainHandler.StartBeacons() mainHandler.StartBeacons()
const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays: nostrSettings.relays }) const appNprofile = nprofileEncode({ pubkey: localProviderClient.publicKey, relays: nostrSettings.relays })
if (wizard) { if (wizard) {
wizard.AddConnectInfo(appNprofile, nostrSettings.relays) wizard.AddConnectInfo(appNprofile, nostrSettings.relays)
} }

View file

@ -7,6 +7,7 @@ import { getLogger } from './services/helpers/logger.js';
import { initMainHandler, initSettings } from './services/main/init.js'; import { initMainHandler, initSettings } from './services/main/init.js';
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { LoadStorageSettingsFromEnv } from './services/storage/index.js'; import { LoadStorageSettingsFromEnv } from './services/storage/index.js';
import { AppInfo } from './services/nostr/nostrPool.js';
//@ts-ignore //@ts-ignore
const { nprofileEncode } = nip19 const { nprofileEncode } = nip19
@ -22,13 +23,28 @@ const start = async () => {
return return
} }
const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const { mainHandler, localProviderClient, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler) const serverMethods = GetServerMethods(mainHandler)
log("initializing nostr middleware") log("initializing nostr middleware")
const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays const relays = settingsManager.getSettings().nostrRelaySettings.relays
const maxEventContentLength = mainHandler.settings.getSettings().nostrRelaySettings.maxEventContentLength const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength
const apps: AppInfo[] = keepOn.apps.map(app => {
return {
appId: app.appId,
privateKey: app.privateKey,
publicKey: app.publicKey,
name: app.name,
provider: app.publicKey === localProviderClient.publicKey ? {
clientId: `client_${localProviderClient.appId}`,
pubkey: settingsManager.getSettings().liquiditySettings.liquidityProviderPub,
relayUrl: settingsManager.getSettings().liquiditySettings.providerRelayUrl
} : undefined
}
})
const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler, const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler,
{ relays, maxEventContentLength, apps, clients: [liquidityProviderInfo] }, {
relays, maxEventContentLength, apps
},
(e, p) => mainHandler.liquidityProvider.onEvent(e, p) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
) )
exitHandler(() => { Stop(); mainHandler.Stop() }) exitHandler(() => { Stop(); mainHandler.Stop() })
@ -37,13 +53,13 @@ const start = async () => {
mainHandler.attachNostrProcessPing(Ping) mainHandler.attachNostrProcessPing(Ping)
mainHandler.attachNostrReset(Reset) mainHandler.attachNostrReset(Reset)
mainHandler.StartBeacons() mainHandler.StartBeacons()
const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays }) const appNprofile = nprofileEncode({ pubkey: localProviderClient.publicKey, relays })
if (wizard) { if (wizard) {
wizard.AddConnectInfo(appNprofile, relays) wizard.AddConnectInfo(appNprofile, relays)
} }
adminManager.setAppNprofile(appNprofile) adminManager.setAppNprofile(appNprofile)
const Server = NewServer(serverMethods, serverOptions(mainHandler)) const Server = NewServer(serverMethods, serverOptions(mainHandler))
Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) Server.Listen(settingsManager.getSettings().serviceSettings.servicePort)
} }
start() start()

View file

@ -1,6 +1,6 @@
import Main from "./services/main/index.js" import Main from "./services/main/index.js"
import Nostr from "./services/nostr/index.js" import Nostr from "./services/nostr/index.js"
import { NostrEvent, NostrSend, NostrSettings } from "./services/nostr/handler.js" import { NostrEvent, NostrSend, NostrSettings } from "./services/nostr/nostrPool.js"
import * as Types from '../proto/autogenerated/ts/types.js' import * as Types from '../proto/autogenerated/ts/types.js'
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js'; import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
import { ERROR, getLogger } from "./services/helpers/logger.js"; import { ERROR, getLogger } from "./services/helpers/logger.js";
@ -50,6 +50,10 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
return return
} }
if (event.kind === 21001) { if (event.kind === 21001) {
if (event.relayConstraint === 'provider') {
log("got noffer request on provider only relay, ignoring")
return
}
const offerReq = j as NofferData const offerReq = j as NofferData
log("🎯 [NOSTR EVENT] Received offer request (kind 21001)", { log("🎯 [NOSTR EVENT] Received offer request (kind 21001)", {
fromPub: event.pub, fromPub: event.pub,
@ -60,18 +64,33 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
mainHandler.offerManager.handleClinkOffer(offerReq, event) mainHandler.offerManager.handleClinkOffer(offerReq, event)
return return
} else if (event.kind === 21002) { } else if (event.kind === 21002) {
if (event.relayConstraint === 'provider') {
log("got debit request on provider only relay, ignoring")
return
}
const debitReq = j as NdebitData const debitReq = j as NdebitData
mainHandler.debitManager.handleNip68Debit(debitReq, event) mainHandler.debitManager.handleNip68Debit(debitReq, event)
return return
} else if (event.kind === 21003) { } else if (event.kind === 21003) {
if (event.relayConstraint === 'provider') {
log("got management request on provider only relay, ignoring")
return
}
const nmanageReq = j as NmanageRequest const nmanageReq = j as NmanageRequest
mainHandler.managementManager.handleRequest(nmanageReq, event); mainHandler.managementManager.handleRequest(nmanageReq, event);
return; return;
} }
if (!j.rpcName) { if (!j.rpcName) {
if (event.relayConstraint === 'service') {
log("got relay response on service only relay, ignoring")
}
onClientEvent(j as { requestId: string }, event.pub) onClientEvent(j as { requestId: string }, event.pub)
return return
} }
if (event.relayConstraint === 'provider') {
log("got service request on provider only relay, ignoring")
return
}
if (j.authIdentifier !== event.pub) { if (j.authIdentifier !== event.pub) {
log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub) log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub)
return return
@ -79,7 +98,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
nostrTransport({ ...j, appId: event.appId }, res => { nostrTransport({ ...j, appId: event.appId }, res => {
nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) }) nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) })
}, event.startAtNano, event.startAtMs) }, event.startAtNano, event.startAtMs)
}) }, beacon => mainHandler.liquidityProvider.onBeaconEvent(beacon))
// Mark nostr connected/ready after initial subscription tick // Mark nostr connected/ready after initial subscription tick
mainHandler.adminManager.setNostrConnected(true) mainHandler.adminManager.setNostrConnected(true)

View file

@ -14,10 +14,22 @@ 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}`
// Sanitize filename to remove invalid characters for filesystem
const sanitizeFileName = (fileName: string): string => {
// Replace invalid filename characters with underscores
// Invalid on most filesystems: / \ : * ? " < > |
return fileName.replace(/[/\\:*?"<>|]/g, '_')
}
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 logPath = `${logsDir}/${fileName}_${date}.log`
// Ensure parent directory exists
const dirPath = logPath.substring(0, logPath.lastIndexOf('/'))
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
const logStream = fs.createWriteStream(logPath, { flags: 'a' });
return (message) => { return (message) => {
logStream.write(message + "\n") logStream.write(message + "\n")
} }
@ -35,13 +47,13 @@ if (!fs.existsSync(`${logsDir}/components`)) {
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/${sanitizeFileName(params.appName)}`))
} }
if (params.userId) { if (params.userId) {
writers.push(openWriter(`users/${params.userId}`)) writers.push(openWriter(`users/${sanitizeFileName(params.userId)}`))
} }
if (params.component) { if (params.component) {
writers.push(openWriter(`components/${params.component}`)) writers.push(openWriter(`components/${sanitizeFileName(params.component)}`))
} }
if (writers.length === 0) { if (writers.length === 0) {
writers.push(rootWriter) writers.push(rootWriter)

View file

@ -1,7 +1,7 @@
import { StateBundler } from "../storage/tlv/stateBundler.js"; import { StateBundler } from "../storage/tlv/stateBundler.js";
import { TlvStorageFactory } from "../storage/tlv/tlvFilesStorageFactory.js"; import { TlvStorageFactory } from "../storage/tlv/tlvFilesStorageFactory.js";
import { NostrSend } from "../nostr/handler.js";
import { ProcessMetricsCollector } from "../storage/tlv/processMetricsCollector.js"; import { ProcessMetricsCollector } from "../storage/tlv/processMetricsCollector.js";
import { NostrSender } from "../nostr/sender.js";
type UtilsSettings = { type UtilsSettings = {
noCollector?: boolean noCollector?: boolean
dataDir: string, dataDir: string,
@ -10,9 +10,11 @@ type UtilsSettings = {
export class Utils { export class Utils {
tlvStorageFactory: TlvStorageFactory tlvStorageFactory: TlvStorageFactory
stateBundler: StateBundler stateBundler: StateBundler
_nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') } nostrSender: NostrSender
constructor({ noCollector, dataDir, allowResetMetricsStorages }: UtilsSettings) {
this.tlvStorageFactory = new TlvStorageFactory(allowResetMetricsStorages) constructor({ noCollector, dataDir, allowResetMetricsStorages }: UtilsSettings, nostrSender: NostrSender) {
this.nostrSender = nostrSender
this.tlvStorageFactory = new TlvStorageFactory(allowResetMetricsStorages, nostrSender)
this.stateBundler = new StateBundler(dataDir, this.tlvStorageFactory) this.stateBundler = new StateBundler(dataDir, this.tlvStorageFactory)
if (!noCollector) { if (!noCollector) {
new ProcessMetricsCollector((metrics) => { new ProcessMetricsCollector((metrics) => {
@ -21,11 +23,6 @@ export class Utils {
} }
} }
attachNostrSend(f: NostrSend) {
this._nostrSend = f
this.tlvStorageFactory.attachNostrSend(f)
}
Stop() { Stop() {
this.stateBundler.Stop() this.stateBundler.Stop()
this.tlvStorageFactory.disconnect() this.tlvStorageFactory.disconnect()

View file

@ -17,12 +17,13 @@ import { SendCoinsReq } from './sendCoinsReq.js';
import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js'; import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js';
import { ERROR, getLogger } from '../helpers/logger.js'; import { ERROR, 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 '../main/liquidityProvider.js'; import { LiquidityProvider } from '../main/liquidityProvider.js';
import { Utils } from '../helpers/utilsWrapper.js'; import { Utils } from '../helpers/utilsWrapper.js';
import { TxPointSettings } from '../storage/tlv/stateBundler.js'; import { TxPointSettings } from '../storage/tlv/stateBundler.js';
import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js'; import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js';
import SettingsManager from '../main/settingsManager.js'; import SettingsManager from '../main/settingsManager.js';
import { LndNodeSettings, LndSettings } from '../main/settings.js'; import { LndNodeSettings, LndSettings } from '../main/settings.js';
import { ListAddressesResponse } from '../../../proto/lnd/walletkit.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 20 const deadLndRetrySeconds = 20
@ -32,6 +33,7 @@ type NodeSettingsOverride = {
lndCertPath: string lndCertPath: string
lndMacaroonPath: string lndMacaroonPath: string
} }
export type LndAddress = { address: string, change: boolean }
export default class { export default class {
lightning: LightningClient lightning: LightningClient
invoices: InvoicesClient invoices: InvoicesClient
@ -53,6 +55,7 @@ export default class {
liquidProvider: LiquidityProvider liquidProvider: LiquidityProvider
utils: Utils utils: Utils
unlockLnd: () => Promise<void> unlockLnd: () => Promise<void>
addressesCache: Record<string, { isChange: boolean }> = {}
constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, unlockLnd: () => Promise<any>, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, unlockLnd: () => Promise<any>, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) {
this.getSettings = getSettings this.getSettings = getSettings
this.utils = utils this.utils = utils
@ -63,7 +66,7 @@ export default class {
this.htlcCb = htlcCb this.htlcCb = htlcCb
this.channelEventCb = channelEventCb this.channelEventCb = channelEventCb
this.liquidProvider = liquidProvider this.liquidProvider = liquidProvider
// Skip LND client initialization if using only liquidity provider // Skip LND client initialization if using only liquidity provider
if (liquidProvider.getSettings().useOnlyLiquidityProvider) { if (liquidProvider.getSettings().useOnlyLiquidityProvider) {
this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization") this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization")
@ -79,7 +82,7 @@ export default class {
this.walletKit = new WalletKitClient(dummyTransport) this.walletKit = new WalletKitClient(dummyTransport)
return return
} }
const { lndAddr, lndCertPath, lndMacaroonPath } = this.getSettings().lndNodeSettings const { lndAddr, lndCertPath, lndMacaroonPath } = this.getSettings().lndNodeSettings
const lndCert = fs.readFileSync(lndCertPath); const lndCert = fs.readFileSync(lndCertPath);
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex'); const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
@ -331,6 +334,26 @@ export default class {
}) })
} }
async IsChangeAddress(address: string): Promise<boolean> {
const cached = this.addressesCache[address]
if (cached) {
return cached.isChange
}
const addresses = await this.ListAddresses()
const addr = addresses.find(a => a.address === address)
if (!addr) {
throw new Error(`address ${address} not found in list of addresses`)
}
return addr.change
}
async ListAddresses(): Promise<LndAddress[]> {
const res = await this.walletKit.listAddresses({ accountName: "", showCustomAccounts: false }, DeadLineMetadata())
const addresses = res.response.accountWithAddresses.map(a => a.addresses.map(a => ({ address: a.address, change: a.isInternal }))).flat()
addresses.forEach(a => this.addressesCache[a.address] = { isChange: a.change })
return addresses
}
async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> { async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> {
// Force use of provider when bypass is enabled (addresses not supported by provider, but we should fail gracefully) // Force use of provider when bypass is enabled (addresses not supported by provider, but we should fail gracefully)
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
@ -372,8 +395,8 @@ export default class {
if (mustUseProvider) { if (mustUseProvider) {
console.log("using provider") console.log("using provider")
const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry)
const providerDst = this.liquidProvider.GetProviderDestination() const providerPubkey = this.liquidProvider.GetProviderPubkey()
return { payRequest: invoice, providerDst } return { payRequest: invoice, providerPubkey }
} }
try { try {
const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, true, memo, blind), DeadLineMetadata()) const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, true, memo, blind), DeadLineMetadata())
@ -392,7 +415,7 @@ export default class {
const decoded = decodeBolt11(paymentRequest) const decoded = decodeBolt11(paymentRequest)
let numSatoshis = 0 let numSatoshis = 0
let paymentHash = '' let paymentHash = ''
for (const section of decoded.sections) { for (const section of decoded.sections) {
if (section.name === 'amount') { if (section.name === 'amount') {
// Amount is in millisatoshis // Amount is in millisatoshis
@ -401,11 +424,11 @@ export default class {
paymentHash = section.value as string paymentHash = section.value as string
} }
} }
if (!paymentHash) { if (!paymentHash) {
throw new Error("Payment hash not found in invoice") throw new Error("Payment hash not found in invoice")
} }
return { numSatoshis, paymentHash } return { numSatoshis, paymentHash }
} catch (err: any) { } catch (err: any) {
throw new Error(`Failed to decode invoice: ${err.message}`) throw new Error(`Failed to decode invoice: ${err.message}`)
@ -416,14 +439,6 @@ export default class {
return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash }
} }
GetFeeLimitAmount(amount: number): number {
return Math.ceil(amount * this.getSettings().lndSettings.feeRateLimit + this.getSettings().lndSettings.feeFixedLimit);
}
GetMaxWithinLimit(amount: number): number {
return Math.max(0, Math.floor(amount * (1 - this.getSettings().lndSettings.feeRateLimit) - this.getSettings().lndSettings.feeFixedLimit))
}
async ChannelBalance(): Promise<{ local: number, remote: number }> { async ChannelBalance(): Promise<{ local: number, remote: number }> {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { local: 0, remote: 0 } return { local: 0, remote: 0 }
@ -433,7 +448,7 @@ 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, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise<PaidInvoice> { async PayInvoice(invoice: string, amount: number, { routingFeeLimit, serviceFee }: { routingFeeLimit: number, serviceFee: number }, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise<PaidInvoice> {
// console.log("Paying invoice") // console.log("Paying invoice")
if (this.outgoingOpsLocked) { if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
@ -442,14 +457,14 @@ export default class {
// Force use of provider when bypass is enabled // Force use of provider when bypass is enabled
const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider
if (mustUseProvider) { if (mustUseProvider) {
const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from) const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from, serviceFee)
const providerDst = this.liquidProvider.GetProviderDestination() const providerPubkey = this.liquidProvider.GetProviderPubkey()
return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } return { feeSat: res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerPubkey }
} }
await this.Health() await this.Health()
try { try {
const abortController = new AbortController() const abortController = new AbortController()
const req = PayInvoiceReq(invoice, amount, feeLimit) const req = PayInvoiceReq(invoice, amount, routingFeeLimit)
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
return new Promise((res, rej) => { return new Promise((res, rej) => {
stream.responses.onError(error => { stream.responses.onError(error => {

View file

@ -101,7 +101,7 @@ export class FlashsatsLSP extends LSP {
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system') const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system')
const fees = +order.payment.fee_total_sat + res.network_fee + res.service_fee const fees = +order.payment.fee_total_sat + res.service_fee
this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) 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 } return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees }
@ -177,7 +177,7 @@ export class OlympusLSP extends LSP {
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system') const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system')
const fees = +order.payment.bolt11.fee_total_sat + res.network_fee + res.service_fee const fees = +order.payment.bolt11.fee_total_sat + res.service_fee
this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) 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 } return { orderId: order.order_id, invoice: order.payment.bolt11.invoice, totalSats: +order.payment.bolt11.order_total_sat, fees }
} }
@ -279,7 +279,7 @@ export class VoltageLSP extends LSP {
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(proposalRes.jit_bolt11, decoded.numSatoshis, 'system') const res = await this.liquidityProvider.PayInvoice(proposalRes.jit_bolt11, decoded.numSatoshis, 'system')
const fees = feeSats + res.network_fee + res.service_fee const fees = feeSats + res.service_fee
this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) 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 } return { orderId: fee.id, invoice: proposalRes.jit_bolt11, totalSats: decoded.numSatoshis, fees }
} }

View file

@ -35,7 +35,7 @@ export type NodeInfo = {
} }
export type Invoice = { export type Invoice = {
payRequest: string payRequest: string
providerDst?: string providerPubkey?: string
} }
export type DecodedInvoice = { export type DecodedInvoice = {
numSatoshis: number numSatoshis: number
@ -45,7 +45,7 @@ export type PaidInvoice = {
feeSat: number feeSat: number
valueSat: number valueSat: number
paymentPreimage: string paymentPreimage: string
providerDst?: string providerPubkey?: string
} }

657
src/services/lnd/swaps.ts Normal file
View file

@ -0,0 +1,657 @@
import zkpInit from '@vulpemventures/secp256k1-zkp';
import axios from 'axios';
import { crypto, initEccLib, Transaction, address, Network } from 'bitcoinjs-lib';
// import bolt11 from 'bolt11';
import {
Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
constructClaimTransaction, targetFee, OutputType,
Networks,
} from 'boltz-core';
import { randomBytes, createHash } from 'crypto';
import { ECPairFactory, ECPairInterface } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import ws from 'ws';
import { getLogger, PubLogger, ERROR } from '../helpers/logger.js';
import SettingsManager from '../main/settingsManager.js';
import * as Types from '../../../proto/autogenerated/ts/types.js';
import { BTCNetwork } from '../main/settings.js';
import Storage from '../storage/index.js';
import LND from './lnd.js';
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js';
type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string }
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo }
type TransactionSwapFees = {
percentage: number,
minerFees: {
claim: number,
lockup: number,
}
}
type TransactionSwapFeesRes = {
BTC?: {
BTC?: {
fees: TransactionSwapFees
}
}
}
type TransactionSwapResponse = {
id: string, refundPublicKey: string, swapTree: string,
timeoutBlockHeight: number, lockupAddress: string, invoice: string,
onchainAmount?: number
}
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
export class Swaps {
settings: SettingsManager
revSwappers: Record<string, ReverseSwaps>
// submarineSwaps: SubmarineSwaps
storage: Storage
lnd: LND
log = getLogger({ component: 'swaps' })
constructor(settings: SettingsManager, storage: Storage) {
this.settings = settings
this.revSwappers = {}
const network = settings.getSettings().lndSettings.network
const { boltzHttpUrl, boltzWebSocketUrl, boltsHttpUrlAlt, boltsWebSocketUrlAlt } = settings.getSettings().swapsSettings
if (boltzHttpUrl && boltzWebSocketUrl) {
this.revSwappers[boltzHttpUrl] = new ReverseSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network })
}
if (boltsHttpUrlAlt && boltsWebSocketUrlAlt) {
this.revSwappers[boltsHttpUrlAlt] = new ReverseSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network })
}
this.storage = storage
}
SetLnd = (lnd: LND) => {
this.lnd = lnd
}
Stop = () => { }
GetKeys = (privateKey: string) => {
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
return keys
}
ListSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise<Types.SwapsList> => {
const completedSwaps = await this.storage.paymentStorage.ListCompletedSwaps(appUserId, payments)
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(appUserId)
return {
swaps: completedSwaps.map(s => {
const p = s.payment
const op = p ? newOp(p) : undefined
return {
operation_payment: op,
swap_operation_id: s.swap.swap_operation_id,
address_paid: s.swap.address_paid,
failure_reason: s.swap.failure_reason,
}
}),
quotes: pendingSwaps.map(s => {
const serviceFee = getServiceFee(s.invoice_amount)
return {
swap_operation_id: s.swap_operation_id,
invoice_amount_sats: s.invoice_amount,
transaction_amount_sats: s.transaction_amount,
chain_fee_sats: s.chain_fee_sats,
service_fee_sats: serviceFee,
swap_fee_sats: s.swap_fee_sats,
service_url: s.service_url,
}
})
}
}
GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote[]> => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
const swappers = Object.values(this.revSwappers)
if (swappers.length === 0) {
throw new Error("No swap services available")
}
const res = await Promise.allSettled(swappers.map(sw => this.getTxSwapQuote(sw, appUserId, amt, getServiceFee)))
const failures: string[] = []
const success: Types.TransactionSwapQuote[] = []
for (const r of res) {
if (r.status === 'fulfilled') {
success.push(r.value)
} else {
failures.push(r.reason.message ? r.reason.message : r.reason.toString())
}
}
if (success.length === 0) {
throw new Error(failures.join("\n"))
}
return success
}
private async getTxSwapQuote(swapper: ReverseSwaps, appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote> {
this.log("getting transaction swap quote")
const feesRes = await swapper.GetFees()
if (!feesRes.ok) {
throw new Error(feesRes.error)
}
const { claim, lockup } = feesRes.fees.minerFees
const minerFee = claim + lockup
const chainTotal = amt + minerFee
const res = await swapper.SwapTransaction(chainTotal)
if (!res.ok) {
throw new Error(res.error)
}
const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice)
const swapFee = decoded.numSatoshis - chainTotal
const serviceFee = getServiceFee(decoded.numSatoshis)
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
app_user_id: appUserId,
swap_quote_id: res.createdResponse.id,
swap_tree: JSON.stringify(res.createdResponse.swapTree),
lockup_address: res.createdResponse.lockupAddress,
refund_public_key: res.createdResponse.refundPublicKey,
timeout_block_height: res.createdResponse.timeoutBlockHeight,
invoice: res.createdResponse.invoice,
invoice_amount: decoded.numSatoshis,
transaction_amount: chainTotal,
swap_fee_sats: swapFee,
chain_fee_sats: minerFee,
preimage: res.preimage,
ephemeral_private_key: res.privKey,
ephemeral_public_key: res.pubkey,
service_url: swapper.getHttpUrl(),
})
return {
swap_operation_id: newSwap.swap_operation_id,
swap_fee_sats: swapFee,
invoice_amount_sats: decoded.numSatoshis,
transaction_amount_sats: amt,
chain_fee_sats: minerFee,
service_fee_sats: serviceFee,
service_url: swapper.getHttpUrl(),
}
}
async PayAddrWithSwap(appUserId: string, swapOpId: string, address: string, payInvoice: (invoice: string, amt: number) => Promise<void>) {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
this.log("paying address with swap", { appUserId, swapOpId, address })
if (!swapOpId) {
throw new Error("request a swap quote before paying an external address")
}
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(swapOpId, appUserId)
if (!txSwap) {
throw new Error("swap quote not found")
}
const info = await this.lnd.GetInfo()
if (info.blockHeight >= txSwap.timeout_block_height) {
throw new Error("swap timeout")
}
const swapper = this.revSwappers[txSwap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
const keys = this.GetKeys(txSwap.ephemeral_private_key)
const data: TransactionSwapData = {
createdResponse: {
id: txSwap.swap_quote_id,
invoice: txSwap.invoice,
lockupAddress: txSwap.lockup_address,
refundPublicKey: txSwap.refund_public_key,
swapTree: txSwap.swap_tree,
timeoutBlockHeight: txSwap.timeout_block_height,
onchainAmount: txSwap.transaction_amount,
},
info: {
destinationAddress: address,
keys,
chainFee: txSwap.chain_fee_sats,
preimage: Buffer.from(txSwap.preimage, 'hex'),
}
}
// the swap and the invoice payment are linked, swap will not start until the invoice payment is started, and will not complete once the invoice payment is completed
let swapResult = { ok: false, error: "swap never completed" } as { ok: true, txId: string } | { ok: false, error: string }
swapper.SubscribeToTransactionSwap(data, result => {
swapResult = result
})
try {
await payInvoice(txSwap.invoice, txSwap.invoice_amount)
if (!swapResult.ok) {
this.log("invoice payment successful, but swap failed")
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error)
throw new Error(swapResult.error)
}
this.log("swap completed successfully")
await this.storage.paymentStorage.FinalizeTransactionSwap(swapOpId, address, swapResult.txId)
} catch (err: any) {
if (swapResult.ok) {
this.log("failed to pay swap invoice, but swap completed successfully", swapResult.txId)
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, err.message)
} else {
this.log("failed to pay swap invoice and swap failed", swapResult.error)
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error)
}
throw err
}
const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats
return {
txId: swapResult.txId,
network_fee: networkFeesTotal
}
}
}
export class ReverseSwaps {
// settings: SettingsManager
private httpUrl: string
private wsUrl: string
log: PubLogger
private network: BTCNetwork
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
this.httpUrl = httpUrl
this.wsUrl = wsUrl
this.network = network
this.log = getLogger({ component: 'ReverseSwaps' })
initEccLib(ecc)
}
getHttpUrl = () => {
return this.httpUrl
}
getWsUrl = () => {
return this.wsUrl
}
calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => {
const pct = fees.percentage / 100
const minerFee = fees.minerFees.claim + fees.minerFees.lockup
const preFee = receiveAmount + minerFee
const fee = Math.ceil(preFee * pct)
const total = preFee + fee
return { total, fee, minerFee }
}
GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/reverse`
const feesRes = await loggedGet<TransactionSwapFeesRes>(this.log, url)
if (!feesRes.ok) {
return { ok: false, error: feesRes.error }
}
if (!feesRes.data.BTC?.BTC?.fees) {
return { ok: false, error: 'No fees found for BTC to BTC swap' }
}
return { ok: true, fees: feesRes.data.BTC.BTC.fees }
}
SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => {
const preimage = randomBytes(32);
const keys = ECPairFactory(ecc).makeRandom()
if (!keys.privateKey) {
return { ok: false, error: 'Failed to generate keys' }
}
const url = `${this.httpUrl}/v2/swap/reverse`
const req: any = {
onchainAmount: txAmount,
to: 'BTC',
from: 'BTC',
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
preimageHash: createHash('sha256').update(preimage).digest('hex'),
}
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
}
const createdResponse = createdResponseRes.data
this.log('Created transaction swap');
this.log(createdResponse);
return {
ok: true, createdResponse,
preimage: Buffer.from(preimage).toString('hex'),
pubkey: Buffer.from(keys.publicKey).toString('hex'),
privKey: Buffer.from(keys.privateKey).toString('hex')
}
}
SubscribeToTransactionSwap = async (data: TransactionSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => {
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
webSocket.on('open', () => {
webSocket.send(JSON.stringify(subReq))
})
let txId = "", isDone = false
const done = () => {
isDone = true
webSocket.close()
swapDone({ ok: true, txId })
}
webSocket.on('error', (err) => {
this.log(ERROR, 'Error in WebSocket', err.message)
})
webSocket.on('close', () => {
if (!isDone) {
this.log(ERROR, 'WebSocket closed before swap was done');
swapDone({ ok: false, error: 'WebSocket closed before swap was done' })
}
})
webSocket.on('message', async (rawMsg) => {
try {
const result = await this.handleSwapTransactionMessage(rawMsg, data, done)
if (result) {
txId = result
}
} catch (err: any) {
this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
isDone = true
webSocket.close()
swapDone({ ok: false, error: err.message })
return
}
})
}
handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, done: () => void) => {
const msg = JSON.parse(rawMsg.toString('utf-8'));
if (msg.event !== 'update') {
return;
}
this.log('Got WebSocket update');
this.log(msg);
switch (msg.args[0].status) {
// "swap.created" means Boltz is waiting for the invoice to be paid
case 'swap.created':
this.log('Waiting invoice to be paid');
return;
// "transaction.mempool" means that Boltz sent an onchain transaction
case 'transaction.mempool':
const txIdRes = await this.handleTransactionMempool(data, msg.args[0].transaction.hex)
if (!txIdRes.ok) {
throw new Error(txIdRes.error)
}
return txIdRes.txId
case 'invoice.settled':
this.log('Transaction swap successful');
done()
return;
}
}
handleTransactionMempool = async (data: TransactionSwapData, txHex: string): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
this.log('Creating claim transaction');
const { createdResponse, info } = data
const { destinationAddress, keys, preimage, chainFee } = info
const boltzPublicKey = Buffer.from(
createdResponse.refundPublicKey,
'hex',
);
// Create a musig signing session and tweak it with the Taptree of the swap scripts
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
boltzPublicKey,
Buffer.from(keys.publicKey),
]);
const tweakedKey = TaprootUtils.tweakMusig(
musig,
// swap tree can either be a string or an object
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
);
// Parse the lockup transaction and find the output relevant for the swap
const lockupTx = Transaction.fromHex(txHex);
const swapOutput = detectSwap(tweakedKey, lockupTx);
if (swapOutput === undefined) {
this.log(ERROR, 'No swap output found in lockup transaction');
return { ok: false, error: 'No swap output found in lockup transaction' }
}
const network = getNetwork(this.network)
// Create a claim transaction to be signed cooperatively via a key path spend
const claimTx = constructClaimTransaction(
[
{
...swapOutput,
keys,
preimage,
cooperative: true,
type: OutputType.Taproot,
txHash: lockupTx.getHash(),
},
],
address.toOutputScript(destinationAddress, network),
chainFee,
)
// Get the partial signature from Boltz
const claimUrl = `${this.httpUrl}/v2/swap/reverse/${createdResponse.id}/claim`
const claimReq = {
index: 0,
transaction: claimTx.toHex(),
preimage: preimage.toString('hex'),
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
}
const boltzSigRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!boltzSigRes.ok) {
return boltzSigRes
}
const boltzSig = boltzSigRes.data
// Aggregate the nonces
musig.aggregateNonces([
[boltzPublicKey, Buffer.from(boltzSig.pubNonce, 'hex')],
]);
// Initialize the session to sign the claim transaction
musig.initializeSession(
claimTx.hashForWitnessV1(
0,
[swapOutput.script],
[swapOutput.value],
Transaction.SIGHASH_DEFAULT,
),
);
// Add the partial signature from Boltz
musig.addPartial(
boltzPublicKey,
Buffer.from(boltzSig.partialSignature, 'hex'),
);
// Create our partial signature
musig.signPartial();
// Witness of the input to the aggregated signature
claimTx.ins[0].witness = [musig.aggregatePartials()];
// Broadcast the finalized transaction
const broadcastUrl = `${this.httpUrl}/v2/chain/BTC/transaction`
const broadcastReq = {
hex: claimTx.toHex(),
}
const broadcastResponse = await loggedPost<any>(this.log, broadcastUrl, broadcastReq)
if (!broadcastResponse.ok) {
return broadcastResponse
}
this.log('Transaction broadcasted', broadcastResponse.data)
const txId = claimTx.getId()
return { ok: true, txId }
}
}
const loggedPost = async <T>(log: PubLogger, url: string, req: any): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try {
const { data } = await axios.post(url, req)
return { ok: true, data: data as T }
} catch (err: any) {
if (err.response?.data) {
log(ERROR, 'Error sending request', err.response.data)
return { ok: false, error: JSON.stringify(err.response.data) }
}
log(ERROR, 'Error sending request', err.message)
return { ok: false, error: err.message }
}
}
const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try {
const { data } = await axios.get(url)
return { ok: true, data: data as T }
} catch (err: any) {
if (err.response?.data) {
log(ERROR, 'Error getting request', err.response.data)
return { ok: false, error: err.response.data }
}
log(ERROR, 'Error getting request', err.message)
return { ok: false, error: err.message }
}
}
const getNetwork = (network: BTCNetwork): Network => {
switch (network) {
case 'mainnet':
return Networks.bitcoinMainnet
case 'testnet':
return Networks.bitcoinTestnet
case 'regtest':
return Networks.bitcoinRegtest
default:
throw new Error(`Invalid network: ${network}`)
}
}
// Submarine swaps currently not supported, keeping the code for future reference
/*
export class SubmarineSwaps {
settings: SettingsManager
log: PubLogger
constructor(settings: SettingsManager) {
this.settings = settings
this.log = getLogger({ component: 'SubmarineSwaps' })
}
SwapInvoice = async (invoice: string, paymentHash: string) => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
this.log(ERROR, 'Swaps are not enabled');
return;
}
const keys = ECPairFactory(ecc).makeRandom()
const refundPublicKey = Buffer.from(keys.publicKey).toString('hex')
const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey }
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/submarine`
this.log('Sending invoice swap request to', url);
const createdResponseRes = await loggedPost<InvoiceSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
}
const createdResponse = createdResponseRes.data
this.log('Created invoice swap');
this.log(createdResponse);
const webSocket = new ws(`${this.settings.getSettings().swapsSettings.boltzWebSocketUrl}/v2/ws`)
const subReq = { op: 'subscribe', channel: 'swap.update', args: [createdResponse.id] }
webSocket.on('open', () => {
webSocket.send(JSON.stringify(subReq))
})
webSocket.on('message', async (rawMsg) => {
try {
await this.handleSwapInvoiceMessage(rawMsg, { createdResponse, info: { paymentHash, keys } }, () => webSocket.close())
} catch (err: any) {
this.log(ERROR, 'Error handling invoice WebSocket message', err.message)
webSocket.close()
return
}
});
}
handleSwapInvoiceMessage = async (rawMsg: ws.RawData, data: InvoiceSwapData, closeWebSocket: () => void) => {
const msg = JSON.parse(rawMsg.toString('utf-8'));
if (msg.event !== 'update') {
return;
}
this.log('Got invoice WebSocket update');
this.log(msg);
switch (msg.args[0].status) {
// "invoice.set" means Boltz is waiting for an onchain transaction to be sent
case 'invoice.set':
this.log('Waiting for onchain transaction');
return;
// Create a partial signature to allow Boltz to do a key path spend to claim the mainchain coins
case 'transaction.claim.pending':
await this.handleInvoiceClaimPending(data)
return;
case 'transaction.claimed':
this.log('Invoice swap successful');
closeWebSocket()
return;
}
}
handleInvoiceClaimPending = async (data: InvoiceSwapData) => {
this.log('Creating cooperative claim transaction');
const { createdResponse, info } = data
const { paymentHash, keys } = info
const { boltzHttpUrl } = this.settings.getSettings().swapsSettings
// Get the information request to create a partial signature
const url = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
const claimTxDetailsRes = await loggedGet<{ preimage: string, transactionHash: string, pubNonce: string }>(this.log, url)
if (!claimTxDetailsRes.ok) {
return claimTxDetailsRes
}
const claimTxDetails = claimTxDetailsRes.data
// Verify that Boltz actually paid the invoice by comparing the preimage hash
// of the invoice to the SHA256 hash of the preimage from the response
const claimTxPreimageHash = createHash('sha256').update(Buffer.from(claimTxDetails.preimage, 'hex')).digest()
const invoicePreimageHash = Buffer.from(paymentHash, 'hex')
if (!claimTxPreimageHash.equals(invoicePreimageHash)) {
this.log(ERROR, 'Boltz provided invalid preimage');
return;
}
const boltzPublicKey = Buffer.from(createdResponse.claimPublicKey, 'hex')
// Create a musig signing instance
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
boltzPublicKey,
Buffer.from(keys.publicKey),
]);
// Tweak that musig with the Taptree of the swap scripts
TaprootUtils.tweakMusig(
musig,
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
);
// Aggregate the nonces
musig.aggregateNonces([
[boltzPublicKey, Buffer.from(claimTxDetails.pubNonce, 'hex')],
]);
// Initialize the session to sign the transaction hash from the response
musig.initializeSession(
Buffer.from(claimTxDetails.transactionHash, 'hex'),
);
// Give our public nonce and the partial signature to Boltz
const claimUrl = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
const claimReq = {
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
partialSignature: Buffer.from(musig.signPartial()).toString('hex'),
}
const claimResponseRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!claimResponseRes.ok) {
return claimResponseRes
}
const claimResponse = claimResponseRes.data
this.log('Claim response', claimResponse)
}
}
*/

View file

@ -5,7 +5,9 @@ import Storage from "../storage/index.js";
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import LND from "../lnd/lnd.js"; import LND from "../lnd/lnd.js";
import SettingsManager from "./settingsManager.js"; import SettingsManager from "./settingsManager.js";
import { Swaps } from "../lnd/swaps.js";
export class AdminManager { export class AdminManager {
settings: SettingsManager
storage: Storage storage: Storage
log = getLogger({ component: "adminManager" }) log = getLogger({ component: "adminManager" })
adminNpub = "" adminNpub = ""
@ -17,10 +19,13 @@ export class AdminManager {
interval: NodeJS.Timer interval: NodeJS.Timer
appNprofile: string appNprofile: string
lnd: LND lnd: LND
swaps: Swaps
nostrConnected: boolean = false nostrConnected: boolean = false
private nostrReset: () => Promise<void> = async () => { this.log("nostr reset not initialized yet") } private nostrReset: () => Promise<void> = async () => { this.log("nostr reset not initialized yet") }
constructor(settings: SettingsManager, storage: Storage) { constructor(settings: SettingsManager, storage: Storage, swaps: Swaps) {
this.settings = settings
this.storage = storage this.storage = storage
this.swaps = swaps
this.dataDir = settings.getStorageSettings().dataDir this.dataDir = settings.getStorageSettings().dataDir
this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub') this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub')
this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll') this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll')
@ -45,6 +50,7 @@ export class AdminManager {
setLND = (lnd: LND) => { setLND = (lnd: LND) => {
this.lnd = lnd this.lnd = lnd
this.swaps.SetLnd(lnd)
} }
setNostrConnected = (connected: boolean) => { setNostrConnected = (connected: boolean) => {
@ -253,6 +259,29 @@ export class AdminManager {
closing_txid: Buffer.from(res.txid).toString('hex') closing_txid: Buffer.from(res.txid).toString('hex')
} }
} }
async ListAdminSwaps(): Promise<Types.SwapsList> {
return this.swaps.ListSwaps("admin", [], p => undefined, amt => 0)
}
async GetAdminTransactionSwapQuotes(req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuoteList> {
const quotes = await this.swaps.GetTxSwapQuotes("admin", req.transaction_amount_sats, () => 0)
return { quotes }
}
async PayAdminTransactionSwap(req: Types.PayAdminTransactionSwapRequest): Promise<Types.AdminSwapResponse> {
const routingFloor = this.settings.getSettings().lndSettings.routingFeeFloor
const routingLimit = this.settings.getSettings().lndSettings.routingFeeLimitBps / 10000
const swap = await this.swaps.PayAddrWithSwap("admin", req.swap_operation_id, req.address, async (invoice, amt) => {
const r = Math.max(Math.ceil(routingLimit * amt), routingFloor)
const payment = await this.lnd.PayInvoice(invoice, 0, { routingFeeLimit: r, serviceFee: 0 }, amt, { useProvider: false, from: 'system' })
await this.storage.metricsStorage.AddRootOperation("invoice_payment", invoice, amt + payment.feeSat)
})
return {
tx_id: swap.txId,
network_fee: swap.network_fee,
}
}
} }
const getDataPath = (dataDir: string, dataPath: string) => { const getDataPath = (dataDir: string, dataPath: string) => {

View file

@ -69,14 +69,15 @@ export default class {
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
} }
const nostrSettings = this.settings.getSettings().nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
const { max, serviceFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats)
return { return {
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: max,
user_identifier: appUser.identifier, user_identifier: appUser.identifier,
network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, network_max_fee_bps: 0,
network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, network_max_fee_fixed: serviceFeeFloor,
service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, service_fee_bps: serviceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }),
@ -104,14 +105,9 @@ export default class {
return this.applicationManager.PayAppUserInvoice(ctx.app_id, { return this.applicationManager.PayAppUserInvoice(ctx.app_id, {
amount: req.amount, amount: req.amount,
invoice: req.invoice, invoice: req.invoice,
user_identifier: ctx.app_user_id user_identifier: ctx.app_user_id,
}) debit_npub: req.debit_npub,
} expected_fees: req.expected_fees,
async PayAddress(ctx: Types.UserContext, req: Types.PayInvoiceRequest): Promise<Types.PayInvoiceResponse> {
return this.applicationManager.PayAppUserInvoice(ctx.app_id, {
amount: req.amount,
invoice: req.invoice,
user_identifier: ctx.app_user_id
}) })
} }

View file

@ -17,7 +17,6 @@ type NsecLinkingData = {
expiry: number expiry: number
} }
export default class { export default class {
storage: Storage storage: Storage
settings: SettingsManager settings: SettingsManager
paymentManager: PaymentManager paymentManager: PaymentManager
@ -47,12 +46,13 @@ export default class {
}, 60 * 1000); // 1 minute }, 60 * 1000); // 1 minute
} }
async StartAppsServiceBeacon(publishBeacon: (app: Application) => void) { async StartAppsServiceBeacon(publishBeacon: (app: Application, fees: Types.CumulativeFees) => void) {
this.serviceBeaconInterval = setInterval(async () => { this.serviceBeaconInterval = setInterval(async () => {
try { try {
const fees = this.paymentManager.GetFees()
const apps = await this.storage.applicationStorage.GetApplications() const apps = await this.storage.applicationStorage.GetApplications()
apps.forEach(app => { apps.forEach(app => {
publishBeacon(app) publishBeacon(app, fees)
}) })
} catch (e) { } catch (e) {
this.log("error in beacon", (e as any).message) this.log("error in beacon", (e as any).message)
@ -154,17 +154,17 @@ export default class {
const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }) const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] })
log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString }) log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString })
const { max, serviceFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats)
return { return {
identifier: u.identifier, identifier: u.identifier,
info: { info: {
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: max,
user_identifier: u.identifier, user_identifier: u.identifier,
network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, network_max_fee_bps: 0,
network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, network_max_fee_fixed: serviceFeeFloor,
service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, service_fee_bps: serviceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
@ -172,7 +172,7 @@ export default class {
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
}, },
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) max_withdrawable: max
} }
} }
@ -213,16 +213,16 @@ export default class {
async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise<Types.AppUser> { async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise<Types.AppUser> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true)
const nostrSettings = this.settings.getSettings().nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
const { max, serviceFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats)
return { return {
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: max,
user_identifier: user.identifier, user_identifier: user.identifier,
network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, network_max_fee_bps: 0,
network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, network_max_fee_fixed: serviceFeeFloor,
service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, service_fee_bps: serviceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
@ -235,9 +235,31 @@ export default class {
async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise<Types.PayInvoiceResponse> { async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise<Types.PayInvoiceResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app) try {
getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats") const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app, {
return paid ack: pendingOp => { this.notifyAppUserPayment(appUser, pendingOp) }
})
this.notifyAppUserPayment(appUser, paid.operation)
getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats")
return paid
} catch (e) {
const failedOp: Types.UserOperation = {
type: Types.UserOperationType.OUTGOING_INVOICE,
paidAtUnix: -1, amount: 0, confirmed: false, identifier: req.invoice, operationId: "",
inbound: false, internal: false, network_fee: 0, service_fee: 0, tx_hash: "",
}
this.notifyAppUserPayment(appUser, failedOp)
throw e
}
}
notifyAppUserPayment = (appUser: ApplicationUser, op: Types.UserOperation) => {
const balance = appUser.user.balance_sats
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } =
{ operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance }
if (appUser.nostr_public_key) { // TODO - fix before support for http streams
this.storage.NostrSender().Send({ type: 'app', appId: appUser.application.app_id }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key })
}
} }
async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise<void> { async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise<void> {

View file

@ -6,20 +6,16 @@ import { ERROR, getLogger } from "../helpers/logger.js";
import { DebitAccess, DebitAccessRules } from '../storage/entity/DebitAccess.js'; import { DebitAccess, DebitAccessRules } from '../storage/entity/DebitAccess.js';
import { Application } from '../storage/entity/Application.js'; import { Application } from '../storage/entity/Application.js';
import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { ApplicationUser } from '../storage/entity/ApplicationUser.js';
import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; import { NostrEvent } from '../nostr/nostrPool.js';
import { UnsignedEvent } from 'nostr-tools';
import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk";
import { debitAccessRulesToDebitRules, newNdebitResponse,debitRulesToDebitAccessRules, import {
nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, debitAccessRulesToDebitRules, newNdebitResponse, debitRulesToDebitAccessRules,
frequencyRuleName,IntervalTypeToSeconds,unitToIntervalType } from "./debitTypes.js"; nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName,
frequencyRuleName, IntervalTypeToSeconds, unitToIntervalType
} from "./debitTypes.js";
export class DebitManager { export class DebitManager {
_nostrSend: NostrSend | null = null
applicationManager: ApplicationManager applicationManager: ApplicationManager
storage: Storage storage: Storage
lnd: LND lnd: LND
logger = getLogger({ component: 'DebitManager' }) logger = getLogger({ component: 'DebitManager' })
@ -29,16 +25,6 @@ export class DebitManager {
this.applicationManager = applicationManager this.applicationManager = applicationManager
} }
attachNostrSend = (nostrSend: NostrSend) => {
this._nostrSend = nostrSend
}
nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => {
if (!this._nostrSend) {
throw new Error("No nostrSend attached")
}
this._nostrSend(initiator, data, relays)
}
GetDebitAuthorizations = async (ctx: Types.UserContext): Promise<Types.DebitAuthorizations> => { GetDebitAuthorizations = async (ctx: Types.UserContext): Promise<Types.DebitAuthorizations> => {
const allDebitsAccesses = await this.storage.debitStorage.GetAllUserDebitAccess(ctx.app_user_id) const allDebitsAccesses = await this.storage.debitStorage.GetAllUserDebitAccess(ctx.app_user_id)
const debits: Types.DebitAuthorization[] = allDebitsAccesses.map(access => ({ const debits: Types.DebitAuthorization[] = allDebitsAccesses.map(access => ({
@ -72,7 +58,7 @@ export class DebitManager {
this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: req.npub, id: req.request_id, appId: ctx.app_id }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: req.npub, id: req.request_id, appId: ctx.app_id })
return return
case Types.DebitResponse_response_type.INVOICE: case Types.DebitResponse_response_type.INVOICE:
await this.paySingleInvoice(ctx, {invoice: req.response.invoice, npub: req.npub, request_id: req.request_id}) await this.paySingleInvoice(ctx, { invoice: req.response.invoice, npub: req.npub, request_id: req.request_id })
return return
case Types.DebitResponse_response_type.AUTHORIZE: case Types.DebitResponse_response_type.AUTHORIZE:
await this.handleAuthorization(ctx, req.response.authorize, { npub: req.npub, request_id: req.request_id }) await this.handleAuthorization(ctx, req.response.authorize, { npub: req.npub, request_id: req.request_id })
@ -82,14 +68,12 @@ export class DebitManager {
} }
} }
paySingleInvoice = async (ctx: Types.UserContext, {invoice,npub,request_id}:{invoice:string, npub:string, request_id:string}) => { paySingleInvoice = async (ctx: Types.UserContext, { invoice, npub, request_id }: { invoice: string, npub: string, request_id: string }) => {
try { try {
this.logger("🔍 [DEBIT REQUEST] Paying single invoice") this.logger("🔍 [DEBIT REQUEST] Paying single invoice")
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice)
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id)
const { op, payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice)
const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage }
this.notifyPaymentSuccess(appUser, debitRes, op, { appId: ctx.app_id, pub: npub, id: request_id }) this.notifyPaymentSuccess(debitRes, { appId: ctx.app_id, pub: npub, id: request_id })
} catch (e: any) { } catch (e: any) {
this.logger("❌ [DEBIT REQUEST] Error in single invoice payment") this.logger("❌ [DEBIT REQUEST] Error in single invoice payment")
this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id })
@ -97,7 +81,7 @@ export class DebitManager {
} }
} }
handleAuthorization = async (ctx: Types.UserContext,debit:Types.DebitToAuthorize, {npub,request_id}:{ npub:string, request_id:string})=>{ handleAuthorization = async (ctx: Types.UserContext, debit: Types.DebitToAuthorize, { npub, request_id }: { npub: string, request_id: string }) => {
this.logger("🔍 [DEBIT REQUEST] Handling authorization", { this.logger("🔍 [DEBIT REQUEST] Handling authorization", {
npub, npub,
request_id, request_id,
@ -122,20 +106,20 @@ export class DebitManager {
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id)
this.validateAccessRules(access, app, appUser) this.validateAccessRules(access, app, appUser)
this.logger("🔍 [DEBIT REQUEST] Sending debit payment") this.logger("🔍 [DEBIT REQUEST] Sending debit payment")
const { op, payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice)
const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage }
this.notifyPaymentSuccess(appUser, debitRes, op, { appId: ctx.app_id, pub: npub, id: request_id }) this.notifyPaymentSuccess(debitRes, { appId: ctx.app_id, pub: npub, id: request_id })
} catch (e: any) { } catch (e: any) {
this.logger("❌ [DEBIT REQUEST] Error in debit authorization") this.logger("❌ [DEBIT REQUEST] Error in debit authorization")
this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id })
throw e throw e
} }
} }
handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => { handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => {
if (!this._nostrSend) { if (!this.storage.NostrSender().IsReady()) {
throw new Error("No nostrSend attached") throw new Error("Nostr sender not ready")
} }
this.logger("📥 [DEBIT REQUEST] Received debit request", { this.logger("📥 [DEBIT REQUEST] Received debit request", {
fromPub: event.pub, fromPub: event.pub,
@ -144,10 +128,10 @@ export class DebitManager {
pointerdata pointerdata
}) })
const res = await this.payNdebitInvoice(event, pointerdata) const res = await this.payNdebitInvoice(event, pointerdata)
this.logger("🔍 [DEBIT REQUEST] Sending ",res.status," response") this.logger("🔍 [DEBIT REQUEST] Sending ", res.status, " response")
if (res.status === 'fail' || res.status === 'authOk') { if (res.status === 'fail' || res.status === 'authOk') {
const e = newNdebitResponse(JSON.stringify(res.debitRes), event) const e = newNdebitResponse(JSON.stringify(res.debitRes), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
return return
} }
const { appUser } = res const { appUser } = res
@ -155,30 +139,27 @@ export class DebitManager {
this.handleAuthRequired(pointerdata, event, res) this.handleAuthRequired(pointerdata, event, res)
return return
} }
const { op, debitRes } = res const { debitRes } = res
this.notifyPaymentSuccess(appUser, debitRes, op, event) this.notifyPaymentSuccess(debitRes, event)
} }
handleAuthRequired = (data:NdebitData, event: NostrEvent, res: AuthRequiredRes) => { handleAuthRequired = (data: NdebitData, event: NostrEvent, res: AuthRequiredRes) => {
if (!res.appUser.nostr_public_key) { if (!res.appUser.nostr_public_key) {
this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId })
return return
} }
const message: Types.LiveDebitRequest & { requestId: string, status: 'OK' } = { ...res.liveDebitReq, requestId: "GetLiveDebitRequests", status: 'OK' } const message: Types.LiveDebitRequest & { requestId: string, status: 'OK' } = { ...res.liveDebitReq, requestId: "GetLiveDebitRequests", status: 'OK' }
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: res.appUser.nostr_public_key }) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: res.appUser.nostr_public_key })
} }
notifyPaymentSuccess = (appUser: ApplicationUser, debitRes: NdebitSuccess, op: Types.UserOperation, event: { pub: string, id: string, appId: string }) => { notifyPaymentSuccess = (debitRes: NdebitSuccess, event: { pub: string, id: string, appId: string }) => {
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' }
if (appUser.nostr_public_key) { // TODO - fix before support for http streams
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key })
}
this.sendDebitResponse(debitRes, event) this.sendDebitResponse(debitRes, event)
} }
sendDebitResponse = (debitRes: NdebitFailure | NdebitSuccess, event: { pub: string, id: string, appId: string }) => { sendDebitResponse = (debitRes: NdebitFailure | NdebitSuccess, event: { pub: string, id: string, appId: string }) => {
const e = newNdebitResponse(JSON.stringify(debitRes), event) const e = newNdebitResponse(JSON.stringify(debitRes), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
} }
payNdebitInvoice = async (event: NostrEvent, pointerdata: NdebitData): Promise<HandleNdebitRes> => { payNdebitInvoice = async (event: NostrEvent, pointerdata: NdebitData): Promise<HandleNdebitRes> => {
@ -286,15 +267,14 @@ export class DebitManager {
} }
await this.validateAccessRules(authorization, app, appUser) await this.validateAccessRules(authorization, app, appUser)
this.logger("🔍 [DEBIT REQUEST] Sending requested debit payment") this.logger("🔍 [DEBIT REQUEST] Sending requested debit payment")
const { op, payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) const { payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11)
return { status: 'invoicePaid', op, app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } return { status: 'invoicePaid', app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } }
} }
sendDebitPayment = async (appId: string, appUserId: string, requestorPub: string, bolt11: string) => { sendDebitPayment = async (appId: string, appUserId: string, requestorPub: string, bolt11: string) => {
const payment = await this.applicationManager.PayAppUserInvoice(appId, { amount: 0, invoice: bolt11, user_identifier: appUserId, debit_npub: requestorPub }) const payment = await this.applicationManager.PayAppUserInvoice(appId, { amount: 0, invoice: bolt11, user_identifier: appUserId, debit_npub: requestorPub })
await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee + payment.network_fee) await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee)
const op = this.newPaymentOperation(payment, bolt11) return { payment }
return { payment, op }
} }
validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise<boolean> => { validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise<boolean> => {
@ -325,21 +305,5 @@ export class DebitManager {
} }
return true return true
} }
newPaymentOperation = (payment: Types.PayInvoiceResponse, bolt11: string) => {
return {
amount: payment.amount_paid,
paidAtUnix: Math.floor(Date.now() / 1000),
inbound: false,
type: Types.UserOperationType.OUTGOING_INVOICE,
identifier: bolt11,
operationId: payment.operation_id,
network_fee: payment.network_fee,
service_fee: payment.service_fee,
confirmed: true,
tx_hash: "",
internal: payment.network_fee === 0
}
}
} }

View file

@ -1,9 +1,9 @@
import * as Types from "../../../proto/autogenerated/ts/types.js"; import * as Types from "../../../proto/autogenerated/ts/types.js";
import { DebitAccessRules } from '../storage/entity/DebitAccess.js'; import { DebitAccessRules } from '../storage/entity/DebitAccess.js';
import { Application } from '../storage/entity/Application.js'; import { Application } from '../storage/entity/Application.js';
import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { ApplicationUser } from '../storage/entity/ApplicationUser.js';
import { UnsignedEvent } from 'nostr-tools'; import { UnsignedEvent } from 'nostr-tools';
import { NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; import { NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk";
export const expirationRuleName = 'expiration' export const expirationRuleName = 'expiration'
export const frequencyRuleName = 'frequency' export const frequencyRuleName = 'frequency'
@ -96,7 +96,7 @@ export const nofferErrors = {
} }
export type AuthRequiredRes = { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, app: Application, appUser: ApplicationUser } export type AuthRequiredRes = { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, app: Application, appUser: ApplicationUser }
export type HandleNdebitRes = { status: 'fail', debitRes: NdebitFailure } export type HandleNdebitRes = { status: 'fail', debitRes: NdebitFailure }
| { status: 'invoicePaid', op: Types.UserOperation, app: Application, appUser: ApplicationUser, debitRes: NdebitSuccess } | { status: 'invoicePaid', app: Application, appUser: ApplicationUser, debitRes: NdebitSuccess }
| AuthRequiredRes | AuthRequiredRes
| { status: 'authOk', debitRes: NdebitSuccess } | { status: 'authOk', debitRes: NdebitSuccess }

View file

@ -12,7 +12,7 @@ import AppUserManager from "./appUserManager.js"
import { Application } from '../storage/entity/Application.js' import { Application } from '../storage/entity/Application.js'
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js' import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js'
import { UnsignedEvent } from 'nostr-tools' import { UnsignedEvent } from 'nostr-tools'
import { NostrSend } from '../nostr/handler.js' import { NostrSend } from '../nostr/nostrPool.js'
import MetricsManager from '../metrics/index.js' import MetricsManager from '../metrics/index.js'
import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityProvider } from "./liquidityProvider.js"
import { LiquidityManager } from "./liquidityManager.js" import { LiquidityManager } from "./liquidityManager.js"
@ -30,7 +30,7 @@ import { Agent } from "https"
import { NotificationsManager } from "./notificationsManager.js" import { NotificationsManager } from "./notificationsManager.js"
import { ApplicationUser } from '../storage/entity/ApplicationUser.js' import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { NostrSettings } from '../nostr/handler.js' import { NostrSettings, AppInfo } from '../nostr/nostrPool.js'
type UserOperationsSub = { type UserOperationsSub = {
id: string id: string
newIncomingInvoice: (operation: Types.UserOperation) => void newIncomingInvoice: (operation: Types.UserOperation) => void
@ -61,8 +61,6 @@ export default class {
rugPullTracker: RugPullTracker rugPullTracker: RugPullTracker
unlocker: Unlocker unlocker: Unlocker
notificationsManager: NotificationsManager notificationsManager: NotificationsManager
//webRTC: webRTC
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
nostrProcessPing: (() => Promise<void>) | null = null nostrProcessPing: (() => Promise<void>) | null = null
nostrReset: (settings: NostrSettings) => void = () => { getLogger({})("nostr reset not initialized yet") } nostrReset: (settings: NostrSettings) => void = () => { getLogger({})("nostr reset not initialized yet") }
constructor(settings: SettingsManager, storage: Storage, adminManager: AdminManager, utils: Utils, unlocker: Unlocker) { constructor(settings: SettingsManager, storage: Storage, adminManager: AdminManager, utils: Utils, unlocker: Unlocker) {
@ -82,7 +80,7 @@ export default class {
this.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker) this.liquidityManager = new LiquidityManager(this.settings, 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.utils, this.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.metricsManager, this.lnd, adminManager.swaps, 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)
@ -102,19 +100,13 @@ export default class {
} }
StartBeacons() { StartBeacons() {
this.applicationManager.StartAppsServiceBeacon(app => { this.applicationManager.StartAppsServiceBeacon((app, fees) => {
this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url }) this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, fees })
}) })
} }
attachNostrSend(f: NostrSend) { attachNostrSend(f: NostrSend) {
this.nostrSend = f this.utils.nostrSender.AttachNostrSend(f)
this.liquidityProvider.attachNostrSend(f)
this.debitManager.attachNostrSend(f)
this.offerManager.attachNostrSend(f)
this.managementManager.attachNostrSend(f)
this.utils.attachNostrSend(f)
//this.webRTC.attachNostrSend(f)
} }
attachNostrProcessPing(f: () => Promise<void>) { attachNostrProcessPing(f: () => Promise<void>) {
@ -168,7 +160,8 @@ export default class {
NewBlockHandler = async (height: number) => { NewBlockHandler = async (height: number) => {
let confirmed: (PendingTx & { confs: number; })[] let confirmed: (PendingTx & { confs: number; })[]
let log = getLogger({}) let log = getLogger({})
this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height)
.catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err))
try { try {
const balanceEvents = await this.paymentManager.GetLndBalance() const balanceEvents = await this.paymentManager.GetLndBalance()
await this.metricsManager.NewBlockCb(height, balanceEvents) await this.metricsManager.NewBlockCb(height, balanceEvents)
@ -220,6 +213,10 @@ export default class {
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) { if (!userAddress) {
const isChange = await this.lnd.IsChangeAddress(address)
if (isChange) {
return
}
await this.metricsManager.AddRootAddressPaid(address, txOutput, amount) await this.metricsManager.AddRootAddressPaid(address, txOutput, amount)
return return
} }
@ -230,11 +227,8 @@ export default class {
return return
} }
log = getLogger({ appName: userAddress.linkedApplication.name }) log = getLogger({ appName: userAddress.linkedApplication.name })
const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id const isManagedUser = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id
let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment) const fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_TX, amount, isManagedUser)
if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) {
fee = 0
}
try { try {
// This call will fail if the transaction is already registered // This call will fail if the transaction is already registered
const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, tx) const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, tx)
@ -274,11 +268,8 @@ export default class {
return return
} }
log = getLogger({ appName: userInvoice.linkedApplication.name }) log = getLogger({ appName: userInvoice.linkedApplication.name })
const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id const isManagedUser = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id
let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment) const fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isManagedUser)
if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) {
fee = 0
}
try { try {
await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, internal, tx) await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, internal, tx)
this.storage.eventsLog.LogEvent({ type: 'invoice_paid', userId: userInvoice.user.user_id, appId: userInvoice.linkedApplication.app_id, appUserId: "", balance: userInvoice.user.balance_sats, data: paymentRequest, amount }) this.storage.eventsLog.LogEvent({ type: 'invoice_paid', userId: userInvoice.user.user_id, appId: userInvoice.linkedApplication.app_id, appUserId: "", balance: userInvoice.user.balance_sats, data: paymentRequest, amount })
@ -385,10 +376,11 @@ export default class {
getLogger({ appName: app.name })("cannot notify user, not a nostr user") getLogger({ appName: app.name })("cannot notify user, not a nostr user")
return return
} }
const balance = user.user.balance_sats
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } =
{ operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance }
const j = JSON.stringify(message) const j = JSON.stringify(message)
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key }) this.utils.nostrSender.Send({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key })
this.SendEncryptedNotification(app, user, op) this.SendEncryptedNotification(app, user, op)
} }
@ -408,7 +400,7 @@ export default class {
}) })
} }
async UpdateBeacon(app: Application, content: { type: 'service', name: string, avatarUrl?: string, nextRelay?: string }) { async UpdateBeacon(app: Application, content: Types.BeaconData) {
if (!app.nostr_public_key) { if (!app.nostr_public_key) {
getLogger({ appName: app.name })("cannot update beacon, public key not set") getLogger({ appName: app.name })("cannot update beacon, public key not set")
return return
@ -421,7 +413,7 @@ export default class {
pubkey: app.nostr_public_key, pubkey: app.nostr_public_key,
tags, tags,
} }
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'event', event }) this.utils.nostrSender.Send({ type: 'app', appId: app.app_id }, { type: 'event', event })
} }
async createZapReceipt(log: PubLogger, invoice: UserReceivingInvoice) { async createZapReceipt(log: PubLogger, invoice: UserReceivingInvoice) {
@ -441,7 +433,7 @@ export default class {
tags, tags,
} }
log({ unsigned: event }) log({ unsigned: event })
this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }, zapInfo.relays || undefined) this.utils.nostrSender.Send({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }, zapInfo.relays || undefined)
} }
async createClinkReceipt(log: PubLogger, invoice: UserReceivingInvoice) { async createClinkReceipt(log: PubLogger, invoice: UserReceivingInvoice) {
@ -465,7 +457,7 @@ export default class {
["clink_version", "1"] ["clink_version", "1"]
], ],
} }
this.nostrSend( this.utils.nostrSender.Send(
{ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'app', appId: invoice.linkedApplication.app_id },
{ type: 'event', event, encrypt: { toPub: invoice.clink_requester_pub } } { type: 'event', event, encrypt: { toPub: invoice.clink_requester_pub } }
) )
@ -474,28 +466,38 @@ export default class {
async ResetNostr() { async ResetNostr() {
const apps = await this.storage.applicationStorage.GetApplications() const apps = await this.storage.applicationStorage.GetApplications()
const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0]
const fees = this.paymentManager.GetFees()
for (const app of apps) { for (const app of apps) {
await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay }) await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay, fees })
} }
const defaultNames = ['wallet', 'wallet-test', this.settings.getSettings().serviceSettings.defaultAppName] const defaultNames = ['wallet', 'wallet-test', this.settings.getSettings().serviceSettings.defaultAppName]
const liquidityProviderApp = apps.find(app => defaultNames.includes(app.name)) const local = apps.find(app => defaultNames.includes(app.name))
if (!liquidityProviderApp) { if (!local) {
throw new Error("wallet app not initialized correctly") throw new Error("local app not initialized correctly")
}
const liquidityProviderInfo = {
privateKey: liquidityProviderApp.nostr_private_key || "",
publicKey: liquidityProviderApp.nostr_public_key || "",
name: "liquidity_provider", clientId: `client_${liquidityProviderApp.app_id}`
} }
this.liquidityProvider.setNostrInfo({ localId: `client_${local.app_id}`, localPubkey: local.nostr_public_key || "" })
const relays = this.settings.getSettings().nostrRelaySettings.relays
const appsInfo: AppInfo[] = apps.map(app => {
return {
appId: app.app_id,
privateKey: app.nostr_private_key || "",
publicKey: app.nostr_public_key || "",
name: app.name,
provider: app.nostr_public_key === local.nostr_public_key ? {
clientId: `client_${local.app_id}`,
pubkey: this.settings.getSettings().liquiditySettings.liquidityProviderPub,
relayUrl: this.settings.getSettings().liquiditySettings.providerRelayUrl
} : undefined
}
})
const s: NostrSettings = { const s: NostrSettings = {
apps: apps.map(a => ({ appId: a.app_id, name: a.name, privateKey: a.nostr_private_key || "", publicKey: a.nostr_public_key || "" })), apps: appsInfo,
relays: this.settings.getSettings().nostrRelaySettings.relays, relays,
maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength, maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength,
clients: [liquidityProviderInfo] /* clients: [local],
providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub */
} }
this.nostrReset(s) this.nostrReset(s)
} }
} }

View file

@ -10,6 +10,8 @@ import { Wizard } from "../wizard/index.js"
import { AdminManager } from "./adminManager.js" import { AdminManager } from "./adminManager.js"
import SettingsManager from "./settingsManager.js" import SettingsManager from "./settingsManager.js"
import { LoadStorageSettingsFromEnv } from "../storage/index.js" import { LoadStorageSettingsFromEnv } from "../storage/index.js"
import { NostrSender } from "../nostr/sender.js"
import { Swaps } from "../lnd/swaps.js"
export type AppData = { export type AppData = {
privateKey: string; privateKey: string;
publicKey: string; publicKey: string;
@ -18,7 +20,8 @@ export type AppData = {
} }
export const initSettings = async (log: PubLogger, storageSettings: StorageSettings): Promise<SettingsManager> => { export const initSettings = async (log: PubLogger, storageSettings: StorageSettings): Promise<SettingsManager> => {
const utils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages }) const nostrSender = new NostrSender()
const utils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages }, nostrSender)
const storageManager = new Storage(storageSettings, utils) const storageManager = new Storage(storageSettings, utils)
await storageManager.Connect(log) await storageManager.Connect(log)
const settingsManager = new SettingsManager(storageManager) const settingsManager = new SettingsManager(storageManager)
@ -30,7 +33,8 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM
const utils = storageManager.utils const utils = storageManager.utils
const unlocker = new Unlocker(settingsManager, storageManager) const unlocker = new Unlocker(settingsManager, storageManager)
await unlocker.Unlock() await unlocker.Unlock()
const adminManager = new AdminManager(settingsManager, storageManager) const swaps = new Swaps(settingsManager, storageManager)
const adminManager = new AdminManager(settingsManager, storageManager, swaps)
let wizard: Wizard | null = null let wizard: Wizard | null = null
if (settingsManager.getSettings().serviceSettings.wizard) { if (settingsManager.getSettings().serviceSettings.wizard) {
wizard = new Wizard(settingsManager, storageManager, adminManager) wizard = new Wizard(settingsManager, storageManager, adminManager)
@ -61,26 +65,22 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM
return { privateKey: app.nostr_private_key, publicKey: app.nostr_public_key, appId: app.app_id, name: app.name } return { privateKey: app.nostr_private_key, publicKey: app.nostr_public_key, appId: app.app_id, name: app.name }
} }
})) }))
const liquidityProviderApp = apps.find(app => defaultNames.includes(app.name)) const localProviderClient = apps.find(app => defaultNames.includes(app.name))
if (!liquidityProviderApp) { if (!localProviderClient) {
throw new Error("wallet app not initialized correctly") throw new Error("local app not initialized correctly")
} }
const liquidityProviderInfo = { mainHandler.liquidityProvider.setNostrInfo({ localId: `client_${localProviderClient.appId}`, localPubkey: localProviderClient.publicKey })
privateKey: liquidityProviderApp.privateKey,
publicKey: liquidityProviderApp.publicKey,
name: "liquidity_provider", clientId: `client_${liquidityProviderApp.appId}`
}
mainHandler.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
const stop = await processArgs(mainHandler) const stop = await processArgs(mainHandler)
if (stop) { if (stop) {
return return
} }
await mainHandler.paymentManager.checkPendingPayments() await mainHandler.paymentManager.checkPaymentStatus()
await mainHandler.paymentManager.checkMissedChainTxs()
await mainHandler.paymentManager.CleanupOldUnpaidInvoices() await mainHandler.paymentManager.CleanupOldUnpaidInvoices()
await mainHandler.appUserManager.CleanupInactiveUsers() await mainHandler.appUserManager.CleanupInactiveUsers()
await mainHandler.appUserManager.CleanupNeverActiveUsers() await mainHandler.appUserManager.CleanupNeverActiveUsers()
await mainHandler.paymentManager.watchDog.Start() await mainHandler.paymentManager.watchDog.Start()
return { mainHandler, apps, liquidityProviderInfo, liquidityProviderApp, wizard, adminManager } return { mainHandler, apps, localProviderClient, wizard, adminManager }
} }
const processArgs = async (mainHandler: Main) => { const processArgs = async (mainHandler: Main) => {

View file

@ -50,8 +50,11 @@ export class LiquidityManager {
} }
beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => {
const providerReady = this.liquidityProvider.IsReady()
if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
if (!providerReady) {
throw new Error("cannot use liquidity provider, it is not ready")
}
return 'provider' return 'provider'
} }
@ -63,7 +66,7 @@ export class LiquidityManager {
if (remote > amount) { if (remote > amount) {
return 'lnd' return 'lnd'
} }
const providerCanHandle = await this.liquidityProvider.CanProviderHandle({ action: 'receive', amount }) const providerCanHandle = this.liquidityProvider.IsReady()
if (!providerCanHandle) { if (!providerCanHandle) {
return 'lnd' return 'lnd'
} }
@ -82,16 +85,24 @@ export class LiquidityManager {
} }
} }
beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => { beforeOutInvoicePayment = async (amount: number, localServiceFee: number): Promise<'lnd' | 'provider'> => {
const providerReady = this.liquidityProvider.IsReady()
if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
if (!providerReady) {
throw new Error("cannot use liquidity provider, it is not ready")
}
return 'provider' return 'provider'
} }
const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount }) if (!providerReady) {
if (canHandle) { return 'lnd'
return 'provider'
} }
return 'lnd' const canHandle = await this.liquidityProvider.CanProviderPay(amount, localServiceFee)
if (!canHandle) {
return 'lnd'
}
return 'provider'
} }
afterOutInvoicePaid = async () => { } afterOutInvoicePaid = async () => { }
shouldDrainProvider = async () => { shouldDrainProvider = async () => {
@ -99,7 +110,7 @@ export class LiquidityManager {
if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
return return
} }
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() const maxW = await this.liquidityProvider.GetMaxWithdrawable()
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
const drainable = Math.min(maxW, remote) const drainable = Math.min(maxW, remote)
if (drainable < 500) { if (drainable < 500) {
@ -129,7 +140,7 @@ export class LiquidityManager {
try { try {
const invoice = await this.lnd.NewInvoice(amt, "liqudity provider drain", defaultInvoiceExpiry, { from: 'system', useProvider: false }) 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 res = await this.liquidityProvider.PayInvoice(invoice.payRequest, amt, 'system')
const fees = res.network_fee + res.service_fee const fees = res.service_fee
this.feesPaid += fees this.feesPaid += fees
this.updateLatestDrain(true, amt) this.updateLatestDrain(true, amt)
} catch (err: any) { } catch (err: any) {
@ -172,7 +183,7 @@ export class LiquidityManager {
if (pendingChannels.pendingOpenChannels.length > 0) { if (pendingChannels.pendingOpenChannels.length > 0) {
return { shouldOpen: false } return { shouldOpen: false }
} }
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() const maxW = await this.liquidityProvider.GetMaxWithdrawable()
if (maxW < threshold) { if (maxW < threshold) {
return { shouldOpen: false } return { shouldOpen: false }
} }

View file

@ -1,26 +1,23 @@
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js' import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js' import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { getLogger } from '../helpers/logger.js' import { ERROR, getLogger } from '../helpers/logger.js'
import { Utils } from '../helpers/utilsWrapper.js' import { Utils } from '../helpers/utilsWrapper.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { InvoicePaidCb } from '../lnd/settings.js' import { InvoicePaidCb } from '../lnd/settings.js'
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { LiquiditySettings } from './settings.js' import { LiquiditySettings } 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 type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
export class LiquidityProvider { export class LiquidityProvider {
getSettings: () => LiquiditySettings getSettings: () => LiquiditySettings
client: ReturnType<typeof newNostrClient> client: ReturnType<typeof newNostrClient>
clientCbs: Record<string, nostrCallback<any>> = {} clientCbs: Record<string, nostrCallback<any>> = {}
clientId: string = "" localId: string = ""
myPub: string = "" localPubkey: string = ""
log = getLogger({ component: 'liquidityProvider' }) log = getLogger({ component: 'liquidityProvider' })
nostrSend: NostrSend | null = null // nostrSend: NostrSend | null = null
configured = false configured = false
pubDestination: string providerPubkey: string
ready: boolean ready: boolean
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
connecting = false connecting = false
@ -28,14 +25,18 @@ export class LiquidityProvider {
queue: ((state: 'ready') => void)[] = [] queue: ((state: 'ready') => void)[] = []
utils: Utils utils: Utils
pendingPayments: Record<string, number> = {} pendingPayments: Record<string, number> = {}
feesCache: Types.CumulativeFees | null = null
lastSeenBeacon = 0
latestReceivedBalance = 0
incrementProviderBalance: (balance: number) => Promise<void> incrementProviderBalance: (balance: number) => Promise<void>
pendingPaymentsAck: Record<string, boolean> = {}
// make the sub process accept client // make the sub process accept client
constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) { constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) {
this.utils = utils this.utils = utils
this.getSettings = getSettings this.getSettings = getSettings
const pubDestination = getSettings().liquidityProviderPub const providerPubkey = getSettings().liquidityProviderPub
const disableLiquidityProvider = getSettings().disableLiquidityProvider const disableLiquidityProvider = getSettings().disableLiquidityProvider
if (!pubDestination) { if (!providerPubkey) {
this.log("No pub provider to liquidity provider, will not be initialized") this.log("No pub provider to liquidity provider, will not be initialized")
return return
} }
@ -43,19 +44,29 @@ export class LiquidityProvider {
this.log("Liquidity provider is disabled, will not be initialized") this.log("Liquidity provider is disabled, will not be initialized")
return return
} }
this.log("connecting to liquidity provider:", pubDestination) this.log("connecting to liquidity provider:", providerPubkey)
this.pubDestination = pubDestination this.providerPubkey = providerPubkey
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
this.incrementProviderBalance = incrementProviderBalance this.incrementProviderBalance = incrementProviderBalance
this.client = newNostrClient({ this.client = newNostrClient({
pubDestination: this.pubDestination, pubDestination: this.providerPubkey,
retrieveNostrUserAuth: async () => this.myPub, retrieveNostrUserAuth: async () => this.localPubkey,
retrieveNostrAdminAuth: async () => this.myPub, retrieveNostrAdminAuth: async () => this.localPubkey,
retrieveNostrMetricsAuth: async () => this.myPub, retrieveNostrMetricsAuth: async () => this.localPubkey,
retrieveNostrGuestWithPubAuth: async () => this.myPub retrieveNostrGuestWithPubAuth: async () => this.localPubkey
}, this.clientSend, this.clientSub) }, this.clientSend, this.clientSub)
this.utils.nostrSender.OnReady(() => {
this.setSetIfConfigured()
if (this.configured) {
clearInterval(this.configuredInterval)
this.Connect()
}
})
this.configuredInterval = setInterval(() => { this.configuredInterval = setInterval(() => {
if (!this.configured && this.utils.nostrSender.IsReady()) {
this.setSetIfConfigured()
}
if (this.configured) { if (this.configured) {
clearInterval(this.configuredInterval) clearInterval(this.configuredInterval)
this.Connect() this.Connect()
@ -63,16 +74,17 @@ export class LiquidityProvider {
}, 1000) }, 1000)
} }
GetProviderDestination() { GetProviderPubkey() {
return this.pubDestination return this.providerPubkey
} }
IsReady = () => { IsReady = () => {
return this.ready && !this.getSettings().disableLiquidityProvider const seenInPast2Minutes = Date.now() - this.lastSeenBeacon < 1000 * 60 * 2
return this.ready && !this.getSettings().disableLiquidityProvider && seenInPast2Minutes
} }
AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => {
if (!this.pubDestination || this.getSettings().disableLiquidityProvider) { if (!this.providerPubkey || this.getSettings().disableLiquidityProvider) {
return 'inactive' return 'inactive'
} }
if (this.IsReady()) { if (this.IsReady()) {
@ -94,6 +106,7 @@ export class LiquidityProvider {
return return
} }
this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0) this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0)
this.lastSeenBeacon = Date.now()
this.ready = true this.ready = true
this.queue.forEach(q => q('ready')) this.queue.forEach(q => q('ready'))
this.log("subbing to user operations") this.log("subbing to user operations")
@ -107,9 +120,12 @@ export class LiquidityProvider {
try { try {
await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider')
this.incrementProviderBalance(res.operation.amount) this.incrementProviderBalance(res.operation.amount)
this.latestReceivedBalance = res.latest_balance
} catch (err: any) { } catch (err: any) {
this.log("error processing incoming invoice", err.message) this.log("error processing incoming invoice", err.message)
} }
} else if (res.operation.type === Types.UserOperationType.OUTGOING_INVOICE) {
delete this.pendingPaymentsAck[res.operation.identifier]
} }
}) })
} }
@ -122,62 +138,78 @@ export class LiquidityProvider {
} }
return res return res
} }
this.feesCache = {
serviceFeeFloor: res.network_max_fee_fixed,
serviceFeeBps: res.service_fee_bps
}
this.latestReceivedBalance = res.balance
this.utils.stateBundler.AddBalancePoint('providerBalance', res.balance) this.utils.stateBundler.AddBalancePoint('providerBalance', res.balance)
this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable) this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable)
return res return res
} }
GetLatestMaxWithdrawable = async () => { GetFees = () => {
if (!this.IsReady()) { if (!this.feesCache) {
return 0 throw new Error("fees not cached")
} }
const res = await this.GetUserState() return this.feesCache
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return 0
}
return res.max_withdrawable
} }
GetLatestBalance = async () => { GetMaxWithdrawable = () => {
if (!this.IsReady() || !this.feesCache) {
return 0
}
const balance = this.latestReceivedBalance
const { serviceFeeFloor, serviceFeeBps } = this.feesCache
const div = 1 + (serviceFeeBps / 10000)
const maxWithoutFixed = Math.floor(balance / div)
const fee = balance - maxWithoutFixed
const m = balance - Math.max(fee, serviceFeeFloor)
return Math.max(m, 0)
}
GetLatestBalance = () => {
if (!this.IsReady()) { if (!this.IsReady()) {
return 0 return 0
} }
const res = await this.GetUserState() return this.latestReceivedBalance
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return 0
}
return res.balance
} }
GetPendingBalance = async () => { GetPendingBalance = async () => {
return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0) return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0)
} }
CalculateExpectedFeeLimit = (amount: number, info: Types.UserInfo) => { GetServiceFee = (amount: number, f?: Types.CumulativeFees) => {
const serviceFeeRate = info.service_fee_bps / 10000 const fees = f ? f : this.GetFees()
const serviceFeeRate = fees.serviceFeeBps / 10000
const serviceFee = Math.ceil(serviceFeeRate * amount) const serviceFee = Math.ceil(serviceFeeRate * amount)
const networkMaxFeeRate = info.network_max_fee_bps / 10000 return Math.max(serviceFee, fees.serviceFeeFloor)
const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + info.network_max_fee_fixed)
return serviceFee + networkFeeLimit
} }
CanProviderHandle = async (req: LiquidityRequest) => { CanProviderPay = async (amount: number, localServiceFee: number): Promise<boolean> => {
if (!this.IsReady()) { if (!this.IsReady()) {
this.log("provider is not ready")
return false return false
} }
const maxW = await this.GetLatestMaxWithdrawable() const maxW = this.GetMaxWithdrawable()
if (req.action === 'spend') { if (maxW < amount) {
return maxW > req.amount this.log("provider does not have enough funds to pay the invoice")
return false
} }
const providerServiceFee = this.GetServiceFee(amount)
if (localServiceFee < providerServiceFee) {
this.log(`local service fee ${localServiceFee} is less than the provider's service fee ${providerServiceFee}`)
return false
}
return true return true
} }
AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => {
try { try {
if (!this.IsReady()) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet or disabled") throw new Error("liquidity provider is not ready yet, disabled or unreachable")
} }
const res = await this.client.NewInvoice({ amountSats: amount, memo, expiry }) const res = await this.client.NewInvoice({ amountSats: amount, memo, expiry })
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
@ -193,23 +225,37 @@ export class LiquidityProvider {
} }
PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => { PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system', feeLimit?: number) => {
try { try {
if (!this.IsReady()) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet or disabled") throw new Error("liquidity provider is not ready yet, disabled or unreachable")
} }
const userInfo = await this.GetUserState() const fees = this.GetFees()
if (userInfo.status === 'ERROR') { const providerServiceFee = this.GetServiceFee(decodedAmount, fees)
throw new Error(userInfo.reason) if (feeLimit && providerServiceFee > feeLimit) {
this.log("fees", fees)
this.log("provider service fee is greater than the fee limit", providerServiceFee, feeLimit)
throw new Error("provider service fee is greater than the fee limit")
} }
this.pendingPayments[invoice] = decodedAmount + this.CalculateExpectedFeeLimit(decodedAmount, userInfo) this.pendingPayments[invoice] = decodedAmount + providerServiceFee
const res = await this.client.PayInvoice({ invoice, amount: 0 }) const timeout = setTimeout(() => {
if (!this.pendingPaymentsAck[invoice]) {
return
}
this.log("10 seconds passed without a payment ack, locking provider until the next beacon")
this.lastSeenBeacon = 0
}, 1000 * 10)
this.pendingPaymentsAck[invoice] = true
const res = await this.client.PayInvoice({ invoice, amount: 0, expected_fees: fees })
delete this.pendingPaymentsAck[invoice]
clearTimeout(timeout)
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
this.log("error paying invoice", res.reason) this.log("error paying invoice", res.reason)
throw new Error(res.reason) throw new Error(res.reason)
} }
const totalPaid = res.amount_paid + res.network_fee + res.service_fee const totalPaid = res.amount_paid + res.service_fee
this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] })
this.latestReceivedBalance = res.latest_balance
this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true })
return res return res
} catch (err) { } catch (err) {
@ -221,7 +267,7 @@ export class LiquidityProvider {
GetPaymentState = async (invoice: string) => { GetPaymentState = async (invoice: string) => {
if (!this.IsReady()) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet or disabled") throw new Error("liquidity provider is not ready yet, disabled or unreachable")
} }
const res = await this.client.GetPaymentState({ invoice }) const res = await this.client.GetPaymentState({ invoice })
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
@ -233,7 +279,7 @@ export class LiquidityProvider {
GetOperations = async () => { GetOperations = async () => {
if (!this.IsReady()) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet or disabled") throw new Error("liquidity provider is not ready yet, disabled or unreachable")
} }
const res = await this.client.GetUserOperations({ const res = await this.client.GetUserOperations({
latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 },
@ -247,31 +293,48 @@ export class LiquidityProvider {
return res return res
} }
setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => { setNostrInfo = ({ localId, localPubkey }: { localPubkey: string, localId: string }) => {
this.log("setting nostr info") this.localId = localId
this.clientId = clientId this.localPubkey = localPubkey
this.myPub = myPub
this.setSetIfConfigured() this.setSetIfConfigured()
// If nostrSender becomes ready after setNostrInfo, ensure we check again
if (!this.configured && this.utils.nostrSender.IsReady()) {
this.setSetIfConfigured()
}
} }
attachNostrSend(f: NostrSend) {
this.log("attaching nostrSend action")
this.nostrSend = f
this.setSetIfConfigured()
}
setSetIfConfigured = () => { setSetIfConfigured = () => {
if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) { if (this.utils.nostrSender.IsReady() && !!this.providerPubkey && !!this.localId && !!this.localPubkey) {
this.configured = true if (!this.configured) {
this.log("configured to send to ", this.pubDestination) this.configured = true
}
}
}
onBeaconEvent = async (beaconData: { content: string, pub: string }) => {
if (beaconData.pub !== this.providerPubkey) {
this.log(ERROR, "got beacon from invalid pub", beaconData.pub, this.providerPubkey)
return
}
const beacon = JSON.parse(beaconData.content) as Types.BeaconData
const err = Types.BeaconDataValidate(beacon)
if (err) {
this.log(ERROR, "error validating beacon data", err.message)
return
}
if (beacon.type !== 'service') {
this.log(ERROR, "got beacon from invalid type", beacon.type)
return
}
this.lastSeenBeacon = Date.now()
if (beacon.fees) {
this.feesCache = beacon.fees
} }
} }
onEvent = async (res: { requestId: string }, fromPub: string) => { onEvent = async (res: { requestId: string }, fromPub: string) => {
if (fromPub !== this.pubDestination) { if (fromPub !== this.providerPubkey) {
this.log("got event from invalid pub", fromPub, this.pubDestination) this.log("got event from invalid pub", fromPub, this.providerPubkey)
return false return false
} }
if (this.clientCbs[res.requestId]) { if (this.clientCbs[res.requestId]) {
@ -288,9 +351,6 @@ export class LiquidityProvider {
} }
clientSend = (to: string, message: NostrRequest): Promise<any> => { clientSend = (to: string, message: NostrRequest): Promise<any> => {
if (!this.configured || !this.nostrSend) {
throw new Error("liquidity provider not initialized")
}
if (!message.requestId) { if (!message.requestId) {
message.requestId = makeId(16) message.requestId = makeId(16)
} }
@ -298,7 +358,7 @@ export class LiquidityProvider {
if (this.clientCbs[reqId]) { if (this.clientCbs[reqId]) {
throw new Error("request was already sent") throw new Error("request was already sent")
} }
this.nostrSend({ type: 'client', clientId: this.clientId }, { this.utils.nostrSender.Send({ type: 'client', clientId: this.localId }, {
type: 'content', type: 'content',
pub: to, pub: to,
content: JSON.stringify(message) content: JSON.stringify(message)
@ -317,9 +377,6 @@ export class LiquidityProvider {
} }
clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => { 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) { if (!message.requestId) {
message.requestId = message.rpcName message.requestId = message.rpcName
} }
@ -336,7 +393,7 @@ export class LiquidityProvider {
this.log("sub for", reqId, "was already registered, overriding") this.log("sub for", reqId, "was already registered, overriding")
return return
} }
this.nostrSend({ type: 'client', clientId: this.clientId }, { this.utils.nostrSender.Send({ type: 'client', clientId: this.localId }, {
type: 'content', type: 'content',
pub: to, pub: to,
content: JSON.stringify(message) content: JSON.stringify(message)

View file

@ -2,7 +2,7 @@ import { getRepository } from "typeorm";
import { User } from "../storage/entity/User.js"; import { User } from "../storage/entity/User.js";
import { UserOffer } from "../storage/entity/UserOffer.js"; import { UserOffer } from "../storage/entity/UserOffer.js";
import { ManagementGrant } from "../storage/entity/ManagementGrant.js"; import { ManagementGrant } from "../storage/entity/ManagementGrant.js";
import { NostrEvent, NostrSend } from "../nostr/handler.js"; import { NostrEvent } from "../nostr/nostrPool.js";
import Storage from "../storage/index.js"; import Storage from "../storage/index.js";
import { OfferManager } from "./offerManager.js"; import { OfferManager } from "./offerManager.js";
import * as Types from "../../../proto/autogenerated/ts/types.js"; import * as Types from "../../../proto/autogenerated/ts/types.js";
@ -13,7 +13,6 @@ import SettingsManager from "./settingsManager.js";
type Result<T> = { state: 'success', result: T } | { state: 'error', err: NmanageFailure } | { state: 'authRequired' } type Result<T> = { state: 'success', result: T } | { state: 'error', err: NmanageFailure } | { state: 'authRequired' }
export class ManagementManager { export class ManagementManager {
private nostrSend: NostrSend;
private storage: Storage; private storage: Storage;
private settings: SettingsManager; private settings: SettingsManager;
private awaitingRequests: Record<string, { request: NmanageRequest, event: NostrEvent }> = {} private awaitingRequests: Record<string, { request: NmanageRequest, event: NostrEvent }> = {}
@ -24,10 +23,6 @@ export class ManagementManager {
this.logger = getLogger({ component: 'ManagementManager' }) this.logger = getLogger({ component: 'ManagementManager' })
} }
attachNostrSend(f: NostrSend) {
this.nostrSend = f
}
ResetManage = async (ctx: Types.UserContext, req: Types.ManageOperation): Promise<void> => { ResetManage = async (ctx: Types.UserContext, req: Types.ManageOperation): Promise<void> => {
await this.storage.managementStorage.removeGrant(ctx.app_user_id, req.npub) await this.storage.managementStorage.removeGrant(ctx.app_user_id, req.npub)
} }
@ -62,12 +57,12 @@ export class ManagementManager {
private sendManageAuthorizationRequest = (appId: string, userPub: string, { requestId, npub }: { requestId: string, npub: string }) => { private sendManageAuthorizationRequest = (appId: string, userPub: string, { requestId, npub }: { requestId: string, npub: string }) => {
const message: Types.LiveManageRequest & { requestId: string, status: 'OK' } = { requestId: "GetLiveManageRequests", status: 'OK', npub: npub, request_id: requestId } const message: Types.LiveManageRequest & { requestId: string, status: 'OK' } = { requestId: "GetLiveManageRequests", status: 'OK', npub: npub, request_id: requestId }
this.logger("Sending manage authorization request to", npub, "for app", appId) this.logger("Sending manage authorization request to", npub, "for app", appId)
this.nostrSend({ type: 'app', appId: appId }, { type: 'content', content: JSON.stringify(message), pub: userPub }) this.storage.NostrSender().Send({ type: 'app', appId: appId }, { type: 'content', content: JSON.stringify(message), pub: userPub })
} }
private sendError(event: NostrEvent, err: NmanageFailure) { private sendError(event: NostrEvent, err: NmanageFailure) {
const e = newNmanageResponse(JSON.stringify(err), event) const e = newNmanageResponse(JSON.stringify(err), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
} }
private async handleAuthRequired(nmanageReq: NmanageRequest, event: NostrEvent) { private async handleAuthRequired(nmanageReq: NmanageRequest, event: NostrEvent) {
@ -107,7 +102,7 @@ export class ManagementManager {
return return
} }
const e = newNmanageResponse(JSON.stringify(r.result), event) const e = newNmanageResponse(JSON.stringify(r.result), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
} catch (err: any) { } catch (err: any) {
this.logger(ERROR, err.message || err) this.logger(ERROR, err.message || err)
this.sendError(event, { res: 'GFY', code: 2, error: 'Temporary Failure' }) this.sendError(event, { res: 'GFY', code: 2, error: 'Temporary Failure' })

View file

@ -4,7 +4,7 @@ import ProductManager from "./productManager.js";
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 { ERROR, getLogger } from "../helpers/logger.js"; import { ERROR, getLogger } from "../helpers/logger.js";
import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; import { NostrEvent } from '../nostr/nostrPool.js';
import { UnsignedEvent } from 'nostr-tools'; import { UnsignedEvent } from 'nostr-tools';
import { UserOffer } from '../storage/entity/UserOffer.js'; import { UserOffer } from '../storage/entity/UserOffer.js';
import { LiquidityManager } from "./liquidityManager.js" import { LiquidityManager } from "./liquidityManager.js"
@ -31,9 +31,6 @@ const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }
} }
} }
export class OfferManager { export class OfferManager {
_nostrSend: NostrSend | null = null
settings: SettingsManager settings: SettingsManager
applicationManager: ApplicationManager applicationManager: ApplicationManager
productManager: ProductManager productManager: ProductManager
@ -50,16 +47,6 @@ export class OfferManager {
this.liquidityManager = liquidityManager this.liquidityManager = liquidityManager
} }
attachNostrSend = (nostrSend: NostrSend) => {
this._nostrSend = nostrSend
}
nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => {
if (!this._nostrSend) {
throw new Error("No nostrSend attached")
}
this._nostrSend(initiator, data, relays)
}
async AddUserOffer(ctx: Types.UserContext, req: Types.OfferConfig): Promise<Types.OfferId> { async AddUserOffer(ctx: Types.UserContext, req: Types.OfferConfig): Promise<Types.OfferId> {
const newOffer = await this.storage.offerStorage.AddUserOffer(ctx.app_user_id, { const newOffer = await this.storage.offerStorage.AddUserOffer(ctx.app_user_id, {
payer_data: req.payer_data, payer_data: req.payer_data,
@ -182,7 +169,7 @@ export class OfferManager {
max: offerInvoice.max max: offerInvoice.max
}) })
const e = newNofferResponse(JSON.stringify({ code, error: codeToMessage(code), range: { min: 10, max: offerInvoice.max } }), event) const e = newNofferResponse(JSON.stringify({ code, error: codeToMessage(code), range: { min: 10, max: offerInvoice.max } }), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
return return
} }
@ -194,7 +181,7 @@ export class OfferManager {
}) })
const e = newNofferResponse(JSON.stringify({ bolt11: offerInvoice.invoice }), event) const e = newNofferResponse(JSON.stringify({ bolt11: offerInvoice.invoice }), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
this.logger("📤 [OFFER RESPONSE] Sent offer response", { this.logger("📤 [OFFER RESPONSE] Sent offer response", {
toPub: event.pub, toPub: event.pub,

View file

@ -12,11 +12,16 @@ import { Payment_PaymentStatus } from '../../../proto/lnd/lightning.js'
import { Event, verifiedSymbol, verifyEvent } from 'nostr-tools' import { Event, verifiedSymbol, verifyEvent } from 'nostr-tools'
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 { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.js'
import { Watchdog } from './watchdog.js' import { Watchdog } from './watchdog.js'
import { LiquidityManager } from './liquidityManager.js' import { LiquidityManager } from './liquidityManager.js'
import { Utils } from '../helpers/utilsWrapper.js' import { Utils } from '../helpers/utilsWrapper.js'
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js' import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { Swaps, TransactionSwapData } from '../lnd/swaps.js'
import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js'
import { LndAddress } from '../lnd/lnd.js'
import Metrics from '../metrics/index.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -36,6 +41,8 @@ interface UserOperationInfo {
}; };
internal?: boolean; internal?: boolean;
} }
export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment } export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment }
const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]` const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]`
const defaultLnAddressMetadata = (text: string, id: string) => `[["text/plain", "${text}"],["text/identifier", "${id}"]]` const defaultLnAddressMetadata = (text: string, id: string) => `[["text/plain", "${text}"],["text/identifier", "${id}"]]`
@ -51,25 +58,34 @@ export default class {
watchDog: Watchdog watchDog: Watchdog
liquidityManager: LiquidityManager liquidityManager: LiquidityManager
utils: Utils utils: Utils
constructor(storage: Storage, lnd: LND, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { swaps: Swaps
invoiceLock: InvoiceLock
metrics: Metrics
constructor(storage: Storage, metrics: Metrics, lnd: LND, swaps: Swaps, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage this.storage = storage
this.metrics = metrics
this.settings = settings this.settings = settings
this.lnd = lnd this.lnd = lnd
this.liquidityManager = liquidityManager this.liquidityManager = liquidityManager
this.utils = utils this.utils = utils
this.watchDog = new Watchdog(settings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker) this.watchDog = new Watchdog(settings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker)
this.swaps = swaps
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
} this.invoiceLock = new InvoiceLock()
Stop() {
this.watchDog.Stop()
} }
checkPendingPayments = async () => {
const log = getLogger({ component: 'pendingPaymentsOnStart' }) Stop() {
this.watchDog.Stop()
this.swaps.Stop()
}
checkPaymentStatus = async () => {
const log = getLogger({ component: 'checkPaymentStatus' })
const pendingPayments = await this.storage.paymentStorage.GetPendingPayments() const pendingPayments = await this.storage.paymentStorage.GetPendingPayments()
for (const p of pendingPayments) { for (const p of pendingPayments) {
log("checking state of payment: ", p.invoice) log("checking status of payment: ", p.invoice)
if (p.internal) { if (p.internal) {
log("found pending internal payment", p.serial_id) log("found pending internal payment", p.serial_id)
} else if (p.liquidityProvider) { } else if (p.liquidityProvider) {
@ -85,7 +101,7 @@ export default class {
checkPendingProviderPayment = async (log: PubLogger, p: UserInvoicePayment) => { checkPendingProviderPayment = async (log: PubLogger, p: UserInvoicePayment) => {
const state = await this.lnd.liquidProvider.GetPaymentState(p.invoice) const state = await this.lnd.liquidProvider.GetPaymentState(p.invoice)
if (state.paid_at_unix < 0) { if (state.paid_at_unix < 0) {
const fullAmount = p.paid_amount + p.service_fees + p.routing_fees const fullAmount = p.paid_amount + p.service_fees
log("found a failed provider payment, refunding", fullAmount, "sats to user", p.user.user_id) log("found a failed provider payment, refunding", fullAmount, "sats to user", p.user.user_id)
await this.storage.StartTransaction(async tx => { await this.storage.StartTransaction(async tx => {
await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx) await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx)
@ -94,18 +110,16 @@ export default class {
return return
} else if (state.paid_at_unix > 0) { } else if (state.paid_at_unix > 0) {
log("provider payment succeeded", p.serial_id, "updating payment info") log("provider payment succeeded", p.serial_id, "updating payment info")
const routingFeeLimit = p.routing_fees
const serviceFee = p.service_fees const serviceFee = p.service_fees
const actualFee = state.network_fee + state.service_fee const networkFee = state.service_fee
await this.storage.StartTransaction(async tx => { await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, serviceFee, true)
if (routingFeeLimit - actualFee > 0) { const remainingFee = serviceFee - networkFee
this.log("refund pending provider payment routing fee", routingFeeLimit, actualFee, "sats") if (remainingFee < 0) {
await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) this.log("WARNING: provider fee was higher than expected,", remainingFee, "were lost")
} }
await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, actualFee, p.service_fees, true, undefined, tx)
}, "pending provider payment success after restart") if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && remainingFee > 0) {
if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && serviceFee > 0) { await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees")
await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees")
} }
const user = await this.storage.userStorage.GetUser(p.user.user_id) const user = await this.storage.userStorage.GetUser(p.user.user_id)
this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId: p.user.user_id, appId: p.linkedApplication?.app_id || "", appUserId: "", balance: user.balance_sats, data: p.invoice, amount: p.paid_amount }) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId: p.user.user_id, appId: p.linkedApplication?.app_id || "", appUserId: "", balance: user.balance_sats, data: p.invoice, amount: p.paid_amount })
@ -126,7 +140,6 @@ export default class {
log(ERROR, "lnd payment not found for pending payment hash ", decoded.paymentHash) log(ERROR, "lnd payment not found for pending payment hash ", decoded.paymentHash)
return return
} }
switch (payment.status) { switch (payment.status) {
case Payment_PaymentStatus.UNKNOWN: case Payment_PaymentStatus.UNKNOWN:
log("pending payment in unknown state", p.serial_id, "no action will be performed") log("pending payment in unknown state", p.serial_id, "no action will be performed")
@ -136,24 +149,22 @@ export default class {
return return
case Payment_PaymentStatus.SUCCEEDED: case Payment_PaymentStatus.SUCCEEDED:
log("pending payment succeeded", p.serial_id, "updating payment info") log("pending payment succeeded", p.serial_id, "updating payment info")
const routingFeeLimit = p.routing_fees
const serviceFee = p.service_fees const serviceFee = p.service_fees
const actualFee = Number(payment.feeSat) const networkFee = Number(payment.feeSat)
await this.storage.StartTransaction(async tx => {
if (routingFeeLimit - actualFee > 0) { await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, p.service_fees, true, undefined)
this.log("refund pending payment routing fee", routingFeeLimit, actualFee, "sats") const remainingFee = serviceFee - networkFee
await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) if (remainingFee < 0) { // should not be possible beacuse of the fee limit
} this.log("WARNING: lnd fee was higher than expected,", remainingFee, "were lost")
await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, actualFee, p.service_fees, true, undefined, tx) }
}, "pending payment success after restart") if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && remainingFee > 0) {
if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && serviceFee > 0) { await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees")
await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees")
} }
const user = await this.storage.userStorage.GetUser(p.user.user_id) const user = await this.storage.userStorage.GetUser(p.user.user_id)
this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId: p.user.user_id, appId: p.linkedApplication?.app_id || "", appUserId: "", balance: user.balance_sats, data: p.invoice, amount: p.paid_amount }) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId: p.user.user_id, appId: p.linkedApplication?.app_id || "", appUserId: "", balance: user.balance_sats, data: p.invoice, amount: p.paid_amount })
return return
case Payment_PaymentStatus.FAILED: case Payment_PaymentStatus.FAILED:
const fullAmount = p.paid_amount + p.service_fees + p.routing_fees const fullAmount = p.paid_amount + p.service_fees
log("found a failed pending payment, refunding", fullAmount, "sats to user", p.user.user_id) log("found a failed pending payment, refunding", fullAmount, "sats to user", p.user.user_id)
await this.storage.StartTransaction(async tx => { await this.storage.StartTransaction(async tx => {
await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx) await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx)
@ -165,32 +176,171 @@ export default class {
} }
} }
getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { checkMissedChainTxs = async () => {
const log = getLogger({ component: 'checkMissedChainTxs' })
if (this.liquidityManager.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping chain tx check")
return
}
try {
const { txs, currentHeight, lndPubkey } = await this.getLatestTransactions(log)
const recoveredCount = await this.processMissedChainTransactions(txs, log)
// Update latest checked height to current block height
await this.storage.liquidityStorage.UpdateLatestCheckedHeight('lnd', lndPubkey, currentHeight)
if (recoveredCount > 0) {
log(`processed ${recoveredCount} missed chain tx(s)`)
} else {
log("no missed chain transactions found")
}
} catch (err: any) {
log(ERROR, "failed to check for missed chain transactions:", err.message || err)
}
}
private async getLatestTransactions(log: PubLogger): Promise<{ txs: Transaction[], currentHeight: number, lndPubkey: string }> {
const lndInfo = await this.lnd.GetInfo()
const lndPubkey = lndInfo.identityPubkey
const startHeight = await this.storage.liquidityStorage.GetLatestCheckedHeight('lnd', lndPubkey)
log(`checking for missed confirmed chain transactions from height ${startHeight}...`)
const { transactions } = await this.lnd.GetTransactions(startHeight)
log(`retrieved ${transactions.length} transactions from LND`)
return { txs: transactions, currentHeight: lndInfo.blockHeight, lndPubkey }
}
private async processMissedChainTransactions(transactions: Transaction[], log: PubLogger): Promise<number> {
let recoveredCount = 0
const addresses = await this.lnd.ListAddresses()
for (const tx of transactions) {
if (!tx.outputDetails || tx.outputDetails.length === 0) {
continue
}
const outputsWithAddresses = await this.collectOutputsWithAddresses(tx)
for (const { output, userAddress } of outputsWithAddresses) {
if (!userAddress) {
await this.processRootAddressOutput(output, tx, addresses, log)
continue
}
const processed = await this.processUserAddressOutput(output, tx, log)
if (processed) {
recoveredCount++
}
}
}
return recoveredCount
}
private async collectOutputsWithAddresses(tx: Transaction) {
const outputs: { output: OutputDetail, userAddress: UserReceivingAddress | null, tx: Transaction }[] = []
for (const output of tx.outputDetails) {
if (!output.address || !output.isOurAddress) {
continue
}
const userAddress = await this.storage.paymentStorage.GetAddressOwner(output.address)
outputs.push({ output, userAddress, tx })
}
return outputs
}
private async processRootAddressOutput(output: OutputDetail, tx: Transaction, addresses: LndAddress[], log: PubLogger): Promise<boolean> {
const addr = addresses.find(a => a.address === output.address)
if (!addr) {
throw new Error(`address ${output.address} not found in list of addresses`)
}
if (addr.change) {
log(`ignoring change address ${output.address}`)
return false
}
const outputIndex = Number(output.outputIndex)
const existingRootOp = await this.metrics.GetRootAddressTransaction(output.address, tx.txHash, outputIndex)
if (existingRootOp) {
return false
}
this.addressPaidCb({ hash: tx.txHash, index: outputIndex }, output.address, Number(output.amount), 'lnd')
.catch(err => log(ERROR, "failed to process root address output:", err.message || err))
return true
}
private async processUserAddressOutput(output: OutputDetail, tx: Transaction, log: PubLogger) {
const existingTx = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner(
output.address,
tx.txHash
)
if (existingTx) {
return false
}
const amount = Number(output.amount)
const outputIndex = Number(output.outputIndex)
log(`processing missed chain tx: address=${output.address}, txHash=${tx.txHash}, amount=${amount}, outputIndex=${outputIndex}`)
this.addressPaidCb({ hash: tx.txHash, index: outputIndex }, output.address, amount, 'lnd')
.catch(err => log(ERROR, "failed to process user address output:", err.message || err))
return true
}
getReceiveServiceFee = (action: Types.UserOperationType, amount: number, managedUser: boolean): number => {
switch (action) { switch (action) {
case Types.UserOperationType.INCOMING_TX: case Types.UserOperationType.INCOMING_TX:
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount) return 0
case Types.UserOperationType.OUTGOING_TX:
return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingTxFee * amount)
case Types.UserOperationType.INCOMING_INVOICE: case Types.UserOperationType.INCOMING_INVOICE:
if (appUser) { // Incoming invoice fees are always 0 (not configurable)
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount) return 0
} case Types.UserOperationType.INCOMING_USER_TO_USER:
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount) if (managedUser) {
case Types.UserOperationType.OUTGOING_INVOICE:
if (appUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount)
}
return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount)
case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER:
if (appUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
} }
return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.rootToUserFee * amount)
default:
throw new Error("Unknown receive action type")
}
}
getInvoicePaymentServiceFee = (amount: number, managedUser: boolean): number => {
if (!managedUser) {
return 0 // Root doesn't pay service fee to themselves
}
return Math.ceil(this.settings.getSettings().serviceFeeSettings.serviceFee * amount)
}
getSendServiceFee = (action: Types.UserOperationType, amount: number, managedUser: boolean): number => {
switch (action) {
case Types.UserOperationType.OUTGOING_TX:
throw new Error("OUTGOING_TX is not a valid send service fee action")
case Types.UserOperationType.OUTGOING_INVOICE:
const fee = this.getInvoicePaymentServiceFee(amount, managedUser)
// Only managed users pay the service fee floor
if (!managedUser) {
return 0
}
return Math.max(fee, this.settings.getSettings().serviceFeeSettings.serviceFeeFloor)
case Types.UserOperationType.OUTGOING_USER_TO_USER:
if (managedUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
}
return Math.ceil(this.settings.getSettings().serviceFeeSettings.rootToUserFee * amount)
default: default:
throw new Error("Unknown service action type") throw new Error("Unknown service action type")
} }
} }
getRoutingFeeLimit = (amount: number): number => {
const { routingFeeLimitBps, routingFeeFloor } = this.settings.getSettings().lndSettings
const limit = Math.floor(amount * routingFeeLimitBps / 10000)
return Math.max(limit, routingFeeFloor)
}
async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) { async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) {
if (!this.settings.getSettings().lndSettings.mockLnd) { if (!this.settings.getSettings().lndSettings.mockLnd) {
throw new Error("mock disabled, cannot set invoice as paid") throw new Error("mock disabled, cannot set invoice as paid")
@ -229,7 +379,7 @@ export default class {
} }
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, { useProvider: use === 'provider', from: 'user' }, req.blind) const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry, { useProvider: use === 'provider', from: 'user' }, req.blind)
const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options, res.providerDst) const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options, res.providerPubkey)
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 {
@ -237,14 +387,19 @@ export default class {
} }
} }
GetMaxPayableInvoice(balance: number, appUser: boolean): number { GetFees = (): Types.CumulativeFees => {
let maxWithinServiceFee = 0 const { serviceFeeBps, serviceFeeFloor } = this.settings.getSettings().serviceFeeSettings
if (appUser) { return { serviceFeeFloor, serviceFeeBps }
maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) }
} else {
maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee))) GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
} const { serviceFeeFloor, serviceFeeBps } = this.GetFees()
return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) const div = 1 + (serviceFeeBps / 10000)
const maxWithoutFixed = Math.floor(balance / div)
const fee = balance - maxWithoutFixed
const m = balance - Math.max(fee, serviceFeeFloor)
const max = Math.max(m, 0)
return { max, serviceFeeFloor, serviceFeeBps }
} }
async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise<Types.DecodeInvoiceResponse> { async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise<Types.DecodeInvoiceResponse> {
const decoded = await this.lnd.DecodeInvoice(req.invoice) const decoded = await this.lnd.DecodeInvoice(req.invoice)
@ -253,12 +408,20 @@ export default class {
} }
} }
async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> { async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, optionals: { swapOperationId?: string, ack?: (op: Types.UserOperation) => void } = {}): Promise<Types.PayInvoiceResponse & { operation: Types.UserOperation }> {
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) {
throw new Error("user is banned, cannot send payment") throw new Error("user is banned, cannot send payment")
} }
if (req.expected_fees) {
const { serviceFeeFloor, serviceFeeBps } = req.expected_fees
const serviceFixed = this.settings.getSettings().serviceFeeSettings.serviceFeeFloor
const serviceBps = this.settings.getSettings().serviceFeeSettings.serviceFeeBps
if (serviceFixed !== serviceFeeFloor || serviceBps !== serviceFeeBps) {
throw new Error("fees do not match the expected fees")
}
}
const decoded = await this.lnd.DecodeInvoice(req.invoice) const decoded = await this.lnd.DecodeInvoice(req.invoice)
if (decoded.numSatoshis !== 0 && req.amount !== 0) { if (decoded.numSatoshis !== 0 && req.amount !== 0) {
throw new Error("invoice has value, do not provide amount the the request") throw new Error("invoice has value, do not provide amount the the request")
@ -267,8 +430,8 @@ export default class {
throw new Error("invoice has no value, an amount must be provided in the request") throw new Error("invoice has no value, an amount must be provided in the request")
} }
const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis)
const isAppUserPayment = userId !== linkedApplication.owner.user_id const isManagedUser = userId !== linkedApplication.owner.user_id
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isManagedUser)
const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice) const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice)
if (internalInvoice && internalInvoice.paid_at_unix > 0) { if (internalInvoice && internalInvoice.paid_at_unix > 0) {
throw new Error("this invoice was already paid") throw new Error("this invoice was already paid")
@ -278,26 +441,42 @@ export default class {
throw new Error("this invoice was already paid") throw new Error("this invoice was already paid")
} }
let paymentInfo = { preimage: "", amtPaid: 0, networkFee: 0, serialId: 0 } let paymentInfo = { preimage: "", amtPaid: 0, networkFee: 0, serialId: 0 }
if (internalInvoice) { if (this.invoiceLock.isLocked(req.invoice)) {
paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub) throw new Error("this invoice is already being paid")
} else {
paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, req.debit_npub)
} }
if (isAppUserPayment && serviceFee > 0) { this.invoiceLock.lock(req.invoice)
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee, "fees") try {
if (internalInvoice) {
paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub)
} else {
paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, { ...optionals, debitNpub: req.debit_npub })
}
this.invoiceLock.unlock(req.invoice)
} catch (err) {
this.invoiceLock.unlock(req.invoice)
throw err
}
const feeDiff = serviceFee - paymentInfo.networkFee
if (isManagedUser && feeDiff > 0) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, feeDiff, "fees")
} }
const user = await this.storage.userStorage.GetUser(userId) const user = await this.storage.userStorage.GetUser(userId)
this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId, appId: linkedApplication.app_id, appUserId: "", balance: user.balance_sats, data: req.invoice, amount: payAmount }) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId, appId: linkedApplication.app_id, appUserId: "", balance: user.balance_sats, data: req.invoice, amount: payAmount })
const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`
const operation = this.newInvoicePaymentOperation({ invoice: req.invoice, opId, amount: paymentInfo.amtPaid, networkFee: paymentInfo.networkFee, serviceFee: serviceFee, confirmed: true, paidAtUnix: Math.floor(Date.now() / 1000) })
return { return {
preimage: paymentInfo.preimage, preimage: paymentInfo.preimage,
amount_paid: paymentInfo.amtPaid, amount_paid: paymentInfo.amtPaid,
operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, operation_id: opId,
network_fee: paymentInfo.networkFee, network_fee: 0,
service_fee: serviceFee, service_fee: serviceFee,
latest_balance: user.balance_sats,
operation
} }
} }
async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, debitNpub?: string) { async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, optionals: { debitNpub?: string, swapOperationId?: string, ack?: (op: Types.UserOperation) => void } = {}) {
if (this.settings.getSettings().serviceSettings.disableExternalPayments) { if (this.settings.getSettings().serviceSettings.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")
} }
@ -310,30 +489,33 @@ export default class {
} }
throw new Error("payment already in progress") 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
const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) const routingFeeLimit = this.getRoutingFeeLimit(payAmount)
const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee)
const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderPubkey() : undefined
const pendingPayment = await this.storage.StartTransaction(async tx => { const pendingPayment = await this.storage.StartTransaction(async tx => {
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, invoice, tx)
return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, debitNpub) return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: 0 }, linkedApplication, provider, tx, optionals)
}, "payment started") }, "payment started")
this.log("ready to pay") this.log("ready to pay")
const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${pendingPayment.serial_id}`
const op = this.newInvoicePaymentOperation({ invoice, opId, amount: payAmount, networkFee: 0, serviceFee: serviceFee, confirmed: false, paidAtUnix: 0 })
optionals.ack?.(op)
try { try {
const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { const payment = await this.lnd.PayInvoice(invoice, amountForLnd, { routingFeeLimit, serviceFee }, payAmount, { useProvider: use === 'provider', from: 'user' }, index => {
this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index)
}) })
if (routingFeeLimit - payment.feeSat > 0) { await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerPubkey)
this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats") const feeDiff = serviceFee - payment.feeSat
await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice) if (feeDiff < 0) { // should not happen to lnd beacuse of the fee limit, culd happen to provider if the fee used to calculate the provider fee are out of date
this.log("WARNING: network fee was higher than expected,", feeDiff, "were lost by", use === 'provider' ? "provider" : "lnd")
} }
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, "payment_refund:" + invoice)
await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, 0, 0, false) await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, 0, 0, false)
throw err throw err
} }
@ -354,60 +536,79 @@ export default class {
} catch (err) { } catch (err) {
await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "internal_payment_refund:" + internalInvoice.invoice) await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "internal_payment_refund:" + internalInvoice.invoice)
this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', payAmount, { used: 'internal', from: 'user' }, linkedApplication.app_id) this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', payAmount, { used: 'internal', from: 'user' }, linkedApplication.app_id)
throw err throw err
} }
} }
async GetTransactionSwapQuotes(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuoteList> {
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const quotes = await this.swaps.GetTxSwapQuotes(ctx.app_user_id, req.transaction_amount_sats, decodedAmt => {
const isManagedUser = ctx.user_id !== app.owner.user_id
return this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decodedAmt, isManagedUser)
})
return { quotes }
}
async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> { async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
throw new Error("address payment currently disabled, use Lightning instead")
await this.watchDog.PaymentRequested() await this.watchDog.PaymentRequested()
this.log("paying address", req.address, "for user", ctx.user_id, "with amount", req.amoutSats) this.log("paying address", req.address, "for user", ctx.user_id, "with amount", req.amountSats)
const maybeBanned = await this.storage.userStorage.GetUser(ctx.user_id) const maybeBanned = await this.storage.userStorage.GetUser(ctx.user_id)
if (maybeBanned.locked) { if (maybeBanned.locked) {
throw new Error("user is banned, cannot send chain tx") throw new Error("user is banned, cannot send chain tx")
} }
const internalAddress = await this.storage.paymentStorage.GetAddressOwner(req.address)
if (internalAddress) {
return this.PayInternalAddress(ctx, req)
}
return this.PayAddressWithSwap(ctx, req)
}
async PayAddressWithSwap(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
this.log("paying external address")
if (!req.swap_operation_id) {
throw new Error("request a swap quote before paying an external address")
}
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
let payment: Types.PayInvoiceResponse
const swap = await this.swaps.PayAddrWithSwap(ctx.app_user_id, req.swap_operation_id, req.address, async (invoice) => {
payment = await this.PayInvoice(ctx.user_id, {
amount: 0,
invoice: invoice
}, app, { swapOperationId: req.swap_operation_id })
})
return {
txId: swap.txId,
network_fee: swap.network_fee,
service_fee: payment!.service_fee,
operation_id: payment!.operation_id,
}
}
async PayInternalAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
this.log("paying internal address")
if (req.swap_operation_id) {
await this.storage.paymentStorage.DeleteTransactionSwap(req.swap_operation_id)
}
const { blockHeight } = await this.lnd.GetInfo() const { blockHeight } = await this.lnd.GetInfo()
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) const isManagedUser = ctx.user_id !== app.owner.user_id
const isAppUserPayment = ctx.user_id !== app.owner.user_id const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, req.amountSats, isManagedUser)
const internalAddress = await this.storage.paymentStorage.GetAddressOwner(req.address)
let txId = ""
let chainFees = 0
if (!internalAddress) {
this.log("paying external address")
const estimate = await this.lnd.EstimateChainFees(req.address, req.amoutSats, 1)
const vBytes = Math.ceil(Number(estimate.feeSat / estimate.satPerVbyte))
chainFees = vBytes * req.satsPerVByte
const total = req.amoutSats + chainFees
// 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)
try {
const payment = await this.lnd.PayAddress(req.address, req.amoutSats, req.satsPerVByte, "", { useProvider: false, from: 'user' })
txId = payment.txid
} catch (err) {
// WARNING, before re-enabling this, make sure to add the tx_hash to the IncrementUserBalance "reason"!!
await this.storage.userStorage.IncrementUserBalance(ctx.user_id, total + serviceFee, req.address)
throw err
}
} else {
this.log("paying internal address")
txId = crypto.randomBytes(32).toString("hex")
const addressData = `${req.address}:${txId}`
await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData)
this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, 'internal')
}
if (isAppUserPayment && serviceFee > 0) { const txId = crypto.randomBytes(32).toString("hex")
const addressData = `${req.address}:${txId}`
await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amountSats + serviceFee, addressData)
this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amountSats, 'internal')
if (isManagedUser && serviceFee > 0) {
await this.storage.userStorage.IncrementUserBalance(app.owner.user_id, serviceFee, 'fees') await this.storage.userStorage.IncrementUserBalance(app.owner.user_id, serviceFee, 'fees')
} }
const chainFees = 0
const newTx = await this.storage.paymentStorage.AddUserTransactionPayment(ctx.user_id, req.address, txId, 0, req.amoutSats, chainFees, serviceFee, !!internalAddress, blockHeight, app) const internalAddress = true
const newTx = await this.storage.paymentStorage.AddUserTransactionPayment(ctx.user_id, req.address, txId, 0, req.amountSats, chainFees, serviceFee, internalAddress, blockHeight, app)
const user = await this.storage.userStorage.GetUser(ctx.user_id) const user = await this.storage.userStorage.GetUser(ctx.user_id)
const txData = `${newTx.address}:${newTx.tx_hash}` const txData = `${newTx.address}:${newTx.tx_hash}`
this.storage.eventsLog.LogEvent({ type: 'address_payment', userId: ctx.user_id, appId: app.app_id, appUserId: "", balance: user.balance_sats, data: txData, amount: req.amoutSats }) this.storage.eventsLog.LogEvent({ type: 'address_payment', userId: ctx.user_id, appId: app.app_id, appUserId: "", balance: user.balance_sats, data: txData, amount: req.amountSats })
return { return {
txId: txId, txId: txId,
operation_id: `${Types.UserOperationType.OUTGOING_TX}-${newTx.serial_id}`, operation_id: `${Types.UserOperationType.OUTGOING_TX}-${newTx.serial_id}`,
@ -416,6 +617,16 @@ export default class {
} }
} }
async ListSwaps(ctx: Types.UserContext): Promise<Types.SwapsList> {
const payments = await this.storage.paymentStorage.ListSwapPayments(ctx.app_user_id)
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isManagedUser = ctx.user_id !== app.owner.user_id
return this.swaps.ListSwaps(ctx.app_user_id, payments, p => {
const opId = `${Types.UserOperationType.OUTGOING_TX}-${p.serial_id}`
return this.newInvoicePaymentOperation({ amount: p.paid_amount, confirmed: p.paid_at_unix !== 0, invoice: p.invoice, opId, networkFee: p.routing_fees, serviceFee: p.service_fees, paidAtUnix: p.paid_at_unix })
}, amt => this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, amt, isManagedUser))
}
balanceCheckUrl(k1: string): string { balanceCheckUrl(k1: string): string {
return `${this.settings.getSettings().serviceSettings.serviceUrl}/api/guest/lnurl_withdraw/info?k1=${k1}` return `${this.settings.getSettings().serviceSettings.serviceUrl}/api/guest/lnurl_withdraw/info?k1=${k1}`
} }
@ -445,21 +656,6 @@ export default class {
async GetLnurlWithdrawInfo(balanceCheckK1: string): Promise<Types.LnurlWithdrawInfoResponse> { async GetLnurlWithdrawInfo(balanceCheckK1: string): Promise<Types.LnurlWithdrawInfoResponse> {
throw new Error("LNURL withdraw currenlty not supported for non application users") throw new Error("LNURL withdraw currenlty not supported for non application users")
/*const key = await this.storage.paymentStorage.UseUserEphemeralKey(balanceCheckK1, 'balanceCheck')
const maxWithdrawable = this.GetMaxPayableInvoice(key.user.balance_sats)
const callbackK1 = await this.storage.paymentStorage.AddUserEphemeralKey(key.user.user_id, 'withdraw')
const newBalanceCheckK1 = await this.storage.paymentStorage.AddUserEphemeralKey(key.user.user_id, 'balanceCheck')
const payInfoK1 = await this.storage.paymentStorage.AddUserEphemeralKey(key.user.user_id, 'pay')
return {
tag: "withdrawRequest",
callback: `${this.settings.serviceUrl}/api/guest/lnurl_withdraw/handle`,
defaultDescription: "lnurl withdraw from lightning.pub",
k1: callbackK1.key,
maxWithdrawable: maxWithdrawable * 1000,
minWithdrawable: 10000,
balanceCheck: this.balanceCheckUrl(newBalanceCheckK1.key),
payLink: `${this.settings.serviceUrl}/api/guest/lnurl_pay/info?k1=${payInfoK1.key}`,
}*/
} }
async HandleLnurlWithdraw(k1: string, invoice: string): Promise<void> { async HandleLnurlWithdraw(k1: string, invoice: string): Promise<void> {
@ -672,6 +868,23 @@ export default class {
} }
} }
newInvoicePaymentOperation = (opInfo: { invoice: string, opId: string, amount: number, networkFee: number, serviceFee: number, confirmed: boolean, paidAtUnix: number }): Types.UserOperation => {
const { invoice, opId, amount, networkFee, serviceFee, confirmed, paidAtUnix } = opInfo
return {
amount: amount,
paidAtUnix: paidAtUnix,
inbound: false,
type: Types.UserOperationType.OUTGOING_INVOICE,
identifier: invoice,
operationId: opId,
network_fee: networkFee,
service_fee: serviceFee,
confirmed,
tx_hash: "",
internal: networkFee === 0
}
}
async GetPaymentState(userId: string, req: Types.GetPaymentStateRequest): Promise<Types.PaymentState> { async GetPaymentState(userId: string, req: Types.GetPaymentStateRequest): Promise<Types.PaymentState> {
const user = await this.storage.userStorage.GetUser(userId) const user = await this.storage.userStorage.GetUser(userId)
if (user.locked) { if (user.locked) {
@ -684,8 +897,10 @@ export default class {
return { return {
paid_at_unix: invoice.paid_at_unix, paid_at_unix: invoice.paid_at_unix,
amount: invoice.paid_amount, amount: invoice.paid_amount,
network_fee: invoice.routing_fees, network_fee: 0,
service_fee: invoice.service_fees, service_fee: invoice.service_fees,
internal: invoice.internal,
operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${invoice.serial_id}`,
} }
} }
@ -722,14 +937,14 @@ export default class {
if (fromUser.balance_sats < amount) { if (fromUser.balance_sats < amount) {
throw new Error("not enough balance to send payment") throw new Error("not enough balance to send payment")
} }
const isAppUserPayment = fromUser.user_id !== linkedApplication.owner.user_id const isManagedUser = fromUser.user_id !== linkedApplication.owner.user_id
let fee = this.getServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) let fee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isManagedUser)
const toDecrement = amount + fee const toDecrement = amount + fee
const paymentEntry = await this.storage.paymentStorage.AddPendingUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication, tx) const paymentEntry = await this.storage.paymentStorage.AddPendingUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication, tx)
await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, toDecrement, `${toUserId}:${paymentEntry.serial_id}`, tx) await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, toDecrement, `${toUserId}:${paymentEntry.serial_id}`, tx)
await this.storage.userStorage.IncrementUserBalance(toUser.user_id, amount, `${fromUserId}:${paymentEntry.serial_id}`, tx) await this.storage.userStorage.IncrementUserBalance(toUser.user_id, amount, `${fromUserId}:${paymentEntry.serial_id}`, tx)
await this.storage.paymentStorage.SetPendingUserToUserPaymentAsPaid(paymentEntry.serial_id, tx) await this.storage.paymentStorage.SetPendingUserToUserPaymentAsPaid(paymentEntry.serial_id, tx)
if (isAppUserPayment && fee > 0) { if (isManagedUser && fee > 0) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee, 'fees', tx) await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee, 'fees', tx)
} }
return paymentEntry return paymentEntry
@ -785,3 +1000,16 @@ export default class {
} }
} }
class InvoiceLock {
locked: Record<string, boolean> = {}
lock(invoice: string) {
this.locked[invoice] = true
}
unlock(invoice: string) {
delete this.locked[invoice]
}
isLocked(invoice: string) {
return this.locked[invoice]
}
}

View file

@ -20,18 +20,18 @@ export class RugPullTracker {
} }
CheckProviderBalance = async (): Promise<{ balance: number, prevBalance?: number }> => { CheckProviderBalance = async (): Promise<{ balance: number, prevBalance?: number }> => {
const pubDst = this.liquidProvider.GetProviderDestination() const pubDst = this.liquidProvider.GetProviderPubkey()
if (!pubDst) { if (!pubDst) {
return { balance: 0 } return { balance: 0 }
} }
const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst) const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst)
const ready = this.liquidProvider.IsReady() const ready = this.liquidProvider.IsReady()
if (ready) { if (ready) {
const balance = await this.liquidProvider.GetLatestBalance() const balance = this.liquidProvider.GetLatestBalance()
const pendingBalance = await this.liquidProvider.GetPendingBalance() const pendingBalance = await this.liquidProvider.GetPendingBalance()
const trackedBalance = balance + pendingBalance const trackedBalance = balance + pendingBalance
if (!providerTracker) { if (!providerTracker) {
this.log("starting to track provider", this.liquidProvider.GetProviderDestination()) this.log("starting to track provider", this.liquidProvider.GetProviderPubkey())
await this.storage.liquidityStorage.CreateTrackedProvider('lnPub', pubDst, trackedBalance) await this.storage.liquidityStorage.CreateTrackedProvider('lnPub', pubDst, trackedBalance)
return { balance: trackedBalance } return { balance: trackedBalance }
} }

View file

@ -1,31 +1,27 @@
import { EnvCacher, EnvMustBeNonEmptyString, EnvMustBeInteger, chooseEnv, chooseEnvBool, chooseEnvInt } from '../helpers/envParser.js' import { EnvCacher, EnvMustBeNonEmptyString, EnvMustBeInteger, chooseEnv, chooseEnvBool, chooseEnvInt } from '../helpers/envParser.js'
import os from 'os' import os from 'os'
import path from 'path' import path from 'path'
import { nip19 } from '@shocknet/clink-sdk'
export type ServiceFeeSettings = { export type ServiceFeeSettings = {
incomingTxFee: number serviceFee: number
outgoingTxFee: number serviceFeeBps: number
incomingAppInvoiceFee: number serviceFeeFloor: number
incomingAppUserInvoiceFee: number
outgoingAppInvoiceFee: number
outgoingAppUserInvoiceFee: number
outgoingAppUserInvoiceFeeBps: number
userToUserFee: number userToUserFee: number
appToUserFee: number rootToUserFee: number
} }
export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceFeeSettings => { export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceFeeSettings => {
const outgoingAppUserInvoiceFeeBps = chooseEnvInt("OUTGOING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb) const oldServiceFeeBps = chooseEnvInt("OUTGOING_INVOICE_FEE_USER_BPS", dbEnv, 60, addToDb)
const serviceFeeBps = chooseEnvInt("SERVICE_FEE_BPS", dbEnv, oldServiceFeeBps, addToDb)
const oldRoutingFeeFloor = chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb)
const serviceFeeFloor = chooseEnvInt("SERVICE_FEE_FLOOR_SATS", dbEnv, oldRoutingFeeFloor, addToDb)
return { return {
incomingTxFee: chooseEnvInt("INCOMING_CHAIN_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000, serviceFeeBps,
outgoingTxFee: chooseEnvInt("OUTGOING_CHAIN_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000, serviceFee: serviceFeeBps / 10000,
incomingAppInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000, serviceFeeFloor,
outgoingAppInvoiceFee: chooseEnvInt("OUTGOING_INVOICE_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000,
incomingAppUserInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb) / 10000,
outgoingAppUserInvoiceFeeBps,
outgoingAppUserInvoiceFee: outgoingAppUserInvoiceFeeBps / 10000,
userToUserFee: chooseEnvInt("TX_FEE_INTERNAL_USER_BPS", dbEnv, 0, addToDb) / 10000, userToUserFee: chooseEnvInt("TX_FEE_INTERNAL_USER_BPS", dbEnv, 0, addToDb) / 10000,
appToUserFee: chooseEnvInt("TX_FEE_INTERNAL_ROOT_BPS", dbEnv, 0, addToDb) / 10000, rootToUserFee: chooseEnvInt("TX_FEE_INTERNAL_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
} }
} }
@ -76,13 +72,14 @@ export type LndNodeSettings = {
lndCertPath: string // cold setting lndCertPath: string // cold setting
lndMacaroonPath: string // cold setting lndMacaroonPath: string // cold setting
} }
const networks = ['mainnet', 'testnet', 'regtest'] as const
export type BTCNetwork = (typeof networks)[number]
export type LndSettings = { export type LndSettings = {
lndLogDir: string lndLogDir: string
feeRateLimit: number routingFeeLimitBps: number
feeFixedLimit: number routingFeeFloor: number
feeRateBps: number
mockLnd: boolean mockLnd: boolean
network: BTCNetwork
} }
const resolveHome = (filepath: string) => { const resolveHome = (filepath: string) => {
@ -111,13 +108,16 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record<string, string | undefi
} }
export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => { export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => {
const feeRateBps: number = chooseEnvInt('OUTBOUND_MAX_FEE_BPS', dbEnv, 60, addToDb) const network = chooseEnv('BTC_NETWORK', dbEnv, 'mainnet', addToDb) as BTCNetwork
const oldRoutingFeeFloor = chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 5, addToDb)
const routingFeeFloor = chooseEnvInt('ROUTING_FEE_FLOOR_SATS', dbEnv, oldRoutingFeeFloor, addToDb)
const routingFeeLimitBps = chooseEnvInt('ROUTING_FEE_LIMIT_BPS', dbEnv, 50, addToDb)
return { return {
lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, path.join(lndDir(), "logs", "bitcoin", "mainnet", "lnd.log"), addToDb), lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb),
feeRateBps: feeRateBps, routingFeeLimitBps,
feeRateLimit: feeRateBps / 10000, routingFeeFloor,
feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 100, addToDb), mockLnd: false,
mockLnd: false network: networks.includes(network) ? network : 'mainnet'
} }
} }
@ -167,13 +167,48 @@ export type LiquiditySettings = {
liquidityProviderPub: string // cold setting liquidityProviderPub: string // cold setting
useOnlyLiquidityProvider: boolean // hot setting useOnlyLiquidityProvider: boolean // hot setting
disableLiquidityProvider: boolean // hot setting disableLiquidityProvider: boolean // hot setting
providerRelayUrl: string
} }
export const LoadLiquiditySettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LiquiditySettings => { export const LoadLiquiditySettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LiquiditySettings => {
//const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB === "null" ? "" : (process.env.LIQUIDITY_PROVIDER_PUB || "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e") const providerNprofile = chooseEnv("PROVIDER_NPROFILE", dbEnv, "nprofile1qyd8wumn8ghj7um5wfn8y7fwwd5x7cmt9ehx2arhdaexkqpqwmk5tuqvafa6ckwc6zmaypyy3af3n4aeds2ql7m0ew42kzsn638q9s9z8p", addToDb)
const liquidityProviderPub = chooseEnv("LIQUIDITY_PROVIDER_PUB", dbEnv, "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e", addToDb) const { liquidityProviderPub, providerRelayUrl } = decodeNprofile(providerNprofile)
const disableLiquidityProvider = chooseEnvBool("DISABLE_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) || liquidityProviderPub === "null" const disableLiquidityProvider = chooseEnvBool("DISABLE_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) || liquidityProviderPub === "null"
const useOnlyLiquidityProvider = chooseEnvBool("USE_ONLY_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) const useOnlyLiquidityProvider = chooseEnvBool("USE_ONLY_LIQUIDITY_PROVIDER", dbEnv, false, addToDb)
return { liquidityProviderPub, useOnlyLiquidityProvider, disableLiquidityProvider }
return { liquidityProviderPub, useOnlyLiquidityProvider, disableLiquidityProvider, providerRelayUrl }
}
const decodeNprofile = (nprofile: string) => {
const decoded = nip19.decode(nprofile)
if (decoded.type !== 'nprofile') {
throw new Error("PROVIDER_NPROFILE must be a valid nprofile")
}
if (!decoded.data.pubkey) {
throw new Error("PROVIDER_NPROFILE must contain a pubkey")
}
if (!decoded.data.relays || decoded.data.relays.length === 0) {
throw new Error("PROVIDER_NPROFILE must contain at least one relay")
}
return { liquidityProviderPub: decoded.data.pubkey, providerRelayUrl: decoded.data.relays[0] }
}
export type SwapsSettings = {
boltzHttpUrl: string
boltzWebSocketUrl: string
boltsHttpUrlAlt: string
boltsWebSocketUrlAlt: string
enableSwaps: boolean
}
export const LoadSwapsSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): SwapsSettings => {
return {
boltzHttpUrl: chooseEnv("BOLTZ_HTTP_URL", dbEnv, "https://swaps.zeuslsp.com/api", addToDb),
boltzWebSocketUrl: chooseEnv("BOLTZ_WEBSOCKET_URL", dbEnv, "wss://swaps.zeuslsp.com/api", addToDb),
boltsHttpUrlAlt: chooseEnv("BOLTZ_HTTP_URL_ALT", dbEnv, "https://api.boltz.exchange/", addToDb),
boltsWebSocketUrlAlt: chooseEnv("BOLTZ_WEBSOCKET_URL_ALT", dbEnv, "wss://api.boltz.exchange/", addToDb),
enableSwaps: chooseEnvBool("ENABLE_SWAPS", dbEnv, false, addToDb)
}
} }

View file

@ -5,7 +5,8 @@ import {
LiquiditySettings, LndNodeSettings, LndSettings, LoadLiquiditySettingsFromEnv, LiquiditySettings, LndNodeSettings, LndSettings, LoadLiquiditySettingsFromEnv,
LoadLSPSettingsFromEnv, LSPSettings, ServiceFeeSettings, ServiceSettings, LoadServiceFeeSettingsFromEnv, LoadLSPSettingsFromEnv, LSPSettings, ServiceFeeSettings, ServiceSettings, LoadServiceFeeSettingsFromEnv,
LoadNostrRelaySettingsFromEnv, LoadServiceSettingsFromEnv, LoadWatchdogSettingsFromEnv, LoadNostrRelaySettingsFromEnv, LoadServiceSettingsFromEnv, LoadWatchdogSettingsFromEnv,
LoadLndNodeSettingsFromEnv, LoadLndSettingsFromEnv, NostrRelaySettings, WatchdogSettings LoadLndNodeSettingsFromEnv, LoadLndSettingsFromEnv, NostrRelaySettings, WatchdogSettings, SwapsSettings, LoadSwapsSettingsFromEnv
} from "./settings.js" } from "./settings.js"
export default class SettingsManager { export default class SettingsManager {
storage: Storage storage: Storage
@ -27,6 +28,7 @@ export default class SettingsManager {
serviceFeeSettings: LoadServiceFeeSettingsFromEnv(dbEnv, addToDb), serviceFeeSettings: LoadServiceFeeSettingsFromEnv(dbEnv, addToDb),
serviceSettings: LoadServiceSettingsFromEnv(dbEnv, addToDb), serviceSettings: LoadServiceSettingsFromEnv(dbEnv, addToDb),
watchDogSettings: LoadWatchdogSettingsFromEnv(dbEnv, addToDb), watchDogSettings: LoadWatchdogSettingsFromEnv(dbEnv, addToDb),
swapsSettings: LoadSwapsSettingsFromEnv(dbEnv, addToDb),
} }
} }
@ -48,9 +50,26 @@ export default class SettingsManager {
for (const key in toAdd) { for (const key in toAdd) {
await this.storage.settingsStorage.setDbEnvIFNeeded(key, toAdd[key]) await this.storage.settingsStorage.setDbEnvIFNeeded(key, toAdd[key])
} }
// Validate fee configuration: routing fee limit must be <= service fee
this.validateFeeSettings(this.settings)
return this.settings return this.settings
} }
private validateFeeSettings(settings: FullSettings): void {
const { serviceFeeSettings, lndSettings } = settings
const serviceFeeBps = serviceFeeSettings.serviceFeeBps
const routingFeeLimitBps = lndSettings.routingFeeLimitBps
const serviceFeeFloor = serviceFeeSettings.serviceFeeFloor
const routingFeeFloor = lndSettings.routingFeeFloor
if (routingFeeLimitBps > serviceFeeBps) {
throw new Error(`ROUTING_FEE_LIMIT_BPS (${routingFeeLimitBps}) must be <= SERVICE_FEE_BPS (${serviceFeeBps}) to ensure Pub keeps a spread`)
}
if (routingFeeFloor > serviceFeeFloor) {
throw new Error(`ROUTING_FEE_FLOOR_SATS (${routingFeeFloor}) must be <= SERVICE_FEE_FLOOR_SATS (${serviceFeeFloor}) to ensure Pub keeps a spread`)
}
}
getStorageSettings(): StorageSettings { getStorageSettings(): StorageSettings {
return this.storage.getStorageSettings() return this.storage.getStorageSettings()
} }
@ -148,5 +167,6 @@ type FullSettings = {
nostrRelaySettings: NostrRelaySettings, nostrRelaySettings: NostrRelaySettings,
serviceFeeSettings: ServiceFeeSettings, serviceFeeSettings: ServiceFeeSettings,
serviceSettings: ServiceSettings, serviceSettings: ServiceSettings,
lspSettings: LSPSettings lspSettings: LSPSettings,
swapsSettings: SwapsSettings
} }

View file

@ -241,12 +241,12 @@ export default class Handler {
ops.outgoingInvoices.forEach(i => { ops.outgoingInvoices.forEach(i => {
if (req.include_operations) operations.push({ type: Types.UserOperationType.OUTGOING_INVOICE, amount: i.paid_amount, inbound: false, paidAtUnix: i.paid_at_unix, confirmed: true, service_fee: i.service_fees, network_fee: i.routing_fees, identifier: "", operationId: "", tx_hash: "", internal: i.internal }) if (req.include_operations) operations.push({ type: Types.UserOperationType.OUTGOING_INVOICE, amount: i.paid_amount, inbound: false, paidAtUnix: i.paid_at_unix, confirmed: true, service_fee: i.service_fees, network_fee: i.routing_fees, identifier: "", operationId: "", tx_hash: "", internal: i.internal })
totalSpent += i.paid_amount totalSpent += i.paid_amount
feesInRange += i.service_fees feesInRange += (i.service_fees - i.routing_fees)
}) })
ops.outgoingTransactions.forEach(tx => { ops.outgoingTransactions.forEach(tx => {
if (req.include_operations) operations.push({ type: Types.UserOperationType.OUTGOING_TX, amount: tx.paid_amount, inbound: false, paidAtUnix: tx.paid_at_unix, confirmed: tx.confs > 1, service_fee: tx.service_fees, network_fee: tx.chain_fees, identifier: "", operationId: "", tx_hash: tx.tx_hash, internal: tx.internal }) if (req.include_operations) operations.push({ type: Types.UserOperationType.OUTGOING_TX, amount: tx.paid_amount, inbound: false, paidAtUnix: tx.paid_at_unix, confirmed: tx.confs > 1, service_fee: tx.service_fees, network_fee: tx.chain_fees, identifier: "", operationId: "", tx_hash: tx.tx_hash, internal: tx.internal })
totalSpent += tx.paid_amount totalSpent += tx.paid_amount
feesInRange += tx.service_fees feesInRange += (tx.service_fees - tx.chain_fees)
}) })
ops.userToUser.forEach(op => { ops.userToUser.forEach(op => {
@ -414,6 +414,10 @@ export default class Handler {
await this.storage.metricsStorage.AddRootOperation("chain", `${address}:${txOutput.hash}:${txOutput.index}`, amount) await this.storage.metricsStorage.AddRootOperation("chain", `${address}:${txOutput.hash}:${txOutput.index}`, amount)
} }
async GetRootAddressTransaction(address: string, txHash: string, index: number) {
return this.storage.metricsStorage.GetRootOperation("chain", `${address}:${txHash}:${index}`)
}
async AddRootInvoicePaid(paymentRequest: string, amount: number) { async AddRootInvoicePaid(paymentRequest: string, amount: number) {
await this.storage.metricsStorage.AddRootOperation("invoice", paymentRequest, amount) await this.storage.metricsStorage.AddRootOperation("invoice", paymentRequest, amount)
} }

View file

@ -1,34 +1,37 @@
//import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools' //import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools'
import WebSocket from 'ws' /* import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket }); Object.assign(global, { WebSocket: WebSocket }); */
import crypto from 'crypto' /* import crypto from 'crypto'
import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44 } from 'nostr-tools' import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools' */
import { ERROR, getLogger } from '../helpers/logger.js' import { ERROR, getLogger } from '../helpers/logger.js'
import { nip19 } from 'nostr-tools' /* import { nip19 } from 'nostr-tools'
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' */
import { ProcessMetrics, ProcessMetricsCollector } from '../storage/tlv/processMetricsCollector.js' import { ProcessMetrics, ProcessMetricsCollector } from '../storage/tlv/processMetricsCollector.js'
import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js'; /* import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js';
const { nprofileEncode } = nip19 const { nprofileEncode } = nip19
const { v2 } = nip44 const { v2 } = nip44
const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2 const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2
const { getConversationKey: getConversationKeyV2 } = utils const { getConversationKey: getConversationKeyV2 } = utils */
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 }
type SendDataContent = { type: "content", content: string, pub: string } type SendDataContent = { type: "content", content: string, pub: string }
type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } } type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
export type SendData = SendDataContent | SendDataEvent export type SendData = SendDataContent | SendDataEvent
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 */
import { NostrPool } from './nostrPool.js'
import { NostrSettings, SendInitiator, SendData, NostrEvent, NostrSend } from './nostrPool.js'
export type NostrSettings = { /* export type NostrSettings = {
apps: AppInfo[] apps: AppInfo[]
relays: string[] relays: string[]
clients: ClientInfo[] clients: ClientInfo[]
maxEventContentLength: number maxEventContentLength: number
} providerDestinationPub: string
} */
export type NostrEvent = { /* export type NostrEvent = {
id: string id: string
pub: string pub: string
content: string content: string
@ -36,7 +39,8 @@ export type NostrEvent = {
startAtNano: string startAtNano: string
startAtMs: number startAtMs: number
kind: number kind: number
} relayConstraint?: 'service' | 'provider'
} */
type SettingsRequest = { type SettingsRequest = {
type: 'settings' type: 'settings'
@ -69,9 +73,14 @@ type ProcessMetricsResponse = {
type: 'processMetrics' type: 'processMetrics'
metrics: ProcessMetrics metrics: ProcessMetrics
} }
type BeaconResponse = {
type: 'beacon'
content: string
pub: string
}
export type ChildProcessRequest = SettingsRequest | SendRequest | PingRequest export type ChildProcessRequest = SettingsRequest | SendRequest | PingRequest
export type ChildProcessResponse = ReadyResponse | EventResponse | ProcessMetricsResponse | PingResponse export type ChildProcessResponse = ReadyResponse | EventResponse | ProcessMetricsResponse | PingResponse | BeaconResponse
const send = (message: ChildProcessResponse) => { const send = (message: ChildProcessResponse) => {
if (process.send) { if (process.send) {
process.send(message, undefined, undefined, err => { process.send(message, undefined, undefined, err => {
@ -82,7 +91,7 @@ const send = (message: ChildProcessResponse) => {
}) })
} }
} }
let subProcessHandler: Handler | undefined let subProcessHandler: NostrPool | undefined
process.on("message", (message: ChildProcessRequest) => { process.on("message", (message: ChildProcessRequest) => {
switch (message.type) { switch (message.type) {
case 'settings': case 'settings':
@ -102,11 +111,15 @@ process.on("message", (message: ChildProcessRequest) => {
const handleNostrSettings = (settings: NostrSettings) => { const handleNostrSettings = (settings: NostrSettings) => {
if (subProcessHandler) { if (subProcessHandler) {
getLogger({ component: "nostrMiddleware" })("got new nostr setting, resetting nostr handler") getLogger({ component: "nostrMiddleware" })("got new nostr setting, resetting nostr handler")
subProcessHandler.Stop() subProcessHandler.UpdateSettings(settings)
initNostrHandler(settings) // initNostrHandler(settings)
return return
} }
initNostrHandler(settings) subProcessHandler = new NostrPool(event => {
send(event)
})
subProcessHandler.UpdateSettings(settings)
// initNostrHandler(settings)
new ProcessMetricsCollector((metrics) => { new ProcessMetricsCollector((metrics) => {
send({ send({
type: 'processMetrics', type: 'processMetrics',
@ -114,14 +127,11 @@ const handleNostrSettings = (settings: NostrSettings) => {
}) })
}) })
} }
const initNostrHandler = (settings: NostrSettings) => { /* const initNostrHandler = (settings: NostrSettings) => {
subProcessHandler = new Handler(settings, event => { subProcessHandler = new NostrPool(event => {
send({ send(event)
type: 'event',
event: event
})
}) })
} } */
const sendToNostr: NostrSend = (initiator, data, relays) => { const sendToNostr: NostrSend = (initiator, data, relays) => {
if (!subProcessHandler) { if (!subProcessHandler) {
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized") getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized")
@ -131,7 +141,7 @@ const sendToNostr: NostrSend = (initiator, data, relays) => {
} }
send({ type: 'ready' }) send({ type: 'ready' })
const supportedKinds = [21000, 21001, 21002, 21003] /* const supportedKinds = [21000, 21001, 21002, 21003]
export default class Handler { export default class Handler {
pool = new SimplePool() pool = new SimplePool()
settings: NostrSettings settings: NostrSettings
@ -218,18 +228,28 @@ export default class Handler {
appIds: appIds, appIds: appIds,
listeningForPubkeys: appIds listeningForPubkeys: appIds
}) })
const subs: Filter[] = [
return relay.subscribe([
{ {
since: Math.ceil(Date.now() / 1000), since: Math.ceil(Date.now() / 1000),
kinds: supportedKinds, kinds: supportedKinds,
'#p': appIds, '#p': appIds,
} }
], { ]
if (this.settings.providerDestinationPub) {
subs.push({
kinds: [30078], '#d': ['Lightning.Pub'],
authors: [this.settings.providerDestinationPub]
})
}
return relay.subscribe(subs, {
oneose: () => { oneose: () => {
this.log("up to date with nostr events") this.log("up to date with nostr events")
}, },
onevent: async (e) => { onevent: async (e) => {
if (e.kind === 30078 && e.pubkey === this.settings.providerDestinationPub) {
send({ type: 'beacon', content: e.content, pub: e.pubkey })
return
}
if (!supportedKinds.includes(e.kind) || !e.pubkey) { if (!supportedKinds.includes(e.kind) || !e.pubkey) {
return return
} }
@ -366,4 +386,4 @@ const splitContent = (content: string, maxLength: number) => {
parts.push(content.slice(i, i + maxLength)) parts.push(content.slice(i, i + maxLength))
} }
return parts return parts
} } */

View file

@ -1,19 +1,17 @@
import { ChildProcess, fork } from 'child_process' import { ChildProcess, fork } from 'child_process'
import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, SendData, SendInitiator } from "./handler.js" import { NostrSettings, NostrEvent, SendData, SendInitiator } from "./nostrPool.js"
import { ChildProcessRequest, ChildProcessResponse } from "./handler.js"
import { Utils } from '../helpers/utilsWrapper.js' import { Utils } from '../helpers/utilsWrapper.js'
import { getLogger, ERROR } from '../helpers/logger.js' import { getLogger, ERROR } from '../helpers/logger.js'
type EventCallback = (event: NostrEvent) => void type EventCallback = (event: NostrEvent) => void
type BeaconCallback = (beacon: { content: string, pub: string }) => void
export default class NostrSubprocess { export default class NostrSubprocess {
childProcess: ChildProcess childProcess: ChildProcess
utils: Utils utils: Utils
awaitingPongs: (() => void)[] = [] awaitingPongs: (() => void)[] = []
log = getLogger({}) log = getLogger({})
constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback) { constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) {
this.utils = utils this.utils = utils
this.childProcess = fork("./build/src/services/nostr/handler") this.childProcess = fork("./build/src/services/nostr/handler")
this.childProcess.on("error", (error) => { this.childProcess.on("error", (error) => {
@ -43,6 +41,9 @@ export default class NostrSubprocess {
this.awaitingPongs.forEach(resolve => resolve()) this.awaitingPongs.forEach(resolve => resolve())
this.awaitingPongs = [] this.awaitingPongs = []
break break
case 'beacon':
beaconCallback({ content: message.content, pub: message.pub })
break
default: default:
console.error("unknown nostr event response", message) console.error("unknown nostr event response", message)
break; break;

View file

@ -0,0 +1,325 @@
import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket });
import crypto from 'crypto'
import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools'
import { ERROR, getLogger, PubLogger } from '../helpers/logger.js'
import { nip19 } from 'nostr-tools'
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js'
import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js';
import { RelayConnection, RelaySettings } from './nostrRelayConnection.js'
const { nprofileEncode } = nip19
const { v2 } = nip44
const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2
const { getConversationKey: getConversationKeyV2 } = utils
// const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
export type SendDataContent = { type: "content", content: string, pub: string }
export type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
export type SendData = SendDataContent | SendDataEvent
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void
export type LinkedProviderInfo = { pubkey: string, clientId: string, relayUrl: string }
export type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string, provider?: LinkedProviderInfo }
// export type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string }
export type NostrSettings = {
apps: AppInfo[]
relays: string[]
// clients: ClientInfo[]
maxEventContentLength: number
// providerDestinationPub: string
}
export type NostrEvent = {
id: string
pub: string
content: string
appId: string
startAtNano: string
startAtMs: number
kind: number
relayConstraint?: 'service' | 'provider'
}
type RelayEvent = { type: 'event', event: NostrEvent } | { type: 'beacon', content: string, pub: string }
type RelayEventCallback = (event: RelayEvent) => void
const splitContent = (content: string, maxLength: number) => {
const parts = []
for (let i = 0; i < content.length; i += maxLength) {
parts.push(content.slice(i, i + maxLength))
}
return parts
}
const actionKinds = [21000, 21001, 21002, 21003]
const beaconKind = 30078
const appTag = "Lightning.Pub"
export class NostrPool {
relays: Record<string, RelayConnection> = {}
apps: Record<string /* app pubKey */, AppInfo> = {}
maxEventContentLength: number
// providerDestinationPub: string | undefined
eventCallback: RelayEventCallback
log = getLogger({ component: "nostrMiddleware" })
handledEvents: Map<string, { handledAt: number }> = new Map() // add expiration handler
providerInfo: (LinkedProviderInfo & { appPub: string }) | undefined = undefined
cleanupInterval: NodeJS.Timeout | undefined = undefined
constructor(eventCallback: RelayEventCallback) {
this.eventCallback = eventCallback
}
StartCleanupInterval() {
this.cleanupInterval = setInterval(() => {
this.handledEvents.forEach((value, key) => {
if (Date.now() - value.handledAt > 1000 * 60 * 60 * 2) {
this.handledEvents.delete(key)
}
})
}, 1000 * 60 * 60 * 1)
}
UpdateSettings(settings: NostrSettings) {
Object.values(this.relays).forEach(relay => relay.Stop())
settings.apps.forEach(app => {
this.log("appId:", app.appId, "pubkey:", app.publicKey, "nprofile:", nprofileEncode({ pubkey: app.publicKey, relays: settings.relays }))
})
this.maxEventContentLength = settings.maxEventContentLength
const { apps, rSettings, providerInfo } = processApps(settings)
this.providerInfo = providerInfo
this.apps = apps
this.relays = {}
for (const r of rSettings) {
this.relays[r.relayUrl] = new RelayConnection(r, (e, r) => this.onEvent(e, r))
}
}
private onEvent = (e: Event, relay: RelayConnection) => {
const validated = this.validateEvent(e, relay)
if (!validated || this.handledEvents.has(e.id)) {
return
}
this.handledEvents.set(e.id, { handledAt: Date.now() })
if (validated.type === 'beacon') {
this.eventCallback({ type: 'beacon', content: e.content, pub: validated.pub })
return
}
const { app } = validated
const startAtMs = Date.now()
const startAtNano = process.hrtime.bigint().toString()
let content = ""
try {
if (e.kind === 21000) {
content = decryptV1(e.content, getConversationKeyV1(app.privateKey, e.pubkey))
} else {
content = decryptV2(e.content, getConversationKeyV2(Buffer.from(app.privateKey, 'hex'), e.pubkey))
}
} catch (e: any) {
this.log(ERROR, "failed to decrypt event", e.message, e.content)
return
}
const relayConstraint = relay.getConstraint()
const nostrEvent: NostrEvent = { id: e.id, content, pub: e.pubkey, appId: app.appId, startAtNano, startAtMs, kind: e.kind, relayConstraint }
this.eventCallback({ type: 'event', event: nostrEvent })
}
private validateEvent(e: Event, relay: RelayConnection): { type: 'event', pub: string, app: AppInfo } | { type: 'beacon', content: string, pub: string } | null {
if (e.kind === 30078 && this.providerInfo && e.pubkey === this.providerInfo.pubkey) {
// Accept beacons from provider relay (which may also be a service relay)
if (relay.isProviderRelay()) {
return { type: 'beacon', content: e.content, pub: e.pubkey }
}
}
if (!actionKinds.includes(e.kind) || !e.pubkey) {
return null
}
const pubTags = e.tags.find(tags => tags && tags.length > 1 && tags[0] === 'p')
if (!pubTags) {
return null
}
const app = this.apps[pubTags[1]]
if (!app) {
return null
}
return { type: 'event', pub: e.pubkey, app }
}
async Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
try {
const keys = this.getSendKeys(initiator)
const r = this.getRelays(initiator, relays)
const privateKey = Buffer.from(keys.privateKey, 'hex')
const toSign = await this.handleSend(data, keys)
await Promise.all(toSign.map(ue => this.sendEvent(ue, keys, r)))
} catch (e: any) {
this.log(ERROR, "failed to send event", e.message || e)
throw e
}
}
private async handleSend(data: SendData, keys: { name: string, privateKey: string, publicKey: string }): Promise<UnsignedEvent[]> {
if (data.type === 'content') {
const parts = splitContent(data.content, this.maxEventContentLength)
if (parts.length > 1) {
const shardsId = crypto.randomBytes(16).toString('hex')
const totalShards = parts.length
const ues = await Promise.all(parts.map((part, index) => this.handleSendDataContent({ ...data, content: JSON.stringify({ part, index, totalShards, shardsId }) }, keys)))
return ues
}
return [await this.handleSendDataContent(data, keys)]
}
const ue = await this.handleSendDataEvent(data, keys)
return [ue]
}
private async handleSendDataContent(data: SendDataContent, keys: { name: string, privateKey: string, publicKey: string }): Promise<UnsignedEvent> {
let content = encryptV1(data.content, getConversationKeyV1(keys.privateKey, data.pub))
return {
content,
created_at: Math.floor(Date.now() / 1000),
kind: 21000,
pubkey: keys.publicKey,
tags: [['p', data.pub]],
}
}
private async handleSendDataEvent(data: SendDataEvent, keys: { name: string, privateKey: string, publicKey: string }): Promise<UnsignedEvent> {
const toSign = data.event
if (data.encrypt) {
toSign.content = encryptV2(data.event.content, getConversationKeyV2(Buffer.from(keys.privateKey, 'hex'), data.encrypt.toPub))
}
if (!toSign.pubkey) {
toSign.pubkey = keys.publicKey
}
return toSign
}
private getServiceRelays() {
return Object.values(this.relays).filter(r => r.isServiceRelay()).map(r => r.GetUrl())
}
private getProviderRelays() {
return Object.values(this.relays).filter(r => r.isProviderRelay()).map(r => r.GetUrl())
}
private async sendEvent(event: UnsignedEvent, keys: { name: string, privateKey: string }, relays: string[]) {
const signed = finalizeEvent(event, Buffer.from(keys.privateKey, 'hex'))
let sent = false
const log = getLogger({ appName: keys.name })
// const r = relays ? relays : this.getServiceRelays()
const pool = new SimplePool()
await Promise.all(pool.publish(relays, signed).map(async p => {
try {
await p
sent = true
} catch (e: any) {
console.log(e)
log(e)
}
}))
if (!sent) {
log("failed to send event")
} else {
//log("sent event")
}
}
private getRelays(initiator: SendInitiator, requestRelays?: string[]) {
if (requestRelays) {
return requestRelays
}
if (initiator.type === 'app') {
return this.getServiceRelays()
} else if (initiator.type === 'client') {
return this.getProviderRelays()
}
throw new Error("unkown initiator type")
}
private getSendKeys(initiator: SendInitiator) {
if (initiator.type === 'app') {
const { appId } = initiator
const found = Object.values(this.apps).find((info: AppInfo) => info.appId === appId)
if (!found) {
throw new Error("unkown app")
}
return { name: found.name, publicKey: found.publicKey, privateKey: found.privateKey }
} else if (initiator.type === 'client') {
const { clientId } = initiator
const providerApp = this.apps[this.providerInfo?.appPub || ""]
if (this.providerInfo && this.providerInfo.clientId === clientId && providerApp) {
return { name: providerApp.name, publicKey: providerApp.publicKey, privateKey: providerApp.privateKey }
}
throw new Error("unkown client")
}
throw new Error("unkown initiator type")
}
}
const processApps = (settings: NostrSettings) => {
const apps: Record<string, AppInfo> = {}
let providerInfo: (LinkedProviderInfo & { appPub: string }) | undefined = undefined
for (const app of settings.apps) {
apps[app.publicKey] = app
// add provider info if the app has a provider
if (app.provider) {
// make sure only one provider is configured
if (providerInfo) {
throw new Error("found more than one provider")
}
providerInfo = { ...app.provider, appPub: app.publicKey }
}
}
let providerAssigned = false
const rSettings: RelaySettings[] = []
new Set(settings.relays).forEach(r => {
const filters = [getServiceFilter(apps)]
// check if this service relay is also a provider relay, and add the beacon filter if so
if (providerInfo && providerInfo.relayUrl === r) {
providerAssigned = true
filters.push(getBeaconFilter(providerInfo.pubkey))
}
// add the relay settings to the list
rSettings.push({
relayUrl: r,
serviceRelay: true,
providerRelay: r === providerInfo?.relayUrl,
filters: filters,
})
})
// if no provider was assigned to a service relay, add the provider relay settings with a provider filter
if (!providerAssigned && providerInfo) {
rSettings.push({
relayUrl: providerInfo.relayUrl,
providerRelay: true,
serviceRelay: false,
filters: [
getProviderFilter(providerInfo.appPub, providerInfo.pubkey),
getBeaconFilter(providerInfo.pubkey),
],
})
}
return { apps, rSettings, providerInfo }
}
const getServiceFilter = (apps: Record<string, AppInfo>): Filter => {
return {
since: Math.ceil(Date.now() / 1000),
kinds: actionKinds,
'#p': Object.keys(apps),
}
}
const getProviderFilter = (appPub: string, providerPub: string): Filter => {
return {
since: Math.ceil(Date.now() / 1000),
kinds: actionKinds,
'#p': [appPub],
authors: [providerPub]
}
}
const getBeaconFilter = (providerPub: string): Filter => {
return {
kinds: [beaconKind], '#d': [appTag],
authors: [providerPub]
}
}

View file

@ -0,0 +1,152 @@
import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket });
import { Event, UnsignedEvent, Relay, Filter } from 'nostr-tools'
import { ERROR, getLogger, PubLogger } from '../helpers/logger.js'
import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js';
// const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
/* export type SendDataContent = { type: "content", content: string, pub: string }
export type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
export type SendData = SendDataContent | SendDataEvent
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void */
/* export type LinkedProviderInfo = { pubDestination: string, clientId: string, relayUrl: string }
export type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string, provider?: LinkedProviderInfo } */
// export type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string }
/* export type NostrSettings = {
apps: AppInfo[]
relays: string[]
// clients: ClientInfo[]
maxEventContentLength: number
// providerDestinationPub: string
}
export type NostrEvent = {
id: string
pub: string
content: string
appId: string
startAtNano: string
startAtMs: number
kind: number
relayConstraint?: 'service' | 'provider'
} */
type RelayCallback = (event: Event, relay: RelayConnection) => void
export type RelaySettings = { relayUrl: string, filters: Filter[], serviceRelay: boolean, providerRelay: boolean }
export class RelayConnection {
eventCallback: RelayCallback
log: PubLogger
relay: Relay | null = null
sub: Subscription | null = null
// relayUrl: string
stopped = false
// filters: Filter[]
settings: RelaySettings
constructor(settings: RelaySettings, eventCallback: RelayCallback, autoconnect = true) {
this.log = getLogger({ component: "relay:" + settings.relayUrl })
// this.relayUrl = relayUrl
// this.filters = filters
this.settings = settings
this.eventCallback = eventCallback
if (autoconnect) {
this.ConnectLoop()
}
}
GetUrl() {
return this.settings.relayUrl
}
Stop() {
this.stopped = true
this.sub?.close()
this.relay?.close()
this.relay = null
this.sub = null
}
isServiceRelay() {
return this.settings.serviceRelay
}
isProviderRelay() {
return this.settings.providerRelay
}
getConstraint(): 'service' | 'provider' | undefined {
if (this.isProviderRelay() && !this.isServiceRelay()) {
return 'provider'
}
if (this.isServiceRelay() && !this.isProviderRelay()) {
return 'service'
}
return undefined
}
async ConnectLoop() {
let failures = 0
while (!this.stopped) {
await this.ConnectPromise()
const pow = Math.pow(2, failures)
const delay = Math.min(pow, 900)
this.log("connection failed, will try again in", delay, "seconds (failures:", failures, ")")
await new Promise(resolve => setTimeout(resolve, delay * 1000))
failures++
}
this.log("nostr handler stopped")
}
async ConnectPromise() {
return new Promise<void>(async (res) => {
this.relay = await this.GetRelay()
if (!this.relay) {
res()
return
}
this.sub = this.Subscribe(this.relay)
this.relay.onclose = (() => {
this.log("disconnected")
this.sub?.close()
if (this.relay) {
this.relay.onclose = null
this.relay.close()
this.relay = null
}
this.sub = null
res()
})
})
}
async GetRelay(): Promise<Relay | null> {
try {
const relay = await Relay.connect(this.settings.relayUrl)
if (!relay.connected) {
throw new Error("failed to connect to relay")
}
return relay
} catch (err: any) {
this.log("failed to connect to relay", err.message || err)
return null
}
}
Subscribe(relay: Relay) {
this.log("🔍 subscribing...")
return relay.subscribe(this.settings.filters, {
oneose: () => this.log("is ready"),
onevent: (e) => this.eventCallback(e, this)
})
}
Send(e: Event) {
if (!this.relay) {
throw new Error("relay not connected")
}
return this.relay.publish(e)
}
}

View file

@ -0,0 +1,36 @@
import { NostrSend, SendData, SendInitiator } from "./nostrPool.js"
import { getLogger } from "../helpers/logger.js"
export class NostrSender {
private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') }
private isReady: boolean = false
private onReadyCallbacks: (() => void)[] = []
private pendingSends: { initiator: SendInitiator, data: SendData, relays?: string[] | undefined }[] = []
private log = getLogger({ component: "nostrSender" })
AttachNostrSend(nostrSend: NostrSend) {
this._nostrSend = nostrSend
this.isReady = true
this.onReadyCallbacks.forEach(cb => cb())
this.onReadyCallbacks = []
this.pendingSends.forEach(send => this._nostrSend(send.initiator, send.data, send.relays))
this.pendingSends = []
}
OnReady(callback: () => void) {
if (this.isReady) {
callback()
} else {
this.onReadyCallbacks.push(callback)
}
}
Send(initiator: SendInitiator, data: SendData, relays?: string[] | undefined) {
if (!this.isReady) {
this.log("tried to send before nostr was ready, caching request")
this.pendingSends.push({ initiator, data, relays })
return
}
this._nostrSend(initiator, data, relays)
}
IsReady() {
return this.isReady
}
}

View file

@ -91,6 +91,21 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.adminManager.CloseChannel(req) return mainHandler.adminManager.CloseChannel(req)
}, },
GetAdminTransactionSwapQuotes: async ({ ctx, req }) => {
const err = Types.TransactionSwapRequestValidate(req, {
transaction_amount_sats_CustomCheck: amt => amt > 0
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.GetAdminTransactionSwapQuotes(req)
},
PayAdminTransactionSwap: async ({ ctx, req }) => {
const err = Types.PayAdminTransactionSwapRequestValidate(req, {
address_CustomCheck: addr => addr !== '',
swap_operation_id_CustomCheck: id => id !== '',
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.PayAdminTransactionSwap(req)
},
GetProvidersDisruption: async () => { GetProvidersDisruption: async () => {
return mainHandler.metricsManager.GetProvidersDisruption() return mainHandler.metricsManager.GetProvidersDisruption()
}, },
@ -130,6 +145,9 @@ export default (mainHandler: Main): Types.ServerMethods => {
GetUserOperations: async ({ ctx, req }) => { GetUserOperations: async ({ ctx, req }) => {
return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req) return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req)
}, },
ListAdminSwaps: async ({ ctx }) => {
return mainHandler.adminManager.ListAdminSwaps()
},
GetPaymentState: async ({ ctx, req }) => { GetPaymentState: async ({ ctx, req }) => {
const err = Types.GetPaymentStateRequestValidate(req, { const err = Types.GetPaymentStateRequestValidate(req, {
invoice_CustomCheck: invoice => invoice !== "" invoice_CustomCheck: invoice => invoice !== ""
@ -141,12 +159,18 @@ export default (mainHandler: Main): Types.ServerMethods => {
PayAddress: async ({ ctx, req }) => { PayAddress: async ({ ctx, req }) => {
const err = Types.PayAddressRequestValidate(req, { const err = Types.PayAddressRequestValidate(req, {
address_CustomCheck: addr => addr !== '', address_CustomCheck: addr => addr !== '',
amoutSats_CustomCheck: amt => amt > 0, amountSats_CustomCheck: amt => amt > 0,
satsPerVByte_CustomCheck: spb => spb > 0 // satsPerVByte_CustomCheck: spb => spb > 0
}) })
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.paymentManager.PayAddress(ctx, req) return mainHandler.paymentManager.PayAddress(ctx, req)
}, },
ListSwaps: async ({ ctx }) => {
return mainHandler.paymentManager.ListSwaps(ctx)
},
GetTransactionSwapQuotes: async ({ ctx, req }) => {
return mainHandler.paymentManager.GetTransactionSwapQuotes(ctx, req)
},
NewInvoice: ({ ctx, req }) => mainHandler.appUserManager.NewInvoice(ctx, req), NewInvoice: ({ ctx, req }) => mainHandler.appUserManager.NewInvoice(ctx, req),
DecodeInvoice: async ({ ctx, req }) => { DecodeInvoice: async ({ ctx, req }) => {
return mainHandler.paymentManager.DecodeInvoice(req) return mainHandler.paymentManager.DecodeInvoice(req)

View file

@ -29,6 +29,7 @@ import { AppUserDevice } from "../entity/AppUserDevice.js"
import * as fs from 'fs' import * as fs from 'fs'
import { UserAccess } from "../entity/UserAccess.js" import { UserAccess } from "../entity/UserAccess.js"
import { AdminSettings } from "../entity/AdminSettings.js" import { AdminSettings } from "../entity/AdminSettings.js"
import { TransactionSwap } from "../entity/TransactionSwap.js"
export type DbSettings = { export type DbSettings = {
@ -74,7 +75,8 @@ export const MainDbEntities = {
'ManagementGrant': ManagementGrant, 'ManagementGrant': ManagementGrant,
'AppUserDevice': AppUserDevice, 'AppUserDevice': AppUserDevice,
'UserAccess': UserAccess, 'UserAccess': UserAccess,
'AdminSettings': AdminSettings 'AdminSettings': AdminSettings,
'TransactionSwap': TransactionSwap
} }
export type MainDbNames = keyof typeof MainDbEntities export type MainDbNames = keyof typeof MainDbEntities
export const MainDbEntitiesNames = Object.keys(MainDbEntities) export const MainDbEntitiesNames = Object.keys(MainDbEntities)

View file

@ -18,6 +18,9 @@ export class TrackedProvider {
@Column({ default: 0 }) @Column({ default: 0 })
latest_distruption_at_unix: number latest_distruption_at_unix: number
@Column({ default: 0 })
latest_checked_height: number
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -0,0 +1,74 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, UpdateDateColumn } from "typeorm";
import { User } from "./User";
@Entity()
export class TransactionSwap {
@PrimaryGeneratedColumn('uuid')
swap_operation_id: string
@Column()
app_user_id: string
@Column()
swap_quote_id: string
@Column()
swap_tree: string
@Column()
lockup_address: string
@Column()
refund_public_key: string
@Column()
timeout_block_height: number
@Column()
invoice: string
@Column()
invoice_amount: number
@Column()
transaction_amount: number
@Column()
swap_fee_sats: number
@Column()
chain_fee_sats: number
@Column()
preimage: string
@Column()
ephemeral_public_key: string
// the private key is used on to perform a swap, it does not hold any funds once the swap is completed
// the swap should only last a few seconds, so it is not a security risk to store the private key in the database
// the key is stored here mostly for recovery purposes, in case something goes wrong with the swap
@Column()
ephemeral_private_key: string
@Column({ default: false })
used: boolean
@Column({ default: "" })
failure_reason: string
@Column({ default: "" })
tx_id: string
@Column({ default: "" })
address_paid: string
@Column({ default: "" })
service_url: string
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -47,6 +47,9 @@ export class UserInvoicePayment {
@Column({ nullable: true }) @Column({ nullable: true })
debit_to_pub: string debit_to_pub: string
@Column({ nullable: true })
swap_operation_id: string
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -118,6 +118,10 @@ export default class {
} */ } */
} }
NostrSender() {
return this.utils.nostrSender
}
getStorageSettings(): StorageSettings { getStorageSettings(): StorageSettings {
return this.settings return this.settings
} }

View file

@ -64,4 +64,13 @@ export class LiquidityStorage {
async UpdateTrackedProviderDisruption(providerType: 'lnd' | 'lnPub', pub: string, latestDisruptionAtUnix: number) { async UpdateTrackedProviderDisruption(providerType: 'lnd' | 'lnPub', pub: string, latestDisruptionAtUnix: number) {
return this.dbs.Update<TrackedProvider>('TrackedProvider', { provider_pubkey: pub, provider_type: providerType }, { latest_distruption_at_unix: latestDisruptionAtUnix }) return this.dbs.Update<TrackedProvider>('TrackedProvider', { provider_pubkey: pub, provider_type: providerType }, { latest_distruption_at_unix: latestDisruptionAtUnix })
} }
async GetLatestCheckedHeight(providerType: 'lnd' | 'lnPub', pub: string): Promise<number> {
const provider = await this.GetTrackedProvider(providerType, pub)
return provider?.latest_checked_height || 0
}
async UpdateLatestCheckedHeight(providerType: 'lnd' | 'lnPub', pub: string, height: number) {
return this.dbs.Update<TrackedProvider>('TrackedProvider', { provider_pubkey: pub, provider_type: providerType }, { latest_checked_height: height })
}
} }

View file

@ -149,6 +149,10 @@ export default class {
return this.dbs.CreateAndSave<RootOperation>('RootOperation', { operation_type: opType, operation_amount: amount, operation_identifier: id, at_unix: Math.floor(Date.now() / 1000) }, txId) return this.dbs.CreateAndSave<RootOperation>('RootOperation', { operation_type: opType, operation_amount: amount, operation_identifier: id, at_unix: Math.floor(Date.now() / 1000) }, txId)
} }
async GetRootOperation(opType: string, id: string, txId?: string) {
return this.dbs.FindOne<RootOperation>('RootOperation', { where: { operation_type: opType, operation_identifier: id } }, txId)
}
async GetRootOperations({ from, to }: { from?: number, to?: number }, txId?: string) { async GetRootOperations({ from, to }: { from?: number, to?: number }, txId?: string) {
const q = getTimeQuery({ from, to }) const q = getTimeQuery({ from, to })
return this.dbs.Find<RootOperation>('RootOperation', q, txId) return this.dbs.Find<RootOperation>('RootOperation', q, txId)

View file

@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TxSwap1762890527098 implements MigrationInterface {
name = 'TxSwap1762890527098'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "transaction_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "lockup_address" varchar NOT NULL, "refund_public_key" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "preimage" varchar NOT NULL, "ephemeral_public_key" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
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, "paymentIndex" integer NOT NULL DEFAULT (-1), "debit_to_pub" varchar, "swap_operation_id" varchar, CONSTRAINT "FK_ef2aa6761ab681bbbd5f94e0fcb" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_6bcac90887eea1dc61d37db2994" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("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", "liquidityProvider", "paymentIndex", "debit_to_pub") SELECT "serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId", "liquidityProvider", "paymentIndex", "debit_to_pub" 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, "liquidityProvider" varchar, "paymentIndex" integer NOT NULL DEFAULT (-1), "debit_to_pub" varchar, CONSTRAINT "FK_ef2aa6761ab681bbbd5f94e0fcb" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_6bcac90887eea1dc61d37db2994" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("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", "liquidityProvider", "paymentIndex", "debit_to_pub") SELECT "serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId", "liquidityProvider", "paymentIndex", "debit_to_pub" 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 TABLE "transaction_swap"`);
}
}

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TxSwapAddress1764779178945 implements MigrationInterface {
name = 'TxSwapAddress1764779178945'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "temporary_transaction_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "lockup_address" varchar NOT NULL, "refund_public_key" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "preimage" varchar NOT NULL, "ephemeral_public_key" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "address_paid" varchar NOT NULL DEFAULT (''))`);
await queryRunner.query(`INSERT INTO "temporary_transaction_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at" FROM "transaction_swap"`);
await queryRunner.query(`DROP TABLE "transaction_swap"`);
await queryRunner.query(`ALTER TABLE "temporary_transaction_swap" RENAME TO "transaction_swap"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "transaction_swap" RENAME TO "temporary_transaction_swap"`);
await queryRunner.query(`CREATE TABLE "transaction_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "lockup_address" varchar NOT NULL, "refund_public_key" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "preimage" varchar NOT NULL, "ephemeral_public_key" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
await queryRunner.query(`INSERT INTO "transaction_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at" FROM "temporary_transaction_swap"`);
await queryRunner.query(`DROP TABLE "temporary_transaction_swap"`);
}
}

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TrackedProviderHeight1766504040000 implements MigrationInterface {
name = 'TrackedProviderHeight1766504040000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tracked_provider" ADD "latest_checked_height" integer NOT NULL DEFAULT (0)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tracked_provider" DROP COLUMN "latest_checked_height"`);
}
}

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class SwapsServiceUrl1768413055036 implements MigrationInterface {
name = 'SwapsServiceUrl1768413055036'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "temporary_transaction_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "lockup_address" varchar NOT NULL, "refund_public_key" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "preimage" varchar NOT NULL, "ephemeral_public_key" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "address_paid" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''))`);
await queryRunner.query(`INSERT INTO "temporary_transaction_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at", "address_paid") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at", "address_paid" FROM "transaction_swap"`);
await queryRunner.query(`DROP TABLE "transaction_swap"`);
await queryRunner.query(`ALTER TABLE "temporary_transaction_swap" RENAME TO "transaction_swap"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "transaction_swap" RENAME TO "temporary_transaction_swap"`);
await queryRunner.query(`CREATE TABLE "transaction_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "lockup_address" varchar NOT NULL, "refund_public_key" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "preimage" varchar NOT NULL, "ephemeral_public_key" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "address_paid" varchar NOT NULL DEFAULT (''))`);
await queryRunner.query(`INSERT INTO "transaction_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at", "address_paid") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at", "address_paid" FROM "temporary_transaction_swap"`);
await queryRunner.query(`DROP TABLE "temporary_transaction_swap"`);
}
}

View file

@ -27,14 +27,19 @@ import { UserAccess1759426050669 } from './1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js'
import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js'
import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js' import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js'
import { TxSwap1762890527098 } from './1762890527098-tx_swap.js'
import { TxSwapAddress1764779178945 } from './1764779178945-tx_swap_address.js'
import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js' import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js'
import { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_provider_height.js'
import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, ClinkRequester1765497600000] UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* 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> => {

View file

@ -1,5 +1,5 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual } from "typeorm" import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } 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';
@ -14,6 +14,7 @@ import { Application } from './entity/Application.js';
import TransactionsQueue from "./db/transactionsQueue.js"; import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js'; import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.js'; import { StorageInterface } from './db/storageInterface.js';
import { TransactionSwap } from './entity/TransactionSwap.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string } export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string }
export const defaultInvoiceExpiry = 60 * 60 export const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
@ -160,7 +161,8 @@ export default class {
return this.dbs.FindOne<UserToUserPayment>('UserToUserPayment', { where: { serial_id: serialId } }, txId) return this.dbs.FindOne<UserToUserPayment>('UserToUserPayment', { where: { serial_id: serialId } }, txId)
} }
async AddPendingExternalPayment(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, networkFee: number }, linkedApplication: Application, liquidityProvider: string | undefined, txId: string, debitNpub?: string): Promise<UserInvoicePayment> { async AddPendingExternalPayment(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, networkFee: number }, linkedApplication: Application, liquidityProvider: string | undefined, txId: string, optionals: { debitNpub?: string, swapOperationId?: string } = {}): Promise<UserInvoicePayment> {
const { debitNpub, swapOperationId } = optionals
const user = await this.userStorage.GetUser(userId, txId) const user = await this.userStorage.GetUser(userId, txId)
return this.dbs.CreateAndSave<UserInvoicePayment>('UserInvoicePayment', { return this.dbs.CreateAndSave<UserInvoicePayment>('UserInvoicePayment', {
user, user,
@ -172,7 +174,8 @@ export default class {
internal: false, internal: false,
linkedApplication, linkedApplication,
liquidityProvider, liquidityProvider,
debit_to_pub: debitNpub debit_to_pub: debitNpub,
swap_operation_id: swapOperationId
}, txId) }, txId)
} }
@ -460,6 +463,58 @@ export default class {
} }
return this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', { where }) return this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', { where })
} }
async AddTransactionSwap(swap: Partial<TransactionSwap>) {
return this.dbs.CreateAndSave<TransactionSwap>('TransactionSwap', swap)
}
async GetTransactionSwap(swapOperationId: string, appUserId: string, txId?: string) {
return this.dbs.FindOne<TransactionSwap>('TransactionSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId)
}
async FinalizeTransactionSwap(swapOperationId: string, address: string, txId: string) {
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
tx_id: txId,
address_paid: address,
})
}
async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string) {
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
failure_reason: failureReason,
address_paid: address,
})
}
async DeleteTransactionSwap(swapOperationId: string, txId?: string) {
return this.dbs.Delete<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, txId)
}
async DeleteExpiredTransactionSwaps(currentHeight: number, txId?: string) {
return this.dbs.Delete<TransactionSwap>('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId)
}
async ListPendingTransactionSwaps(appUserId: string, txId?: string) {
return this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId)
}
async ListSwapPayments(userId: string, txId?: string) {
return this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), user: { user_id: userId } } }, txId)
}
async ListCompletedSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) {
const completed = await this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: true, app_user_id: appUserId } }, txId)
// const payments = await this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), } }, txId)
const paymentsMap = new Map<string, UserInvoicePayment>()
payments.forEach(p => {
paymentsMap.set(p.swap_operation_id, p)
})
return completed.map(c => ({
swap: c, payment: paymentsMap.get(c.swap_operation_id)
}))
}
} }
const orFail = async <T>(resultPromise: Promise<T | null>) => { const orFail = async <T>(resultPromise: Promise<T | null>) => {

View file

@ -2,11 +2,12 @@ import { ChildProcess, fork } from 'child_process';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { AddTlvOperation, ITlvStorageOperation, SuccessTlvOperationResponse, LoadLatestTlvOperation, LoadTlvFileOperation, NewTlvStorageOperation, SerializableLatestData, SerializableTlvFile, TlvOperationResponse, TlvStorageSettings, WebRtcMessageOperation, ProcessMetricsTlvOperation, ZipStoragesOperation, ResetTlvStorageOperation, PingTlvOperation } from './tlvFilesStorageProcessor'; import { AddTlvOperation, ITlvStorageOperation, SuccessTlvOperationResponse, LoadLatestTlvOperation, LoadTlvFileOperation, NewTlvStorageOperation, SerializableLatestData, SerializableTlvFile, TlvOperationResponse, TlvStorageSettings, WebRtcMessageOperation, ProcessMetricsTlvOperation, ZipStoragesOperation, ResetTlvStorageOperation, PingTlvOperation } from './tlvFilesStorageProcessor';
import { LatestData, TlvFile } from './tlvFilesStorage'; import { LatestData, TlvFile } from './tlvFilesStorage';
import { NostrSend, SendData, SendInitiator } from '../../nostr/handler'; import { SendData, SendInitiator } from '../../nostr/nostrPool.js';
import { WebRtcUserInfo } from '../../webRTC'; import { WebRtcUserInfo } from '../../webRTC';
import * as Types from '../../../../proto/autogenerated/ts/types.js' import * as Types from '../../../../proto/autogenerated/ts/types.js'
import { ProcessMetrics } from './processMetricsCollector'; import { ProcessMetrics } from './processMetricsCollector';
import { getLogger, ERROR } from '../../helpers/logger.js'; import { getLogger, ERROR } from '../../helpers/logger.js';
import { NostrSender } from '../../nostr/sender';
export type TlvStorageInterface = { export type TlvStorageInterface = {
AddTlv: (appId: string, dataName: string, tlv: Uint8Array) => Promise<number> AddTlv: (appId: string, dataName: string, tlv: Uint8Array) => Promise<number>
LoadLatest: (limit?: number) => Promise<LatestData> LoadLatest: (limit?: number) => Promise<LatestData>
@ -17,12 +18,14 @@ export class TlvStorageFactory extends EventEmitter {
private process: ChildProcess; private process: ChildProcess;
private isConnected: boolean = false; private isConnected: boolean = false;
private debug: boolean = false; private debug: boolean = false;
private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') } // private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') }
private nostrSender: NostrSender
private allowResetMetricsStorages: boolean private allowResetMetricsStorages: boolean
log = getLogger({component: 'TlvStorageFactory'}) log = getLogger({ component: 'TlvStorageFactory' })
constructor(allowResetMetricsStorages: boolean) { constructor(allowResetMetricsStorages: boolean, nostrSender: NostrSender) {
super(); super();
this.allowResetMetricsStorages = allowResetMetricsStorages this.allowResetMetricsStorages = allowResetMetricsStorages
this.nostrSender = nostrSender
this.initializeSubprocess(); this.initializeSubprocess();
} }
@ -30,15 +33,8 @@ export class TlvStorageFactory extends EventEmitter {
this.debug = debug; this.debug = debug;
} }
attachNostrSend(f: NostrSend) {
this._nostrSend = f
}
private nostrSend = (opResponse: SuccessTlvOperationResponse<{ initiator: SendInitiator, data: SendData, relays?: string[] }>) => { private nostrSend = (opResponse: SuccessTlvOperationResponse<{ initiator: SendInitiator, data: SendData, relays?: string[] }>) => {
if (!this._nostrSend) { this.nostrSender.Send(opResponse.data.initiator, opResponse.data.data, opResponse.data.relays)
throw new Error("No nostrSend attached")
}
this._nostrSend(opResponse.data.initiator, opResponse.data.data, opResponse.data.relays)
} }
private initializeSubprocess() { private initializeSubprocess() {
@ -134,10 +130,15 @@ export class TlvStorageFactory extends EventEmitter {
return this.handleOp<Types.WebRtcAnswer>(op) return this.handleOp<Types.WebRtcAnswer>(op)
} }
ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise<void> { async ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise<void> {
const opId = Math.random().toString() const opId = Math.random().toString()
const op: ProcessMetricsTlvOperation = { type: 'processMetrics', opId, metrics, processName } const op: ProcessMetricsTlvOperation = { type: 'processMetrics', opId, metrics, processName }
return this.handleOp<void>(op) try {
return this.handleOp<void>(op)
} catch (error: any) {
this.log(ERROR, 'Error processing metrics', error.message)
}
return
} }

View file

@ -2,8 +2,8 @@ import { PubLogger, getLogger } from '../../helpers/logger.js';
import webRTC, { WebRtcUserInfo } from '../../webRTC/index.js'; import webRTC, { WebRtcUserInfo } from '../../webRTC/index.js';
import { TlvFilesStorage } from './tlvFilesStorage.js'; import { TlvFilesStorage } from './tlvFilesStorage.js';
import * as Types from '../../../../proto/autogenerated/ts/types.js' import * as Types from '../../../../proto/autogenerated/ts/types.js'
import { SendData } from '../../nostr/handler.js'; import { SendData } from '../../nostr/nostrPool.js';
import { SendInitiator } from '../../nostr/handler.js'; import { SendInitiator } from '../../nostr/nostrPool.js';
import { ProcessMetrics, ProcessMetricsCollector } from './processMetricsCollector.js'; import { ProcessMetrics, ProcessMetricsCollector } from './processMetricsCollector.js';
import { integerToUint8Array } from '../../helpers/tlv.js'; import { integerToUint8Array } from '../../helpers/tlv.js';
import { zip } from 'zip-a-folder' import { zip } from 'zip-a-folder'

View file

@ -3,7 +3,7 @@ import wrtc from 'wrtc'
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { ERROR, getLogger } from "../helpers/logger.js" import { ERROR, getLogger } from "../helpers/logger.js"
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { NostrSend, SendData, SendInitiator } from "../nostr/handler.js" import { NostrSend, SendData, SendInitiator } from "../nostr/nostrPool.js"
import { encodeTLbV, encodeTLV, encodeTLVDataPacket } from '../helpers/tlv.js' import { encodeTLbV, encodeTLV, encodeTLVDataPacket } from '../helpers/tlv.js'
import { Utils } from '../helpers/utilsWrapper.js' import { Utils } from '../helpers/utilsWrapper.js'
import { TlvFilesStorage } from '../storage/tlv/tlvFilesStorage.js' import { TlvFilesStorage } from '../storage/tlv/tlvFilesStorage.js'
@ -111,7 +111,7 @@ export default class webRTC {
const packet = packets[i] const packet = packets[i]
const tlv = encodeTLVDataPacket({ dataId: id, packetNum: i + 1, totalPackets: packets.length, data: packet }) const tlv = encodeTLVDataPacket({ dataId: id, packetNum: i + 1, totalPackets: packets.length, data: packet })
const bytes = encodeTLbV(tlv) const bytes = encodeTLbV(tlv)
channel.send(bytes) channel.send(new Uint8Array(bytes))
} }
} catch (e: any) { } catch (e: any) {
this.log(ERROR, 'ondatachannel', e.message || e) this.log(ERROR, 'ondatachannel', e.message || e)

View file

@ -5,7 +5,7 @@ DATABASE_FILE=db.sqlite
JWT_SECRET=bigsecrethere JWT_SECRET=bigsecrethere
ALLOW_BALANCE_MIGRATION=true ALLOW_BALANCE_MIGRATION=true
OUTBOUND_MAX_FEE_BPS=60 OUTBOUND_MAX_FEE_BPS=60
OUTBOUND_MAX_FEE_EXTRA_SATS=100 OUTBOUND_MAX_FEE_EXTRA_SATS=10
INCOMING_CHAIN_FEE_ROOT_BPS=0 INCOMING_CHAIN_FEE_ROOT_BPS=0
OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets
INCOMING_INVOICE_FEE_ROOT_BPS=0 INCOMING_INVOICE_FEE_ROOT_BPS=0

View file

@ -24,11 +24,10 @@ const testSuccessfulExternalPayment = async (T: TestBase) => {
T.d("paid 500 sats invoice from user1") T.d("paid 500 sats invoice from user1")
const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId)
const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id)
expect(u1.balance_sats).to.be.equal(1496) expect(u1.balance_sats).to.be.equal(1490)
T.d("user1 balance is now 1496 (2000 - (500 + 3 fee + 1 routing))") T.d("user1 balance is now 1490 (2000 - (500 + 10fee))")
expect(owner.balance_sats).to.be.equal(3) expect(owner.balance_sats).to.be.equal(9)
T.d("app balance is 3 sats") T.d("app balance is 9 sats")
} }
const testFailedExternalPayment = async (T: TestBase) => { const testFailedExternalPayment = async (T: TestBase) => {
@ -41,11 +40,11 @@ const testFailedExternalPayment = async (T: TestBase) => {
await expectThrowsAsync(T.main.paymentManager.PayInvoice(T.user1.userId, { invoice: invoice.payRequest, amount: 0 }, application), "not enough balance to decrement") await expectThrowsAsync(T.main.paymentManager.PayInvoice(T.user1.userId, { invoice: invoice.payRequest, amount: 0 }, application), "not enough balance to decrement")
T.d("payment failed as expected, with the expected error message") T.d("payment failed as expected, with the expected error message")
const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId)
expect(u1.balance_sats).to.be.equal(1496) expect(u1.balance_sats).to.be.equal(1490)
T.d("user1 balance is still 1496") T.d("user1 balance is still 1490")
const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id)
expect(owner.balance_sats).to.be.equal(3) expect(owner.balance_sats).to.be.equal(9)
T.d("app balance is still 3 sats") T.d("app balance is still 9 sats")
} }
const testSuccesfulReceivedExternalChainPayment = async (T: TestBase) => { const testSuccesfulReceivedExternalChainPayment = async (T: TestBase) => {

View file

@ -23,10 +23,10 @@ const testSuccessfulInternalPayment = async (T: TestBase) => {
const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id)
expect(u2.balance_sats).to.be.equal(1000) expect(u2.balance_sats).to.be.equal(1000)
T.d("user2 balance is 1000") T.d("user2 balance is 1000")
expect(u1.balance_sats).to.be.equal(994) expect(u1.balance_sats).to.be.equal(990)
T.d("user1 balance is 994 cuz he paid 6 sats fee") T.d("user1 balance is 990 cuz he paid 10 sats fee")
expect(owner.balance_sats).to.be.equal(6) expect(owner.balance_sats).to.be.equal(10)
T.d("app balance is 6 sats") T.d("app balance is 10 sats")
} }
const testFailedInternalPayment = async (T: TestBase) => { const testFailedInternalPayment = async (T: TestBase) => {

View file

@ -21,30 +21,30 @@ export default async (T: TestBase) => {
const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => { const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => {
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: 3000, memo: "liquidityTest" })
await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100, 2000, { from: 'system', useProvider: false }) await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, { routingFeeLimit: 100, serviceFee: 100 }, 3000, { 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(3000)
T.d("user balance is 2000") T.d("user balance is 3000")
const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance()
T.expect(providerBalance).to.equal(2000) T.expect(providerBalance).to.equal(3000)
T.d("provider balance is 2000") T.d("provider balance is 3000")
T.d("testInboundPaymentFromProvider done") T.d("testInboundPaymentFromProvider done")
} }
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, { from: 'system', useProvider: false }) const invoice = await T.externalAccessToOtherLnd.NewInvoice(2000, "", 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(988) // 3000 - (2000 + 12)
const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance()
T.expect(providerBalance).to.equal(992) // 2000 - (1000 + 6 +2) T.expect(providerBalance).to.equal(988) // 3000 - (2000 + 12)
T.d("testOutboundPaymentFromProvider done") T.d("testOutboundPaymentFromProvider done")
} }

View file

@ -8,6 +8,7 @@ import LND from '../services/lnd/lnd.js'
import { LiquidityProvider } from "../services/main/liquidityProvider.js" import { LiquidityProvider } from "../services/main/liquidityProvider.js"
import { Utils } from "../services/helpers/utilsWrapper.js" import { Utils } from "../services/helpers/utilsWrapper.js"
import { LoadStorageSettingsFromEnv } from "../services/storage/index.js" import { LoadStorageSettingsFromEnv } from "../services/storage/index.js"
import { NostrSender } from "../services/nostr/sender.js"
export type ChainTools = { export type ChainTools = {
mine: (amount: number) => Promise<void> mine: (amount: number) => Promise<void>
@ -15,7 +16,8 @@ export type ChainTools = {
export const setupNetwork = async (): Promise<ChainTools> => { export const setupNetwork = async (): Promise<ChainTools> => {
const storageSettings = GetTestStorageSettings(LoadStorageSettingsFromEnv()) const storageSettings = GetTestStorageSettings(LoadStorageSettingsFromEnv())
const setupUtils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages }) const nostrSender = new NostrSender()
const setupUtils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages }, nostrSender)
//const settingsManager = new SettingsManager(storageSettings) //const settingsManager = new SettingsManager(storageSettings)
const core = new BitcoinCoreWrapper(LoadBitcoinCoreSettingsFromEnv()) const core = new BitcoinCoreWrapper(LoadBitcoinCoreSettingsFromEnv())
await core.InitAddress() await core.InitAddress()
@ -23,7 +25,7 @@ export const setupNetwork = async (): Promise<ChainTools> => {
const lndSettings = LoadLndSettingsFromEnv({}) const lndSettings = LoadLndSettingsFromEnv({})
const lndNodeSettings = LoadLndNodeSettingsFromEnv({}) const lndNodeSettings = LoadLndNodeSettingsFromEnv({})
const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false } const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false, providerRelayUrl: "" }
const alice = new LND(() => ({ lndSettings, lndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const alice = new LND(() => ({ lndSettings, lndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await tryUntil<void>(async i => { await tryUntil<void>(async i => {

View file

@ -1,6 +1,6 @@
import { getLogger } from '../services/helpers/logger.js' import { getLogger } from '../services/helpers/logger.js'
import { initMainHandler, initSettings } from '../services/main/init.js' import { initMainHandler, initSettings } from '../services/main/init.js'
import { SendData } from '../services/nostr/handler.js' import { SendData } from '../services/nostr/nostrPool.js'
import { TestBase, TestUserData } from './testBase.js' import { TestBase, TestUserData } from './testBase.js'
import * as Types from '../../proto/autogenerated/ts/types.js' import * as Types from '../../proto/autogenerated/ts/types.js'
import { GetTestStorageSettings, LoadStorageSettingsFromEnv } from '../services/storage/index.js' import { GetTestStorageSettings, LoadStorageSettingsFromEnv } from '../services/storage/index.js'
@ -20,19 +20,19 @@ export const initBootstrappedInstance = async (T: TestBase) => {
if (!initialized) { if (!initialized) {
throw new Error("failed to initialize bootstrapped main handler") throw new Error("failed to initialize bootstrapped main handler")
} }
const { mainHandler: bootstrapped, liquidityProviderInfo, liquidityProviderApp } = initialized const { mainHandler: bootstrapped, localProviderClient } = initialized
T.main.attachNostrSend(async (_, data, r) => { T.main.attachNostrSend(async (_, data, r) => {
if (data.type === 'event') { if (data.type === 'event') {
throw new Error("unsupported event type") throw new Error("unsupported event type")
} }
if (data.pub !== liquidityProviderInfo.publicKey) { if (data.pub !== localProviderClient.publicKey) {
throw new Error("invalid pub " + data.pub + " expected " + liquidityProviderInfo.publicKey) throw new Error("invalid pub " + data.pub + " expected " + localProviderClient.publicKey)
} }
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.liquidityProvider.onEvent(j, T.app.publicKey) bootstrapped.liquidityProvider.onEvent(j, T.app.publicKey)
}) })
bootstrapped.liquidityProvider.attachNostrSend(async (_, data, r) => { bootstrapped.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")
@ -42,10 +42,10 @@ export const initBootstrappedInstance = async (T: TestBase) => {
} }
bootstrapped.liquidityProvider.onEvent(res, data.pub) bootstrapped.liquidityProvider.onEvent(res, data.pub)
}) })
bootstrapped.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) bootstrapped.liquidityProvider.setNostrInfo({ localId: `client_${localProviderClient.appId}`, localPubkey: localProviderClient.publicKey })
await new Promise<void>(res => { await new Promise<void>(res => {
const interval = setInterval(async () => { const interval = setInterval(async () => {
const canHandle = await bootstrapped.liquidityProvider.CanProviderHandle({ action: 'receive', amount: 2000 }) const canHandle = bootstrapped.liquidityProvider.IsReady()
if (canHandle) { if (canHandle) {
clearInterval(interval) clearInterval(interval)
res() res()
@ -54,10 +54,10 @@ export const initBootstrappedInstance = async (T: TestBase) => {
} }
}, 500) }, 500)
}) })
const bUser = await bootstrapped.applicationManager.AddAppUser(liquidityProviderApp.appId, { identifier: "user1_bootstrapped", balance: 0, fail_if_exists: true }) const bUser = await bootstrapped.applicationManager.AddAppUser(localProviderClient.appId, { identifier: "user1_bootstrapped", balance: 0, fail_if_exists: true })
const bootstrappedUser: TestUserData = { userId: bUser.info.userId, appUserIdentifier: bUser.identifier, appId: liquidityProviderApp.appId } const bootstrappedUser: TestUserData = { userId: bUser.info.userId, appUserIdentifier: bUser.identifier, appId: localProviderClient.appId }
return { return {
bootstrapped, liquidityProviderInfo, liquidityProviderApp, bootstrappedUser, stop: () => { bootstrapped, localProviderClient, bootstrappedUser, stop: () => {
bootstrapped.Stop() bootstrapped.Stop()
} }
} }

View file

@ -29,16 +29,15 @@ const testSpamExternalPayment = async (T: TestBase) => {
const failedPayments = res.filter(r => !r.success) const failedPayments = res.filter(r => !r.success)
console.log(failedPayments) console.log(failedPayments)
failedPayments.forEach(f => expect(f.err).to.be.equal("not enough balance to decrement")) failedPayments.forEach(f => expect(f.err).to.be.equal("not enough balance to decrement"))
successfulPayments.forEach(s => expect(s.result).to.contain({ amount_paid: 500, network_fee: 1, service_fee: 3 })) successfulPayments.forEach(s => expect(s.result).to.contain({ amount_paid: 500, network_fee: 0, service_fee: 10 }))
expect(successfulPayments.length).to.be.equal(3) expect(successfulPayments.length).to.be.equal(3)
expect(failedPayments.length).to.be.equal(7) expect(failedPayments.length).to.be.equal(7)
T.d("3 payments succeeded, 7 failed as expected") T.d("3 payments succeeded, 7 failed as expected")
const u = await T.main.storage.userStorage.GetUser(T.user1.userId) const u = await T.main.storage.userStorage.GetUser(T.user1.userId)
const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id)
expect(u.balance_sats).to.be.equal(488) expect(u.balance_sats).to.be.equal(470)
T.d("user1 balance is now 488 (2000 - (500 + 3 fee + 1 routing) * 3)") T.d("user1 balance is now 470 (2000 - (500 + 10 fee) * 3)")
expect(owner.balance_sats).to.be.equal(9) expect(owner.balance_sats).to.be.equal(27)
T.d("app balance is 9 sats") T.d("app balance is 27 sats")
} }

View file

@ -15,6 +15,7 @@ import { AdminManager } from '../services/main/adminManager.js'
import { TlvStorageFactory } from '../services/storage/tlv/tlvFilesStorageFactory.js' import { TlvStorageFactory } from '../services/storage/tlv/tlvFilesStorageFactory.js'
import { ChainTools } from './networkSetup.js' import { ChainTools } from './networkSetup.js'
import { LiquiditySettings, LoadLndSettingsFromEnv, LoadSecondLndSettingsFromEnv, LoadThirdLndSettingsFromEnv } from '../services/main/settings.js' import { LiquiditySettings, LoadLndSettingsFromEnv, LoadSecondLndSettingsFromEnv, LoadThirdLndSettingsFromEnv } from '../services/main/settings.js'
import { NostrSender } from '../services/nostr/sender.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
@ -46,7 +47,8 @@ export type StorageTestBase = {
export const setupStorageTest = async (d: Describe): Promise<StorageTestBase> => { export const setupStorageTest = async (d: Describe): Promise<StorageTestBase> => {
const settings = GetTestStorageSettings(LoadStorageSettingsFromEnv()) const settings = GetTestStorageSettings(LoadStorageSettingsFromEnv())
const utils = new Utils({ dataDir: settings.dataDir, allowResetMetricsStorages: true }) const nostrSender = new NostrSender()
const utils = new Utils({ dataDir: settings.dataDir, allowResetMetricsStorages: true }, nostrSender)
const storageManager = new Storage(settings, utils) const storageManager = new Storage(settings, utils)
await storageManager.Connect(console.log) await storageManager.Connect(console.log)
return { return {
@ -79,11 +81,11 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise<Te
const u2 = await main.applicationManager.AddAppUser(app.appId, { identifier: "user2", balance: 0, fail_if_exists: true }) const u2 = await main.applicationManager.AddAppUser(app.appId, { identifier: "user2", balance: 0, fail_if_exists: true })
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 nostrSender = new NostrSender()
const extermnalUtils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages }) const extermnalUtils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages }, nostrSender)
/* const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }) /* const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToMainLnd.Warmup() */ await externalAccessToMainLnd.Warmup() */
const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false } const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false, providerRelayUrl: "" }
const lndSettings = LoadLndSettingsFromEnv({}) const lndSettings = LoadLndSettingsFromEnv({})
const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const otherLndSetting = () => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }) const otherLndSetting = () => ({ lndSettings, lndNodeSettings: secondLndNodeSettings })
@ -119,7 +121,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, amount, { from: 'system', useProvider: false }) await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, { routingFeeLimit: 100, serviceFee: 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}`)