diff --git a/datasource.js b/datasource.js index 7d4baa01..dfae53bf 100644 --- a/datasource.js +++ b/datasource.js @@ -21,6 +21,7 @@ import { ManagementGrant } from "./build/src/services/storage/entity/ManagementG import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js" import { UserAccess } from "./build/src/services/storage/entity/UserAccess.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 { 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 { 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 { 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({ type: "better-sqlite3", @@ -49,10 +55,12 @@ export default new DataSource({ migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, 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, 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, }) -//npx typeorm migration:generate ./src/services/storage/migrations/admin_settings -d ./datasource.js \ No newline at end of file +//npx typeorm migration:generate ./src/services/storage/migrations/swaps_service_url -d ./datasource.js \ No newline at end of file diff --git a/decode.js b/decode.js new file mode 100755 index 00000000..c14efa18 --- /dev/null +++ b/decode.js @@ -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 ') + 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) +} + diff --git a/env.example b/env.example index 09761f8c..066debda 100644 --- a/env.example +++ b/env.example @@ -2,6 +2,7 @@ # 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 + #LND_CONNECTION # Defaults typical for straight Linux # Containers, Mac and Windows may need more detailed paths @@ -9,15 +10,22 @@ #LND_CERT_PATH=~/.lnd/tls.cert #LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon #LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log +#BTC_NETWORK=mainnet # Bypass LND entirely and daisychain off the bootstrap provider (testing only) #USE_ONLY_LIQUIDITY_PROVIDER=false #BOOTSTRAP_PEER # 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 -# To disable this feature entirely overwrite the env with "null" -#LIQUIDITY_PROVIDER_PUB=null +# The provider pubkey is extracted from PROVIDER_NPROFILE (nprofile contains both pubkey and relay URL) +# The developers node is used by default, or you may specify another. +# To disable this feature entirely set DISABLE_LIQUIDITY_PROVIDER=true #DISABLE_LIQUIDITY_PROVIDER=false +#PROVIDER_NPROFILE=nprofile1qyd8wumn8ghj7um5wfn8y7fwwd5x7cmt9ehx2arhdaexkqpqwmk5tuqvafa6ckwc6zmaypyy3af3n4aeds2ql7m0ew42kzsn638q9s9z8p + +#SWAPS +#BOLTZ_HTTP_URL= +#BOLTZ_WEBSOCKET_URL= +#ENABLE_SWAPS=false #DB #DATABASE_FILE=db.sqlite @@ -33,48 +41,44 @@ #LOCALHOST +# For REST Management #ADMIN_TOKEN= #PORT=1776 #JWT_SECRET= +#PUSH_SERVICE +# For Wallet notifs via FCM #SHOCK_PUSH_URL= -#Lightning Address Bridge +#LNA Bridge +# Tell wallets where they can get a noffer-based Lightning Address #BRIDGE_URL=https://shockwallet.app #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% -#OUTBOUND_MAX_FEE_BPS=60 -#OUTBOUND_MAX_FEE_EXTRA_SATS=100 -# If the back-end doesn't have adequate channel capacity, buy one from an LSP -# Will execute when it costs less than 1% of balance and uses a trusted peer -#BOOTSTRAP=1 +#SERVICE_FEE_BPS=60 +#ROUTING_FEE_LIMIT_BPS=50 +#SERVICE_FEE_FLOOR_SATS=10 +#ROUTING_FEE_FLOOR_SATS=5 + -#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 -# Applied to either debits or credits and sent to an admin account -# BPS are basis points, 100 BPS = 1% -#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 +# Internal transaction fees (placeholder, bugged, do not use) +#TX_FEE_INTERNAL_ROOT_BPS=0 -#APP_FEES -# An extra fee applied at the app level and sent to the application owner -#INCOMING_INVOICE_FEE_USER_BPS=0 -#OUTGOING_INVOICE_FEE_USER_BPS=0 -#TX_FEE_INTERNAL_USER_BPS=0 +#LSP +# If the back-end doesn't have adequate channel capacity, buy one from an LSP +#OLYMPUS_LSP_URL=https://lsps1.lnolymp.us/api/v1 +#FLASHSATS_LSP_URL=https://lsp.flashsats.xyz/lsp/channel +#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 # Default relay may become rate-limited without a paid subscription @@ -84,9 +88,9 @@ LSP_MAX_FEE_BPS=100 #LNURL # Optional -# If undefined, LNURLs (including Lightning Address) will be disabled -# To enable, add a reachable https endpoint for requests (or purchase a subscription) -# You also need an SSL reverse proxy from the domain to this local host +# If undefined, LNURLs will be disabled but wallets may still use bridge for Lightning Addresses +# To enable, add a reachable https endpoint for requests +# You also need an SSL reverse proxy from the domain to the Pub # Read more at https://docs.shock.network #SERVICE_URL=https://yourdomainhere.xyz #LNURL_META_TEXT=LNURL via Lightning.Pub @@ -97,7 +101,7 @@ LSP_MAX_FEE_BPS=100 # Read more at https://docs.shock.network #SUBSCRIBER=1 -#DEV_OPTS +#DEV #MOCK_LND=false #ALLOW_BALANCE_MIGRATION=false #MIGRATE_DB=false @@ -109,10 +113,8 @@ LSP_MAX_FEE_BPS=100 #SKIP_SANITY_CHECK=false # A read-only token that can be used with dashboard to view reports #METRICS_TOKEN= -# Disable outbound payments aka honeypot mode -#DISABLE_EXTERNAL_PAYMENTS=false #ALLOW_RESET_METRICS_STORAGES=false -ALLOW_HTTP_UPGRADE=false +#ALLOW_HTTP_UPGRADE=false #WATCHDOG SECURITY # 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 # Max difference between users balance and LND balance at Pub startup #WATCHDOG_MAX_DIFF_SATS=0 +# Disable outbound payments aka honeypot mode +#DISABLE_EXTERNAL_PAYMENTS=false \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f9ad7caf..701b7072 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,10 +18,13 @@ "@types/express": "^4.17.21", "@types/node": "^17.0.31", "@types/secp256k1": "^4.0.3", + "@vulpemventures/secp256k1-zkp": "^3.2.1", "axios": "^1.9.0", "bech32": "^2.0.0", "better-sqlite3": "^12.2.0", "bitcoin-core": "^4.2.0", + "bitcoinjs-lib": "^6.1.7", + "boltz-core": "^3.0.0", "chai": "^4.3.7", "chai-string": "^1.5.0", "copyfiles": "^2.4.1", @@ -29,6 +32,7 @@ "csv": "^6.3.8", "dotenv": "^16.4.5", "eccrypto": "^1.1.6", + "ecpair": "^3.0.0", "express": "^4.21.2", "globby": "^13.1.2", "grpc-tools": "^1.12.4", @@ -42,6 +46,7 @@ "rimraf": "^3.0.2", "rxjs": "^7.5.5", "secp256k1": "^5.0.1", + "tiny-secp256k1": "^2.2.4", "ts-node": "^10.7.0", "ts-proto": "^1.131.2", "typeorm": "^0.3.26", @@ -70,6 +75,12 @@ "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": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.7.0.tgz", @@ -443,6 +454,12 @@ "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": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1014,6 +1031,16 @@ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "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": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -1114,6 +1141,18 @@ "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1686,6 +1725,12 @@ "license": "Apache-2.0", "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": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1766,12 +1811,54 @@ "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": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", "license": "MIT", - "optional": true, "dependencies": { "safe-buffer": "^5.0.1" } @@ -1794,6 +1881,71 @@ "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": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -1828,6 +1980,26 @@ "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": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", @@ -1873,6 +2045,42 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "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": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1916,6 +2124,25 @@ "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": { "version": "5.7.1", "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", "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" @@ -2547,7 +2773,6 @@ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "license": "MIT", - "optional": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -2561,7 +2786,6 @@ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "license": "MIT", - "optional": true, "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -2982,6 +3206,52 @@ "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": { "version": "1.1.1", "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", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -3792,7 +4061,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3807,7 +4075,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", - "optional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -4355,6 +4622,88 @@ ], "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": { "version": "4.17.21", "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", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "license": "MIT", - "optional": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -5796,6 +6144,16 @@ ], "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": { "version": "1.2.1", "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", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "license": "MIT", - "optional": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -6413,6 +6770,28 @@ "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": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -6813,6 +7192,27 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", @@ -7034,6 +7434,12 @@ "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": { "version": "0.3.26", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", @@ -7387,6 +7793,24 @@ "devOptional": true, "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -7489,6 +7913,44 @@ "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": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 56a45e1e..7db3acdb 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,13 @@ "@types/express": "^4.17.21", "@types/node": "^17.0.31", "@types/secp256k1": "^4.0.3", + "@vulpemventures/secp256k1-zkp": "^3.2.1", "axios": "^1.9.0", "bech32": "^2.0.0", "better-sqlite3": "^12.2.0", "bitcoin-core": "^4.2.0", + "bitcoinjs-lib": "^6.1.7", + "boltz-core": "^3.0.0", "chai": "^4.3.7", "chai-string": "^1.5.0", "copyfiles": "^2.4.1", @@ -47,6 +50,7 @@ "csv": "^6.3.8", "dotenv": "^16.4.5", "eccrypto": "^1.1.6", + "ecpair": "^3.0.0", "express": "^4.21.2", "globby": "^13.1.2", "grpc-tools": "^1.12.4", @@ -60,6 +64,7 @@ "rimraf": "^3.0.2", "rxjs": "^7.5.5", "secp256k1": "^5.0.1", + "tiny-secp256k1": "^2.2.4", "ts-node": "^10.7.0", "ts-proto": "^1.131.2", "typeorm": "^0.3.26", @@ -88,4 +93,4 @@ "typescript": "5.5.4" }, "overrides": {} -} +} \ No newline at end of file diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index a4116e6c..99023c69 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -93,6 +93,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [MessagingToken](#MessagingToken) - This methods has an __empty__ __response__ body +- GetAdminTransactionSwapQuotes + - auth type: __Admin__ + - input: [TransactionSwapRequest](#TransactionSwapRequest) + - output: [TransactionSwapQuoteList](#TransactionSwapQuoteList) + - GetAppsMetrics - auth type: __Metrics__ - 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) - output: [UsageMetricTlv](#UsageMetricTlv) +- GetTransactionSwapQuotes + - auth type: __User__ + - input: [TransactionSwapRequest](#TransactionSwapRequest) + - output: [TransactionSwapQuoteList](#TransactionSwapQuoteList) + - GetUsageMetrics - auth type: __Metrics__ - 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) - This methods has an __empty__ __response__ body +- ListAdminSwaps + - auth type: __Admin__ + - This methods has an __empty__ __request__ body + - output: [SwapsList](#SwapsList) + - ListChannels - auth type: __Admin__ - This methods has an __empty__ __request__ body - output: [LndChannels](#LndChannels) +- ListSwaps + - auth type: __User__ + - This methods has an __empty__ __request__ body + - output: [SwapsList](#SwapsList) + - LndGetInfo - auth type: __Admin__ - 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) - output: [PayAddressResponse](#PayAddressResponse) +- PayAdminTransactionSwap + - auth type: __Admin__ + - input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest) + - output: [AdminSwapResponse](#AdminSwapResponse) + - PayInvoice - auth type: __User__ - 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) - 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 - auth type: __App__ - 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) - output: [UsageMetricTlv](#UsageMetricTlv) +- GetTransactionSwapQuotes + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/swap/quote__ + - input: [TransactionSwapRequest](#TransactionSwapRequest) + - output: [TransactionSwapQuoteList](#TransactionSwapQuoteList) + - GetUsageMetrics - auth type: __Metrics__ - 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) - 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 - auth type: __Admin__ - 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 - 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 - auth type: __Admin__ - 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) - output: [PayAddressResponse](#PayAddressResponse) +- PayAdminTransactionSwap + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/swap/transaction/pay__ + - input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest) + - output: [AdminSwapResponse](#AdminSwapResponse) + - PayAppUserInvoice - auth type: __App__ - http method: __post__ @@ -1038,6 +1098,10 @@ The nostr server will send back a message response, and inside the body there wi - __name__: _string_ - __price_sats__: _number_ +### AdminSwapResponse + - __network_fee__: _number_ + - __tx_id__: _string_ + ### AppMetrics - __app__: _[Application](#Application)_ - __available__: _number_ @@ -1092,6 +1156,13 @@ The nostr server will send back a message response, and inside the body there wi - __nostr_pub__: _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 - __available_chunks__: ARRAY of: _number_ - __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 - __invitation_link__: _string_ +### CumulativeFees + - __serviceFeeBps__: _number_ + - __serviceFeeFloor__: _number_ + ### DebitAuthorization - __authorized__: _boolean_ - __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_ ### LiveUserOperation + - __latest_balance__: _number_ - __operation__: _[UserOperation](#UserOperation)_ ### LndChannels @@ -1448,8 +1524,9 @@ The nostr server will send back a message response, and inside the body there wi ### PayAddressRequest - __address__: _string_ - - __amoutSats__: _number_ + - __amountSats__: _number_ - __satsPerVByte__: _number_ + - __swap_operation_id__: _string_ *this field is optional ### PayAddressResponse - __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_ - __txId__: _string_ +### PayAdminTransactionSwapRequest + - __address__: _string_ + - __swap_operation_id__: _string_ + ### PayAppUserInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional + - __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional - __invoice__: _string_ - __user_identifier__: _string_ ### PayInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional + - __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional - __invoice__: _string_ ### PayInvoiceResponse - __amount_paid__: _number_ + - __latest_balance__: _number_ - __network_fee__: _number_ - __operation_id__: _string_ - __preimage__: _string_ @@ -1480,7 +1564,9 @@ The nostr server will send back a message response, and inside the body there wi ### PaymentState - __amount__: _number_ + - __internal__: _boolean_ - __network_fee__: _number_ + - __operation_id__: _string_ - __paid_at_unix__: _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_ - __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 - __policy__: _[ChannelPolicy](#ChannelPolicy)_ - __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index 8c20a0a6..a2b0ecb0 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -66,81 +66,86 @@ type Client struct { BanDebit func(req DebitOperation) error BanUser func(req BanUserRequest) (*BanUserResponse, error) // batching method: BatchUser not implemented - CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error) - CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error) - DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error) - DeleteUserOffer func(req OfferId) error - EditDebit func(req DebitAuthorizationRequest) error - EncryptionExchange func(req EncryptionExchangeRequest) error - EnrollAdminToken func(req EnrollAdminTokenRequest) error - EnrollMessagingToken func(req MessagingToken) error - GetApp func() (*Application, error) - GetAppUser func(req GetAppUserRequest) (*AppUser, error) - GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) - GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) - GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) - GetDebitAuthorizations func() (*DebitAuthorizations, error) - GetErrorStats func() (*ErrorStats, error) - GetHttpCreds func() (*HttpCreds, error) - GetInviteLinkState func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error) - GetLNURLChannelLink func() (*LnurlLinkResponse, error) - GetLiveDebitRequests func() (*LiveDebitRequest, error) - GetLiveManageRequests func() (*LiveManageRequest, error) - GetLiveUserOperations func() (*LiveUserOperation, error) - GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error) - GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error) - GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error) - GetLnurlPayLink func() (*LnurlLinkResponse, error) - GetLnurlWithdrawInfo func(query GetLnurlWithdrawInfo_Query) (*LnurlWithdrawInfoResponse, error) - GetLnurlWithdrawLink func() (*LnurlLinkResponse, error) - GetManageAuthorizations func() (*ManageAuthorizations, error) - GetMigrationUpdate func() (*MigrationUpdate, error) - GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error) - GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error) - GetProvidersDisruption func() (*ProvidersDisruption, error) - GetSeed func() (*LndSeed, error) - GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error) - GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error) - GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error) - GetUserInfo func() (*UserInfo, error) - GetUserOffer func(req OfferId) (*OfferConfig, error) - GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error) - GetUserOffers func() (*UserOffers, error) - GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) - HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) - HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) - HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error - Health func() error - LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error - ListChannels func() (*LndChannels, error) - LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error) - NewAddress func(req NewAddressRequest) (*NewAddressResponse, error) - NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error) - NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error) - OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error) - PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) - PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) - PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) - PingSubProcesses func() error - RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) - ResetDebit func(req DebitOperation) error - ResetManage func(req ManageOperation) error - ResetMetricsStorages func() error - ResetNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) - RespondToDebit func(req DebitResponse) error - SendAppUserToAppPayment func(req SendAppUserToAppPaymentRequest) error - SendAppUserToAppUserPayment func(req SendAppUserToAppUserPaymentRequest) error - SetMockAppBalance func(req SetMockAppBalanceRequest) error - SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error - SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error - SubToWebRtcCandidates func() (*WebRtcCandidate, error) - SubmitWebRtcMessage func(req WebRtcMessage) (*WebRtcAnswer, 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) + CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error) + CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error) + DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error) + DeleteUserOffer func(req OfferId) error + EditDebit func(req DebitAuthorizationRequest) error + EncryptionExchange func(req EncryptionExchangeRequest) error + EnrollAdminToken func(req EnrollAdminTokenRequest) error + EnrollMessagingToken func(req MessagingToken) error + GetAdminTransactionSwapQuotes func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error) + GetApp func() (*Application, error) + GetAppUser func(req GetAppUserRequest) (*AppUser, error) + GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) + GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) + GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) + GetDebitAuthorizations func() (*DebitAuthorizations, error) + GetErrorStats func() (*ErrorStats, error) + GetHttpCreds func() (*HttpCreds, error) + GetInviteLinkState func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error) + GetLNURLChannelLink func() (*LnurlLinkResponse, error) + GetLiveDebitRequests func() (*LiveDebitRequest, error) + GetLiveManageRequests func() (*LiveManageRequest, error) + GetLiveUserOperations func() (*LiveUserOperation, error) + GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error) + GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error) + GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error) + GetLnurlPayLink func() (*LnurlLinkResponse, error) + GetLnurlWithdrawInfo func(query GetLnurlWithdrawInfo_Query) (*LnurlWithdrawInfoResponse, error) + GetLnurlWithdrawLink func() (*LnurlLinkResponse, error) + GetManageAuthorizations func() (*ManageAuthorizations, error) + GetMigrationUpdate func() (*MigrationUpdate, error) + GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error) + GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error) + GetProvidersDisruption func() (*ProvidersDisruption, error) + GetSeed func() (*LndSeed, error) + GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error) + GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error) + GetTransactionSwapQuotes func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error) + GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error) + GetUserInfo func() (*UserInfo, error) + GetUserOffer func(req OfferId) (*OfferConfig, error) + GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error) + GetUserOffers func() (*UserOffers, error) + GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) + HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) + HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) + HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error + Health func() error + LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error + ListAdminSwaps func() (*SwapsList, error) + ListChannels func() (*LndChannels, error) + ListSwaps func() (*SwapsList, error) + LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error) + NewAddress func(req NewAddressRequest) (*NewAddressResponse, error) + NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error) + NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error) + OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error) + PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) + PayAdminTransactionSwap func(req PayAdminTransactionSwapRequest) (*AdminSwapResponse, error) + PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) + PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) + PingSubProcesses func() error + RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) + ResetDebit func(req DebitOperation) error + ResetManage func(req ManageOperation) error + ResetMetricsStorages func() error + ResetNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) + RespondToDebit func(req DebitResponse) error + SendAppUserToAppPayment func(req SendAppUserToAppPaymentRequest) error + SendAppUserToAppUserPayment func(req SendAppUserToAppUserPaymentRequest) error + SetMockAppBalance func(req SetMockAppBalanceRequest) error + SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error + SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error + SubToWebRtcCandidates func() (*WebRtcCandidate, error) + SubmitWebRtcMessage func(req WebRtcMessage) (*WebRtcAnswer, 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 { @@ -662,6 +667,35 @@ func NewClient(params ClientParams) *Client { } 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) { auth, err := params.RetrieveAppAuth() if err != nil { @@ -1285,6 +1319,35 @@ func NewClient(params ClientParams) *Client { } 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) { auth, err := params.RetrieveMetricsAuth() if err != nil { @@ -1580,6 +1643,32 @@ func NewClient(params ClientParams) *Client { } 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) { auth, err := params.RetrieveAdminAuth() if err != nil { @@ -1602,6 +1691,32 @@ func NewClient(params ClientParams) *Client { } 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) { auth, err := params.RetrieveAdminAuth() if err != nil { @@ -1777,6 +1892,35 @@ func NewClient(params ClientParams) *Client { } 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) { auth, err := params.RetrieveAppAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index e8e90df5..841ced1d 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -123,6 +123,10 @@ type AddProductRequest struct { Name string `json:"name"` Price_sats int64 `json:"price_sats"` } +type AdminSwapResponse struct { + Network_fee int64 `json:"network_fee"` + Tx_id string `json:"tx_id"` +} type AppMetrics struct { App *Application `json:"app"` Available int64 `json:"available"` @@ -177,6 +181,13 @@ type BannedAppUser struct { Nostr_pub string `json:"nostr_pub"` 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 { Available_chunks []int64 `json:"available_chunks"` Base_64_data []string `json:"base_64_data"` @@ -222,6 +233,10 @@ type CreateOneTimeInviteLinkRequest struct { type CreateOneTimeInviteLinkResponse struct { Invitation_link string `json:"invitation_link"` } +type CumulativeFees struct { + Servicefeebps int64 `json:"serviceFeeBps"` + Servicefeefloor int64 `json:"serviceFeeFloor"` +} type DebitAuthorization struct { Authorized bool `json:"authorized"` Debit_id string `json:"debit_id"` @@ -360,7 +375,8 @@ type LiveManageRequest struct { Request_id string `json:"request_id"` } type LiveUserOperation struct { - Operation *UserOperation `json:"operation"` + Latest_balance int64 `json:"latest_balance"` + Operation *UserOperation `json:"operation"` } type LndChannels struct { Open_channels []OpenChannel `json:"open_channels"` @@ -532,9 +548,10 @@ type OperationsCursor struct { Ts int64 `json:"ts"` } type PayAddressRequest struct { - Address string `json:"address"` - Amoutsats int64 `json:"amoutSats"` - Satspervbyte int64 `json:"satsPerVByte"` + Address string `json:"address"` + Amountsats int64 `json:"amountSats"` + Satspervbyte int64 `json:"satsPerVByte"` + Swap_operation_id string `json:"swap_operation_id"` } type PayAddressResponse struct { Network_fee int64 `json:"network_fee"` @@ -542,32 +559,41 @@ type PayAddressResponse struct { Service_fee int64 `json:"service_fee"` Txid string `json:"txId"` } +type PayAdminTransactionSwapRequest struct { + Address string `json:"address"` + Swap_operation_id string `json:"swap_operation_id"` +} type PayAppUserInvoiceRequest struct { - Amount int64 `json:"amount"` - Debit_npub string `json:"debit_npub"` - Invoice string `json:"invoice"` - User_identifier string `json:"user_identifier"` + Amount int64 `json:"amount"` + Debit_npub string `json:"debit_npub"` + Expected_fees *CumulativeFees `json:"expected_fees"` + Invoice string `json:"invoice"` + User_identifier string `json:"user_identifier"` } type PayInvoiceRequest struct { - Amount int64 `json:"amount"` - Debit_npub string `json:"debit_npub"` - Invoice string `json:"invoice"` + Amount int64 `json:"amount"` + Debit_npub string `json:"debit_npub"` + Expected_fees *CumulativeFees `json:"expected_fees"` + Invoice string `json:"invoice"` } type PayInvoiceResponse struct { - Amount_paid int64 `json:"amount_paid"` - Network_fee int64 `json:"network_fee"` - Operation_id string `json:"operation_id"` - Preimage string `json:"preimage"` - Service_fee int64 `json:"service_fee"` + Amount_paid int64 `json:"amount_paid"` + Latest_balance int64 `json:"latest_balance"` + Network_fee int64 `json:"network_fee"` + Operation_id string `json:"operation_id"` + Preimage string `json:"preimage"` + Service_fee int64 `json:"service_fee"` } type PayerData struct { Data map[string]string `json:"data"` } type PaymentState struct { - Amount int64 `json:"amount"` - Network_fee int64 `json:"network_fee"` - Paid_at_unix int64 `json:"paid_at_unix"` - Service_fee int64 `json:"service_fee"` + Amount int64 `json:"amount"` + Internal bool `json:"internal"` + Network_fee int64 `json:"network_fee"` + Operation_id string `json:"operation_id"` + Paid_at_unix int64 `json:"paid_at_unix"` + Service_fee int64 `json:"service_fee"` } type Product struct { Id string `json:"id"` @@ -639,6 +665,31 @@ type SingleMetricReq struct { Page int64 `json:"page"` 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 { Policy *ChannelPolicy `json:"policy"` Update *UpdateChannelPolicyRequest_update `json:"update"` diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index 18102374..c1740acd 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -477,6 +477,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } 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': if (!methods.GetUserInfo) { throw new Error('method GetUserInfo not found' ) @@ -533,6 +545,16 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } 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': if (!methods.NewAddress) { throw new Error('method NewAddress not found' ) @@ -847,6 +869,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { 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.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') app.post('/api/app/get', async (req, res) => { 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 }]) } 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') app.post('/api/reports/usage', async (req, res) => { 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 }]) } 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') app.get('/api/admin/channels', async (req, res) => { 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 }]) } 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') app.post('/api/admin/lnd/getinfo', async (req, res) => { 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 }]) } 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') app.post('/api/app/invoice/pay', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'PayAppUserInvoice', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 9c37e1b3..a24ff3e6 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -273,6 +273,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise => { + 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 => { const auth = await params.retrieveAppAuth() if (auth === null) throw new Error('retrieveAppAuth() returned null') @@ -603,6 +617,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise => { + 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 => { const auth = await params.retrieveMetricsAuth() if (auth === null) throw new Error('retrieveMetricsAuth() returned null') @@ -753,6 +781,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + ListAdminSwaps: async (): Promise => { + 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 => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') @@ -767,6 +809,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + ListSwaps: async (): Promise => { + 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 => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') @@ -853,6 +909,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise => { + 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 => { const auth = await params.retrieveAppAuth() if (auth === null) throw new Error('retrieveAppAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 38795d01..df969c7b 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -230,6 +230,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise => { + 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 => { const auth = await params.retrieveNostrMetricsAuth() 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' } }, + GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise => { + 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 => { const auth = await params.retrieveNostrMetricsAuth() 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' } }, + ListAdminSwaps: async (): Promise => { + 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 => { const auth = await params.retrieveNostrAdminAuth() 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' } }, + ListSwaps: async (): Promise => { + 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 => { const auth = await params.retrieveNostrAdminAuth() 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' } }, + PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise => { + 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 => { const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index cd9e708f..24a96cd6 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -359,6 +359,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } 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': if (!methods.GetUserInfo) { throw new Error('method not defined: GetUserInfo') @@ -415,6 +427,16 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } 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': if (!methods.NewAddress) { throw new Error('method not defined: NewAddress') @@ -665,6 +687,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { 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 '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': try { 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 }]) }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 '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': try { 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 }]) }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 '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': try { 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 }]) }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 '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': try { 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 }]) }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 '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': try { if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 239d0686..e397cd94 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: export type AdminContext = { 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 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 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 | GetAdminTransactionSwapQuotes_Output | GetInviteLinkState_Output | GetSeed_Output | ListAdminSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminTransactionSwap_Output | UpdateChannelPolicy_Output export type AppContext = { app_id: string } @@ -35,8 +35,8 @@ export type UserContext = { app_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 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 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 | 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 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_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_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_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_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_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_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_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_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_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) @@ -342,6 +357,7 @@ export type ServerMethods = { EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise + GetAdminTransactionSwapQuotes?: (req: GetAdminTransactionSwapQuotes_Input & {ctx: AdminContext }) => Promise GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise @@ -369,6 +385,7 @@ export type ServerMethods = { GetSeed?: (req: GetSeed_Input & {ctx: AdminContext }) => Promise GetSingleBundleMetrics?: (req: GetSingleBundleMetrics_Input & {ctx: MetricsContext }) => Promise GetSingleUsageMetrics?: (req: GetSingleUsageMetrics_Input & {ctx: MetricsContext }) => Promise + GetTransactionSwapQuotes?: (req: GetTransactionSwapQuotes_Input & {ctx: UserContext }) => Promise GetUsageMetrics?: (req: GetUsageMetrics_Input & {ctx: MetricsContext }) => Promise GetUserInfo?: (req: GetUserInfo_Input & {ctx: UserContext }) => Promise GetUserOffer?: (req: GetUserOffer_Input & {ctx: UserContext }) => Promise @@ -380,13 +397,16 @@ export type ServerMethods = { HandleLnurlWithdraw?: (req: HandleLnurlWithdraw_Input & {ctx: GuestContext }) => Promise Health?: (req: Health_Input & {ctx: GuestContext }) => Promise LinkNPubThroughToken?: (req: LinkNPubThroughToken_Input & {ctx: GuestWithPubContext }) => Promise + ListAdminSwaps?: (req: ListAdminSwaps_Input & {ctx: AdminContext }) => Promise ListChannels?: (req: ListChannels_Input & {ctx: AdminContext }) => Promise + ListSwaps?: (req: ListSwaps_Input & {ctx: UserContext }) => Promise LndGetInfo?: (req: LndGetInfo_Input & {ctx: AdminContext }) => Promise NewAddress?: (req: NewAddress_Input & {ctx: UserContext }) => Promise NewInvoice?: (req: NewInvoice_Input & {ctx: UserContext }) => Promise NewProductInvoice?: (req: NewProductInvoice_Input & {ctx: UserContext }) => Promise OpenChannel?: (req: OpenChannel_Input & {ctx: AdminContext }) => Promise PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise + PayAdminTransactionSwap?: (req: PayAdminTransactionSwap_Input & {ctx: AdminContext }) => Promise PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise @@ -651,6 +671,29 @@ export const AddProductRequestValidate = (o?: AddProductRequest, opts: AddProduc 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 = { app: Application available: number @@ -979,6 +1022,48 @@ export const BannedAppUserValidate = (o?: BannedAppUser, opts: BannedAppUserOpti 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 = { available_chunks: number[] base_64_data: string[] @@ -1252,6 +1337,29 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL 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 = { authorized: boolean debit_id: string @@ -2089,17 +2197,22 @@ export const LiveManageRequestValidate = (o?: LiveManageRequest, opts: LiveManag } export type LiveUserOperation = { + latest_balance: number operation: UserOperation } export const LiveUserOperationOptionalFields: [] = [] export type LiveUserOperationOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] + latest_balance_CustomCheck?: (v: number) => boolean operation_Options?: UserOperationOptions } 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 (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`) if (operationErr !== null) return operationErr @@ -3130,15 +3243,18 @@ export const OperationsCursorValidate = (o?: OperationsCursor, opts: OperationsC export type PayAddressRequest = { address: string - amoutSats: number + amountSats: 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 & { - checkOptionalsAreSet?: [] + checkOptionalsAreSet?: PayAddressRequestOptionalField[] address_CustomCheck?: (v: string) => boolean - amoutSats_CustomCheck?: (v: number) => boolean + amountSats_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 => { 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 (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 (opts.amoutSats_CustomCheck && !opts.amoutSats_CustomCheck(o.amoutSats)) return new Error(`${path}.amoutSats: custom check failed`) + if (typeof o.amountSats !== 'number') return new Error(`${path}.amountSats: is not a number`) + 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 (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 } @@ -3189,18 +3308,43 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr 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 = { amount: number debit_npub?: string + expected_fees?: CumulativeFees invoice: string user_identifier: string } -export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' -export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub'] +export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees' +export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees'] export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean + expected_fees_Options?: CumulativeFeesOptions invoice_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 (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 (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 = { amount: number debit_npub?: string + expected_fees?: CumulativeFees invoice: string } -export type PayInvoiceRequestOptionalField = 'debit_npub' -export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub'] +export type PayInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees' +export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees'] export type PayInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean + expected_fees_Options?: CumulativeFeesOptions invoice_CustomCheck?: (v: string) => boolean } 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 (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 (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 = { amount_paid: number + latest_balance: number network_fee: number operation_id: string preimage: string @@ -3263,6 +3422,7 @@ export const PayInvoiceResponseOptionalFields: [] = [] export type PayInvoiceResponseOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] amount_paid_CustomCheck?: (v: number) => boolean + latest_balance_CustomCheck?: (v: number) => boolean network_fee_CustomCheck?: (v: number) => boolean operation_id_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 (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 (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 = { amount: number + internal: boolean network_fee: number + operation_id: string paid_at_unix: number service_fee: number } @@ -3320,7 +3485,9 @@ export const PaymentStateOptionalFields: [] = [] export type PaymentStateOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] amount_CustomCheck?: (v: number) => boolean + internal_CustomCheck?: (v: boolean) => boolean network_fee_CustomCheck?: (v: number) => boolean + operation_id_CustomCheck?: (v: string) => boolean paid_at_unix_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 (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 (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 (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 } +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 = { policy: ChannelPolicy update: UpdateChannelPolicyRequest_update diff --git a/proto/service/methods.proto b/proto/service/methods.proto index 4cff7379..feaf3e97 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -175,6 +175,27 @@ service LightningPub { 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) { option (auth_type) = "Metrics"; option (http_method) = "post"; @@ -480,7 +501,7 @@ service LightningPub { option (http_method) = "post"; option (http_route) = "/api/user/operations"; option (nostr) = true; - } + } rpc NewAddress(structs.NewAddressRequest) returns (structs.NewAddressResponse) { option (auth_type) = "User"; @@ -496,6 +517,20 @@ service LightningPub { 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){ option (auth_type) = "User"; option (http_method) = "post"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 201fd30b..00552fdd 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -389,7 +389,8 @@ message PayAppUserInvoiceRequest { string user_identifier = 1; string invoice = 2; int64 amount = 3; - optional string debit_npub = 4; + optional string debit_npub = 4; + optional CumulativeFees expected_fees = 5; } message SendAppUserToAppUserPaymentRequest { @@ -430,8 +431,9 @@ message NewAddressResponse{ } message PayAddressRequest{ string address = 1; - int64 amoutSats = 2; + int64 amountSats = 2; int64 satsPerVByte = 3; + optional string swap_operation_id = 4; } message PayAddressResponse{ @@ -465,7 +467,8 @@ message DecodeInvoiceResponse{ message PayInvoiceRequest{ string invoice = 1; int64 amount = 2; - optional string debit_npub = 3; + optional string debit_npub = 3; + optional CumulativeFees expected_fees = 4; } message PayInvoiceResponse{ @@ -474,6 +477,7 @@ message PayInvoiceResponse{ string operation_id = 3; int64 service_fee = 4; int64 network_fee = 5; + int64 latest_balance = 6; } message GetPaymentStateRequest{ @@ -486,6 +490,9 @@ message PaymentState{ int64 amount = 2; int64 service_fee = 3; int64 network_fee = 4; + bool internal = 5; + string operation_id = 6; + } message LnurlLinkResponse{ @@ -604,6 +611,7 @@ message GetProductBuyLinkResponse { message LiveUserOperation { UserOperation operation = 1; + int64 latest_balance = 2; } message MigrationUpdate { optional ClosureMigration closure = 1; @@ -824,3 +832,57 @@ message MessagingToken { string device_id = 1; 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; +} \ No newline at end of file diff --git a/src/e2e.ts b/src/e2e.ts index d46e2d59..bb2cb17d 100644 --- a/src/e2e.ts +++ b/src/e2e.ts @@ -7,6 +7,7 @@ import { getLogger } from './services/helpers/logger.js'; import { initMainHandler, initSettings } from './services/main/init.js'; import { nip19 } from 'nostr-tools' import { LoadStorageSettingsFromEnv } from './services/storage/index.js'; +import { AppInfo } from './services/nostr/nostrPool.js'; //@ts-ignore const { nprofileEncode } = nip19 @@ -20,18 +21,35 @@ const start = async () => { return } - const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn + const { mainHandler, localProviderClient, wizard, adminManager } = keepOn const serverMethods = GetServerMethods(mainHandler) const nostrSettings = settingsManager.getSettings().nostrRelaySettings 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, - { ...nostrSettings, apps, clients: [liquidityProviderInfo] }, + { + relays, maxEventContentLength, apps + }, (e, p) => mainHandler.liquidityProvider.onEvent(e, p) ) log("starting server") mainHandler.attachNostrSend(Send) mainHandler.StartBeacons() - const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays: nostrSettings.relays }) + const appNprofile = nprofileEncode({ pubkey: localProviderClient.publicKey, relays: nostrSettings.relays }) if (wizard) { wizard.AddConnectInfo(appNprofile, nostrSettings.relays) } diff --git a/src/index.ts b/src/index.ts index aa4c3c0b..fbe6802c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { getLogger } from './services/helpers/logger.js'; import { initMainHandler, initSettings } from './services/main/init.js'; import { nip19 } from 'nostr-tools' import { LoadStorageSettingsFromEnv } from './services/storage/index.js'; +import { AppInfo } from './services/nostr/nostrPool.js'; //@ts-ignore const { nprofileEncode } = nip19 @@ -22,13 +23,28 @@ const start = async () => { return } - const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn + const { mainHandler, localProviderClient, wizard, adminManager } = keepOn const serverMethods = GetServerMethods(mainHandler) log("initializing nostr middleware") - const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays - const maxEventContentLength = mainHandler.settings.getSettings().nostrRelaySettings.maxEventContentLength + 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, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler, - { relays, maxEventContentLength, apps, clients: [liquidityProviderInfo] }, + { + relays, maxEventContentLength, apps + }, (e, p) => mainHandler.liquidityProvider.onEvent(e, p) ) exitHandler(() => { Stop(); mainHandler.Stop() }) @@ -37,13 +53,13 @@ const start = async () => { mainHandler.attachNostrProcessPing(Ping) mainHandler.attachNostrReset(Reset) mainHandler.StartBeacons() - const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays }) + const appNprofile = nprofileEncode({ pubkey: localProviderClient.publicKey, relays }) if (wizard) { wizard.AddConnectInfo(appNprofile, relays) } adminManager.setAppNprofile(appNprofile) const Server = NewServer(serverMethods, serverOptions(mainHandler)) - Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) + Server.Listen(settingsManager.getSettings().serviceSettings.servicePort) } start() diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index cc1db630..034dbce8 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -1,6 +1,6 @@ import Main from "./services/main/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 NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js'; import { ERROR, getLogger } from "./services/helpers/logger.js"; @@ -50,6 +50,10 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett return } if (event.kind === 21001) { + if (event.relayConstraint === 'provider') { + log("got noffer request on provider only relay, ignoring") + return + } const offerReq = j as NofferData log("🎯 [NOSTR EVENT] Received offer request (kind 21001)", { fromPub: event.pub, @@ -60,18 +64,33 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett mainHandler.offerManager.handleClinkOffer(offerReq, event) return } else if (event.kind === 21002) { + if (event.relayConstraint === 'provider') { + log("got debit request on provider only relay, ignoring") + return + } const debitReq = j as NdebitData mainHandler.debitManager.handleNip68Debit(debitReq, event) return } else if (event.kind === 21003) { + if (event.relayConstraint === 'provider') { + log("got management request on provider only relay, ignoring") + return + } const nmanageReq = j as NmanageRequest mainHandler.managementManager.handleRequest(nmanageReq, event); return; } if (!j.rpcName) { + if (event.relayConstraint === 'service') { + log("got relay response on service only relay, ignoring") + } onClientEvent(j as { requestId: string }, event.pub) return } + if (event.relayConstraint === 'provider') { + log("got service request on provider only relay, ignoring") + return + } if (j.authIdentifier !== event.pub) { log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub) return @@ -79,7 +98,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett 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 }) }) }, event.startAtNano, event.startAtMs) - }) + }, beacon => mainHandler.liquidityProvider.onBeaconEvent(beacon)) // Mark nostr connected/ready after initial subscription tick mainHandler.adminManager.setNostrConnected(true) diff --git a/src/services/helpers/logger.ts b/src/services/helpers/logger.ts index b4cffc73..4d6601c9 100644 --- a/src/services/helpers/logger.ts +++ b/src/services/helpers/logger.ts @@ -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)") } 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 now = new Date() 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) => { logStream.write(message + "\n") } @@ -35,13 +47,13 @@ if (!fs.existsSync(`${logsDir}/components`)) { export const getLogger = (params: LoggerParams): PubLogger => { const writers: Writer[] = [] if (params.appName) { - writers.push(openWriter(`apps/${params.appName}`)) + writers.push(openWriter(`apps/${sanitizeFileName(params.appName)}`)) } if (params.userId) { - writers.push(openWriter(`users/${params.userId}`)) + writers.push(openWriter(`users/${sanitizeFileName(params.userId)}`)) } if (params.component) { - writers.push(openWriter(`components/${params.component}`)) + writers.push(openWriter(`components/${sanitizeFileName(params.component)}`)) } if (writers.length === 0) { writers.push(rootWriter) diff --git a/src/services/helpers/utilsWrapper.ts b/src/services/helpers/utilsWrapper.ts index d7d159f3..9bd97705 100644 --- a/src/services/helpers/utilsWrapper.ts +++ b/src/services/helpers/utilsWrapper.ts @@ -1,7 +1,7 @@ import { StateBundler } from "../storage/tlv/stateBundler.js"; import { TlvStorageFactory } from "../storage/tlv/tlvFilesStorageFactory.js"; -import { NostrSend } from "../nostr/handler.js"; import { ProcessMetricsCollector } from "../storage/tlv/processMetricsCollector.js"; +import { NostrSender } from "../nostr/sender.js"; type UtilsSettings = { noCollector?: boolean dataDir: string, @@ -10,9 +10,11 @@ type UtilsSettings = { export class Utils { tlvStorageFactory: TlvStorageFactory stateBundler: StateBundler - _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') } - constructor({ noCollector, dataDir, allowResetMetricsStorages }: UtilsSettings) { - this.tlvStorageFactory = new TlvStorageFactory(allowResetMetricsStorages) + nostrSender: NostrSender + + constructor({ noCollector, dataDir, allowResetMetricsStorages }: UtilsSettings, nostrSender: NostrSender) { + this.nostrSender = nostrSender + this.tlvStorageFactory = new TlvStorageFactory(allowResetMetricsStorages, nostrSender) this.stateBundler = new StateBundler(dataDir, this.tlvStorageFactory) if (!noCollector) { new ProcessMetricsCollector((metrics) => { @@ -21,11 +23,6 @@ export class Utils { } } - attachNostrSend(f: NostrSend) { - this._nostrSend = f - this.tlvStorageFactory.attachNostrSend(f) - } - Stop() { this.stateBundler.Stop() this.tlvStorageFactory.disconnect() diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 7ba9758a..daf7b411 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -17,12 +17,13 @@ import { SendCoinsReq } from './sendCoinsReq.js'; import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js'; import { ERROR, getLogger } from '../helpers/logger.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 { TxPointSettings } from '../storage/tlv/stateBundler.js'; import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js'; import SettingsManager from '../main/settingsManager.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 deadLndRetrySeconds = 20 @@ -32,6 +33,7 @@ type NodeSettingsOverride = { lndCertPath: string lndMacaroonPath: string } +export type LndAddress = { address: string, change: boolean } export default class { lightning: LightningClient invoices: InvoicesClient @@ -53,6 +55,7 @@ export default class { liquidProvider: LiquidityProvider utils: Utils unlockLnd: () => Promise + addressesCache: Record = {} constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, unlockLnd: () => Promise, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { this.getSettings = getSettings this.utils = utils @@ -63,7 +66,7 @@ export default class { this.htlcCb = htlcCb this.channelEventCb = channelEventCb this.liquidProvider = liquidProvider - + // Skip LND client initialization if using only liquidity provider if (liquidProvider.getSettings().useOnlyLiquidityProvider) { this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization") @@ -79,7 +82,7 @@ export default class { this.walletKit = new WalletKitClient(dummyTransport) return } - + const { lndAddr, lndCertPath, lndMacaroonPath } = this.getSettings().lndNodeSettings const lndCert = fs.readFileSync(lndCertPath); const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex'); @@ -331,6 +334,26 @@ export default class { }) } + async IsChangeAddress(address: string): Promise { + 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 { + 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 { // Force use of provider when bypass is enabled (addresses not supported by provider, but we should fail gracefully) if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { @@ -372,8 +395,8 @@ export default class { if (mustUseProvider) { console.log("using provider") const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) - const providerDst = this.liquidProvider.GetProviderDestination() - return { payRequest: invoice, providerDst } + const providerPubkey = this.liquidProvider.GetProviderPubkey() + return { payRequest: invoice, providerPubkey } } try { const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, true, memo, blind), DeadLineMetadata()) @@ -392,7 +415,7 @@ export default class { const decoded = decodeBolt11(paymentRequest) let numSatoshis = 0 let paymentHash = '' - + for (const section of decoded.sections) { if (section.name === 'amount') { // Amount is in millisatoshis @@ -401,11 +424,11 @@ export default class { paymentHash = section.value as string } } - + if (!paymentHash) { throw new Error("Payment hash not found in invoice") } - + return { numSatoshis, paymentHash } } catch (err: any) { 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 } } - 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 }> { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return { local: 0, remote: 0 } @@ -433,7 +448,7 @@ export default class { const r = res.response 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 { + async PayInvoice(invoice: string, amount: number, { routingFeeLimit, serviceFee }: { routingFeeLimit: number, serviceFee: number }, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise { // console.log("Paying invoice") if (this.outgoingOpsLocked) { this.log("outgoing ops locked, rejecting payment request") @@ -442,14 +457,14 @@ export default class { // Force use of provider when bypass is enabled const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider if (mustUseProvider) { - const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from) - const providerDst = this.liquidProvider.GetProviderDestination() - return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } + const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from, serviceFee) + const providerPubkey = this.liquidProvider.GetProviderPubkey() + return { feeSat: res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerPubkey } } await this.Health() try { 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 }) return new Promise((res, rej) => { stream.responses.onError(error => { diff --git a/src/services/lnd/lsp.ts b/src/services/lnd/lsp.ts index e734c62c..92643c2a 100644 --- a/src/services/lnd/lsp.ts +++ b/src/services/lnd/lsp.ts @@ -101,7 +101,7 @@ export class FlashsatsLSP extends LSP { return null } 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) 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 } 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) 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 } 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) return { orderId: fee.id, invoice: proposalRes.jit_bolt11, totalSats: decoded.numSatoshis, fees } } diff --git a/src/services/lnd/settings.ts b/src/services/lnd/settings.ts index 43a125a5..842cd57b 100644 --- a/src/services/lnd/settings.ts +++ b/src/services/lnd/settings.ts @@ -35,7 +35,7 @@ export type NodeInfo = { } export type Invoice = { payRequest: string - providerDst?: string + providerPubkey?: string } export type DecodedInvoice = { numSatoshis: number @@ -45,7 +45,7 @@ export type PaidInvoice = { feeSat: number valueSat: number paymentPreimage: string - providerDst?: string + providerPubkey?: string } diff --git a/src/services/lnd/swaps.ts b/src/services/lnd/swaps.ts new file mode 100644 index 00000000..727f0e88 --- /dev/null +++ b/src/services/lnd/swaps.ts @@ -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 + // 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 => { + 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 => { + 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 { + 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) { + 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(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(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(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 (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 (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(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) + } +} +*/ \ No newline at end of file diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 01383599..294635ed 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -5,7 +5,9 @@ import Storage from "../storage/index.js"; import * as Types from '../../../proto/autogenerated/ts/types.js' import LND from "../lnd/lnd.js"; import SettingsManager from "./settingsManager.js"; +import { Swaps } from "../lnd/swaps.js"; export class AdminManager { + settings: SettingsManager storage: Storage log = getLogger({ component: "adminManager" }) adminNpub = "" @@ -17,10 +19,13 @@ export class AdminManager { interval: NodeJS.Timer appNprofile: string lnd: LND + swaps: Swaps nostrConnected: boolean = false private nostrReset: () => Promise = 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.swaps = swaps this.dataDir = settings.getStorageSettings().dataDir this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub') this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll') @@ -45,6 +50,7 @@ export class AdminManager { setLND = (lnd: LND) => { this.lnd = lnd + this.swaps.SetLnd(lnd) } setNostrConnected = (connected: boolean) => { @@ -253,6 +259,29 @@ export class AdminManager { closing_txid: Buffer.from(res.txid).toString('hex') } } + + async ListAdminSwaps(): Promise { + return this.swaps.ListSwaps("admin", [], p => undefined, amt => 0) + } + + async GetAdminTransactionSwapQuotes(req: Types.TransactionSwapRequest): Promise { + const quotes = await this.swaps.GetTxSwapQuotes("admin", req.transaction_amount_sats, () => 0) + return { quotes } + } + async PayAdminTransactionSwap(req: Types.PayAdminTransactionSwapRequest): Promise { + 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) => { diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 5f78570e..9db8b5e6 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -69,14 +69,15 @@ export default class { throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing } const nostrSettings = this.settings.getSettings().nostrRelaySettings + const { max, serviceFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) return { userId: ctx.user_id, balance: user.balance_sats, - max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true), + max_withdrawable: max, user_identifier: appUser.identifier, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: 0, + network_max_fee_fixed: serviceFeeFloor, + service_fee_bps: serviceFeeBps, 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] }), 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, { amount: req.amount, invoice: req.invoice, - user_identifier: ctx.app_user_id - }) - } - async PayAddress(ctx: Types.UserContext, req: Types.PayInvoiceRequest): Promise { - return this.applicationManager.PayAppUserInvoice(ctx.app_id, { - amount: req.amount, - 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, }) } diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index c874d36a..aaafd8a7 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -17,7 +17,6 @@ type NsecLinkingData = { expiry: number } export default class { - storage: Storage settings: SettingsManager paymentManager: PaymentManager @@ -47,12 +46,13 @@ export default class { }, 60 * 1000); // 1 minute } - async StartAppsServiceBeacon(publishBeacon: (app: Application) => void) { + async StartAppsServiceBeacon(publishBeacon: (app: Application, fees: Types.CumulativeFees) => void) { this.serviceBeaconInterval = setInterval(async () => { try { + const fees = this.paymentManager.GetFees() const apps = await this.storage.applicationStorage.GetApplications() apps.forEach(app => { - publishBeacon(app) + publishBeacon(app, fees) }) } catch (e) { 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] }) 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 { identifier: u.identifier, info: { userId: u.user.user_id, balance: u.user.balance_sats, - max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true), + max_withdrawable: max, user_identifier: u.identifier, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: 0, + network_max_fee_fixed: serviceFeeFloor, + service_fee_bps: serviceFeeBps, 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] }), 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 }, - 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 { const app = await this.storage.applicationStorage.GetApplication(appId) 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 { max, serviceFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) return { max_withdrawable: max, identifier: req.user_identifier, info: { 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, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: 0, + network_max_fee_fixed: serviceFeeFloor, + service_fee_bps: serviceFeeBps, 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] }), 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 { const app = await this.storage.applicationStorage.GetApplication(appId) const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) - const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app) - getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats") - return paid + try { + const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app, { + 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 { diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 126a469c..28579d31 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -6,20 +6,16 @@ import { ERROR, getLogger } from "../helpers/logger.js"; import { DebitAccess, DebitAccessRules } from '../storage/entity/DebitAccess.js'; import { Application } from '../storage/entity/Application.js'; import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; -import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; -import { UnsignedEvent } from 'nostr-tools'; +import { NostrEvent } from '../nostr/nostrPool.js'; import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; -import { debitAccessRulesToDebitRules, newNdebitResponse,debitRulesToDebitAccessRules, - nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, - frequencyRuleName,IntervalTypeToSeconds,unitToIntervalType } from "./debitTypes.js"; +import { + debitAccessRulesToDebitRules, newNdebitResponse, debitRulesToDebitAccessRules, + nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, + frequencyRuleName, IntervalTypeToSeconds, unitToIntervalType +} from "./debitTypes.js"; export class DebitManager { - - - _nostrSend: NostrSend | null = null - applicationManager: ApplicationManager - storage: Storage lnd: LND logger = getLogger({ component: 'DebitManager' }) @@ -29,16 +25,6 @@ export class DebitManager { 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 => { const allDebitsAccesses = await this.storage.debitStorage.GetAllUserDebitAccess(ctx.app_user_id) 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 }) return 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 case Types.DebitResponse_response_type.AUTHORIZE: 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 { this.logger("🔍 [DEBIT REQUEST] Paying single invoice") - const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) - 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 { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) 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) { 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 }) @@ -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", { npub, request_id, @@ -122,20 +106,20 @@ export class DebitManager { const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) this.validateAccessRules(access, app, appUser) 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 } - 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) { 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 }) throw e } - + } handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => { - if (!this._nostrSend) { - throw new Error("No nostrSend attached") + if (!this.storage.NostrSender().IsReady()) { + throw new Error("Nostr sender not ready") } this.logger("📥 [DEBIT REQUEST] Received debit request", { fromPub: event.pub, @@ -144,10 +128,10 @@ export class DebitManager { 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') { 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 } const { appUser } = res @@ -155,30 +139,27 @@ export class DebitManager { this.handleAuthRequired(pointerdata, event, res) return } - const { op, debitRes } = res - this.notifyPaymentSuccess(appUser, debitRes, op, event) + const { debitRes } = res + this.notifyPaymentSuccess(debitRes, event) } - handleAuthRequired = (data:NdebitData, event: NostrEvent, res: AuthRequiredRes) => { + handleAuthRequired = (data: NdebitData, event: NostrEvent, res: AuthRequiredRes) => { if (!res.appUser.nostr_public_key) { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId }) return } 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 }) => { - 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 }) - } + notifyPaymentSuccess = (debitRes: NdebitSuccess, event: { pub: string, id: string, appId: string }) => { this.sendDebitResponse(debitRes, event) } sendDebitResponse = (debitRes: NdebitFailure | NdebitSuccess, event: { pub: string, id: string, appId: string }) => { 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 => { @@ -286,15 +267,14 @@ export class DebitManager { } await this.validateAccessRules(authorization, app, appUser) this.logger("🔍 [DEBIT REQUEST] Sending requested debit payment") - const { op, payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) - return { status: 'invoicePaid', op, app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } + const { payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) + return { status: 'invoicePaid', app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } } 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 }) - await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee + payment.network_fee) - const op = this.newPaymentOperation(payment, bolt11) - return { payment, op } + await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee) + return { payment } } validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise => { @@ -325,21 +305,5 @@ export class DebitManager { } 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 - } - } } diff --git a/src/services/main/debitTypes.ts b/src/services/main/debitTypes.ts index 83aca293..719de95f 100644 --- a/src/services/main/debitTypes.ts +++ b/src/services/main/debitTypes.ts @@ -1,9 +1,9 @@ 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 { ApplicationUser } from '../storage/entity/ApplicationUser.js'; 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 frequencyRuleName = 'frequency' @@ -96,7 +96,7 @@ export const nofferErrors = { } export type AuthRequiredRes = { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, app: Application, appUser: ApplicationUser } 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 | { status: 'authOk', debitRes: NdebitSuccess } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index fcbb3cf8..ab528b84 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -12,7 +12,7 @@ import AppUserManager from "./appUserManager.js" import { Application } from '../storage/entity/Application.js' import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js' import { UnsignedEvent } from 'nostr-tools' -import { NostrSend } from '../nostr/handler.js' +import { NostrSend } from '../nostr/nostrPool.js' import MetricsManager from '../metrics/index.js' import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityManager } from "./liquidityManager.js" @@ -30,7 +30,7 @@ import { Agent } from "https" import { NotificationsManager } from "./notificationsManager.js" import { ApplicationUser } from '../storage/entity/ApplicationUser.js' import SettingsManager from './settingsManager.js' -import { NostrSettings } from '../nostr/handler.js' +import { NostrSettings, AppInfo } from '../nostr/nostrPool.js' type UserOperationsSub = { id: string newIncomingInvoice: (operation: Types.UserOperation) => void @@ -61,8 +61,6 @@ export default class { rugPullTracker: RugPullTracker unlocker: Unlocker notificationsManager: NotificationsManager - //webRTC: webRTC - nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } nostrProcessPing: (() => Promise) | null = null nostrReset: (settings: NostrSettings) => void = () => { getLogger({})("nostr reset not initialized yet") } 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.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.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) @@ -102,19 +100,13 @@ export default class { } StartBeacons() { - this.applicationManager.StartAppsServiceBeacon(app => { - this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url }) + this.applicationManager.StartAppsServiceBeacon((app, fees) => { + this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, fees }) }) } attachNostrSend(f: NostrSend) { - this.nostrSend = 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) + this.utils.nostrSender.AttachNostrSend(f) } attachNostrProcessPing(f: () => Promise) { @@ -168,7 +160,8 @@ export default class { NewBlockHandler = async (height: number) => { let confirmed: (PendingTx & { confs: number; })[] let log = getLogger({}) - + this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height) + .catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err)) try { const balanceEvents = await this.paymentManager.GetLndBalance() await this.metricsManager.NewBlockCb(height, balanceEvents) @@ -220,6 +213,10 @@ export default class { const { blockHeight } = await this.lnd.GetInfo() const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx) if (!userAddress) { + const isChange = await this.lnd.IsChangeAddress(address) + if (isChange) { + return + } await this.metricsManager.AddRootAddressPaid(address, txOutput, amount) return } @@ -230,11 +227,8 @@ export default class { return } log = getLogger({ appName: userAddress.linkedApplication.name }) - const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id - let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment) - if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) { - fee = 0 - } + const isManagedUser = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id + const fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_TX, amount, isManagedUser) try { // 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) @@ -274,11 +268,8 @@ export default class { return } log = getLogger({ appName: userInvoice.linkedApplication.name }) - const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id - let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment) - if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) { - fee = 0 - } + const isManagedUser = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id + const fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isManagedUser) try { 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 }) @@ -385,10 +376,11 @@ export default class { getLogger({ appName: app.name })("cannot notify user, not a nostr user") return } - - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } + const balance = user.user.balance_sats + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = + { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } 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) } @@ -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) { getLogger({ appName: app.name })("cannot update beacon, public key not set") return @@ -421,7 +413,7 @@ export default class { pubkey: app.nostr_public_key, 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) { @@ -441,7 +433,7 @@ export default class { tags, } 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) { @@ -465,7 +457,7 @@ export default class { ["clink_version", "1"] ], } - this.nostrSend( + this.utils.nostrSender.Send( { type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event, encrypt: { toPub: invoice.clink_requester_pub } } ) @@ -474,28 +466,38 @@ export default class { async ResetNostr() { const apps = await this.storage.applicationStorage.GetApplications() const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] + const fees = this.paymentManager.GetFees() 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 liquidityProviderApp = apps.find(app => defaultNames.includes(app.name)) - if (!liquidityProviderApp) { - throw new Error("wallet app not initialized correctly") - } - const liquidityProviderInfo = { - privateKey: liquidityProviderApp.nostr_private_key || "", - publicKey: liquidityProviderApp.nostr_public_key || "", - name: "liquidity_provider", clientId: `client_${liquidityProviderApp.app_id}` + const local = apps.find(app => defaultNames.includes(app.name)) + if (!local) { + throw new Error("local app not initialized correctly") } + 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 = { - apps: apps.map(a => ({ appId: a.app_id, name: a.name, privateKey: a.nostr_private_key || "", publicKey: a.nostr_public_key || "" })), - relays: this.settings.getSettings().nostrRelaySettings.relays, + apps: appsInfo, + relays, maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength, - clients: [liquidityProviderInfo] + /* clients: [local], + providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub */ } this.nostrReset(s) } -} - - +} \ No newline at end of file diff --git a/src/services/main/init.ts b/src/services/main/init.ts index 8c20195c..3cbba602 100644 --- a/src/services/main/init.ts +++ b/src/services/main/init.ts @@ -10,6 +10,8 @@ import { Wizard } from "../wizard/index.js" import { AdminManager } from "./adminManager.js" import SettingsManager from "./settingsManager.js" import { LoadStorageSettingsFromEnv } from "../storage/index.js" +import { NostrSender } from "../nostr/sender.js" +import { Swaps } from "../lnd/swaps.js" export type AppData = { privateKey: string; publicKey: string; @@ -18,7 +20,8 @@ export type AppData = { } export const initSettings = async (log: PubLogger, storageSettings: StorageSettings): Promise => { - 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) await storageManager.Connect(log) const settingsManager = new SettingsManager(storageManager) @@ -30,7 +33,8 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM const utils = storageManager.utils const unlocker = new Unlocker(settingsManager, storageManager) 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 if (settingsManager.getSettings().serviceSettings.wizard) { 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 } } })) - const liquidityProviderApp = apps.find(app => defaultNames.includes(app.name)) - if (!liquidityProviderApp) { - throw new Error("wallet app not initialized correctly") + const localProviderClient = apps.find(app => defaultNames.includes(app.name)) + if (!localProviderClient) { + throw new Error("local app not initialized correctly") } - const liquidityProviderInfo = { - privateKey: liquidityProviderApp.privateKey, - publicKey: liquidityProviderApp.publicKey, - name: "liquidity_provider", clientId: `client_${liquidityProviderApp.appId}` - } - mainHandler.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) + mainHandler.liquidityProvider.setNostrInfo({ localId: `client_${localProviderClient.appId}`, localPubkey: localProviderClient.publicKey }) const stop = await processArgs(mainHandler) if (stop) { return } - await mainHandler.paymentManager.checkPendingPayments() + await mainHandler.paymentManager.checkPaymentStatus() + await mainHandler.paymentManager.checkMissedChainTxs() await mainHandler.paymentManager.CleanupOldUnpaidInvoices() await mainHandler.appUserManager.CleanupInactiveUsers() await mainHandler.appUserManager.CleanupNeverActiveUsers() await mainHandler.paymentManager.watchDog.Start() - return { mainHandler, apps, liquidityProviderInfo, liquidityProviderApp, wizard, adminManager } + return { mainHandler, apps, localProviderClient, wizard, adminManager } } const processArgs = async (mainHandler: Main) => { diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index 183988ef..8715ac7b 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -50,8 +50,11 @@ export class LiquidityManager { } beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { - + const providerReady = this.liquidityProvider.IsReady() if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + if (!providerReady) { + throw new Error("cannot use liquidity provider, it is not ready") + } return 'provider' } @@ -63,7 +66,7 @@ export class LiquidityManager { if (remote > amount) { return 'lnd' } - const providerCanHandle = await this.liquidityProvider.CanProviderHandle({ action: 'receive', amount }) + const providerCanHandle = this.liquidityProvider.IsReady() if (!providerCanHandle) { 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 (!providerReady) { + throw new Error("cannot use liquidity provider, it is not ready") + } return 'provider' } - const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount }) - if (canHandle) { - return 'provider' + if (!providerReady) { + return 'lnd' } - return 'lnd' + const canHandle = await this.liquidityProvider.CanProviderPay(amount, localServiceFee) + if (!canHandle) { + return 'lnd' + } + return 'provider' } + afterOutInvoicePaid = async () => { } shouldDrainProvider = async () => { @@ -99,7 +110,7 @@ export class LiquidityManager { if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { return } - const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() + const maxW = await this.liquidityProvider.GetMaxWithdrawable() const { remote } = await this.lnd.ChannelBalance() const drainable = Math.min(maxW, remote) if (drainable < 500) { @@ -129,7 +140,7 @@ export class LiquidityManager { try { const invoice = await this.lnd.NewInvoice(amt, "liqudity provider drain", defaultInvoiceExpiry, { from: 'system', useProvider: false }) const res = await this.liquidityProvider.PayInvoice(invoice.payRequest, amt, 'system') - const fees = res.network_fee + res.service_fee + const fees = res.service_fee this.feesPaid += fees this.updateLatestDrain(true, amt) } catch (err: any) { @@ -172,7 +183,7 @@ export class LiquidityManager { if (pendingChannels.pendingOpenChannels.length > 0) { return { shouldOpen: false } } - const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() + const maxW = await this.liquidityProvider.GetMaxWithdrawable() if (maxW < threshold) { return { shouldOpen: false } } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 10f9bfad..1f8fe2dd 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -1,26 +1,23 @@ import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js' import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js' import * as Types from '../../../proto/autogenerated/ts/types.js' -import { getLogger } from '../helpers/logger.js' +import { ERROR, getLogger } from '../helpers/logger.js' import { Utils } from '../helpers/utilsWrapper.js' -import { NostrEvent, NostrSend } from '../nostr/handler.js' import { InvoicePaidCb } from '../lnd/settings.js' import Storage from '../storage/index.js' import SettingsManager from './settingsManager.js' import { LiquiditySettings } from './settings.js' -export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } - export type nostrCallback = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export class LiquidityProvider { getSettings: () => LiquiditySettings client: ReturnType clientCbs: Record> = {} - clientId: string = "" - myPub: string = "" + localId: string = "" + localPubkey: string = "" log = getLogger({ component: 'liquidityProvider' }) - nostrSend: NostrSend | null = null + // nostrSend: NostrSend | null = null configured = false - pubDestination: string + providerPubkey: string ready: boolean invoicePaidCb: InvoicePaidCb connecting = false @@ -28,14 +25,18 @@ export class LiquidityProvider { queue: ((state: 'ready') => void)[] = [] utils: Utils pendingPayments: Record = {} + feesCache: Types.CumulativeFees | null = null + lastSeenBeacon = 0 + latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise + pendingPaymentsAck: Record = {} // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { this.utils = utils this.getSettings = getSettings - const pubDestination = getSettings().liquidityProviderPub + const providerPubkey = getSettings().liquidityProviderPub const disableLiquidityProvider = getSettings().disableLiquidityProvider - if (!pubDestination) { + if (!providerPubkey) { this.log("No pub provider to liquidity provider, will not be initialized") return } @@ -43,19 +44,29 @@ export class LiquidityProvider { this.log("Liquidity provider is disabled, will not be initialized") return } - this.log("connecting to liquidity provider:", pubDestination) - this.pubDestination = pubDestination + this.log("connecting to liquidity provider:", providerPubkey) + this.providerPubkey = providerPubkey this.invoicePaidCb = invoicePaidCb this.incrementProviderBalance = incrementProviderBalance this.client = newNostrClient({ - pubDestination: this.pubDestination, - retrieveNostrUserAuth: async () => this.myPub, - retrieveNostrAdminAuth: async () => this.myPub, - retrieveNostrMetricsAuth: async () => this.myPub, - retrieveNostrGuestWithPubAuth: async () => this.myPub + pubDestination: this.providerPubkey, + retrieveNostrUserAuth: async () => this.localPubkey, + retrieveNostrAdminAuth: async () => this.localPubkey, + retrieveNostrMetricsAuth: async () => this.localPubkey, + retrieveNostrGuestWithPubAuth: async () => this.localPubkey }, this.clientSend, this.clientSub) + this.utils.nostrSender.OnReady(() => { + this.setSetIfConfigured() + if (this.configured) { + clearInterval(this.configuredInterval) + this.Connect() + } + }) this.configuredInterval = setInterval(() => { + if (!this.configured && this.utils.nostrSender.IsReady()) { + this.setSetIfConfigured() + } if (this.configured) { clearInterval(this.configuredInterval) this.Connect() @@ -63,16 +74,17 @@ export class LiquidityProvider { }, 1000) } - GetProviderDestination() { - return this.pubDestination + GetProviderPubkey() { + return this.providerPubkey } 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'> => { - if (!this.pubDestination || this.getSettings().disableLiquidityProvider) { + if (!this.providerPubkey || this.getSettings().disableLiquidityProvider) { return 'inactive' } if (this.IsReady()) { @@ -94,6 +106,7 @@ export class LiquidityProvider { return } this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0) + this.lastSeenBeacon = Date.now() this.ready = true this.queue.forEach(q => q('ready')) this.log("subbing to user operations") @@ -107,9 +120,12 @@ export class LiquidityProvider { try { await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') this.incrementProviderBalance(res.operation.amount) + this.latestReceivedBalance = res.latest_balance } catch (err: any) { 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 } + 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('providerMaxWithdrawable', res.max_withdrawable) return res } - GetLatestMaxWithdrawable = async () => { - if (!this.IsReady()) { - return 0 + GetFees = () => { + if (!this.feesCache) { + throw new Error("fees not cached") } - const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.max_withdrawable + return this.feesCache } - 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()) { return 0 } - const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.balance + return this.latestReceivedBalance } GetPendingBalance = async () => { return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0) } - CalculateExpectedFeeLimit = (amount: number, info: Types.UserInfo) => { - const serviceFeeRate = info.service_fee_bps / 10000 + GetServiceFee = (amount: number, f?: Types.CumulativeFees) => { + const fees = f ? f : this.GetFees() + const serviceFeeRate = fees.serviceFeeBps / 10000 const serviceFee = Math.ceil(serviceFeeRate * amount) - const networkMaxFeeRate = info.network_max_fee_bps / 10000 - const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + info.network_max_fee_fixed) - return serviceFee + networkFeeLimit + return Math.max(serviceFee, fees.serviceFeeFloor) } - CanProviderHandle = async (req: LiquidityRequest) => { + CanProviderPay = async (amount: number, localServiceFee: number): Promise => { if (!this.IsReady()) { + this.log("provider is not ready") return false } - const maxW = await this.GetLatestMaxWithdrawable() - if (req.action === 'spend') { - return maxW > req.amount + const maxW = this.GetMaxWithdrawable() + if (maxW < 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 } AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { try { 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 }) 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 { 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() - if (userInfo.status === 'ERROR') { - throw new Error(userInfo.reason) + const fees = this.GetFees() + const providerServiceFee = this.GetServiceFee(decodedAmount, fees) + 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) - const res = await this.client.PayInvoice({ invoice, amount: 0 }) + this.pendingPayments[invoice] = decodedAmount + providerServiceFee + 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') { this.log("error paying invoice", 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.latestReceivedBalance = res.latest_balance this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) return res } catch (err) { @@ -221,7 +267,7 @@ export class LiquidityProvider { GetPaymentState = async (invoice: string) => { 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 }) if (res.status === 'ERROR') { @@ -233,7 +279,7 @@ export class LiquidityProvider { GetOperations = async () => { 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({ latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, @@ -247,31 +293,48 @@ export class LiquidityProvider { return res } - setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => { - this.log("setting nostr info") - this.clientId = clientId - this.myPub = myPub + setNostrInfo = ({ localId, localPubkey }: { localPubkey: string, localId: string }) => { + this.localId = localId + this.localPubkey = localPubkey 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 = () => { - if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) { - this.configured = true - this.log("configured to send to ", this.pubDestination) + if (this.utils.nostrSender.IsReady() && !!this.providerPubkey && !!this.localId && !!this.localPubkey) { + if (!this.configured) { + 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) => { - if (fromPub !== this.pubDestination) { - this.log("got event from invalid pub", fromPub, this.pubDestination) + if (fromPub !== this.providerPubkey) { + this.log("got event from invalid pub", fromPub, this.providerPubkey) return false } if (this.clientCbs[res.requestId]) { @@ -288,9 +351,6 @@ export class LiquidityProvider { } clientSend = (to: string, message: NostrRequest): Promise => { - if (!this.configured || !this.nostrSend) { - throw new Error("liquidity provider not initialized") - } if (!message.requestId) { message.requestId = makeId(16) } @@ -298,7 +358,7 @@ export class LiquidityProvider { if (this.clientCbs[reqId]) { 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', pub: to, content: JSON.stringify(message) @@ -317,9 +377,6 @@ export class LiquidityProvider { } clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => { - if (!this.configured || !this.nostrSend) { - throw new Error("liquidity provider not initialized") - } if (!message.requestId) { message.requestId = message.rpcName } @@ -336,7 +393,7 @@ export class LiquidityProvider { this.log("sub for", reqId, "was already registered, overriding") return } - this.nostrSend({ type: 'client', clientId: this.clientId }, { + this.utils.nostrSender.Send({ type: 'client', clientId: this.localId }, { type: 'content', pub: to, content: JSON.stringify(message) diff --git a/src/services/main/managementManager.ts b/src/services/main/managementManager.ts index 2255a8e3..273dd3bc 100644 --- a/src/services/main/managementManager.ts +++ b/src/services/main/managementManager.ts @@ -2,7 +2,7 @@ import { getRepository } from "typeorm"; import { User } from "../storage/entity/User.js"; import { UserOffer } from "../storage/entity/UserOffer.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 { OfferManager } from "./offerManager.js"; import * as Types from "../../../proto/autogenerated/ts/types.js"; @@ -13,7 +13,6 @@ import SettingsManager from "./settingsManager.js"; type Result = { state: 'success', result: T } | { state: 'error', err: NmanageFailure } | { state: 'authRequired' } export class ManagementManager { - private nostrSend: NostrSend; private storage: Storage; private settings: SettingsManager; private awaitingRequests: Record = {} @@ -24,10 +23,6 @@ export class ManagementManager { this.logger = getLogger({ component: 'ManagementManager' }) } - attachNostrSend(f: NostrSend) { - this.nostrSend = f - } - ResetManage = async (ctx: Types.UserContext, req: Types.ManageOperation): Promise => { 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 }) => { 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.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) { 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) { @@ -107,7 +102,7 @@ export class ManagementManager { return } 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) { this.logger(ERROR, err.message || err) this.sendError(event, { res: 'GFY', code: 2, error: 'Temporary Failure' }) diff --git a/src/services/main/offerManager.ts b/src/services/main/offerManager.ts index 11113746..70400a20 100644 --- a/src/services/main/offerManager.ts +++ b/src/services/main/offerManager.ts @@ -4,7 +4,7 @@ import ProductManager from "./productManager.js"; import Storage from '../storage/index.js' import LND from "../lnd/lnd.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 { UserOffer } from '../storage/entity/UserOffer.js'; import { LiquidityManager } from "./liquidityManager.js" @@ -31,9 +31,6 @@ const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay } } } export class OfferManager { - - - _nostrSend: NostrSend | null = null settings: SettingsManager applicationManager: ApplicationManager productManager: ProductManager @@ -50,16 +47,6 @@ export class OfferManager { 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 { const newOffer = await this.storage.offerStorage.AddUserOffer(ctx.app_user_id, { payer_data: req.payer_data, @@ -182,7 +169,7 @@ export class OfferManager { max: offerInvoice.max }) 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 } @@ -194,7 +181,7 @@ export class OfferManager { }) 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", { toPub: event.pub, diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 08bf8ee0..3c7812db 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -12,11 +12,16 @@ import { Payment_PaymentStatus } from '../../../proto/lnd/lightning.js' import { Event, verifiedSymbol, verifyEvent } from 'nostr-tools' import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js' import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js' +import { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.js' import { Watchdog } from './watchdog.js' import { LiquidityManager } from './liquidityManager.js' import { Utils } from '../helpers/utilsWrapper.js' import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.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 { serial_id: number paid_amount: number @@ -36,6 +41,8 @@ interface UserOperationInfo { }; internal?: boolean; } + + export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment } const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]` const defaultLnAddressMetadata = (text: string, id: string) => `[["text/plain", "${text}"],["text/identifier", "${id}"]]` @@ -51,25 +58,34 @@ export default class { watchDog: Watchdog liquidityManager: LiquidityManager 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.metrics = metrics this.settings = settings this.lnd = lnd this.liquidityManager = liquidityManager this.utils = utils this.watchDog = new Watchdog(settings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker) + this.swaps = swaps this.addressPaidCb = addressPaidCb this.invoicePaidCb = invoicePaidCb - } - Stop() { - this.watchDog.Stop() + this.invoiceLock = new InvoiceLock() } - 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() for (const p of pendingPayments) { - log("checking state of payment: ", p.invoice) + log("checking status of payment: ", p.invoice) if (p.internal) { log("found pending internal payment", p.serial_id) } else if (p.liquidityProvider) { @@ -85,7 +101,7 @@ export default class { checkPendingProviderPayment = async (log: PubLogger, p: UserInvoicePayment) => { const state = await this.lnd.liquidProvider.GetPaymentState(p.invoice) 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) await this.storage.StartTransaction(async tx => { await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx) @@ -94,18 +110,16 @@ export default class { return } else if (state.paid_at_unix > 0) { log("provider payment succeeded", p.serial_id, "updating payment info") - const routingFeeLimit = p.routing_fees const serviceFee = p.service_fees - const actualFee = state.network_fee + state.service_fee - await this.storage.StartTransaction(async tx => { - if (routingFeeLimit - actualFee > 0) { - this.log("refund pending provider payment routing fee", routingFeeLimit, actualFee, "sats") - await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) - } - 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 && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees") + const networkFee = state.service_fee + await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, serviceFee, true) + const remainingFee = serviceFee - networkFee + if (remainingFee < 0) { + this.log("WARNING: provider fee was higher than expected,", remainingFee, "were lost") + } + + if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && remainingFee > 0) { + await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees") } 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 }) @@ -126,7 +140,6 @@ export default class { log(ERROR, "lnd payment not found for pending payment hash ", decoded.paymentHash) return } - switch (payment.status) { case Payment_PaymentStatus.UNKNOWN: log("pending payment in unknown state", p.serial_id, "no action will be performed") @@ -136,24 +149,22 @@ export default class { return case Payment_PaymentStatus.SUCCEEDED: log("pending payment succeeded", p.serial_id, "updating payment info") - const routingFeeLimit = p.routing_fees const serviceFee = p.service_fees - const actualFee = Number(payment.feeSat) - await this.storage.StartTransaction(async tx => { - if (routingFeeLimit - actualFee > 0) { - this.log("refund pending payment routing fee", routingFeeLimit, actualFee, "sats") - await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) - } - 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 && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees") + const networkFee = Number(payment.feeSat) + + await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, p.service_fees, true, undefined) + const remainingFee = serviceFee - networkFee + if (remainingFee < 0) { // should not be possible beacuse of the fee limit + this.log("WARNING: lnd fee was higher than expected,", remainingFee, "were lost") + } + if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && remainingFee > 0) { + await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees") } 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 }) return 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) await this.storage.StartTransaction(async 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 { + 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 { + 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) { case Types.UserOperationType.INCOMING_TX: - return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount) - case Types.UserOperationType.OUTGOING_TX: - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingTxFee * amount) + return 0 case Types.UserOperationType.INCOMING_INVOICE: - if (appUser) { - return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount) - } - return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount) - 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) { + // Incoming invoice fees are always 0 (not configurable) + return 0 + case Types.UserOperationType.INCOMING_USER_TO_USER: + if (managedUser) { 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: 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) { if (!this.settings.getSettings().lndSettings.mockLnd) { 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 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 : "" this.storage.eventsLog.LogEvent({ type: 'new_invoice', userId: user.user_id, appUserId: "", appId, balance: user.balance_sats, data: userInvoice.invoice, amount: req.amountSats }) return { @@ -237,14 +387,19 @@ export default class { } } - GetMaxPayableInvoice(balance: number, appUser: boolean): number { - let maxWithinServiceFee = 0 - if (appUser) { - 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))) - } - return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) + GetFees = (): Types.CumulativeFees => { + const { serviceFeeBps, serviceFeeFloor } = this.settings.getSettings().serviceFeeSettings + return { serviceFeeFloor, serviceFeeBps } + } + + GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } { + const { serviceFeeFloor, serviceFeeBps } = this.GetFees() + 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 { 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 { + async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, optionals: { swapOperationId?: string, ack?: (op: Types.UserOperation) => void } = {}): Promise { await this.watchDog.PaymentRequested() const maybeBanned = await this.storage.userStorage.GetUser(userId) if (maybeBanned.locked) { 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) if (decoded.numSatoshis !== 0 && req.amount !== 0) { 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") } const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) - const isAppUserPayment = userId !== linkedApplication.owner.user_id - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) + const isManagedUser = userId !== linkedApplication.owner.user_id + const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isManagedUser) const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice) if (internalInvoice && internalInvoice.paid_at_unix > 0) { throw new Error("this invoice was already paid") @@ -278,26 +441,42 @@ export default class { throw new Error("this invoice was already paid") } let paymentInfo = { preimage: "", amtPaid: 0, networkFee: 0, serialId: 0 } - 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, req.debit_npub) + if (this.invoiceLock.isLocked(req.invoice)) { + throw new Error("this invoice is already being paid") } - if (isAppUserPayment && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee, "fees") + this.invoiceLock.lock(req.invoice) + 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) 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 { preimage: paymentInfo.preimage, amount_paid: paymentInfo.amtPaid, - operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, - network_fee: paymentInfo.networkFee, + operation_id: opId, + network_fee: 0, 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) { 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") } + const { amountForLnd, payAmount, serviceFee } = amounts const totalAmountToDecrement = payAmount + serviceFee - const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) - const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) - const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined + const routingFeeLimit = this.getRoutingFeeLimit(payAmount) + const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee) + const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderPubkey() : undefined const pendingPayment = await this.storage.StartTransaction(async tx => { - await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx) - return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, debitNpub) + await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, invoice, tx) + return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: 0 }, linkedApplication, provider, tx, optionals) }, "payment started") 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 { - 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) }) - if (routingFeeLimit - payment.feeSat > 0) { - this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats") - await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice) + await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerPubkey) + const feeDiff = serviceFee - payment.feeSat + 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 } } 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) throw err } @@ -354,60 +536,79 @@ export default class { } catch (err) { 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) - throw err } - } + + async GetTransactionSwapQuotes(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise { + 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 { - throw new Error("address payment currently disabled, use Lightning instead") 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) if (maybeBanned.locked) { 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 { + 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 { + 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 app = await this.storage.applicationStorage.GetApplication(ctx.app_id) - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) - const isAppUserPayment = ctx.user_id !== app.owner.user_id - 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') - } + const isManagedUser = ctx.user_id !== app.owner.user_id + const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, req.amountSats, isManagedUser) - 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') } - - const newTx = await this.storage.paymentStorage.AddUserTransactionPayment(ctx.user_id, req.address, txId, 0, req.amoutSats, chainFees, serviceFee, !!internalAddress, blockHeight, app) + const chainFees = 0 + 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 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 { txId: txId, operation_id: `${Types.UserOperationType.OUTGOING_TX}-${newTx.serial_id}`, @@ -416,6 +617,16 @@ export default class { } } + async ListSwaps(ctx: Types.UserContext): Promise { + 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 { 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 { 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 { @@ -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 { const user = await this.storage.userStorage.GetUser(userId) if (user.locked) { @@ -684,8 +897,10 @@ export default class { return { paid_at_unix: invoice.paid_at_unix, amount: invoice.paid_amount, - network_fee: invoice.routing_fees, + network_fee: 0, 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) { throw new Error("not enough balance to send payment") } - const isAppUserPayment = fromUser.user_id !== linkedApplication.owner.user_id - let fee = this.getServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) + const isManagedUser = fromUser.user_id !== linkedApplication.owner.user_id + let fee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isManagedUser) const toDecrement = amount + fee 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.IncrementUserBalance(toUser.user_id, amount, `${fromUserId}:${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) } return paymentEntry @@ -785,3 +1000,16 @@ export default class { } } + +class InvoiceLock { + locked: Record = {} + lock(invoice: string) { + this.locked[invoice] = true + } + unlock(invoice: string) { + delete this.locked[invoice] + } + isLocked(invoice: string) { + return this.locked[invoice] + } +} \ No newline at end of file diff --git a/src/services/main/rugPullTracker.ts b/src/services/main/rugPullTracker.ts index ab5889a5..87d4f2df 100644 --- a/src/services/main/rugPullTracker.ts +++ b/src/services/main/rugPullTracker.ts @@ -20,18 +20,18 @@ export class RugPullTracker { } CheckProviderBalance = async (): Promise<{ balance: number, prevBalance?: number }> => { - const pubDst = this.liquidProvider.GetProviderDestination() + const pubDst = this.liquidProvider.GetProviderPubkey() if (!pubDst) { return { balance: 0 } } const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst) const ready = this.liquidProvider.IsReady() if (ready) { - const balance = await this.liquidProvider.GetLatestBalance() + const balance = this.liquidProvider.GetLatestBalance() const pendingBalance = await this.liquidProvider.GetPendingBalance() const trackedBalance = balance + pendingBalance 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) return { balance: trackedBalance } } diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index f6c7dffe..464b53a2 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -1,31 +1,27 @@ import { EnvCacher, EnvMustBeNonEmptyString, EnvMustBeInteger, chooseEnv, chooseEnvBool, chooseEnvInt } from '../helpers/envParser.js' import os from 'os' import path from 'path' +import { nip19 } from '@shocknet/clink-sdk' export type ServiceFeeSettings = { - incomingTxFee: number - outgoingTxFee: number - incomingAppInvoiceFee: number - incomingAppUserInvoiceFee: number - outgoingAppInvoiceFee: number - outgoingAppUserInvoiceFee: number - outgoingAppUserInvoiceFeeBps: number + serviceFee: number + serviceFeeBps: number + serviceFeeFloor: number userToUserFee: number - appToUserFee: number + rootToUserFee: number } export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record, 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 { - incomingTxFee: chooseEnvInt("INCOMING_CHAIN_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000, - outgoingTxFee: chooseEnvInt("OUTGOING_CHAIN_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000, - incomingAppInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000, - 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, + serviceFeeBps, + serviceFee: serviceFeeBps / 10000, + serviceFeeFloor, 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 lndMacaroonPath: string // cold setting } +const networks = ['mainnet', 'testnet', 'regtest'] as const +export type BTCNetwork = (typeof networks)[number] export type LndSettings = { lndLogDir: string - feeRateLimit: number - feeFixedLimit: number - feeRateBps: number + routingFeeLimitBps: number + routingFeeFloor: number mockLnd: boolean - + network: BTCNetwork } const resolveHome = (filepath: string) => { @@ -111,13 +108,16 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record, 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 { - lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, path.join(lndDir(), "logs", "bitcoin", "mainnet", "lnd.log"), addToDb), - feeRateBps: feeRateBps, - feeRateLimit: feeRateBps / 10000, - feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 100, addToDb), - mockLnd: false + lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb), + routingFeeLimitBps, + routingFeeFloor, + mockLnd: false, + network: networks.includes(network) ? network : 'mainnet' } } @@ -167,13 +167,48 @@ export type LiquiditySettings = { liquidityProviderPub: string // cold setting useOnlyLiquidityProvider: boolean // hot setting disableLiquidityProvider: boolean // hot setting + providerRelayUrl: string } export const LoadLiquiditySettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): LiquiditySettings => { - //const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB === "null" ? "" : (process.env.LIQUIDITY_PROVIDER_PUB || "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e") - const liquidityProviderPub = chooseEnv("LIQUIDITY_PROVIDER_PUB", dbEnv, "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e", addToDb) + const providerNprofile = chooseEnv("PROVIDER_NPROFILE", dbEnv, "nprofile1qyd8wumn8ghj7um5wfn8y7fwwd5x7cmt9ehx2arhdaexkqpqwmk5tuqvafa6ckwc6zmaypyy3af3n4aeds2ql7m0ew42kzsn638q9s9z8p", addToDb) + const { liquidityProviderPub, providerRelayUrl } = decodeNprofile(providerNprofile) + const disableLiquidityProvider = chooseEnvBool("DISABLE_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) || liquidityProviderPub === "null" 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, 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) + } } diff --git a/src/services/main/settingsManager.ts b/src/services/main/settingsManager.ts index e5046f50..f32fba93 100644 --- a/src/services/main/settingsManager.ts +++ b/src/services/main/settingsManager.ts @@ -5,7 +5,8 @@ import { LiquiditySettings, LndNodeSettings, LndSettings, LoadLiquiditySettingsFromEnv, LoadLSPSettingsFromEnv, LSPSettings, ServiceFeeSettings, ServiceSettings, LoadServiceFeeSettingsFromEnv, LoadNostrRelaySettingsFromEnv, LoadServiceSettingsFromEnv, LoadWatchdogSettingsFromEnv, - LoadLndNodeSettingsFromEnv, LoadLndSettingsFromEnv, NostrRelaySettings, WatchdogSettings + LoadLndNodeSettingsFromEnv, LoadLndSettingsFromEnv, NostrRelaySettings, WatchdogSettings, SwapsSettings, LoadSwapsSettingsFromEnv + } from "./settings.js" export default class SettingsManager { storage: Storage @@ -27,6 +28,7 @@ export default class SettingsManager { serviceFeeSettings: LoadServiceFeeSettingsFromEnv(dbEnv, addToDb), serviceSettings: LoadServiceSettingsFromEnv(dbEnv, addToDb), watchDogSettings: LoadWatchdogSettingsFromEnv(dbEnv, addToDb), + swapsSettings: LoadSwapsSettingsFromEnv(dbEnv, addToDb), } } @@ -48,9 +50,26 @@ export default class SettingsManager { for (const key in toAdd) { 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 } + 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 { return this.storage.getStorageSettings() } @@ -148,5 +167,6 @@ type FullSettings = { nostrRelaySettings: NostrRelaySettings, serviceFeeSettings: ServiceFeeSettings, serviceSettings: ServiceSettings, - lspSettings: LSPSettings + lspSettings: LSPSettings, + swapsSettings: SwapsSettings } \ No newline at end of file diff --git a/src/services/metrics/index.ts b/src/services/metrics/index.ts index 9774d4c1..3dbafedb 100644 --- a/src/services/metrics/index.ts +++ b/src/services/metrics/index.ts @@ -241,12 +241,12 @@ export default class Handler { 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 }) totalSpent += i.paid_amount - feesInRange += i.service_fees + feesInRange += (i.service_fees - i.routing_fees) }) 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 }) totalSpent += tx.paid_amount - feesInRange += tx.service_fees + feesInRange += (tx.service_fees - tx.chain_fees) }) ops.userToUser.forEach(op => { @@ -414,6 +414,10 @@ export default class Handler { 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) { await this.storage.metricsStorage.AddRootOperation("invoice", paymentRequest, amount) } diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index e054d8de..d1fa46ec 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -1,34 +1,37 @@ //import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools' -import WebSocket from 'ws' -Object.assign(global, { WebSocket: WebSocket }); -import crypto from 'crypto' -import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44 } from 'nostr-tools' +/* 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 } from '../helpers/logger.js' -import { nip19 } from 'nostr-tools' -import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' +/* import { nip19 } from 'nostr-tools' +import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.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 { v2 } = nip44 const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2 -const { getConversationKey: getConversationKeyV2 } = utils -const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL +const { getConversationKey: getConversationKeyV2 } = utils */ +/* const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL type AppInfo = { appId: 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 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 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[] relays: string[] clients: ClientInfo[] maxEventContentLength: number -} + providerDestinationPub: string +} */ -export type NostrEvent = { +/* export type NostrEvent = { id: string pub: string content: string @@ -36,7 +39,8 @@ export type NostrEvent = { startAtNano: string startAtMs: number kind: number -} + relayConstraint?: 'service' | 'provider' +} */ type SettingsRequest = { type: 'settings' @@ -69,9 +73,14 @@ type ProcessMetricsResponse = { type: 'processMetrics' metrics: ProcessMetrics } +type BeaconResponse = { + type: 'beacon' + content: string + pub: string +} 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) => { if (process.send) { 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) => { switch (message.type) { case 'settings': @@ -102,11 +111,15 @@ process.on("message", (message: ChildProcessRequest) => { const handleNostrSettings = (settings: NostrSettings) => { if (subProcessHandler) { getLogger({ component: "nostrMiddleware" })("got new nostr setting, resetting nostr handler") - subProcessHandler.Stop() - initNostrHandler(settings) + subProcessHandler.UpdateSettings(settings) + // initNostrHandler(settings) return } - initNostrHandler(settings) + subProcessHandler = new NostrPool(event => { + send(event) + }) + subProcessHandler.UpdateSettings(settings) + // initNostrHandler(settings) new ProcessMetricsCollector((metrics) => { send({ type: 'processMetrics', @@ -114,14 +127,11 @@ const handleNostrSettings = (settings: NostrSettings) => { }) }) } -const initNostrHandler = (settings: NostrSettings) => { - subProcessHandler = new Handler(settings, event => { - send({ - type: 'event', - event: event - }) +/* const initNostrHandler = (settings: NostrSettings) => { + subProcessHandler = new NostrPool(event => { + send(event) }) -} +} */ const sendToNostr: NostrSend = (initiator, data, relays) => { if (!subProcessHandler) { getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized") @@ -131,7 +141,7 @@ const sendToNostr: NostrSend = (initiator, data, relays) => { } send({ type: 'ready' }) -const supportedKinds = [21000, 21001, 21002, 21003] +/* const supportedKinds = [21000, 21001, 21002, 21003] export default class Handler { pool = new SimplePool() settings: NostrSettings @@ -218,18 +228,28 @@ export default class Handler { appIds: appIds, listeningForPubkeys: appIds }) - - return relay.subscribe([ + const subs: Filter[] = [ { since: Math.ceil(Date.now() / 1000), kinds: supportedKinds, '#p': appIds, } - ], { + ] + if (this.settings.providerDestinationPub) { + subs.push({ + kinds: [30078], '#d': ['Lightning.Pub'], + authors: [this.settings.providerDestinationPub] + }) + } + return relay.subscribe(subs, { oneose: () => { this.log("up to date with nostr events") }, 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) { return } @@ -366,4 +386,4 @@ const splitContent = (content: string, maxLength: number) => { parts.push(content.slice(i, i + maxLength)) } return parts -} \ No newline at end of file +} */ \ No newline at end of file diff --git a/src/services/nostr/index.ts b/src/services/nostr/index.ts index 50fdf61b..3dce7b41 100644 --- a/src/services/nostr/index.ts +++ b/src/services/nostr/index.ts @@ -1,19 +1,17 @@ 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 { getLogger, ERROR } from '../helpers/logger.js' type EventCallback = (event: NostrEvent) => void - - - - +type BeaconCallback = (beacon: { content: string, pub: string }) => void export default class NostrSubprocess { childProcess: ChildProcess utils: Utils awaitingPongs: (() => void)[] = [] log = getLogger({}) - constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback) { + constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) { this.utils = utils this.childProcess = fork("./build/src/services/nostr/handler") this.childProcess.on("error", (error) => { @@ -43,6 +41,9 @@ export default class NostrSubprocess { this.awaitingPongs.forEach(resolve => resolve()) this.awaitingPongs = [] break + case 'beacon': + beaconCallback({ content: message.content, pub: message.pub }) + break default: console.error("unknown nostr event response", message) break; diff --git a/src/services/nostr/nostrPool.ts b/src/services/nostr/nostrPool.ts new file mode 100644 index 00000000..d41da382 --- /dev/null +++ b/src/services/nostr/nostrPool.ts @@ -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 = {} + apps: Record = {} + maxEventContentLength: number + // providerDestinationPub: string | undefined + eventCallback: RelayEventCallback + log = getLogger({ component: "nostrMiddleware" }) + handledEvents: Map = 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 { + 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 { + 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 { + 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 = {} + 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): 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] + } +} diff --git a/src/services/nostr/nostrRelayConnection.ts b/src/services/nostr/nostrRelayConnection.ts new file mode 100644 index 00000000..7d2d3056 --- /dev/null +++ b/src/services/nostr/nostrRelayConnection.ts @@ -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(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 { + 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) + } + + +} \ No newline at end of file diff --git a/src/services/nostr/sender.ts b/src/services/nostr/sender.ts new file mode 100644 index 00000000..1fd336a5 --- /dev/null +++ b/src/services/nostr/sender.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index c9f92932..8b9b80a3 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -91,6 +91,21 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) 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 () => { return mainHandler.metricsManager.GetProvidersDisruption() }, @@ -130,6 +145,9 @@ export default (mainHandler: Main): Types.ServerMethods => { GetUserOperations: async ({ ctx, req }) => { return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req) }, + ListAdminSwaps: async ({ ctx }) => { + return mainHandler.adminManager.ListAdminSwaps() + }, GetPaymentState: async ({ ctx, req }) => { const err = Types.GetPaymentStateRequestValidate(req, { invoice_CustomCheck: invoice => invoice !== "" @@ -141,12 +159,18 @@ export default (mainHandler: Main): Types.ServerMethods => { PayAddress: async ({ ctx, req }) => { const err = Types.PayAddressRequestValidate(req, { address_CustomCheck: addr => addr !== '', - amoutSats_CustomCheck: amt => amt > 0, - satsPerVByte_CustomCheck: spb => spb > 0 + amountSats_CustomCheck: amt => amt > 0, + // satsPerVByte_CustomCheck: spb => spb > 0 }) if (err != null) throw new Error(err.message) 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), DecodeInvoice: async ({ ctx, req }) => { return mainHandler.paymentManager.DecodeInvoice(req) diff --git a/src/services/storage/db/db.ts b/src/services/storage/db/db.ts index 98ab32bd..335f6848 100644 --- a/src/services/storage/db/db.ts +++ b/src/services/storage/db/db.ts @@ -29,6 +29,7 @@ import { AppUserDevice } from "../entity/AppUserDevice.js" import * as fs from 'fs' import { UserAccess } from "../entity/UserAccess.js" import { AdminSettings } from "../entity/AdminSettings.js" +import { TransactionSwap } from "../entity/TransactionSwap.js" export type DbSettings = { @@ -74,7 +75,8 @@ export const MainDbEntities = { 'ManagementGrant': ManagementGrant, 'AppUserDevice': AppUserDevice, 'UserAccess': UserAccess, - 'AdminSettings': AdminSettings + 'AdminSettings': AdminSettings, + 'TransactionSwap': TransactionSwap } export type MainDbNames = keyof typeof MainDbEntities export const MainDbEntitiesNames = Object.keys(MainDbEntities) diff --git a/src/services/storage/entity/TrackedProvider.ts b/src/services/storage/entity/TrackedProvider.ts index 5c07a64c..e9381205 100644 --- a/src/services/storage/entity/TrackedProvider.ts +++ b/src/services/storage/entity/TrackedProvider.ts @@ -18,6 +18,9 @@ export class TrackedProvider { @Column({ default: 0 }) latest_distruption_at_unix: number + @Column({ default: 0 }) + latest_checked_height: number + @CreateDateColumn() created_at: Date diff --git a/src/services/storage/entity/TransactionSwap.ts b/src/services/storage/entity/TransactionSwap.ts new file mode 100644 index 00000000..7403e2a1 --- /dev/null +++ b/src/services/storage/entity/TransactionSwap.ts @@ -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 +} \ No newline at end of file diff --git a/src/services/storage/entity/UserInvoicePayment.ts b/src/services/storage/entity/UserInvoicePayment.ts index b9b83b4c..ce999e72 100644 --- a/src/services/storage/entity/UserInvoicePayment.ts +++ b/src/services/storage/entity/UserInvoicePayment.ts @@ -47,6 +47,9 @@ export class UserInvoicePayment { @Column({ nullable: true }) debit_to_pub: string + @Column({ nullable: true }) + swap_operation_id: string + @CreateDateColumn() created_at: Date diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 33bb9784..fea4c8c9 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -118,6 +118,10 @@ export default class { } */ } + NostrSender() { + return this.utils.nostrSender + } + getStorageSettings(): StorageSettings { return this.settings } diff --git a/src/services/storage/liquidityStorage.ts b/src/services/storage/liquidityStorage.ts index 74364e33..1b349d40 100644 --- a/src/services/storage/liquidityStorage.ts +++ b/src/services/storage/liquidityStorage.ts @@ -64,4 +64,13 @@ export class LiquidityStorage { async UpdateTrackedProviderDisruption(providerType: 'lnd' | 'lnPub', pub: string, latestDisruptionAtUnix: number) { return this.dbs.Update('TrackedProvider', { provider_pubkey: pub, provider_type: providerType }, { latest_distruption_at_unix: latestDisruptionAtUnix }) } + + async GetLatestCheckedHeight(providerType: 'lnd' | 'lnPub', pub: string): Promise { + 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', { provider_pubkey: pub, provider_type: providerType }, { latest_checked_height: height }) + } } \ No newline at end of file diff --git a/src/services/storage/metricsStorage.ts b/src/services/storage/metricsStorage.ts index 2a93249f..44da304c 100644 --- a/src/services/storage/metricsStorage.ts +++ b/src/services/storage/metricsStorage.ts @@ -149,6 +149,10 @@ export default class { return this.dbs.CreateAndSave('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', { where: { operation_type: opType, operation_identifier: id } }, txId) + } + async GetRootOperations({ from, to }: { from?: number, to?: number }, txId?: string) { const q = getTimeQuery({ from, to }) return this.dbs.Find('RootOperation', q, txId) diff --git a/src/services/storage/migrations/1762890527098-tx_swap.ts b/src/services/storage/migrations/1762890527098-tx_swap.ts new file mode 100644 index 00000000..af366d67 --- /dev/null +++ b/src/services/storage/migrations/1762890527098-tx_swap.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TxSwap1762890527098 implements MigrationInterface { + name = 'TxSwap1762890527098' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } + +} diff --git a/src/services/storage/migrations/1764779178945-tx_swap_address.ts b/src/services/storage/migrations/1764779178945-tx_swap_address.ts new file mode 100644 index 00000000..b62f44bd --- /dev/null +++ b/src/services/storage/migrations/1764779178945-tx_swap_address.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TxSwapAddress1764779178945 implements MigrationInterface { + name = 'TxSwapAddress1764779178945' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } + +} diff --git a/src/services/storage/migrations/1766504040000-tracked_provider_height.ts b/src/services/storage/migrations/1766504040000-tracked_provider_height.ts new file mode 100644 index 00000000..5e0b349d --- /dev/null +++ b/src/services/storage/migrations/1766504040000-tracked_provider_height.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TrackedProviderHeight1766504040000 implements MigrationInterface { + name = 'TrackedProviderHeight1766504040000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tracked_provider" ADD "latest_checked_height" integer NOT NULL DEFAULT (0)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tracked_provider" DROP COLUMN "latest_checked_height"`); + } +} + diff --git a/src/services/storage/migrations/1768413055036-swaps_service_url.ts b/src/services/storage/migrations/1768413055036-swaps_service_url.ts new file mode 100644 index 00000000..cdf34238 --- /dev/null +++ b/src/services/storage/migrations/1768413055036-swaps_service_url.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class SwapsServiceUrl1768413055036 implements MigrationInterface { + name = 'SwapsServiceUrl1768413055036' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } + +} diff --git a/src/services/storage/migrations/runner.ts b/src/services/storage/migrations/runner.ts index c87adf78..d14b8381 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -27,14 +27,19 @@ import { UserAccess1759426050669 } from './1759426050669-user_access.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.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 { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_provider_height.js' +import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.js' export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, 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 TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => { diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 4b27775b..ed3f84dd 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -1,5 +1,5 @@ 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 { UserTransactionPayment } from './entity/UserTransactionPayment.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 { LoggedEvent } from './eventsLog.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, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string } export const defaultInvoiceExpiry = 60 * 60 export default class { @@ -160,7 +161,8 @@ export default class { return this.dbs.FindOne('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 { + 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 { + const { debitNpub, swapOperationId } = optionals const user = await this.userStorage.GetUser(userId, txId) return this.dbs.CreateAndSave('UserInvoicePayment', { user, @@ -172,7 +174,8 @@ export default class { internal: false, linkedApplication, liquidityProvider, - debit_to_pub: debitNpub + debit_to_pub: debitNpub, + swap_operation_id: swapOperationId }, txId) } @@ -460,6 +463,58 @@ export default class { } return this.dbs.Find('UserReceivingInvoice', { where }) } + + async AddTransactionSwap(swap: Partial) { + return this.dbs.CreateAndSave('TransactionSwap', swap) + } + + async GetTransactionSwap(swapOperationId: string, appUserId: string, txId?: string) { + return this.dbs.FindOne('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', { 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', { swap_operation_id: swapOperationId }, { + used: true, + failure_reason: failureReason, + address_paid: address, + }) + } + + async DeleteTransactionSwap(swapOperationId: string, txId?: string) { + return this.dbs.Delete('TransactionSwap', { swap_operation_id: swapOperationId }, txId) + } + + async DeleteExpiredTransactionSwaps(currentHeight: number, txId?: string) { + return this.dbs.Delete('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId) + } + + async ListPendingTransactionSwaps(appUserId: string, txId?: string) { + return this.dbs.Find('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId) + } + + async ListSwapPayments(userId: string, txId?: string) { + return this.dbs.Find('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', { where: { used: true, app_user_id: appUserId } }, txId) + // const payments = await this.dbs.Find('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), } }, txId) + const paymentsMap = new Map() + 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 (resultPromise: Promise) => { diff --git a/src/services/storage/tlv/tlvFilesStorageFactory.ts b/src/services/storage/tlv/tlvFilesStorageFactory.ts index 6f8bbb51..0e396703 100644 --- a/src/services/storage/tlv/tlvFilesStorageFactory.ts +++ b/src/services/storage/tlv/tlvFilesStorageFactory.ts @@ -2,11 +2,12 @@ import { ChildProcess, fork } from 'child_process'; import { EventEmitter } from 'events'; import { AddTlvOperation, ITlvStorageOperation, SuccessTlvOperationResponse, LoadLatestTlvOperation, LoadTlvFileOperation, NewTlvStorageOperation, SerializableLatestData, SerializableTlvFile, TlvOperationResponse, TlvStorageSettings, WebRtcMessageOperation, ProcessMetricsTlvOperation, ZipStoragesOperation, ResetTlvStorageOperation, PingTlvOperation } from './tlvFilesStorageProcessor'; import { LatestData, TlvFile } from './tlvFilesStorage'; -import { NostrSend, SendData, SendInitiator } from '../../nostr/handler'; +import { SendData, SendInitiator } from '../../nostr/nostrPool.js'; import { WebRtcUserInfo } from '../../webRTC'; import * as Types from '../../../../proto/autogenerated/ts/types.js' import { ProcessMetrics } from './processMetricsCollector'; import { getLogger, ERROR } from '../../helpers/logger.js'; +import { NostrSender } from '../../nostr/sender'; export type TlvStorageInterface = { AddTlv: (appId: string, dataName: string, tlv: Uint8Array) => Promise LoadLatest: (limit?: number) => Promise @@ -17,12 +18,14 @@ export class TlvStorageFactory extends EventEmitter { private process: ChildProcess; private isConnected: 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 - log = getLogger({component: 'TlvStorageFactory'}) - constructor(allowResetMetricsStorages: boolean) { + log = getLogger({ component: 'TlvStorageFactory' }) + constructor(allowResetMetricsStorages: boolean, nostrSender: NostrSender) { super(); this.allowResetMetricsStorages = allowResetMetricsStorages + this.nostrSender = nostrSender this.initializeSubprocess(); } @@ -30,15 +33,8 @@ export class TlvStorageFactory extends EventEmitter { this.debug = debug; } - attachNostrSend(f: NostrSend) { - this._nostrSend = f - } - private nostrSend = (opResponse: SuccessTlvOperationResponse<{ initiator: SendInitiator, data: SendData, relays?: string[] }>) => { - if (!this._nostrSend) { - throw new Error("No nostrSend attached") - } - this._nostrSend(opResponse.data.initiator, opResponse.data.data, opResponse.data.relays) + this.nostrSender.Send(opResponse.data.initiator, opResponse.data.data, opResponse.data.relays) } private initializeSubprocess() { @@ -134,10 +130,15 @@ export class TlvStorageFactory extends EventEmitter { return this.handleOp(op) } - ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise { + async ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise { const opId = Math.random().toString() const op: ProcessMetricsTlvOperation = { type: 'processMetrics', opId, metrics, processName } - return this.handleOp(op) + try { + return this.handleOp(op) + } catch (error: any) { + this.log(ERROR, 'Error processing metrics', error.message) + } + return } diff --git a/src/services/storage/tlv/tlvFilesStorageProcessor.ts b/src/services/storage/tlv/tlvFilesStorageProcessor.ts index 0dd62d3a..2b4189e4 100644 --- a/src/services/storage/tlv/tlvFilesStorageProcessor.ts +++ b/src/services/storage/tlv/tlvFilesStorageProcessor.ts @@ -2,8 +2,8 @@ import { PubLogger, getLogger } from '../../helpers/logger.js'; import webRTC, { WebRtcUserInfo } from '../../webRTC/index.js'; import { TlvFilesStorage } from './tlvFilesStorage.js'; import * as Types from '../../../../proto/autogenerated/ts/types.js' -import { SendData } from '../../nostr/handler.js'; -import { SendInitiator } from '../../nostr/handler.js'; +import { SendData } from '../../nostr/nostrPool.js'; +import { SendInitiator } from '../../nostr/nostrPool.js'; import { ProcessMetrics, ProcessMetricsCollector } from './processMetricsCollector.js'; import { integerToUint8Array } from '../../helpers/tlv.js'; import { zip } from 'zip-a-folder' diff --git a/src/services/webRTC/index.ts b/src/services/webRTC/index.ts index 043af1ba..8ee8d884 100644 --- a/src/services/webRTC/index.ts +++ b/src/services/webRTC/index.ts @@ -3,7 +3,7 @@ import wrtc from 'wrtc' import Storage from '../storage/index.js' import { ERROR, getLogger } from "../helpers/logger.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 { Utils } from '../helpers/utilsWrapper.js' import { TlvFilesStorage } from '../storage/tlv/tlvFilesStorage.js' @@ -111,7 +111,7 @@ export default class webRTC { const packet = packets[i] const tlv = encodeTLVDataPacket({ dataId: id, packetNum: i + 1, totalPackets: packets.length, data: packet }) const bytes = encodeTLbV(tlv) - channel.send(bytes) + channel.send(new Uint8Array(bytes)) } } catch (e: any) { this.log(ERROR, 'ondatachannel', e.message || e) diff --git a/src/tests/.env.test b/src/tests/.env.test index 8d0e92e2..dd0221bc 100644 --- a/src/tests/.env.test +++ b/src/tests/.env.test @@ -5,7 +5,7 @@ DATABASE_FILE=db.sqlite JWT_SECRET=bigsecrethere ALLOW_BALANCE_MIGRATION=true OUTBOUND_MAX_FEE_BPS=60 -OUTBOUND_MAX_FEE_EXTRA_SATS=100 +OUTBOUND_MAX_FEE_EXTRA_SATS=10 INCOMING_CHAIN_FEE_ROOT_BPS=0 OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets INCOMING_INVOICE_FEE_ROOT_BPS=0 diff --git a/src/tests/externalPayment.spec.ts b/src/tests/externalPayment.spec.ts index a92d0054..7a03e828 100644 --- a/src/tests/externalPayment.spec.ts +++ b/src/tests/externalPayment.spec.ts @@ -24,11 +24,10 @@ const testSuccessfulExternalPayment = async (T: TestBase) => { T.d("paid 500 sats invoice from user1") const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(u1.balance_sats).to.be.equal(1496) - T.d("user1 balance is now 1496 (2000 - (500 + 3 fee + 1 routing))") - expect(owner.balance_sats).to.be.equal(3) - T.d("app balance is 3 sats") - + expect(u1.balance_sats).to.be.equal(1490) + T.d("user1 balance is now 1490 (2000 - (500 + 10fee))") + expect(owner.balance_sats).to.be.equal(9) + T.d("app balance is 9 sats") } 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") T.d("payment failed as expected, with the expected error message") const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) - expect(u1.balance_sats).to.be.equal(1496) - T.d("user1 balance is still 1496") + expect(u1.balance_sats).to.be.equal(1490) + T.d("user1 balance is still 1490") const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(owner.balance_sats).to.be.equal(3) - T.d("app balance is still 3 sats") + expect(owner.balance_sats).to.be.equal(9) + T.d("app balance is still 9 sats") } const testSuccesfulReceivedExternalChainPayment = async (T: TestBase) => { diff --git a/src/tests/internalPayment.spec.ts b/src/tests/internalPayment.spec.ts index f94bb994..80b35675 100644 --- a/src/tests/internalPayment.spec.ts +++ b/src/tests/internalPayment.spec.ts @@ -23,10 +23,10 @@ const testSuccessfulInternalPayment = async (T: TestBase) => { const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) expect(u2.balance_sats).to.be.equal(1000) T.d("user2 balance is 1000") - expect(u1.balance_sats).to.be.equal(994) - T.d("user1 balance is 994 cuz he paid 6 sats fee") - expect(owner.balance_sats).to.be.equal(6) - T.d("app balance is 6 sats") + expect(u1.balance_sats).to.be.equal(990) + T.d("user1 balance is 990 cuz he paid 10 sats fee") + expect(owner.balance_sats).to.be.equal(10) + T.d("app balance is 10 sats") } const testFailedInternalPayment = async (T: TestBase) => { diff --git a/src/tests/liquidityProvider.spec.ts b/src/tests/liquidityProvider.spec.ts index 30b9f37d..460969da 100644 --- a/src/tests/liquidityProvider.spec.ts +++ b/src/tests/liquidityProvider.spec.ts @@ -21,30 +21,30 @@ export default async (T: TestBase) => { const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => { 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)) 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.d("user balance is 2000") + T.expect(userBalance.balance).to.equal(3000) + T.d("user balance is 3000") const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() - T.expect(providerBalance).to.equal(2000) - T.d("provider balance is 2000") + T.expect(providerBalance).to.equal(3000) + T.d("provider balance is 3000") T.d("testInboundPaymentFromProvider done") } const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => { 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 res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 }) 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() - T.expect(providerBalance).to.equal(992) // 2000 - (1000 + 6 +2) + T.expect(providerBalance).to.equal(988) // 3000 - (2000 + 12) T.d("testOutboundPaymentFromProvider done") } \ No newline at end of file diff --git a/src/tests/networkSetup.ts b/src/tests/networkSetup.ts index 870016e6..38a2e0fb 100644 --- a/src/tests/networkSetup.ts +++ b/src/tests/networkSetup.ts @@ -8,6 +8,7 @@ import LND from '../services/lnd/lnd.js' import { LiquidityProvider } from "../services/main/liquidityProvider.js" import { Utils } from "../services/helpers/utilsWrapper.js" import { LoadStorageSettingsFromEnv } from "../services/storage/index.js" +import { NostrSender } from "../services/nostr/sender.js" export type ChainTools = { mine: (amount: number) => Promise @@ -15,7 +16,8 @@ export type ChainTools = { export const setupNetwork = async (): Promise => { 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 core = new BitcoinCoreWrapper(LoadBitcoinCoreSettingsFromEnv()) await core.InitAddress() @@ -23,7 +25,7 @@ export const setupNetwork = async (): Promise => { const lndSettings = LoadLndSettingsFromEnv({}) const lndNodeSettings = LoadLndNodeSettingsFromEnv({}) 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 bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await tryUntil(async i => { diff --git a/src/tests/setupBootstrapped.ts b/src/tests/setupBootstrapped.ts index dfcf16c7..81e2e8bd 100644 --- a/src/tests/setupBootstrapped.ts +++ b/src/tests/setupBootstrapped.ts @@ -1,6 +1,6 @@ import { getLogger } from '../services/helpers/logger.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 * as Types from '../../proto/autogenerated/ts/types.js' import { GetTestStorageSettings, LoadStorageSettingsFromEnv } from '../services/storage/index.js' @@ -20,19 +20,19 @@ export const initBootstrappedInstance = async (T: TestBase) => { if (!initialized) { 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) => { if (data.type === 'event') { throw new Error("unsupported event type") } - if (data.pub !== liquidityProviderInfo.publicKey) { - throw new Error("invalid pub " + data.pub + " expected " + liquidityProviderInfo.publicKey) + if (data.pub !== localProviderClient.publicKey) { + throw new Error("invalid pub " + data.pub + " expected " + localProviderClient.publicKey) } const j = JSON.parse(data.content) as { requestId: string } console.log("sending new operation to provider") bootstrapped.liquidityProvider.onEvent(j, T.app.publicKey) }) - bootstrapped.liquidityProvider.attachNostrSend(async (_, data, r) => { + bootstrapped.attachNostrSend(async (_, data, r) => { const res = await handleSend(T, data) if (data.type === 'event') { throw new Error("unsupported event type") @@ -42,10 +42,10 @@ export const initBootstrappedInstance = async (T: TestBase) => { } 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(res => { const interval = setInterval(async () => { - const canHandle = await bootstrapped.liquidityProvider.CanProviderHandle({ action: 'receive', amount: 2000 }) + const canHandle = bootstrapped.liquidityProvider.IsReady() if (canHandle) { clearInterval(interval) res() @@ -54,10 +54,10 @@ export const initBootstrappedInstance = async (T: TestBase) => { } }, 500) }) - const bUser = await bootstrapped.applicationManager.AddAppUser(liquidityProviderApp.appId, { identifier: "user1_bootstrapped", balance: 0, fail_if_exists: true }) - const bootstrappedUser: TestUserData = { userId: bUser.info.userId, appUserIdentifier: bUser.identifier, appId: liquidityProviderApp.appId } + 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: localProviderClient.appId } return { - bootstrapped, liquidityProviderInfo, liquidityProviderApp, bootstrappedUser, stop: () => { + bootstrapped, localProviderClient, bootstrappedUser, stop: () => { bootstrapped.Stop() } } diff --git a/src/tests/spamExternalPayments.spec.ts b/src/tests/spamExternalPayments.spec.ts index e7956632..c81567c5 100644 --- a/src/tests/spamExternalPayments.spec.ts +++ b/src/tests/spamExternalPayments.spec.ts @@ -29,16 +29,15 @@ const testSpamExternalPayment = async (T: TestBase) => { const failedPayments = res.filter(r => !r.success) console.log(failedPayments) 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(failedPayments.length).to.be.equal(7) T.d("3 payments succeeded, 7 failed as expected") const u = await T.main.storage.userStorage.GetUser(T.user1.userId) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(u.balance_sats).to.be.equal(488) - T.d("user1 balance is now 488 (2000 - (500 + 3 fee + 1 routing) * 3)") - expect(owner.balance_sats).to.be.equal(9) - T.d("app balance is 9 sats") - + expect(u.balance_sats).to.be.equal(470) + T.d("user1 balance is now 470 (2000 - (500 + 10 fee) * 3)") + expect(owner.balance_sats).to.be.equal(27) + T.d("app balance is 27 sats") } diff --git a/src/tests/testBase.ts b/src/tests/testBase.ts index fb3f0e90..791bb6ce 100644 --- a/src/tests/testBase.ts +++ b/src/tests/testBase.ts @@ -15,6 +15,7 @@ import { AdminManager } from '../services/main/adminManager.js' import { TlvStorageFactory } from '../services/storage/tlv/tlvFilesStorageFactory.js' import { ChainTools } from './networkSetup.js' import { LiquiditySettings, LoadLndSettingsFromEnv, LoadSecondLndSettingsFromEnv, LoadThirdLndSettingsFromEnv } from '../services/main/settings.js' +import { NostrSender } from '../services/nostr/sender.js' chai.use(chaiString) export const expect = chai.expect export type Describe = (message: string, failure?: boolean) => void @@ -46,7 +47,8 @@ export type StorageTestBase = { export const setupStorageTest = async (d: Describe): Promise => { 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) await storageManager.Connect(console.log) return { @@ -79,11 +81,11 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }) await externalAccessToMainLnd.Warmup() */ - const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false } + const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false, providerRelayUrl: "" } const lndSettings = LoadLndSettingsFromEnv({}) const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() 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) => { 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 }) - 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) expect(u.balance_sats).to.be.equal(amount) T.d(`user ${user.appUserIdentifier} balance is now ${amount}`)