Compare commits

..

92 commits

Author SHA1 Message Date
Patrick Mulligan
8c8f2d4d3d fix(lnd): allow self-payments for LNURL-withdraw
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
When the user's wallet (e.g. Zeus) is connected to the same LND node
that LP uses, LNURL-withdraw fails because LND rejects the payment
with "no self-payments allowed". This is safe because LP always
decrements the user's balance before paying and refunds on failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:19:45 -05:00
Patrick Mulligan
f8d17a91a7 feat(extensions): pay from caller's balance via PayAppUserInvoice
When userPubkey is provided, resolve the ApplicationUser and call
applicationManager.PayAppUserInvoice instead of paymentManager.PayInvoice
directly. This ensures notifyAppUserPayment fires, sending
LiveUserOperation events via Nostr for real-time balance updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:19:45 -05:00
Patrick Mulligan
121c22c4b8 feat(withdraw): track creator pubkey on withdraw links
Store the Nostr pubkey of the user who creates a withdraw link so the
LNURL callback debits the correct user's balance instead of the app
owner's. Pass userPubkey through from RPC handler to WithdrawManager.

- Add creator_pubkey column (migration v4)
- Store creatorPubkey on link creation
- Pass creator_pubkey to payInvoice on LNURL callback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:19:45 -05:00
Patrick Mulligan
03f78c0362 feat: route Nostr RPC to extension methods
Initialize extension system before nostrMiddleware so registered
RPC methods are available. Extension methods (e.g. withdraw.createLink)
are intercepted and routed to the extension loader before falling
through to the standard nostrTransport.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
df2df9bc26 feat(withdraw): add HTTP API for creating withdraw links
Add POST /api/v1/withdraw/create endpoint to allow external apps (ATM,
web clients) to create LNURL-withdraw links via HTTP instead of RPC.

Changes:
- Add handleCreateWithdrawLink HTTP handler
- Fix route ordering: callback routes before wildcard :unique_hash
- Extract app_id from Authorization header (Bearer app_<id>)
- Use is_unique=false for simple single-use ATM links

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
5ecb8166b1 feat(server): add CORS support for extension HTTP routes
Enable CORS on the extension HTTP server to allow cross-origin requests
from ATM apps and other web-based clients.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
dcfb9caa67 feat: integrate extension system with withdraw extension support
- Add extension loader initialization to startup
- Create mainHandlerAdapter to bridge mainHandler with extension context
- Mount extension HTTP routes on separate port (main port + 1)
- Configure EXTENSION_SERVICE_URL for LNURL link generation

The withdraw extension provides LUD-03 LNURL-withdraw support for
creating withdraw links that allow users to pull funds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
39f4575b7c feat(extensions): add LNURL-withdraw extension
Implements LUD-03 (LNURL-withdraw) for creating withdraw links
that allow anyone to pull funds from a Lightning wallet.

Features:
- Create withdraw links with min/max amounts
- Quick vouchers: batch creation of single-use codes
- Multi-use links with wait time between uses
- Unique QR codes per use (prevents sharing exploits)
- Webhook notifications on successful withdrawals
- Full LNURL protocol compliance for wallet compatibility

Use cases:
- Faucets
- Gift cards / prepaid cards
- Tips / donations
- User onboarding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
3d74e468b1 fix: use fresh balance in PayAppUserInvoice notification
notifyAppUserPayment was sending the stale cached balance from the
entity loaded before PayInvoice decremented it. Update the entity's
balance_sats from the PayInvoice response so LiveUserOperation events
contain the correct post-payment balance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
f761196898 chore: update Docker build and dependencies
- Add .dockerignore for runtime state files (sqlite, logs, secrets)
- Bump Node.js base image from 18 to 20
- Add @types/better-sqlite3 dev dependency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
bc63d30af6 fix: correct nip44v1 secp256k1 getSharedSecret argument types
The @noble/curves secp256k1.getSharedSecret expects Uint8Array arguments,
not hex strings. Use hex.decode() to convert the private and public keys.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
b6d2143134 feat(extensions): add getLnurlPayInfo to ExtensionContext
Enables extensions to get LNURL-pay info for users by pubkey,
supporting Lightning Address (LUD-16) and zap (NIP-57) functionality.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
3cf74cbc2c docs(extensions): add comprehensive extension loader documentation
Covers architecture, API reference, lifecycle, database isolation,
RPC methods, HTTP routes, event handling, and complete examples.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
93e50c7f76 feat(extensions): add extension loader infrastructure
Adds a modular extension system for Lightning.Pub that allows
third-party functionality to be added without modifying core code.

Features:
- ExtensionLoader: discovers and loads extensions from directory
- ExtensionContext: provides extensions with access to Lightning.Pub APIs
- ExtensionDatabase: isolated SQLite database per extension
- Lifecycle management: initialize, shutdown, health checks
- RPC method registration: extensions can add new RPC methods
- Event dispatching: routes payments and Nostr events to extensions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:19:44 -05:00
Patrick Mulligan
72c9872b23 fix(watchdog): handle LND restarts without locking outgoing operations
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
When the payment index advances (e.g. after an LND restart or external
payment), update the cached offset instead of immediately locking.
Only lock if both a history mismatch AND a balance discrepancy are
detected — indicating a real security concern rather than a benign
LND restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:19:20 -05:00
Patrick Mulligan
5e5e30c7a2 fix(lnd): wait for chain/graph sync before marking LND ready
Warmup() previously only checked that LND responded to GetInfo(), but
did not verify syncedToChain/syncedToGraph. This caused LP to accept
requests while LND was still syncing, leading to "not synced" errors
on every Health() check. Now waits for full sync with a 10min timeout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:18:06 -05:00
Patrick Mulligan
611eb4fc04 fix(nostr): close SimplePool after publishing to prevent connection leak
Each sendEvent() call created a new SimplePool() but never closed it,
causing relay WebSocket connections to accumulate indefinitely (~20/min).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:16:28 -05:00
Patrick Mulligan
6512e10f08 fix(handlers): await NostrSend calls throughout codebase
Update all NostrSend call sites to properly handle the async nature
of the function now that it returns Promise<void>.

Changes:
- handler.ts: Add async to sendResponse, await nostrSend calls
- debitManager.ts: Add logging for Kind 21002 response sending
- nostrMiddleware.ts: Update nostrSend signature
- tlvFilesStorageProcessor.ts: Update nostrSend signature
- webRTC/index.ts: Add async/await for nostrSend calls

This ensures Kind 21002 (ndebit) responses are properly sent to
wallet clients, fixing the "Debit request failed" issue in ShockWallet.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:16:28 -05:00
Patrick Mulligan
e9b5dacb3b fix(nostr): update NostrSend type to Promise<void> with error handling
The NostrSend type was incorrectly typed as returning void when it actually
returns Promise<void>. This caused async errors to be silently swallowed.

Changes:
- Update NostrSend type signature to return Promise<void>
- Make NostrSender._nostrSend default to async function
- Add .catch() error handling in NostrSender.Send() to log failures
- Add logging to track event publishing status

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:16:28 -05:00
Justin (shocknet)
a2b7ac1673
Merge pull request #903 from shocknet/bump-fee-api
bimp fee
2026-03-04 13:27:32 -05:00
boufni95
70544bd551
rate fix 2026-03-04 18:26:21 +00:00
boufni95
74513a1412
bimp fee 2026-03-04 17:57:50 +00:00
Justin (shocknet)
266f526453
Merge pull request #901 from shocknet/bump-fee
bump fee api
2026-03-04 12:38:46 -05:00
boufni95
169284021f
bump fee api 2026-03-04 17:19:42 +00:00
Justin (shocknet)
67f9dfde8b
Merge pull request #900 from shocknet/refund-filed-swps
refund failed swaps
2026-03-03 21:04:18 -05:00
boufni95
7c8cca0a55
refund failed swaps 2026-03-03 19:49:54 +00:00
Justin (shocknet)
0a30cf537f
Merge pull request #899 from shocknet/activate-users-cleanup
fk fix
2026-03-02 15:21:42 -05:00
shocknet-justin
595e5bb257
fk fix 2026-03-02 15:16:08 -05:00
Justin (shocknet)
8576a1d138
Merge pull request #898 from shocknet/activate-users-cleanup
cleanup fix
2026-03-02 15:13:23 -05:00
shocknet-justin
c18b79dc50
use number 2026-03-02 15:10:24 -05:00
shocknet-justin
bfa71f7439
cleanup fix 2026-03-02 15:08:56 -05:00
Justin (shocknet)
a0b77ec1ca
Merge pull request #897 from shocknet/activate-users-cleanup
clean users table
2026-03-02 15:05:45 -05:00
shocknet-justin
cfb7dd1e6e
serial id 2026-03-02 15:01:12 -05:00
shocknet-justin
be6f48427f
clean users table 2026-03-02 14:56:48 -05:00
Justin (shocknet)
21cb960c2e
Merge pull request #896 from shocknet/activate-users-cleanup
cleanup db fix
2026-03-02 14:46:53 -05:00
shocknet-justin
7af841c330
cleanup db fix 2026-03-02 14:44:32 -05:00
Justin (shocknet)
71b55c06d4
Merge pull request #894 from shocknet/activate-users-cleanup
activte cleanup
2026-03-02 14:34:37 -05:00
shocknet-justin
432f9d0b42
bump never active to 90 2026-03-02 14:33:53 -05:00
boufni95
574f229cee
activte cleanup 2026-03-02 18:49:22 +00:00
Justin (shocknet)
f0418fb389
Merge pull request #891 from shocknet/assets_liabilities
assets and liabilities
2026-02-27 00:42:29 -05:00
Justin (shocknet)
dcfa9250fe
Merge pull request #889 from shocknet/tx-swaps
tx swaps polish
2026-02-27 00:42:02 -05:00
boufni95
ef14ec9ddf
assets and liabilities 2026-02-26 21:54:28 +00:00
boufni95
2c8d57dd6e
address amt validation 2026-02-25 18:14:56 +00:00
boufni95
f8fe946b40
tx swaps polish 2026-02-24 18:36:03 +00:00
Justin (shocknet)
e411b7aa7f
Merge pull request #887 from shocknet/test
notification types and topic id
2026-02-20 14:51:58 -05:00
Justin (shocknet)
d949addb16
Merge pull request #886 from shocknet/lnd-log-level
use debug log level,  no level = ERROR
2026-02-20 14:39:17 -05:00
boufni95
9e66f7d72e
use debug log level, no level = ERROR 2026-02-20 19:37:11 +00:00
Justin (shocknet)
ae3b39ee04
Merge pull request #885 from shocknet/fix-pending-tx
fetch each pending tx to validate
2026-02-20 13:46:48 -05:00
boufni95
c83028c419
fetch each pending tx to validate 2026-02-20 18:40:40 +00:00
Mothana
c146d46c59 notification types and topic id 2026-02-20 17:08:38 +04:00
Justin (shocknet)
6aa90d63ba
Merge pull request #884 from shocknet/new-block-logs
new block handler logs
2026-02-19 17:25:13 -05:00
boufni95
8ada2ac165
new block handler logs 2026-02-19 21:45:31 +00:00
Justin (shocknet)
6e8a181d34
Merge pull request #883 from shocknet/missed-tx-process
await tx added to db
2026-02-19 16:38:51 -05:00
boufni95
875d1274de
await tx added to db 2026-02-19 21:24:24 +00:00
Justin (shocknet)
791a5756a7
Merge pull request #882 from shocknet/watchdog-root-ops
account for root ops and change
2026-02-19 13:54:26 -05:00
boufni95
305b268d73
fixes 2026-02-19 18:53:24 +00:00
boufni95
6da8434073
account for root ops and change 2026-02-19 18:11:41 +00:00
Justin (shocknet)
c85968ce2f
Merge pull request #881 from shocknet/paid_at_unix_default
paid at unix default
2026-02-17 16:30:44 -05:00
boufni95
e32dff939e
paid at unix default 2026-02-17 21:29:34 +00:00
Justin (shocknet)
97de703feb
Merge pull request #879 from shocknet/swaps-info
add time info to swaps
2026-02-17 15:23:38 -05:00
shocknet-justin
a10cf126b3
comment 2026-02-17 13:53:02 -05:00
boufni95
edbbbd4aec add time info to swaps 2026-02-17 18:12:33 +00:00
Justin (shocknet)
f4b302ed43
Merge pull request #878 from shocknet/restart-sub-process
restart sub p (reopened for review)
2026-02-17 09:46:01 -05:00
shocknet-justin
c3e3f99c7d
fix subprocess restart issues
- Fix event listener memory leak by removing all listeners before restart
- Properly cleanup old process with SIGTERM before forking new one
- Fix Stop() to actually kill process (was using kill(0) which only checks existence)
- Store callbacks in instance to avoid re-attaching listeners on restart
- Add isShuttingDown flag to prevent restart when stopping intentionally
- Add 100ms delay before restart to ensure clean process termination
- Check if process is killed before sending messages
- Update Reset() to store new settings for future restarts

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 17:22:47 -05:00
boufni95
e271d35c2d
restart sub p 2026-02-12 17:22:47 -05:00
boufni95
3389045de7
missing ops 2026-02-11 15:34:32 -05:00
shocknet-justin
9fe681d73b
swap clear interval 2026-02-11 15:34:32 -05:00
Justin (shocknet)
ecf7c43416
Merge pull request #875 from shocknet/socket-ping-pong
socket ping pong
2026-02-05 15:09:56 -05:00
boufni95
81f7400748 socket ping pong 2026-02-05 20:07:57 +00:00
Justin (shocknet)
c1bcda5654
Merge pull request #874 from shocknet/swap-fixes
swap fixes
2026-02-05 14:25:08 -05:00
boufni95
3a6b22cff8 swap fixes 2026-02-05 19:00:00 +00:00
Justin (shocknet)
a362cb4b91
Merge pull request #869 from shocknet/admin-swaps
admin swaps
2026-02-05 12:05:58 -05:00
boufni95
0a2556d0d4 cleanup tmp fix 2026-02-04 20:30:52 +00:00
shocknet-justin
3046e73464 zkpinit workaround 2026-02-04 15:27:52 -05:00
boufni95
e24fa4f055 better fix 2026-02-04 20:09:15 +00:00
boufni95
d9ec58165f log 2026-02-04 20:03:46 +00:00
boufni95
7b81a90c1a fix 2026-02-04 20:00:01 +00:00
boufni95
1f09bcc679 tmp fix 2026-02-04 19:59:22 +00:00
boufni95
070aa758d8 swap logs 2026-02-04 19:42:53 +00:00
boufni95
23156986b1 handle failure cases 2026-02-04 18:31:37 +00:00
boufni95
f262d46d1f fixes 2026-02-04 17:58:29 +00:00
Justin (shocknet)
02d3580613
Merge pull request #872 from shocknet/lnd_logs
lnd logs
2026-02-04 12:27:28 -05:00
boufni95
ae4a5cb5d0 lnd logs 2026-02-04 16:53:59 +00:00
boufni95
f5eb1f3253 amount input 2026-02-02 21:15:42 +00:00
boufni95
3255730ae2 swap refunds 2026-01-30 20:37:44 +00:00
Justin (shocknet)
e15668b442
Merge pull request #871 from shocknet/handle-child-signal
handle exit signal when code is null
2026-01-28 11:02:53 -05:00
boufni95
3b827626a6 handle exit signal when code is null 2026-01-28 15:53:57 +00:00
boufni95
e71f631993 Merge branch 'master' into admin-swaps 2026-01-27 16:10:28 +00:00
boufni95
d120ad7f99 admin swaps 2026-01-27 16:08:52 +00:00
Justin (shocknet)
44c55f3baa
Merge pull request #870 from sergey3bv/fix/log-doxing
Removing user ID from logs
2026-01-23 13:30:03 -05:00
Sergey B.
03792f3111 fix: removing user ID from logs 2026-01-23 15:30:52 +03:00
boufni95
a596e186fe admin swaps 2026-01-21 16:04:33 +00:00
51 changed files with 4463 additions and 1062 deletions

View file

@ -1,3 +1,14 @@
/**
* TypeORM DataSource used only by the TypeORM CLI (e.g. migration:generate).
*
* Migrations at runtime are run from src/services/storage/migrations/runner.ts (allMigrations),
* not from this file. The app never uses this DataSource to run migrations.
*
* Workflow: update the migrations array in this file *before* running
* migration:generate, so TypeORM knows the current schema (entities + existing migrations).
* We do not update this file immediately after adding a new migration; update it when you
* are about to generate the next migration.
*/
import { DataSource } from "typeorm"
import { User } from "./build/src/services/storage/entity/User.js"
import { UserReceivingInvoice } from "./build/src/services/storage/entity/UserReceivingInvoice.js"
@ -22,11 +33,13 @@ import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice
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 { InvoiceSwap } from "./build/src/services/storage/entity/InvoiceSwap.js"
import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
import { LndNodeInfo1720187506189 } from './build/src/services/storage/migrations/1720187506189-lnd_node_info.js'
import { LiquidityProvider1719335699480 } from './build/src/services/storage/migrations/1719335699480-liquidity_provider.js'
import { LndNodeInfo1720187506189 } from './build/src/services/storage/migrations/1720187506189-lnd_node_info.js'
import { TrackedProvider1720814323679 } from './build/src/services/storage/migrations/1720814323679-tracked_provider.js'
import { CreateInviteTokenTable1721751414878 } from './build/src/services/storage/migrations/1721751414878-create_invite_token_table.js'
import { PaymentIndex1721760297610 } from './build/src/services/storage/migrations/1721760297610-payment_index.js'
import { DebitAccess1726496225078 } from './build/src/services/storage/migrations/1726496225078-debit_access.js'
@ -35,6 +48,7 @@ import { DebitToPub1727105758354 } from './build/src/services/storage/migrations
import { UserCbUrl1727112281043 } from './build/src/services/storage/migrations/1727112281043-user_cb_url.js'
import { UserOffer1733502626042 } from './build/src/services/storage/migrations/1733502626042-user_offer.js'
import { ManagementGrant1751307732346 } from './build/src/services/storage/migrations/1751307732346-management_grant.js'
import { ManagementGrantBanned1751989251513 } from './build/src/services/storage/migrations/1751989251513-management_grant_banned.js'
import { InvoiceCallbackUrls1752425992291 } from './build/src/services/storage/migrations/1752425992291-invoice_callback_urls.js'
import { OldSomethingLeftover1753106599604 } from './build/src/services/storage/migrations/1753106599604-old_something_leftover.js'
import { UserReceivingInvoiceIdx1753109184611 } from './build/src/services/storage/migrations/1753109184611-user_receiving_invoice_idx.js'
@ -47,20 +61,32 @@ import { TxSwap1762890527098 } from './build/src/services/storage/migrations/176
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'
import { SwapsServiceUrl1768413055036 } from './build/src/services/storage/migrations/1768413055036-swaps_service_url.js'
import { InvoiceSwaps1769529793283 } from './build/src/services/storage/migrations/1769529793283-invoice_swaps.js'
import { InvoiceSwapsFixes1769805357459 } from './build/src/services/storage/migrations/1769805357459-invoice_swaps_fixes.js'
import { ApplicationUserTopicId1770038768784 } from './build/src/services/storage/migrations/1770038768784-application_user_topic_id.js'
import { SwapTimestamps1771347307798 } from './build/src/services/storage/migrations/1771347307798-swap_timestamps.js'
export default new DataSource({
type: "better-sqlite3",
database: "db.sqlite",
// logging: true,
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878,
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611,
AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000],
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036,
InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798
],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap],
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap, InvoiceSwap],
// synchronize: true,
})
//npx typeorm migration:generate ./src/services/storage/migrations/swaps_service_url -d ./datasource.js
//npx typeorm migration:generate ./src/services/storage/migrations/tx_swap_timestamps -d ./datasource.js

View file

@ -1,3 +1,14 @@
/**
* TypeORM DataSource used only by the TypeORM CLI (e.g. migration:generate).
*
* Migrations at runtime are run from src/services/storage/migrations/runner.ts (allMigrations),
* not from this file. The app never uses this DataSource to run migrations.
*
* Workflow: update the migrations array in this file *before* running
* migration:generate, so TypeORM knows the current schema (entities + existing migrations).
* We do not update this file immediately after adding a new migration; update it when you
* are about to generate the next migration.
*/
import { DataSource } from "typeorm"
import { BalanceEvent } from "./build/src/services/storage/entity/BalanceEvent.js"
import { ChannelBalanceEvent } from "./build/src/services/storage/entity/ChannelsBalanceEvent.js"
@ -8,12 +19,16 @@ import { LndMetrics1703170330183 } from './build/src/services/storage/migrations
import { ChannelRouting1709316653538 } from './build/src/services/storage/migrations/1709316653538-channel_routing.js'
import { HtlcCount1724266887195 } from './build/src/services/storage/migrations/1724266887195-htlc_count.js'
import { BalanceEvents1724860966825 } from './build/src/services/storage/migrations/1724860966825-balance_events.js'
import { RootOps1732566440447 } from './build/src/services/storage/migrations/1732566440447-root_ops.js'
import { RootOpsTime1745428134124 } from './build/src/services/storage/migrations/1745428134124-root_ops_time.js'
import { ChannelEvents1750777346411 } from './build/src/services/storage/migrations/1750777346411-channel_events.js'
import { RootOpPending1771524665409 } from './build/src/services/storage/migrations/1771524665409-root_op_pending.js'
export default new DataSource({
type: "better-sqlite3",
database: "metrics.sqlite",
entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting, RootOperation, ChannelEvent],
migrations: [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825]
migrations: [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825,
RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411, RootOpPending1771524665409]
});
//npx typeorm migration:generate ./src/services/storage/migrations/channel_events -d ./metricsDatasource.js
//npx typeorm migration:generate ./src/services/storage/migrations/root_op_pending -d ./metricsDatasource.js

View file

@ -58,6 +58,11 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- BumpTx
- auth type: __Admin__
- input: [BumpTx](#BumpTx)
- This methods has an __empty__ __response__ body
- CloseChannel
- auth type: __Admin__
- input: [CloseChannelRequest](#CloseChannelRequest)
@ -93,6 +98,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
- GetAdminInvoiceSwapQuotes
- auth type: __Admin__
- input: [InvoiceSwapRequest](#InvoiceSwapRequest)
- output: [InvoiceSwapQuoteList](#InvoiceSwapQuoteList)
- GetAdminTransactionSwapQuotes
- auth type: __Admin__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
@ -103,6 +113,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [AppsMetricsRequest](#AppsMetricsRequest)
- output: [AppsMetrics](#AppsMetrics)
- GetAssetsAndLiabilities
- auth type: __Admin__
- input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq)
- output: [AssetsAndLiabilities](#AssetsAndLiabilities)
- GetBundleMetrics
- auth type: __Metrics__
- input: [LatestBundleMetricReq](#LatestBundleMetricReq)
@ -243,20 +258,25 @@ 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
- ListAdminInvoiceSwaps
- auth type: __Admin__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- output: [InvoiceSwapsList](#InvoiceSwapsList)
- ListAdminTxSwaps
- auth type: __Admin__
- This methods has an __empty__ __request__ body
- output: [TxSwapsList](#TxSwapsList)
- ListChannels
- auth type: __Admin__
- This methods has an __empty__ __request__ body
- output: [LndChannels](#LndChannels)
- ListSwaps
- ListTxSwaps
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- output: [TxSwapsList](#TxSwapsList)
- LndGetInfo
- auth type: __Admin__
@ -290,10 +310,15 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse)
- PayAdminInvoiceSwap
- auth type: __Admin__
- input: [PayAdminInvoiceSwapRequest](#PayAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- PayAdminTransactionSwap
- auth type: __Admin__
- input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest)
- output: [AdminSwapResponse](#AdminSwapResponse)
- output: [AdminTxSwapResponse](#AdminTxSwapResponse)
- PayInvoice
- auth type: __User__
@ -305,6 +330,11 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- RefundAdminInvoiceSwap
- auth type: __Admin__
- input: [RefundAdminInvoiceSwapRequest](#RefundAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- ResetDebit
- auth type: __User__
- input: [DebitOperation](#DebitOperation)
@ -484,6 +514,13 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- BumpTx
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/tx/bump__
- input: [BumpTx](#BumpTx)
- This methods has an __empty__ __response__ body
- CloseChannel
- auth type: __Admin__
- http method: __post__
@ -540,6 +577,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
- GetAdminInvoiceSwapQuotes
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/invoice/quote__
- input: [InvoiceSwapRequest](#InvoiceSwapRequest)
- output: [InvoiceSwapQuoteList](#InvoiceSwapQuoteList)
- GetAdminTransactionSwapQuotes
- auth type: __Admin__
- http method: __post__
@ -575,6 +619,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [AppsMetricsRequest](#AppsMetricsRequest)
- output: [AppsMetrics](#AppsMetrics)
- GetAssetsAndLiabilities
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/assets/liabilities__
- input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq)
- output: [AssetsAndLiabilities](#AssetsAndLiabilities)
- GetBundleMetrics
- auth type: __Metrics__
- http method: __post__
@ -743,7 +794,7 @@ The nostr server will send back a message response, and inside the body there wi
- GetTransactionSwapQuotes
- auth type: __User__
- http method: __post__
- http route: __/api/user/swap/quote__
- http route: __/api/user/swap/transaction/quote__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuoteList](#TransactionSwapQuoteList)
@ -834,12 +885,19 @@ 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
- ListAdminInvoiceSwaps
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/list__
- http route: __/api/admin/swap/invoice/list__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- output: [InvoiceSwapsList](#InvoiceSwapsList)
- ListAdminTxSwaps
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/transaction/list__
- This methods has an __empty__ __request__ body
- output: [TxSwapsList](#TxSwapsList)
- ListChannels
- auth type: __Admin__
@ -848,12 +906,12 @@ 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
- ListTxSwaps
- auth type: __User__
- http method: __post__
- http route: __/api/user/swap/list__
- http route: __/api/user/swap/transaction/list__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- output: [TxSwapsList](#TxSwapsList)
- LndGetInfo
- auth type: __Admin__
@ -899,12 +957,19 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse)
- PayAdminInvoiceSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/invoice/pay__
- input: [PayAdminInvoiceSwapRequest](#PayAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- PayAdminTransactionSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/transaction/pay__
- input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest)
- output: [AdminSwapResponse](#AdminSwapResponse)
- output: [AdminTxSwapResponse](#AdminTxSwapResponse)
- PayAppUserInvoice
- auth type: __App__
@ -927,6 +992,13 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- RefundAdminInvoiceSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/invoice/refund__
- input: [RefundAdminInvoiceSwapRequest](#RefundAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- RequestNPubLinkingToken
- auth type: __App__
- http method: __post__
@ -1098,7 +1170,10 @@ The nostr server will send back a message response, and inside the body there wi
- __name__: _string_
- __price_sats__: _number_
### AdminSwapResponse
### AdminInvoiceSwapResponse
- __tx_id__: _string_
### AdminTxSwapResponse
- __network_fee__: _number_
- __tx_id__: _string_
@ -1135,6 +1210,21 @@ The nostr server will send back a message response, and inside the body there wi
- __include_operations__: _boolean_ *this field is optional
- __to_unix__: _number_ *this field is optional
### AssetOperation
- __amount__: _number_
- __tracked__: _[TrackedOperation](#TrackedOperation)_ *this field is optional
- __ts__: _number_
### AssetsAndLiabilities
- __liquidity_providers__: ARRAY of: _[LiquidityAssetProvider](#LiquidityAssetProvider)_
- __lnds__: ARRAY of: _[LndAssetProvider](#LndAssetProvider)_
- __users_balance__: _number_
### AssetsAndLiabilitiesReq
- __limit_invoices__: _number_ *this field is optional
- __limit_payments__: _number_ *this field is optional
- __limit_providers__: _number_ *this field is optional
### AuthApp
- __app__: _[Application](#Application)_
- __auth_token__: _string_
@ -1163,6 +1253,11 @@ The nostr server will send back a message response, and inside the body there wi
- __nextRelay__: _string_ *this field is optional
- __type__: _string_
### BumpTx
- __output_index__: _number_
- __sat_per_vbyte__: _number_
- __txid__: _string_
### BundleData
- __available_chunks__: ARRAY of: _number_
- __base_64_data__: ARRAY of: _string_
@ -1331,6 +1426,36 @@ The nostr server will send back a message response, and inside the body there wi
- __token__: _string_
- __url__: _string_
### InvoiceSwapOperation
- __completed_at_unix__: _number_ *this field is optional
- __failure_reason__: _string_ *this field is optional
- __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional
- __quote__: _[InvoiceSwapQuote](#InvoiceSwapQuote)_
### InvoiceSwapQuote
- __address__: _string_
- __chain_fee_sats__: _number_
- __expires_at_block_height__: _number_
- __invoice__: _string_
- __invoice_amount_sats__: _number_
- __paid_at_unix__: _number_
- __service_fee_sats__: _number_
- __service_url__: _string_
- __swap_fee_sats__: _number_
- __swap_operation_id__: _string_
- __transaction_amount_sats__: _number_
- __tx_id__: _string_
### InvoiceSwapQuoteList
- __quotes__: ARRAY of: _[InvoiceSwapQuote](#InvoiceSwapQuote)_
### InvoiceSwapRequest
- __amount_sats__: _number_
### InvoiceSwapsList
- __current_block_height__: _number_
- __swaps__: ARRAY of: _[InvoiceSwapOperation](#InvoiceSwapOperation)_
### LatestBundleMetricReq
- __limit__: _number_ *this field is optional
@ -1340,6 +1465,10 @@ The nostr server will send back a message response, and inside the body there wi
### LinkNPubThroughTokenRequest
- __token__: _string_
### LiquidityAssetProvider
- __pubkey__: _string_
- __tracked__: _[TrackedLiquidityProvider](#TrackedLiquidityProvider)_ *this field is optional
### LiveDebitRequest
- __debit__: _[LiveDebitRequest_debit](#LiveDebitRequest_debit)_
- __npub__: _string_
@ -1353,6 +1482,10 @@ The nostr server will send back a message response, and inside the body there wi
- __latest_balance__: _number_
- __operation__: _[UserOperation](#UserOperation)_
### LndAssetProvider
- __pubkey__: _string_
- __tracked__: _[TrackedLndProvider](#TrackedLndProvider)_ *this field is optional
### LndChannels
- __open_channels__: ARRAY of: _[OpenChannel](#OpenChannel)_
@ -1534,6 +1667,11 @@ The nostr server will send back a message response, and inside the body there wi
- __service_fee__: _number_
- __txId__: _string_
### PayAdminInvoiceSwapRequest
- __no_claim__: _boolean_ *this field is optional
- __sat_per_v_byte__: _number_
- __swap_operation_id__: _string_
### PayAdminTransactionSwapRequest
- __address__: _string_
- __swap_operation_id__: _string_
@ -1584,6 +1722,18 @@ The nostr server will send back a message response, and inside the body there wi
### ProvidersDisruption
- __disruptions__: ARRAY of: _[ProviderDisruption](#ProviderDisruption)_
### PushNotificationEnvelope
- __app_npub_hex__: _string_
- __encrypted_payload__: _string_
- __topic_id__: _string_
### PushNotificationPayload
- __data__: _[PushNotificationPayload_data](#PushNotificationPayload_data)_
### RefundAdminInvoiceSwapRequest
- __sat_per_v_byte__: _number_
- __swap_operation_id__: _string_
### RelaysMigration
- __relays__: ARRAY of: _string_
@ -1640,19 +1790,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_
### TrackedLiquidityProvider
- __balance__: _number_
- __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_
- __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_
### SwapsList
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_
- __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_
### TrackedLndProvider
- __channels_balance__: _number_
- __confirmed_balance__: _number_
- __incoming_tx__: ARRAY of: _[AssetOperation](#AssetOperation)_
- __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_
- __outgoing_tx__: ARRAY of: _[AssetOperation](#AssetOperation)_
- __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_
- __unconfirmed_balance__: _number_
### TrackedOperation
- __amount__: _number_
- __ts__: _number_
- __type__: _[TrackedOperationType](#TrackedOperationType)_
### TransactionSwapQuote
- __chain_fee_sats__: _number_
- __completed_at_unix__: _number_
- __expires_at_block_height__: _number_
- __invoice_amount_sats__: _number_
- __paid_at_unix__: _number_
- __service_fee_sats__: _number_
- __service_url__: _string_
- __swap_fee_sats__: _number_
@ -1665,6 +1827,16 @@ The nostr server will send back a message response, and inside the body there wi
### TransactionSwapRequest
- __transaction_amount_sats__: _number_
### TxSwapOperation
- __address_paid__: _string_ *this field is optional
- __failure_reason__: _string_ *this field is optional
- __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional
- __quote__: _[TransactionSwapQuote](#TransactionSwapQuote)_
- __tx_id__: _string_ *this field is optional
### TxSwapsList
- __swaps__: ARRAY of: _[TxSwapOperation](#TxSwapOperation)_
### UpdateChannelPolicyRequest
- __policy__: _[ChannelPolicy](#ChannelPolicy)_
- __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_
@ -1707,6 +1879,7 @@ The nostr server will send back a message response, and inside the body there wi
- __nmanage__: _string_
- __noffer__: _string_
- __service_fee_bps__: _number_
- __topic_id__: _string_
- __userId__: _string_
- __user_identifier__: _string_
@ -1771,6 +1944,10 @@ The nostr server will send back a message response, and inside the body there wi
- __BUNDLE_METRIC__
- __USAGE_METRIC__
### TrackedOperationType
- __ROOT__
- __USER__
### UserOperationType
- __INCOMING_INVOICE__
- __INCOMING_TX__

View file

@ -66,6 +66,7 @@ type Client struct {
BanDebit func(req DebitOperation) error
BanUser func(req BanUserRequest) (*BanUserResponse, error)
// batching method: BatchUser not implemented
BumpTx func(req BumpTx) error
CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error)
CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error)
DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error)
@ -74,11 +75,13 @@ type Client struct {
EncryptionExchange func(req EncryptionExchangeRequest) error
EnrollAdminToken func(req EnrollAdminTokenRequest) error
EnrollMessagingToken func(req MessagingToken) error
GetAdminInvoiceSwapQuotes func(req InvoiceSwapRequest) (*InvoiceSwapQuoteList, 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)
GetAssetsAndLiabilities func(req AssetsAndLiabilitiesReq) (*AssetsAndLiabilities, error)
GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error)
GetDebitAuthorizations func() (*DebitAuthorizations, error)
GetErrorStats func() (*ErrorStats, error)
@ -114,19 +117,22 @@ type Client struct {
HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error
Health func() error
LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error
ListAdminSwaps func() (*SwapsList, error)
ListAdminInvoiceSwaps func() (*InvoiceSwapsList, error)
ListAdminTxSwaps func() (*TxSwapsList, error)
ListChannels func() (*LndChannels, error)
ListSwaps func() (*SwapsList, error)
ListTxSwaps func() (*TxSwapsList, 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)
PayAdminInvoiceSwap func(req PayAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error)
PayAdminTransactionSwap func(req PayAdminTransactionSwapRequest) (*AdminTxSwapResponse, error)
PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error)
PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error)
PingSubProcesses func() error
RefundAdminInvoiceSwap func(req RefundAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error)
RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
ResetDebit func(req DebitOperation) error
ResetManage func(req ManageOperation) error
@ -460,6 +466,30 @@ func NewClient(params ClientParams) *Client {
return &res, nil
},
// batching method: BatchUser not implemented
BumpTx: func(req BumpTx) error {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return err
}
finalRoute := "/api/admin/tx/bump"
body, err := json.Marshal(req)
if err != nil {
return err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return err
}
if result.Status == "ERROR" {
return fmt.Errorf(result.Reason)
}
return nil
},
CloseChannel: func(req CloseChannelRequest) (*CloseChannelResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
@ -667,6 +697,35 @@ func NewClient(params ClientParams) *Client {
}
return nil
},
GetAdminInvoiceSwapQuotes: func(req InvoiceSwapRequest) (*InvoiceSwapQuoteList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/invoice/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 := InvoiceSwapQuoteList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetAdminTransactionSwapQuotes: func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
@ -809,6 +868,35 @@ func NewClient(params ClientParams) *Client {
}
return &res, nil
},
GetAssetsAndLiabilities: func(req AssetsAndLiabilitiesReq) (*AssetsAndLiabilities, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/assets/liabilities"
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 := AssetsAndLiabilities{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetBundleMetrics: func(req LatestBundleMetricReq) (*BundleMetrics, error) {
auth, err := params.RetrieveMetricsAuth()
if err != nil {
@ -1324,7 +1412,7 @@ func NewClient(params ClientParams) *Client {
if err != nil {
return nil, err
}
finalRoute := "/api/user/swap/quote"
finalRoute := "/api/user/swap/transaction/quote"
body, err := json.Marshal(req)
if err != nil {
return nil, err
@ -1643,12 +1731,12 @@ func NewClient(params ClientParams) *Client {
}
return nil
},
ListAdminSwaps: func() (*SwapsList, error) {
ListAdminInvoiceSwaps: func() (*InvoiceSwapsList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/list"
finalRoute := "/api/admin/swap/invoice/list"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
@ -1662,7 +1750,33 @@ func NewClient(params ClientParams) *Client {
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := SwapsList{}
res := InvoiceSwapsList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
ListAdminTxSwaps: func() (*TxSwapsList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/transaction/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 := TxSwapsList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
@ -1691,12 +1805,12 @@ func NewClient(params ClientParams) *Client {
}
return &res, nil
},
ListSwaps: func() (*SwapsList, error) {
ListTxSwaps: func() (*TxSwapsList, error) {
auth, err := params.RetrieveUserAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/user/swap/list"
finalRoute := "/api/user/swap/transaction/list"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
@ -1710,7 +1824,7 @@ func NewClient(params ClientParams) *Client {
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := SwapsList{}
res := TxSwapsList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
@ -1892,7 +2006,36 @@ func NewClient(params ClientParams) *Client {
}
return &res, nil
},
PayAdminTransactionSwap: func(req PayAdminTransactionSwapRequest) (*AdminSwapResponse, error) {
PayAdminInvoiceSwap: func(req PayAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/invoice/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 := AdminInvoiceSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
PayAdminTransactionSwap: func(req PayAdminTransactionSwapRequest) (*AdminTxSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
@ -1914,7 +2057,7 @@ func NewClient(params ClientParams) *Client {
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := AdminSwapResponse{}
res := AdminTxSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
@ -2000,6 +2143,35 @@ func NewClient(params ClientParams) *Client {
}
return nil
},
RefundAdminInvoiceSwap: func(req RefundAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/invoice/refund"
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 := AdminInvoiceSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
RequestNPubLinkingToken: func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) {
auth, err := params.RetrieveAppAuth()
if err != nil {

View file

@ -79,6 +79,13 @@ const (
USAGE_METRIC SingleMetricType = "USAGE_METRIC"
)
type TrackedOperationType string
const (
ROOT TrackedOperationType = "ROOT"
USER TrackedOperationType = "USER"
)
type UserOperationType string
const (
@ -123,7 +130,10 @@ type AddProductRequest struct {
Name string `json:"name"`
Price_sats int64 `json:"price_sats"`
}
type AdminSwapResponse struct {
type AdminInvoiceSwapResponse struct {
Tx_id string `json:"tx_id"`
}
type AdminTxSwapResponse struct {
Network_fee int64 `json:"network_fee"`
Tx_id string `json:"tx_id"`
}
@ -160,6 +170,21 @@ type AppsMetricsRequest struct {
Include_operations bool `json:"include_operations"`
To_unix int64 `json:"to_unix"`
}
type AssetOperation struct {
Amount int64 `json:"amount"`
Tracked *TrackedOperation `json:"tracked"`
Ts int64 `json:"ts"`
}
type AssetsAndLiabilities struct {
Liquidity_providers []LiquidityAssetProvider `json:"liquidity_providers"`
Lnds []LndAssetProvider `json:"lnds"`
Users_balance int64 `json:"users_balance"`
}
type AssetsAndLiabilitiesReq struct {
Limit_invoices int64 `json:"limit_invoices"`
Limit_payments int64 `json:"limit_payments"`
Limit_providers int64 `json:"limit_providers"`
}
type AuthApp struct {
App *Application `json:"app"`
Auth_token string `json:"auth_token"`
@ -188,6 +213,11 @@ type BeaconData struct {
Nextrelay string `json:"nextRelay"`
Type string `json:"type"`
}
type BumpTx struct {
Output_index int64 `json:"output_index"`
Sat_per_vbyte int64 `json:"sat_per_vbyte"`
Txid string `json:"txid"`
}
type BundleData struct {
Available_chunks []int64 `json:"available_chunks"`
Base_64_data []string `json:"base_64_data"`
@ -356,6 +386,36 @@ type HttpCreds struct {
Token string `json:"token"`
Url string `json:"url"`
}
type InvoiceSwapOperation struct {
Completed_at_unix int64 `json:"completed_at_unix"`
Failure_reason string `json:"failure_reason"`
Operation_payment *UserOperation `json:"operation_payment"`
Quote *InvoiceSwapQuote `json:"quote"`
}
type InvoiceSwapQuote struct {
Address string `json:"address"`
Chain_fee_sats int64 `json:"chain_fee_sats"`
Expires_at_block_height int64 `json:"expires_at_block_height"`
Invoice string `json:"invoice"`
Invoice_amount_sats int64 `json:"invoice_amount_sats"`
Paid_at_unix int64 `json:"paid_at_unix"`
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"`
Tx_id string `json:"tx_id"`
}
type InvoiceSwapQuoteList struct {
Quotes []InvoiceSwapQuote `json:"quotes"`
}
type InvoiceSwapRequest struct {
Amount_sats int64 `json:"amount_sats"`
}
type InvoiceSwapsList struct {
Current_block_height int64 `json:"current_block_height"`
Swaps []InvoiceSwapOperation `json:"swaps"`
}
type LatestBundleMetricReq struct {
Limit int64 `json:"limit"`
}
@ -365,6 +425,10 @@ type LatestUsageMetricReq struct {
type LinkNPubThroughTokenRequest struct {
Token string `json:"token"`
}
type LiquidityAssetProvider struct {
Pubkey string `json:"pubkey"`
Tracked *TrackedLiquidityProvider `json:"tracked"`
}
type LiveDebitRequest struct {
Debit *LiveDebitRequest_debit `json:"debit"`
Npub string `json:"npub"`
@ -378,6 +442,10 @@ type LiveUserOperation struct {
Latest_balance int64 `json:"latest_balance"`
Operation *UserOperation `json:"operation"`
}
type LndAssetProvider struct {
Pubkey string `json:"pubkey"`
Tracked *TrackedLndProvider `json:"tracked"`
}
type LndChannels struct {
Open_channels []OpenChannel `json:"open_channels"`
}
@ -559,6 +627,11 @@ type PayAddressResponse struct {
Service_fee int64 `json:"service_fee"`
Txid string `json:"txId"`
}
type PayAdminInvoiceSwapRequest struct {
No_claim bool `json:"no_claim"`
Sat_per_v_byte int64 `json:"sat_per_v_byte"`
Swap_operation_id string `json:"swap_operation_id"`
}
type PayAdminTransactionSwapRequest struct {
Address string `json:"address"`
Swap_operation_id string `json:"swap_operation_id"`
@ -609,6 +682,18 @@ type ProviderDisruption struct {
type ProvidersDisruption struct {
Disruptions []ProviderDisruption `json:"disruptions"`
}
type PushNotificationEnvelope struct {
App_npub_hex string `json:"app_npub_hex"`
Encrypted_payload string `json:"encrypted_payload"`
Topic_id string `json:"topic_id"`
}
type PushNotificationPayload struct {
Data *PushNotificationPayload_data `json:"data"`
}
type RefundAdminInvoiceSwapRequest struct {
Sat_per_v_byte int64 `json:"sat_per_v_byte"`
Swap_operation_id string `json:"swap_operation_id"`
}
type RelaysMigration struct {
Relays []string `json:"relays"`
}
@ -665,19 +750,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 TrackedLiquidityProvider struct {
Balance int64 `json:"balance"`
Invoices []AssetOperation `json:"invoices"`
Payments []AssetOperation `json:"payments"`
}
type SwapsList struct {
Quotes []TransactionSwapQuote `json:"quotes"`
Swaps []SwapOperation `json:"swaps"`
type TrackedLndProvider struct {
Channels_balance int64 `json:"channels_balance"`
Confirmed_balance int64 `json:"confirmed_balance"`
Incoming_tx []AssetOperation `json:"incoming_tx"`
Invoices []AssetOperation `json:"invoices"`
Outgoing_tx []AssetOperation `json:"outgoing_tx"`
Payments []AssetOperation `json:"payments"`
Unconfirmed_balance int64 `json:"unconfirmed_balance"`
}
type TrackedOperation struct {
Amount int64 `json:"amount"`
Ts int64 `json:"ts"`
Type TrackedOperationType `json:"type"`
}
type TransactionSwapQuote struct {
Chain_fee_sats int64 `json:"chain_fee_sats"`
Completed_at_unix int64 `json:"completed_at_unix"`
Expires_at_block_height int64 `json:"expires_at_block_height"`
Invoice_amount_sats int64 `json:"invoice_amount_sats"`
Paid_at_unix int64 `json:"paid_at_unix"`
Service_fee_sats int64 `json:"service_fee_sats"`
Service_url string `json:"service_url"`
Swap_fee_sats int64 `json:"swap_fee_sats"`
@ -690,6 +787,16 @@ type TransactionSwapQuoteList struct {
type TransactionSwapRequest struct {
Transaction_amount_sats int64 `json:"transaction_amount_sats"`
}
type TxSwapOperation struct {
Address_paid string `json:"address_paid"`
Failure_reason string `json:"failure_reason"`
Operation_payment *UserOperation `json:"operation_payment"`
Quote *TransactionSwapQuote `json:"quote"`
Tx_id string `json:"tx_id"`
}
type TxSwapsList struct {
Swaps []TxSwapOperation `json:"swaps"`
}
type UpdateChannelPolicyRequest struct {
Policy *ChannelPolicy `json:"policy"`
Update *UpdateChannelPolicyRequest_update `json:"update"`
@ -732,6 +839,7 @@ type UserInfo struct {
Nmanage string `json:"nmanage"`
Noffer string `json:"noffer"`
Service_fee_bps int64 `json:"service_fee_bps"`
Topic_id string `json:"topic_id"`
Userid string `json:"userId"`
User_identifier string `json:"user_identifier"`
}
@ -830,6 +938,18 @@ type NPubLinking_state struct {
Linking_token *string `json:"linking_token"`
Unlinked *Empty `json:"unlinked"`
}
type PushNotificationPayload_data_type string
const (
RECEIVED_OPERATION PushNotificationPayload_data_type = "received_operation"
SENT_OPERATION PushNotificationPayload_data_type = "sent_operation"
)
type PushNotificationPayload_data struct {
Type PushNotificationPayload_data_type `json:"type"`
Received_operation *UserOperation `json:"received_operation"`
Sent_operation *UserOperation `json:"sent_operation"`
}
type UpdateChannelPolicyRequest_update_type string
const (

View file

@ -545,12 +545,12 @@ 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' )
case 'ListTxSwaps':
if (!methods.ListTxSwaps) {
throw new Error('method ListTxSwaps not found' )
} else {
opStats.validate = opStats.guard
const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
const res = await methods.ListTxSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
@ -693,6 +693,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...ctx }, ...callsMetrics])
} 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.BumpTx) throw new Error('method: BumpTx is not implemented')
app.post('/api/admin/tx/bump', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'BumpTx', 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.BumpTx) throw new Error('method: BumpTx 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.BumpTxValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
await methods.BumpTx({rpcName:'BumpTx', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.CloseChannel) throw new Error('method: CloseChannel is not implemented')
app.post('/api/admin/channel/close', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'CloseChannel', batch: false, nostr: false, batchSize: 0}
@ -869,6 +891,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.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes is not implemented')
app.post('/api/admin/swap/invoice/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminInvoiceSwapQuotes', 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.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes 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.InvoiceSwapRequestValidate(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.GetAdminInvoiceSwapQuotes({rpcName:'GetAdminInvoiceSwapQuotes', 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.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}
@ -976,6 +1020,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.GetAssetsAndLiabilities) throw new Error('method: GetAssetsAndLiabilities is not implemented')
app.post('/api/admin/assets/liabilities', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAssetsAndLiabilities', 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.GetAssetsAndLiabilities) throw new Error('method: GetAssetsAndLiabilities 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.AssetsAndLiabilitiesReqValidate(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.GetAssetsAndLiabilities({rpcName:'GetAssetsAndLiabilities', 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.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented')
app.post('/api/reports/bundle', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetBundleMetrics', batch: false, nostr: false, batchSize: 0}
@ -1362,7 +1428,7 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
} 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) => {
app.post('/api/user/swap/transaction/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 = {}
@ -1607,20 +1673,39 @@ 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}
if (!opts.allowNotImplementedMethods && !methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps is not implemented')
app.post('/api/admin/swap/invoice/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListAdminInvoiceSwaps', 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')
if (!methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps 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 })
const response = await methods.ListAdminInvoiceSwaps({rpcName:'ListAdminInvoiceSwaps', 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.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps is not implemented')
app.post('/api/admin/swap/transaction/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListAdminTxSwaps', 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.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps 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.ListAdminTxSwaps({rpcName:'ListAdminTxSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1645,20 +1730,20 @@ 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}
if (!opts.allowNotImplementedMethods && !methods.ListTxSwaps) throw new Error('method: ListTxSwaps is not implemented')
app.post('/api/user/swap/transaction/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListTxSwaps', 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')
if (!methods.ListTxSwaps) throw new Error('method: ListTxSwaps 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 })
const response = await methods.ListTxSwaps({rpcName:'ListTxSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1793,6 +1878,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.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap is not implemented')
app.post('/api/admin/swap/invoice/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAdminInvoiceSwap', 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.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap 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.PayAdminInvoiceSwapRequestValidate(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.PayAdminInvoiceSwap({rpcName:'PayAdminInvoiceSwap', 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.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}
@ -1878,6 +1985,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.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap is not implemented')
app.post('/api/admin/swap/invoice/refund', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'RefundAdminInvoiceSwap', 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.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap 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.RefundAdminInvoiceSwapRequestValidate(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.RefundAdminInvoiceSwap({rpcName:'RefundAdminInvoiceSwap', 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.RequestNPubLinkingToken) throw new Error('method: RequestNPubLinkingToken is not implemented')
app.post('/api/app/user/npub/token', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'RequestNPubLinkingToken', batch: false, nostr: false, batchSize: 0}

View file

@ -176,6 +176,17 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
BumpTx: async (request: Types.BumpTx): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/tx/bump'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
CloseChannel: async (request: Types.CloseChannelRequest): Promise<ResultError | ({ status: 'OK' }& Types.CloseChannelResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
@ -273,6 +284,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminInvoiceSwapQuotes: async (request: Types.InvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.InvoiceSwapQuoteList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/invoice/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.InvoiceSwapQuoteListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
@ -343,6 +368,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAssetsAndLiabilities: async (request: Types.AssetsAndLiabilitiesReq): Promise<ResultError | ({ status: 'OK' }& Types.AssetsAndLiabilities)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/assets/liabilities'
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.AssetsAndLiabilitiesValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetBundleMetrics: async (request: Types.LatestBundleMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.BundleMetrics)> => {
const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null')
@ -620,7 +659,7 @@ export default (params: ClientParams) => ({
GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/swap/quote'
let finalRoute = '/api/user/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') {
@ -781,16 +820,30 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListAdminSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
ListAdminInvoiceSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.InvoiceSwapsList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/list'
let finalRoute = '/api/admin/swap/invoice/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)
const error = Types.InvoiceSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListAdminTxSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.TxSwapsList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/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.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -809,16 +862,16 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
ListTxSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.TxSwapsList)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/swap/list'
let finalRoute = '/api/user/swap/transaction/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)
const error = Types.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -909,7 +962,21 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
PayAdminInvoiceSwap: async (request: Types.PayAdminInvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminInvoiceSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/invoice/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.AdminInvoiceSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminTxSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/pay'
@ -918,7 +985,7 @@ export default (params: ClientParams) => ({
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminSwapResponseValidate(result)
const error = Types.AdminTxSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -962,6 +1029,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
RefundAdminInvoiceSwap: async (request: Types.RefundAdminInvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminInvoiceSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/invoice/refund'
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.AdminInvoiceSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
RequestNPubLinkingToken: async (request: Types.RequestNPubLinkingTokenRequest): Promise<ResultError | ({ status: 'OK' }& Types.RequestNPubLinkingTokenResponse)> => {
const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null')

View file

@ -137,6 +137,18 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
BumpTx: async (request: Types.BumpTx): Promise<ResultError | ({ status: 'OK' })> => {
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:'BumpTx',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
CloseChannel: async (request: Types.CloseChannelRequest): Promise<ResultError | ({ status: 'OK' }& Types.CloseChannelResponse)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
@ -230,6 +242,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminInvoiceSwapQuotes: async (request: Types.InvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.InvoiceSwapQuoteList)> => {
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:'GetAdminInvoiceSwapQuotes',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.InvoiceSwapQuoteListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
@ -260,6 +287,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAssetsAndLiabilities: async (request: Types.AssetsAndLiabilitiesReq): Promise<ResultError | ({ status: 'OK' }& Types.AssetsAndLiabilities)> => {
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:'GetAssetsAndLiabilities',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.AssetsAndLiabilitiesValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetBundleMetrics: async (request: Types.LatestBundleMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.BundleMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
@ -666,16 +708,30 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListAdminSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
ListAdminInvoiceSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.InvoiceSwapsList)> => {
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 })
const data = await send(params.pubDestination, {rpcName:'ListAdminInvoiceSwaps',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)
const error = Types.InvoiceSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListAdminTxSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.TxSwapsList)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'ListAdminTxSwaps',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.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -694,16 +750,16 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
ListTxSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.TxSwapsList)> => {
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 })
const data = await send(params.pubDestination, {rpcName:'ListTxSwaps',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)
const error = Types.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -798,7 +854,22 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
PayAdminInvoiceSwap: async (request: Types.PayAdminInvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminInvoiceSwapResponse)> => {
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:'PayAdminInvoiceSwap',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.AdminInvoiceSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminTxSwapResponse)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
@ -808,7 +879,7 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminSwapResponseValidate(result)
const error = Types.AdminTxSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -839,6 +910,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
RefundAdminInvoiceSwap: async (request: Types.RefundAdminInvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminInvoiceSwapResponse)> => {
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:'RefundAdminInvoiceSwap',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.AdminInvoiceSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
ResetDebit: async (request: Types.DebitOperation): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')

View file

@ -427,12 +427,12 @@ 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')
case 'ListTxSwaps':
if (!methods.ListTxSwaps) {
throw new Error('method not defined: ListTxSwaps')
} else {
opStats.validate = opStats.guard
const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
const res = await methods.ListTxSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
@ -575,6 +575,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...ctx }, ...callsMetrics])
}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 'BumpTx':
try {
if (!methods.BumpTx) throw new Error('method: BumpTx 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.BumpTxValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
await methods.BumpTx({rpcName:'BumpTx', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'CloseChannel':
try {
if (!methods.CloseChannel) throw new Error('method: CloseChannel is not implemented')
@ -687,6 +703,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 'GetAdminInvoiceSwapQuotes':
try {
if (!methods.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes 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.InvoiceSwapRequestValidate(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.GetAdminInvoiceSwapQuotes({rpcName:'GetAdminInvoiceSwapQuotes', 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 'GetAdminTransactionSwapQuotes':
try {
if (!methods.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented')
@ -719,6 +751,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 'GetAssetsAndLiabilities':
try {
if (!methods.GetAssetsAndLiabilities) throw new Error('method: GetAssetsAndLiabilities 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.AssetsAndLiabilitiesReqValidate(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.GetAssetsAndLiabilities({rpcName:'GetAssetsAndLiabilities', 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 'GetBundleMetrics':
try {
if (!methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented')
@ -1122,14 +1170,27 @@ 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':
case 'ListAdminInvoiceSwaps':
try {
if (!methods.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented')
if (!methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps 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 })
const response = await methods.ListAdminInvoiceSwaps({rpcName:'ListAdminInvoiceSwaps', 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 'ListAdminTxSwaps':
try {
if (!methods.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps 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.ListAdminTxSwaps({rpcName:'ListAdminTxSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1148,14 +1209,14 @@ 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':
case 'ListTxSwaps':
try {
if (!methods.ListSwaps) throw new Error('method: ListSwaps is not implemented')
if (!methods.ListTxSwaps) throw new Error('method: ListTxSwaps 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 })
const response = await methods.ListTxSwaps({rpcName:'ListTxSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1254,6 +1315,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 'PayAdminInvoiceSwap':
try {
if (!methods.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap 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.PayAdminInvoiceSwapRequestValidate(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.PayAdminInvoiceSwap({rpcName:'PayAdminInvoiceSwap', 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 'PayAdminTransactionSwap':
try {
if (!methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
@ -1299,6 +1376,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 'RefundAdminInvoiceSwap':
try {
if (!methods.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap 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.RefundAdminInvoiceSwapRequestValidate(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.RefundAdminInvoiceSwap({rpcName:'RefundAdminInvoiceSwap', 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 'ResetDebit':
try {
if (!methods.ResetDebit) throw new Error('method: ResetDebit is not implemented')

File diff suppressed because it is too large Load diff

View file

@ -112,6 +112,20 @@ service LightningPub {
option (nostr) = true;
};
rpc GetAssetsAndLiabilities(structs.AssetsAndLiabilitiesReq) returns (structs.AssetsAndLiabilities) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/assets/liabilities";
option (nostr) = true;
};
rpc BumpTx(structs.BumpTx) returns (structs.Empty) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/tx/bump";
option (nostr) = true;
};
rpc AddApp(structs.AddAppRequest) returns (structs.AuthApp) {
option (auth_type) = "Admin";
option (http_method) = "post";
@ -175,6 +189,34 @@ service LightningPub {
option (nostr) = true;
}
rpc GetAdminInvoiceSwapQuotes(structs.InvoiceSwapRequest) returns (structs.InvoiceSwapQuoteList) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/invoice/quote";
option (nostr) = true;
}
rpc ListAdminInvoiceSwaps(structs.Empty) returns (structs.InvoiceSwapsList) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/invoice/list";
option (nostr) = true;
}
rpc PayAdminInvoiceSwap(structs.PayAdminInvoiceSwapRequest) returns (structs.AdminInvoiceSwapResponse) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/invoice/pay";
option (nostr) = true;
}
rpc RefundAdminInvoiceSwap(structs.RefundAdminInvoiceSwapRequest) returns (structs.AdminInvoiceSwapResponse) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/invoice/refund";
option (nostr) = true;
}
rpc GetAdminTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList) {
option (auth_type) = "Admin";
option (http_method) = "post";
@ -182,17 +224,17 @@ service LightningPub {
option (nostr) = true;
}
rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminSwapResponse) {
rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminTxSwapResponse) {
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) {
rpc ListAdminTxSwaps(structs.Empty) returns (structs.TxSwapsList) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/list";
option (http_route) = "/api/admin/swap/transaction/list";
option (nostr) = true;
}
@ -520,14 +562,14 @@ service LightningPub {
rpc GetTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/swap/quote";
option (http_route) = "/api/user/swap/transaction/quote";
option (nostr) = true;
}
rpc ListSwaps(structs.Empty) returns (structs.SwapsList){
rpc ListTxSwaps(structs.Empty) returns (structs.TxSwapsList){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/swap/list";
option (http_route) = "/api/user/swap/transaction/list";
option (nostr) = true;
}

View file

@ -19,6 +19,68 @@ message EncryptionExchangeRequest {
string deviceId = 2;
}
message AssetsAndLiabilitiesReq {
optional int64 limit_invoices = 1;
optional int64 limit_payments = 2;
optional int64 limit_providers = 4;
}
message BumpTx {
string txid = 1;
int64 output_index = 2;
int64 sat_per_vbyte = 3;
}
enum TrackedOperationType {
USER = 0;
ROOT = 1;
}
message TrackedOperation {
int64 ts = 1;
int64 amount = 2;
TrackedOperationType type = 3;
}
message AssetOperation {
int64 ts = 1;
int64 amount = 2;
optional TrackedOperation tracked = 3;
}
message TrackedLndProvider {
int64 confirmed_balance = 1;
int64 unconfirmed_balance = 2;
int64 channels_balance = 3;
repeated AssetOperation payments = 4;
repeated AssetOperation invoices = 5;
repeated AssetOperation incoming_tx = 6;
repeated AssetOperation outgoing_tx = 7;
}
message TrackedLiquidityProvider {
int64 balance = 1;
repeated AssetOperation payments = 2;
repeated AssetOperation invoices = 3;
}
message LndAssetProvider {
string pubkey = 1;
optional TrackedLndProvider tracked = 2;
}
message LiquidityAssetProvider {
string pubkey = 1;
optional TrackedLiquidityProvider tracked = 2;
}
message AssetsAndLiabilities {
int64 users_balance = 1;
repeated LndAssetProvider lnds = 2;
repeated LiquidityAssetProvider liquidity_providers = 3;
}
message UserHealthState {
string downtime_reason = 1;
}
@ -37,6 +99,8 @@ message ErrorStats {
ErrorStat past1m = 5;
}
message MetricsFile {
}
@ -541,6 +605,7 @@ message UserInfo{
string callback_url = 10;
string bridge_url = 11;
string nmanage = 12;
string topic_id = 13;
}
@ -833,6 +898,56 @@ message MessagingToken {
string firebase_messaging_token = 2;
}
message InvoiceSwapRequest {
int64 amount_sats = 1;
}
message InvoiceSwapQuote {
string swap_operation_id = 1;
string invoice = 2;
int64 invoice_amount_sats = 3;
string address = 4;
int64 transaction_amount_sats = 5;
int64 chain_fee_sats = 6;
int64 service_fee_sats = 7;
string service_url = 8;
int64 swap_fee_sats = 9;
string tx_id = 10;
int64 paid_at_unix = 11;
int64 expires_at_block_height = 12;
}
message InvoiceSwapQuoteList {
repeated InvoiceSwapQuote quotes = 1;
}
message InvoiceSwapOperation {
InvoiceSwapQuote quote = 1;
optional UserOperation operation_payment = 2;
optional string failure_reason = 3;
optional int64 completed_at_unix = 6;
}
message InvoiceSwapsList {
repeated InvoiceSwapOperation swaps = 1;
int64 current_block_height = 3;
}
message RefundAdminInvoiceSwapRequest {
string swap_operation_id = 1;
int64 sat_per_v_byte = 2;
}
message PayAdminInvoiceSwapRequest {
string swap_operation_id = 1;
int64 sat_per_v_byte = 2;
optional bool no_claim = 3;
}
message AdminInvoiceSwapResponse {
string tx_id = 1;
}
message TransactionSwapRequest {
int64 transaction_amount_sats = 2;
}
@ -851,27 +966,31 @@ message TransactionSwapQuote {
int64 chain_fee_sats = 5;
int64 service_fee_sats = 7;
string service_url = 8;
int64 expires_at_block_height = 9;
int64 paid_at_unix = 10;
int64 completed_at_unix = 11;
}
message TransactionSwapQuoteList {
repeated TransactionSwapQuote quotes = 1;
}
message AdminSwapResponse {
message AdminTxSwapResponse {
string tx_id = 1;
int64 network_fee = 2;
}
message SwapOperation {
string swap_operation_id = 1;
message TxSwapOperation {
TransactionSwapQuote quote = 1;
optional UserOperation operation_payment = 2;
optional string failure_reason = 3;
string address_paid = 4;
optional string address_paid = 4;
optional string tx_id = 5;
}
message SwapsList {
repeated SwapOperation swaps = 1;
repeated TransactionSwapQuote quotes = 2;
message TxSwapsList {
repeated TxSwapOperation swaps = 1;
}
message CumulativeFees {
@ -885,4 +1004,19 @@ message BeaconData {
optional string avatarUrl = 3;
optional string nextRelay = 4;
optional CumulativeFees fees = 5;
}
message PushNotificationEnvelope {
string topic_id = 1;
string app_npub_hex = 2;
string encrypted_payload = 3; // encrypted PushNotificationPayload
}
message PushNotificationPayload {
oneof data {
UserOperation received_operation = 1;
UserOperation sent_operation = 2;
}
}

View file

@ -1,17 +1,17 @@
import fs from 'fs'
export const DEBUG = Symbol("DEBUG")
export const ERROR = Symbol("ERROR")
export const WARN = Symbol("WARN")
export const INFO = Symbol("INFO")
type LoggerParams = { appName?: string, userId?: string, component?: string }
export type PubLogger = (...message: (string | number | object | symbol)[]) => void
type Writer = (message: string) => void
const logsDir = process.env.LOGS_DIR || "logs"
const logLevel = process.env.LOG_LEVEL || "DEBUG"
const logLevel = process.env.LOG_LEVEL || "INFO"
try {
fs.mkdirSync(logsDir)
} catch { }
if (logLevel !== "DEBUG" && logLevel !== "WARN" && logLevel !== "ERROR") {
throw new Error("Invalid log level " + logLevel + " must be one of (DEBUG, WARN, ERROR)")
if (logLevel !== "DEBUG" && logLevel !== "INFO" && logLevel !== "ERROR") {
throw new Error("Invalid log level " + logLevel + " must be one of (DEBUG, INFO, ERROR)")
}
const z = (n: number) => n < 10 ? `0${n}` : `${n}`
// Sanitize filename to remove invalid characters for filesystem
@ -67,19 +67,17 @@ export const getLogger = (params: LoggerParams): PubLogger => {
}
message[0] = "DEBUG"
break;
case WARN:
case INFO:
if (logLevel === "ERROR") {
return
}
message[0] = "WARN"
message[0] = "INFO"
break;
case ERROR:
message[0] = "ERROR"
break;
default:
if (logLevel !== "DEBUG") {
return
}
// treats logs without a level as ERROR level, without prefix so it can be found and fixed if needed
}
const now = new Date()
const timestamp = `${now.getFullYear()}-${z(now.getMonth() + 1)}-${z(now.getDate())} ${z(now.getHours())}:${z(now.getMinutes())}:${z(now.getSeconds())}`

View file

@ -15,7 +15,7 @@ import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
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 { ERROR, getLogger, DEBUG, INFO } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider } from '../main/liquidityProvider.js';
import { Utils } from '../helpers/utilsWrapper.js';
@ -23,7 +23,7 @@ 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';
import { ListAddressesResponse, PublishResponse } from '../../../proto/lnd/walletkit.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 20
@ -69,7 +69,7 @@ export default class {
// Skip LND client initialization if using only liquidity provider
if (liquidProvider.getSettings().useOnlyLiquidityProvider) {
this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization")
this.log(INFO, "USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization")
// Create minimal dummy clients - they won't be used but prevent null reference errors
// Use insecure credentials directly (can't combine them)
const { lndAddr } = this.getSettings().lndNodeSettings
@ -126,13 +126,13 @@ export default class {
}
async Warmup() {
this.log(INFO, "Warming up LND")
// Skip LND warmup if using only liquidity provider
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND warmup")
this.log(INFO, "USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND warmup")
this.ready = true
return
}
// console.log("Warming up LND")
this.SubscribeAddressPaid()
this.SubscribeInvoicePaid()
await this.SubscribeNewBlock()
@ -151,7 +151,7 @@ export default class {
this.ready = true
res()
} catch (err) {
this.log("LND is not ready yet, will try again in 1 second")
this.log(INFO, "LND is not ready yet, will try again in 1 second")
}
if (Date.now() - now > 1000 * 60 * 10) {
clearInterval(interval)
@ -161,6 +161,13 @@ export default class {
})
}
async PublishTransaction(txHex: string): Promise<PublishResponse> {
const res = await this.walletKit.publishTransaction({
txHex: Buffer.from(txHex, 'hex'), label: ""
}, DeadLineMetadata())
return res.response
}
async GetInfo(): Promise<NodeInfo> {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
// Return dummy info when bypass is enabled
@ -174,27 +181,26 @@ export default class {
uris: []
}
}
// console.log("Getting info")
const res = await this.lightning.getInfo({}, DeadLineMetadata())
return res.response
}
async ListPendingChannels(): Promise<PendingChannelsResponse> {
this.log(DEBUG, "Listing pending channels")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { pendingOpenChannels: [], pendingClosingChannels: [], pendingForceClosingChannels: [], waitingCloseChannels: [], totalLimboBalance: 0n }
}
// console.log("Listing pending channels")
const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata())
return res.response
}
async ListChannels(peerLookup = false): Promise<ListChannelsResponse> {
// console.log("Listing channels")
this.log(DEBUG, "Listing channels")
const res = await this.lightning.listChannels({
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup
}, DeadLineMetadata())
return res.response
}
async ListClosedChannels(): Promise<ClosedChannelsResponse> {
// console.log("Listing closed channels")
this.log(DEBUG, "Listing closed channels")
const res = await this.lightning.closedChannels({
abandoned: true,
breach: true,
@ -211,7 +217,6 @@ export default class {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return
}
// console.log("Checking health")
if (!this.ready) {
throw new Error("not ready")
}
@ -222,69 +227,69 @@ export default class {
}
RestartStreams() {
// console.log("Restarting streams")
this.log(INFO, "Restarting streams")
if (!this.ready || this.abortController.signal.aborted) {
return
}
this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds")
this.log(INFO, "LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds")
const interval = setInterval(async () => {
try {
await this.unlockLnd()
this.log("LND is back online")
this.log(INFO, "LND is back online")
clearInterval(interval)
await this.Warmup()
} catch (err) {
this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds")
this.log(INFO, "LND still dead, will try again in", deadLndRetrySeconds, "seconds")
}
}, deadLndRetrySeconds * 1000)
}
async SubscribeChannelEvents() {
// console.log("Subscribing to channel events")
this.log(DEBUG, "Subscribing to channel events")
const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(async channel => {
const channels = await this.ListChannels()
this.channelEventCb(channel, channels.channels)
})
stream.responses.onError(error => {
this.log("Error with subscribeChannelEvents stream")
this.log(ERROR, "Error with subscribeChannelEvents stream")
})
stream.responses.onComplete(() => {
this.log("subscribeChannelEvents stream closed")
this.log(INFO, "subscribeChannelEvents stream closed")
})
}
async SubscribeHtlcEvents() {
// console.log("Subscribing to htlc events")
this.log(DEBUG, "Subscribing to htlc events")
const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(htlc => {
this.htlcCb(htlc)
})
stream.responses.onError(error => {
this.log("Error with subscribeHtlcEvents stream")
this.log(ERROR, "Error with subscribeHtlcEvents stream")
})
stream.responses.onComplete(() => {
this.log("subscribeHtlcEvents stream closed")
this.log(INFO, "subscribeHtlcEvents stream closed")
})
}
async SubscribeNewBlock() {
// console.log("Subscribing to new block")
this.log(DEBUG, "Subscribing to new block")
const { blockHeight } = await this.GetInfo()
const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal })
stream.responses.onMessage(block => {
this.newBlockCb(block.height)
})
stream.responses.onError(error => {
this.log("Error with new block stream")
this.log(ERROR, "Error with new block stream")
})
stream.responses.onComplete(() => {
this.log("new block stream closed")
this.log(INFO, "new block stream closed")
})
}
SubscribeAddressPaid(): void {
// console.log("Subscribing to address paid")
this.log(DEBUG, "Subscribing to address paid")
const stream = this.lightning.subscribeTransactions({
account: "",
endHeight: 0,
@ -303,15 +308,15 @@ export default class {
}
})
stream.responses.onError(error => {
this.log("Error with onchain tx stream")
this.log(ERROR, "Error with onchain tx stream")
})
stream.responses.onComplete(() => {
this.log("onchain tx stream closed")
this.log(INFO, "onchain tx stream closed")
})
}
SubscribeInvoicePaid(): void {
// console.log("Subscribing to invoice paid")
this.log(DEBUG, "Subscribing to invoice paid")
const stream = this.lightning.subscribeInvoices({
settleIndex: BigInt(this.latestKnownSettleIndex),
addIndex: 0n,
@ -324,14 +329,14 @@ export default class {
})
let restarted = false
stream.responses.onError(error => {
this.log("Error with invoice stream")
this.log(ERROR, "Error with invoice stream")
if (!restarted) {
restarted = true
this.RestartStreams()
}
})
stream.responses.onComplete(() => {
this.log("invoice stream closed")
this.log(INFO, "invoice stream closed")
if (!restarted) {
restarted = true
this.RestartStreams()
@ -353,6 +358,7 @@ export default class {
}
async ListAddresses(): Promise<LndAddress[]> {
this.log(DEBUG, "Listing addresses")
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 })
@ -360,11 +366,11 @@ export default class {
}
async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> {
this.log(DEBUG, "Creating new address")
// Force use of provider when bypass is enabled (addresses not supported by provider, but we should fail gracefully)
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
throw new Error("Address generation not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled")
}
// console.log("Creating new address")
let lndAddressType: AddressType
switch (addressType) {
case Types.AddressType.NESTED_PUBKEY_HASH:
@ -394,11 +400,11 @@ export default class {
}
async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise<Invoice> {
// console.log("Creating new invoice")
this.log(DEBUG, "Creating new invoice")
// Force use of provider when bypass is enabled
const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider
if (mustUseProvider) {
console.log("using provider")
this.log(INFO, "using provider")
const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry)
const providerPubkey = this.liquidProvider.GetProviderPubkey()
return { payRequest: invoice, providerPubkey }
@ -414,6 +420,7 @@ export default class {
}
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
this.log(DEBUG, "Decoding invoice")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
// Use light-bolt11-decoder when LND is bypassed
try {
@ -439,24 +446,23 @@ export default class {
throw new Error(`Failed to decode invoice: ${err.message}`)
}
}
// console.log("Decoding invoice")
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash }
}
async ChannelBalance(): Promise<{ local: number, remote: number }> {
this.log(DEBUG, "Getting channel balance")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { local: 0, remote: 0 }
}
// console.log("Getting channel balance")
const res = await this.lightning.channelBalance({})
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, { routingFeeLimit, serviceFee }: { routingFeeLimit: number, serviceFee: number }, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise<PaidInvoice> {
// console.log("Paying invoice")
this.log(DEBUG, "Paying invoice")
if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request")
this.log(ERROR, "outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync")
}
// Force use of provider when bypass is enabled
@ -473,7 +479,7 @@ export default class {
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onError(error => {
this.log("invoice payment failed", error)
this.log(ERROR, "invoice payment failed", error)
rej(error)
})
let indexSent = false
@ -485,7 +491,7 @@ export default class {
}
switch (payment.status) {
case Payment_PaymentStatus.FAILED:
this.log("invoice payment failed", payment.failureReason)
this.log(ERROR, "invoice payment failed", payment.failureReason)
rej(PaymentFailureReason[payment.failureReason])
return
case Payment_PaymentStatus.SUCCEEDED:
@ -503,7 +509,7 @@ export default class {
}
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
// console.log("Estimating chain fees")
this.log(DEBUG, "Estimating chain fees")
await this.Health()
const res = await this.lightning.estimateFee({
addrToAmount: { [address]: BigInt(amount) },
@ -516,13 +522,13 @@ export default class {
}
async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise<SendCoinsResponse> {
this.log(DEBUG, "Paying address")
// Address payments not supported when bypass is enabled
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
throw new Error("Address payments not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled")
}
// console.log("Paying address")
if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request")
this.log(ERROR, "outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync")
}
if (useProvider) {
@ -540,19 +546,19 @@ export default class {
}
async GetTransactions(startHeight: number): Promise<TransactionDetails> {
// console.log("Getting transactions")
const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata())
this.log(DEBUG, "Getting transactions")
const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "", }, DeadLineMetadata())
return res.response
}
async GetChannelInfo(chanId: string) {
// console.log("Getting channel info")
this.log(DEBUG, "Getting channel info")
const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata())
return res.response
}
async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) {
// console.log("Updating channel policy")
this.log(DEBUG, "Updating channel policy")
const split = chanPoint.split(':')
const res = await this.lightning.updateChannelPolicy({
@ -570,19 +576,19 @@ export default class {
}
async GetChannelBalance() {
// console.log("Getting channel balance")
this.log(DEBUG, "Getting channel balance")
const res = await this.lightning.channelBalance({}, DeadLineMetadata())
return res.response
}
async GetWalletBalance() {
// console.log("Getting wallet balance")
this.log(DEBUG, "Getting wallet balance")
const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
return res.response
}
async GetTotalBalace() {
// console.log("Getting total balance")
this.log(DEBUG, "Getting total balance")
const walletBalance = await this.GetWalletBalance()
const confirmedWalletBalance = Number(walletBalance.confirmedBalance)
this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance)
@ -597,10 +603,10 @@ export default class {
}
async GetBalance(): Promise<BalanceInfo> { // TODO: remove this
this.log(DEBUG, "Getting balance")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { confirmedBalance: 0, unconfirmedBalance: 0, totalBalance: 0, channelsBalance: [] }
}
// console.log("Getting balance")
const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
const { response } = await this.lightning.listChannels({
@ -616,33 +622,47 @@ export default class {
}
async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> {
this.log(DEBUG, "Getting forwarding history")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { forwardingEvents: [], lastOffsetIndex: indexOffset }
}
// console.log("Getting forwarding history")
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata())
return response
}
async GetAllPaidInvoices(max: number) {
async GetAllInvoices(max: number) {
this.log(DEBUG, "Getting all paid invoices")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { invoices: [] }
}
// console.log("Getting all paid invoices")
const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata())
return res.response
}
async GetAllPayments(max: number) {
this.log(DEBUG, "Getting all payments")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { payments: [] }
}
// console.log("Getting all payments")
const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n })
return res.response
}
async BumpFee(txId: string, outputIndex: number, satPerVbyte: number) {
this.log(DEBUG, "Bumping fee")
const res = await this.walletKit.bumpFee({
budget: 0n, immediate: false, targetConf: 0, satPerVbyte: BigInt(satPerVbyte), outpoint: {
txidStr: txId,
outputIndex: outputIndex,
txidBytes: Buffer.alloc(0)
},
force: false,
satPerByte: 0
}, DeadLineMetadata())
return res.response
}
async GetPayment(paymentIndex: number) {
// console.log("Getting payment")
this.log(DEBUG, "Getting payment")
if (paymentIndex === 0) {
throw new Error("payment index starts from 1")
}
@ -654,10 +674,10 @@ export default class {
}
async GetLatestPaymentIndex(from = 0) {
this.log(DEBUG, "Getting latest payment index")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return from
}
// console.log("Getting latest payment index")
let indexOffset = BigInt(from)
while (true) {
const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset, maxPayments: 0n, reversed: false, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata())
@ -669,7 +689,7 @@ export default class {
}
async ConnectPeer(addr: { pubkey: string, host: string }) {
// console.log("Connecting to peer")
this.log(DEBUG, "Connecting to peer")
const res = await this.lightning.connectPeer({
addr,
perm: true,
@ -679,7 +699,7 @@ export default class {
}
async GetPaymentFromHash(paymentHash: string): Promise<Payment | null> {
// console.log("Getting payment from hash")
this.log(DEBUG, "Getting payment from hash")
const abortController = new AbortController()
const stream = this.router.trackPaymentV2({
paymentHash: Buffer.from(paymentHash, 'hex'),
@ -701,13 +721,12 @@ export default class {
}
async GetTx(txid: string) {
// console.log("Getting transaction")
const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata())
return res.response
}
async AddPeer(pub: string, host: string, port: number) {
// console.log("Adding peer")
this.log(DEBUG, "Adding peer")
const res = await this.lightning.connectPeer({
addr: {
pubkey: pub,
@ -720,19 +739,19 @@ export default class {
}
async ListPeers() {
// console.log("Listing peers")
this.log(DEBUG, "Listing peers")
const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata())
return res.response
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number, satsPerVByte: number): Promise<OpenStatusUpdate> {
// console.log("Opening channel")
this.log(DEBUG, "Opening channel")
const abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte)
const stream = this.lightning.openChannel(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onMessage(message => {
console.log("message", message)
this.log(DEBUG, "open channel message", message)
switch (message.update.oneofKind) {
case 'chanPending':
res(message)
@ -740,14 +759,14 @@ export default class {
}
})
stream.responses.onError(error => {
console.log("error", error)
this.log(ERROR, "open channel error", error)
rej(error)
})
})
}
async CloseChannel(fundingTx: string, outputIndex: number, force: boolean, satPerVByte: number): Promise<PendingUpdate> {
// console.log("Closing channel")
this.log(DEBUG, "Closing channel")
const stream = this.lightning.closeChannel({
deliveryAddress: "",
force: force,
@ -766,7 +785,7 @@ export default class {
}, DeadLineMetadata())
return new Promise((res, rej) => {
stream.responses.onMessage(message => {
console.log("message", message)
this.log(DEBUG, "close channel message", message)
switch (message.update.oneofKind) {
case 'closePending':
res(message.update.closePending)
@ -774,7 +793,7 @@ export default class {
}
})
stream.responses.onError(error => {
console.log("error", error)
this.log(ERROR, "close channel error", error)
rej(error)
})
})

View file

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

View file

@ -0,0 +1,299 @@
import secp256k1ZkpModule from '@vulpemventures/secp256k1-zkp';
const zkpInit = (secp256k1ZkpModule as any).default || secp256k1ZkpModule;
import { initEccLib, Transaction, address } from 'bitcoinjs-lib';
// import bolt11 from 'bolt11';
import {
Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
constructClaimTransaction, OutputType, constructRefundTransaction
} 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 { BTCNetwork } from '../../main/settings.js';
import { loggedGet, loggedPost, getNetwork } from './swapHelpers.js';
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 ReverseSwaps {
// settings: SettingsManager
private httpUrl: string
private wsUrl: string
log: PubLogger
private network: BTCNetwork
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
this.httpUrl = httpUrl
this.wsUrl = wsUrl
this.network = network
this.log = getLogger({ component: 'ReverseSwaps' })
initEccLib(ecc)
}
getHttpUrl = () => {
return this.httpUrl
}
getWsUrl = () => {
return this.wsUrl
}
calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => {
const pct = fees.percentage / 100
const minerFee = fees.minerFees.claim + fees.minerFees.lockup
const preFee = receiveAmount + minerFee
const fee = Math.ceil(preFee * pct)
const total = preFee + fee
return { total, fee, minerFee }
}
GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/reverse`
const feesRes = await loggedGet<TransactionSwapFeesRes>(this.log, url)
if (!feesRes.ok) {
return { ok: false, error: feesRes.error }
}
if (!feesRes.data.BTC?.BTC?.fees) {
return { ok: false, error: 'No fees found for BTC to BTC swap' }
}
return { ok: true, fees: feesRes.data.BTC.BTC.fees }
}
SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => {
const preimage = randomBytes(32);
const keys = ECPairFactory(ecc).makeRandom()
if (!keys.privateKey) {
return { ok: false, error: 'Failed to generate keys' }
}
const url = `${this.httpUrl}/v2/swap/reverse`
const req: any = {
onchainAmount: txAmount,
to: 'BTC',
from: 'BTC',
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
preimageHash: createHash('sha256').update(preimage).digest('hex'),
}
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
}
const createdResponse = createdResponseRes.data
this.log('Created transaction swap');
this.log(createdResponse);
return {
ok: true, createdResponse,
preimage: Buffer.from(preimage).toString('hex'),
pubkey: Buffer.from(keys.publicKey).toString('hex'),
privKey: Buffer.from(keys.privateKey).toString('hex')
}
}
SubscribeToTransactionSwap = async (data: TransactionSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => {
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
webSocket.on('open', () => {
webSocket.send(JSON.stringify(subReq))
})
const interval = setInterval(() => {
webSocket.ping()
}, 30 * 1000)
let txId = "", isDone = false
const done = (failureReason?: string) => {
isDone = true
clearInterval(interval)
webSocket.close()
if (failureReason) {
swapDone({ ok: false, error: failureReason })
} else {
swapDone({ ok: true, txId })
}
}
webSocket.on('pong', () => {
this.log('WebSocket transaction swap pong received')
})
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');
done('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)
done(err.message)
return
}
})
}
handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, done: (failureReason?: string) => 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;
case 'invoice.expired':
case 'swap.expired':
case 'transaction.failed':
done(`swap ${data.createdResponse.id} failed with status ${msg.args[0].status}`)
return;
default:
this.log('Unknown swap transaction WebSocket message', msg)
return;
}
}
handleTransactionMempool = async (data: TransactionSwapData, txHex: string): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
this.log('Creating claim transaction');
const { createdResponse, info } = data
const { destinationAddress, keys, preimage, chainFee } = info
const boltzPublicKey = Buffer.from(
createdResponse.refundPublicKey,
'hex',
);
// Create a musig signing session and tweak it with the Taptree of the swap scripts
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
boltzPublicKey,
Buffer.from(keys.publicKey),
]);
const tweakedKey = TaprootUtils.tweakMusig(
musig,
// swap tree can either be a string or an object
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
);
// Parse the lockup transaction and find the output relevant for the swap
const lockupTx = Transaction.fromHex(txHex);
const swapOutput = detectSwap(tweakedKey, lockupTx);
if (swapOutput === undefined) {
this.log(ERROR, 'No swap output found in lockup transaction');
return { ok: false, error: 'No swap output found in lockup transaction' }
}
const network = getNetwork(this.network)
// Create a claim transaction to be signed cooperatively via a key path spend
const claimTx = constructClaimTransaction(
[
{
...swapOutput,
keys,
preimage,
cooperative: true,
type: OutputType.Taproot,
txHash: lockupTx.getHash(),
},
],
address.toOutputScript(destinationAddress, network),
chainFee,
)
// Get the partial signature from Boltz
const claimUrl = `${this.httpUrl}/v2/swap/reverse/${createdResponse.id}/claim`
const claimReq = {
index: 0,
transaction: claimTx.toHex(),
preimage: preimage.toString('hex'),
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
}
const boltzSigRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!boltzSigRes.ok) {
return boltzSigRes
}
const boltzSig = boltzSigRes.data
// Aggregate the nonces
musig.aggregateNonces([
[boltzPublicKey, Buffer.from(boltzSig.pubNonce, 'hex')],
]);
// Initialize the session to sign the claim transaction
musig.initializeSession(
claimTx.hashForWitnessV1(
0,
[swapOutput.script],
[swapOutput.value],
Transaction.SIGHASH_DEFAULT,
),
);
// Add the partial signature from Boltz
musig.addPartial(
boltzPublicKey,
Buffer.from(boltzSig.partialSignature, 'hex'),
);
// Create our partial signature
musig.signPartial();
// Witness of the input to the aggregated signature
claimTx.ins[0].witness = [musig.aggregatePartials()];
// Broadcast the finalized transaction
const broadcastUrl = `${this.httpUrl}/v2/chain/BTC/transaction`
const broadcastReq = {
hex: claimTx.toHex(),
}
const broadcastResponse = await loggedPost<any>(this.log, broadcastUrl, broadcastReq)
if (!broadcastResponse.ok) {
return broadcastResponse
}
this.log('Transaction broadcasted', broadcastResponse.data)
const txId = claimTx.getId()
return { ok: true, txId }
}
}

View file

@ -0,0 +1,539 @@
import secp256k1ZkpModule from '@vulpemventures/secp256k1-zkp';
const zkpInit = (secp256k1ZkpModule as any).default || secp256k1ZkpModule;
// import bolt11 from 'bolt11';
import {
Musig, SwapTreeSerializer, TaprootUtils, constructRefundTransaction,
detectSwap, OutputType, targetFee
} from 'boltz-core';
import { randomBytes, createHash } from 'crypto';
import { ECPairFactory, ECPairInterface } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import { Transaction, address } from 'bitcoinjs-lib';
import ws from 'ws';
import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js';
import { loggedGet, loggedPost, getNetwork } from './swapHelpers.js';
import { BTCNetwork } from '../../main/settings.js';
/* type InvoiceSwapFees = {
hash: string,
rate: number,
limits: {
maximal: number,
minimal: number,
maximalZeroConf: number
},
fees: {
percentage: number,
minerFees: number,
}
} */
type InvoiceSwapFees = {
percentage: number,
minerFees: number,
}
type InvoiceSwapFeesRes = {
BTC?: {
BTC?: {
fees: InvoiceSwapFees
}
}
}
type InvoiceSwapResponse = {
id: string, claimPublicKey: string, swapTree: string, timeoutBlockHeight: number,
expectedAmount: number, address: string
}
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
export type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo }
export class SubmarineSwaps {
private httpUrl: string
private wsUrl: string
private network: BTCNetwork
log: PubLogger
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
this.httpUrl = httpUrl
this.wsUrl = wsUrl
this.network = network
this.log = getLogger({ component: 'SubmarineSwaps' })
}
getHttpUrl = () => {
return this.httpUrl
}
getWsUrl = () => {
return this.wsUrl
}
GetFees = async (): Promise<{ ok: true, fees: InvoiceSwapFees, } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/submarine`
const feesRes = await loggedGet<InvoiceSwapFeesRes>(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 }
}
SwapInvoice = async (invoice: string): Promise<{ ok: true, createdResponse: InvoiceSwapResponse, pubkey: string, privKey: string } | { ok: false, error: string }> => {
const keys = ECPairFactory(ecc).makeRandom()
if (!keys.privateKey) {
return { ok: false, error: 'Failed to generate keys' }
}
const refundPublicKey = Buffer.from(keys.publicKey).toString('hex')
const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey }
const url = `${this.httpUrl}/v2/swap/submarine`
this.log('Sending invoice swap request to', url);
const createdResponseRes = await loggedPost<InvoiceSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
}
const createdResponse = createdResponseRes.data
this.log('Created invoice swap');
this.log(createdResponse);
return {
ok: true, createdResponse,
pubkey: refundPublicKey,
privKey: Buffer.from(keys.privateKey).toString('hex')
}
}
/**
* Get the lockup transaction for a swap from Boltz
*/
private getLockupTransaction = async (swapId: string): Promise<{ ok: true, data: { hex: string } } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/submarine/${swapId}/transaction`
return await loggedGet<{ hex: string }>(this.log, url)
}
/**
* Get partial refund signature from Boltz for cooperative refund
*/
private getPartialRefundSignature = async (
swapId: string,
pubNonce: Buffer,
transaction: Transaction,
index: number
): Promise<{ ok: true, data: { pubNonce: string, partialSignature: string } } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/submarine/${swapId}/refund`
const req = {
index,
pubNonce: pubNonce.toString('hex'),
transaction: transaction.toHex()
}
return await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, url, req)
}
/**
* Constructs a Taproot refund transaction (cooperative or uncooperative)
*/
private constructTaprootRefund = async (
swapId: string,
claimPublicKey: string,
swapTree: string,
timeoutBlockHeight: number,
lockupTx: Transaction,
privateKey: ECPairInterface,
refundAddress: string,
feePerVbyte: number,
cooperative: boolean = true,
allowUncooperativeFallback: boolean = true,
cooperativeErrorMessage?: string
): Promise<{
ok: true,
transaction: Transaction,
cooperativeError?: string
} | {
ok: false,
error: string
}> => {
this.log(`Constructing ${cooperative ? 'cooperative' : 'uncooperative'} Taproot refund for swap ${swapId}`)
const boltzPublicKey = Buffer.from(claimPublicKey, 'hex')
const swapTreeDeserialized = SwapTreeSerializer.deserializeSwapTree(swapTree)
// Create musig and tweak it
let musig = new Musig(await zkpInit(), privateKey, randomBytes(32), [
boltzPublicKey,
Buffer.from(privateKey.publicKey),
])
const tweakedKey = TaprootUtils.tweakMusig(musig, swapTreeDeserialized.tree)
// Detect the swap output in the lockup transaction
const swapOutput = detectSwap(tweakedKey, lockupTx)
if (!swapOutput) {
return { ok: false, error: 'Could not detect swap output in lockup transaction' }
}
const network = getNetwork(this.network)
// const decodedAddress = address.fromBech32(refundAddress)
const details = [
{
...swapOutput,
keys: privateKey,
cooperative,
type: OutputType.Taproot,
txHash: lockupTx.getHash(),
swapTree: swapTreeDeserialized,
internalKey: musig.getAggregatedPublicKey(),
}
]
const outputScript = address.toOutputScript(refundAddress, network)
// Construct the refund transaction: targetFee converts sat/vbyte rate to flat fee
const refundTx = targetFee(
feePerVbyte,
(fee) => constructRefundTransaction(
details,
outputScript,
cooperative ? 0 : timeoutBlockHeight,
fee,
true
)
)
if (!cooperative) {
return {
ok: true,
transaction: refundTx,
cooperativeError: cooperativeErrorMessage,
}
}
// For cooperative refund, get Boltz's partial signature
try {
musig = new Musig(await zkpInit(), privateKey, randomBytes(32), [
boltzPublicKey,
Buffer.from(privateKey.publicKey),
])
// Get the partial signature from Boltz
const boltzSigRes = await this.getPartialRefundSignature(
swapId,
Buffer.from(musig.getPublicNonce()),
refundTx,
0
)
if (!boltzSigRes.ok) {
this.log(ERROR, 'Failed to get Boltz partial signature')
if (!allowUncooperativeFallback) {
return { ok: false, error: `Failed to get Boltz partial signature: ${boltzSigRes.error}` }
}
this.log(ERROR, 'Falling back to uncooperative refund')
// Fallback to uncooperative refund
return await this.constructTaprootRefund(
swapId,
claimPublicKey,
swapTree,
timeoutBlockHeight,
lockupTx,
privateKey,
refundAddress,
feePerVbyte,
false,
allowUncooperativeFallback,
boltzSigRes.error
)
}
const boltzSig = boltzSigRes.data
// Aggregate nonces
musig.aggregateNonces([
[boltzPublicKey, Musig.parsePubNonce(boltzSig.pubNonce)],
])
// Tweak musig again after aggregating nonces
TaprootUtils.tweakMusig(musig, swapTreeDeserialized.tree)
// Initialize session and sign
musig.initializeSession(
TaprootUtils.hashForWitnessV1(
details,
refundTx,
0
)
)
musig.signPartial()
musig.addPartial(boltzPublicKey, Buffer.from(boltzSig.partialSignature, 'hex'))
// Set the witness to the aggregated signature
refundTx.ins[0].witness = [musig.aggregatePartials()]
return { ok: true, transaction: refundTx }
} catch (error: any) {
this.log(ERROR, 'Cooperative refund failed:', error.message)
if (!allowUncooperativeFallback) {
return { ok: false, error: `Cooperative refund failed: ${error.message}` }
}
// Fallback to uncooperative refund
return await this.constructTaprootRefund(
swapId,
claimPublicKey,
swapTree,
timeoutBlockHeight,
lockupTx,
privateKey,
refundAddress,
feePerVbyte,
false,
allowUncooperativeFallback,
error.message
)
}
}
/**
* Broadcasts a refund transaction
*/
private broadcastRefundTransaction = async (transaction: Transaction): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/chain/BTC/transaction`
const req = { hex: transaction.toHex() }
const result = await loggedPost<{ id: string }>(this.log, url, req)
if (!result.ok) {
return result
}
return { ok: true, txId: result.data.id }
}
/**
* Refund a submarine swap
* @param swapId - The swap ID
* @param claimPublicKey - Boltz's claim public key
* @param swapTree - The swap tree
* @param timeoutBlockHeight - The timeout block height
* @param privateKey - The refund private key (hex string)
* @param refundAddress - The address to refund to
* @param currentHeight - The current block height
* @param lockupTxHex - The lockup transaction hex (optional, will fetch from Boltz if not provided)
* @param feePerVbyte - Fee rate in sat/vbyte (optional, will use default if not provided)
*/
RefundSwap = async (params: {
swapId: string,
claimPublicKey: string,
swapTree: string,
timeoutBlockHeight: number,
privateKeyHex: string,
refundAddress: string,
currentHeight: number,
lockupTxHex?: string,
feePerVbyte?: number,
allowEarlyRefund?: boolean
}): Promise<{ ok: true, publish: { done: false, txHex: string, txId: string } | { done: true, txId: string } } | { ok: false, error: string }> => {
const { swapId, claimPublicKey, swapTree, timeoutBlockHeight, privateKeyHex, refundAddress, currentHeight, lockupTxHex, feePerVbyte = 2, allowEarlyRefund = false } = params
this.log('Starting refund process for swap:', swapId)
// Get the lockup transaction (from parameter or fetch from Boltz)
let lockupTx: Transaction
if (lockupTxHex) {
this.log('Using provided lockup transaction hex')
lockupTx = Transaction.fromHex(lockupTxHex)
} else {
this.log('Fetching lockup transaction from Boltz')
const lockupTxRes = await this.getLockupTransaction(swapId)
if (!lockupTxRes.ok) {
return { ok: false, error: `Failed to get lockup transaction: ${lockupTxRes.error}` }
}
lockupTx = Transaction.fromHex(lockupTxRes.data.hex)
}
this.log('Lockup transaction retrieved:', lockupTx.getId())
const hasTimedOut = currentHeight >= timeoutBlockHeight
// For stuck swaps, only allow refund after timeout. For completed (failed) swaps,
// we may attempt a cooperative refund before timeout.
if (!hasTimedOut && !allowEarlyRefund) {
return {
ok: false,
error: `Swap has not timed out yet. Current height: ${currentHeight}, timeout: ${timeoutBlockHeight}`
}
}
if (hasTimedOut) {
this.log(`Swap has timed out. Current height: ${currentHeight}, timeout: ${timeoutBlockHeight}`)
} else {
this.log(`Swap has not timed out yet, attempting cooperative refund`)
}
// Parse the private key
const privateKey = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKeyHex, 'hex'))
// Construct the refund transaction (tries cooperative first, then falls back to uncooperative)
const refundTxRes = await this.constructTaprootRefund(
swapId,
claimPublicKey,
swapTree,
timeoutBlockHeight,
lockupTx,
privateKey,
refundAddress,
feePerVbyte,
true, // Try cooperative first
hasTimedOut // only allow uncooperative fallback once timeout has passed
)
if (!refundTxRes.ok) {
return { ok: false, error: refundTxRes.error }
}
const cooperative = !refundTxRes.cooperativeError
this.log(`Refund transaction constructed (${cooperative ? 'cooperative' : 'uncooperative'}):`, refundTxRes.transaction.getId())
if (!cooperative) {
return { ok: true, publish: { done: false, txHex: refundTxRes.transaction.toHex(), txId: refundTxRes.transaction.getId() } }
}
// Broadcast the refund transaction
const broadcastRes = await this.broadcastRefundTransaction(refundTxRes.transaction)
if (!broadcastRes.ok) {
return { ok: false, error: `Failed to broadcast refund transaction: ${broadcastRes.error}` }
}
this.log('Refund transaction broadcasted successfully:', broadcastRes.txId)
return { ok: true, publish: { done: true, txId: broadcastRes.txId } }
}
SubscribeToInvoiceSwap = (data: InvoiceSwapData, swapDone: (result: { ok: true } | { ok: false, error: string }) => void, waitingTx: () => void) => {
this.log("subscribing to invoice swap", { id: data.createdResponse.id })
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))
})
const interval = setInterval(() => {
webSocket.ping()
}, 30 * 1000)
let isDone = false
const done = (failureReason?: string) => {
isDone = true
clearInterval(interval)
webSocket.close()
if (failureReason) {
swapDone({ ok: false, error: failureReason })
} else {
swapDone({ ok: true })
}
}
webSocket.on('pong', () => {
this.log('WebSocket invoice swap pong received')
})
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');
done('WebSocket closed before swap was done')
}
})
webSocket.on('message', async (rawMsg) => {
try {
await this.handleSwapInvoiceMessage(rawMsg, data, done, waitingTx)
} catch (err: any) {
this.log(ERROR, 'Error handling invoice WebSocket message', err.message)
done(err.message)
return
}
});
return () => {
webSocket.close()
}
}
handleSwapInvoiceMessage = async (rawMsg: ws.RawData, data: InvoiceSwapData, closeWebSocket: (failureReason?: string) => void, waitingTx: () => 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');
waitingTx()
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;
case 'swap.expired':
case 'transaction.lockupFailed':
case 'invoice.failedToPay':
closeWebSocket(`swap ${data.createdResponse.id} failed with status ${msg.args[0].status}`)
return;
default:
this.log('Unknown swap invoice WebSocket message', msg)
return;
}
}
handleInvoiceClaimPending = async (data: InvoiceSwapData) => {
this.log('Creating cooperative claim transaction');
const { createdResponse, info } = data
const { paymentHash, keys } = info
// Get the information request to create a partial signature
const url = `${this.httpUrl}/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 = `${this.httpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
const claimReq = {
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
partialSignature: Buffer.from(musig.signPartial()).toString('hex'),
}
const claimResponseRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!claimResponseRes.ok) {
return claimResponseRes
}
const claimResponse = claimResponseRes.data
this.log('Claim response', claimResponse)
}
}

View file

@ -0,0 +1,50 @@
import axios from 'axios';
import { Network } from 'bitcoinjs-lib';
// import bolt11 from 'bolt11';
import {
Networks,
} from 'boltz-core';
import { PubLogger, ERROR } from '../../helpers/logger.js';
import { BTCNetwork } from '../../main/settings.js';
export const loggedPost = async <T>(log: PubLogger, url: string, req: any): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try {
const { data } = await axios.post(url, req)
return { ok: true, data: data as T }
} catch (err: any) {
if (err.response?.data) {
log(ERROR, 'Error sending request', err.response.data)
return { ok: false, error: JSON.stringify(err.response.data) }
}
log(ERROR, 'Error sending request', err.message)
return { ok: false, error: err.message }
}
}
export const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try {
const { data } = await axios.get(url)
return { ok: true, data: data as T }
} catch (err: any) {
if (err.response?.data) {
log(ERROR, 'Error getting request', err.response.data)
return { ok: false, error: err.response.data }
}
log(ERROR, 'Error getting request', err.message)
return { ok: false, error: err.message }
}
}
export 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}`)
}
}

View file

@ -0,0 +1,448 @@
import { ECPairFactory } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import { getLogger } from '../../helpers/logger.js';
import SettingsManager from '../../main/settingsManager.js';
import * as Types from '../../../../proto/autogenerated/ts/types.js';
import Storage from '../../storage/index.js';
import LND from '../lnd.js';
import { UserInvoicePayment } from '../../storage/entity/UserInvoicePayment.js';
import { ReverseSwaps, TransactionSwapData } from './reverseSwaps.js';
import { SubmarineSwaps, InvoiceSwapData } from './submarineSwaps.js';
import { InvoiceSwap } from '../../storage/entity/InvoiceSwap.js';
import { TransactionSwap } from '../../storage/entity/TransactionSwap.js';
export class Swaps {
settings: SettingsManager
revSwappers: Record<string, ReverseSwaps>
subSwappers: Record<string, SubmarineSwaps>
storage: Storage
lnd: LND
waitingSwaps: Record<string, boolean> = {}
log = getLogger({ component: 'swaps' })
constructor(settings: SettingsManager, storage: Storage) {
this.settings = settings
this.revSwappers = {}
this.subSwappers = {}
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 })
this.subSwappers[boltzHttpUrl] = new SubmarineSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network })
}
if (boltsHttpUrlAlt && boltsWebSocketUrlAlt) {
this.revSwappers[boltsHttpUrlAlt] = new ReverseSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network })
this.subSwappers[boltsHttpUrlAlt] = new SubmarineSwaps({ 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
}
GetInvoiceSwapQuotes = async (appUserId: string, invoice: string): Promise<Types.InvoiceSwapQuote[]> => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
const swappers = Object.values(this.subSwappers)
if (swappers.length === 0) {
throw new Error("No swap services available")
}
const res = await Promise.allSettled(swappers.map(sw => this.getInvoiceSwapQuote(sw, appUserId, invoice)))
const failures: string[] = []
const success: Types.InvoiceSwapQuote[] = []
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 mapInvoiceSwapQuote = (s: InvoiceSwap): Types.InvoiceSwapQuote => {
return {
swap_operation_id: s.swap_operation_id,
invoice: s.invoice,
invoice_amount_sats: s.invoice_amount,
address: s.address,
transaction_amount_sats: s.transaction_amount,
chain_fee_sats: s.chain_fee_sats,
service_fee_sats: 0,
service_url: s.service_url,
swap_fee_sats: s.swap_fee_sats,
tx_id: s.tx_id,
paid_at_unix: s.paid_at_unix || (s.tx_id ? 1 : 0),
expires_at_block_height: s.timeout_block_height,
}
}
ListInvoiceSwaps = async (appUserId: string): Promise<Types.InvoiceSwapsList> => {
const info = await this.lnd.GetInfo()
const currentBlockHeight = info.blockHeight
const completedSwaps = await this.storage.paymentStorage.ListCompletedInvoiceSwaps(appUserId)
const pendingSwaps = await this.storage.paymentStorage.ListPendingInvoiceSwaps(appUserId)
const quotes: Types.InvoiceSwapOperation[] = pendingSwaps.map(s => ({ quote: this.mapInvoiceSwapQuote(s) }))
const operations: Types.InvoiceSwapOperation[] = completedSwaps.map(s => ({
quote: this.mapInvoiceSwapQuote(s),
failure_reason: s.failure_reason,
completed_at_unix: s.completed_at_unix || 1,
}))
return {
current_block_height: currentBlockHeight,
swaps: operations.concat(quotes),
}
}
RefundInvoiceSwap = async (swapOperationId: string, satPerVByte: number, refundAddress: string, currentHeight: number): Promise<{ published: false, txHex: string, txId: string } | { published: true, txId: string }> => {
this.log("refunding invoice swap", { swapOperationId, satPerVByte, refundAddress, currentHeight })
const swap = await this.storage.paymentStorage.GetRefundableInvoiceSwap(swapOperationId)
if (!swap) {
throw new Error("Swap not found or already used")
}
const allowEarlyRefund = !!swap.failure_reason
const swapper = this.subSwappers[swap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
const result = await swapper.RefundSwap({
swapId: swap.swap_quote_id,
claimPublicKey: swap.claim_public_key,
currentHeight,
privateKeyHex: swap.ephemeral_private_key,
refundAddress,
swapTree: swap.swap_tree,
timeoutBlockHeight: swap.timeout_block_height,
allowEarlyRefund,
feePerVbyte: satPerVByte,
lockupTxHex: swap.lockup_tx_hex,
})
if (!result.ok) {
throw new Error(result.error)
}
if (result.publish.done) {
return { published: true, txId: result.publish.txId }
}
return { published: false, txHex: result.publish.txHex, txId: result.publish.txId }
}
PayInvoiceSwap = async (appUserId: string, swapOpId: string, satPerVByte: number, payAddress: (address: string, amt: number) => Promise<{ txId: string }>): Promise<void> => {
this.log("paying invoice swap", { appUserId, swapOpId, satPerVByte })
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
if (!swapOpId) {
throw new Error("swap operation id is required")
}
if (!satPerVByte) {
throw new Error("sat per v byte is required")
}
const swap = await this.storage.paymentStorage.GetInvoiceSwap(swapOpId, appUserId)
if (!swap) {
throw new Error("swap not found")
}
const swapper = this.subSwappers[swap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
if (this.waitingSwaps[swapOpId]) {
throw new Error("swap already in progress")
}
this.waitingSwaps[swapOpId] = true
const data = this.getInvoiceSwapData(swap)
let txId = ""
const close = swapper.SubscribeToInvoiceSwap(data, async (result) => {
if (result.ok) {
await this.storage.paymentStorage.FinalizeInvoiceSwap(swapOpId)
this.log("invoice swap completed", { swapOpId, txId })
} else {
await this.storage.paymentStorage.FailInvoiceSwap(swapOpId, result.error, txId)
this.log("invoice swap failed", { swapOpId, error: result.error })
}
}, () => payAddress(swap.address, swap.transaction_amount)
.then(res => { txId = res.txId })
.catch(err => { close(); this.log("error paying address", err.message || err) }))
}
ResumeInvoiceSwaps = async () => {
this.log("resuming invoice swaps")
const swaps = await this.storage.paymentStorage.ListUnfinishedInvoiceSwaps()
this.log("resuming", swaps.length, "invoice swaps")
for (const swap of swaps) {
try {
this.resumeInvoiceSwap(swap)
} catch (err: any) {
this.log("error resuming invoice swap", err.message || err)
}
}
}
private resumeInvoiceSwap = (swap: InvoiceSwap) => {
// const swap = await this.storage.paymentStorage.GetInvoiceSwap(swapOpId, appUserId)
if (!swap || !swap.tx_id || swap.used) {
throw new Error("swap to resume not found, or does not have a tx id")
}
const swapper = this.subSwappers[swap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
const data = this.getInvoiceSwapData(swap)
swapper.SubscribeToInvoiceSwap(data, async (result) => {
if (result.ok) {
await this.storage.paymentStorage.FinalizeInvoiceSwap(swap.swap_operation_id)
this.log("invoice swap completed", { swapOpId: swap.swap_operation_id, txId: swap.tx_id })
} else {
await this.storage.paymentStorage.FailInvoiceSwap(swap.swap_operation_id, result.error)
this.log("invoice swap failed", { swapOpId: swap.swap_operation_id, error: result.error })
}
}, () => { throw new Error("swap tx already paid") })
}
private getInvoiceSwapData = (swap: InvoiceSwap) => {
return {
createdResponse: {
address: swap.address,
claimPublicKey: swap.claim_public_key,
id: swap.swap_quote_id,
swapTree: swap.swap_tree,
timeoutBlockHeight: swap.timeout_block_height,
expectedAmount: swap.transaction_amount,
},
info: {
keys: this.GetKeys(swap.ephemeral_private_key),
paymentHash: swap.payment_hash,
}
}
}
private async getInvoiceSwapQuote(swapper: SubmarineSwaps, appUserId: string, invoice: string): Promise<Types.InvoiceSwapQuote> {
const feesRes = await swapper.GetFees()
if (!feesRes.ok) {
throw new Error(feesRes.error)
}
const decoded = await this.lnd.DecodeInvoice(invoice)
const amt = decoded.numSatoshis
const fee = Math.ceil((feesRes.fees.percentage / 100) * amt) + feesRes.fees.minerFees
const res = await swapper.SwapInvoice(invoice)
if (!res.ok) {
throw new Error(res.error)
}
const newSwap = await this.storage.paymentStorage.AddInvoiceSwap({
app_user_id: appUserId,
swap_quote_id: res.createdResponse.id,
swap_tree: JSON.stringify(res.createdResponse.swapTree),
timeout_block_height: res.createdResponse.timeoutBlockHeight,
ephemeral_public_key: res.pubkey,
ephemeral_private_key: res.privKey,
invoice: invoice,
invoice_amount: amt,
transaction_amount: res.createdResponse.expectedAmount,
swap_fee_sats: fee,
chain_fee_sats: 0,
service_url: swapper.getHttpUrl(),
address: res.createdResponse.address,
claim_public_key: res.createdResponse.claimPublicKey,
payment_hash: decoded.paymentHash,
})
return {
swap_operation_id: newSwap.swap_operation_id,
invoice: invoice,
invoice_amount_sats: amt,
address: res.createdResponse.address,
transaction_amount_sats: res.createdResponse.expectedAmount,
chain_fee_sats: 0,
service_fee_sats: 0,
service_url: swapper.getHttpUrl(),
swap_fee_sats: fee,
tx_id: newSwap.tx_id,
paid_at_unix: newSwap.paid_at_unix,
expires_at_block_height: newSwap.timeout_block_height,
}
}
private mapTransactionSwapQuote = (s: TransactionSwap, getServiceFee: (amt: number) => number): Types.TransactionSwapQuote => {
const serviceFee = getServiceFee(s.invoice_amount)
return {
swap_operation_id: s.swap_operation_id,
transaction_amount_sats: s.transaction_amount,
invoice_amount_sats: s.invoice_amount,
chain_fee_sats: s.chain_fee_sats,
service_fee_sats: serviceFee,
swap_fee_sats: s.swap_fee_sats,
expires_at_block_height: s.timeout_block_height,
service_url: s.service_url,
paid_at_unix: s.paid_at_unix,
completed_at_unix: s.completed_at_unix,
}
}
ListTxSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise<Types.TxSwapsList> => {
const completedSwaps = await this.storage.paymentStorage.ListCompletedTxSwaps(appUserId, payments)
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(appUserId)
const quotes: Types.TxSwapOperation[] = pendingSwaps.map(s => ({ quote: this.mapTransactionSwapQuote(s, getServiceFee) }))
const swaps: Types.TxSwapOperation[] = completedSwaps.map(s => ({
quote: this.mapTransactionSwapQuote(s.swap, getServiceFee),
operation_payment: s.payment ? newOp(s.payment) : undefined,
address_paid: s.swap.address_paid,
tx_id: s.swap.tx_id,
failure_reason: s.swap.failure_reason,
}))
return {
swaps: swaps.concat(quotes),
}
}
GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote[]> => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
const swappers = Object.values(this.revSwappers)
if (swappers.length === 0) {
throw new Error("No swap services available")
}
const res = await Promise.allSettled(swappers.map(sw => this.getTxSwapQuote(sw, appUserId, amt, getServiceFee)))
const failures: string[] = []
const success: Types.TransactionSwapQuote[] = []
for (const r of res) {
if (r.status === 'fulfilled') {
success.push(r.value)
} else {
failures.push(r.reason.message ? r.reason.message : r.reason.toString())
}
}
if (success.length === 0) {
throw new Error(failures.join("\n"))
}
return success
}
private async getTxSwapQuote(swapper: ReverseSwaps, appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote> {
this.log("getting transaction swap quote")
const feesRes = await swapper.GetFees()
if (!feesRes.ok) {
throw new Error(feesRes.error)
}
const { claim, lockup } = feesRes.fees.minerFees
const minerFee = claim + lockup
const chainTotal = amt + minerFee
const res = await swapper.SwapTransaction(chainTotal)
if (!res.ok) {
throw new Error(res.error)
}
const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice)
const swapFee = decoded.numSatoshis - chainTotal
const serviceFee = getServiceFee(decoded.numSatoshis)
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
app_user_id: appUserId,
swap_quote_id: res.createdResponse.id,
swap_tree: JSON.stringify(res.createdResponse.swapTree),
lockup_address: res.createdResponse.lockupAddress,
refund_public_key: res.createdResponse.refundPublicKey,
timeout_block_height: res.createdResponse.timeoutBlockHeight,
invoice: res.createdResponse.invoice,
invoice_amount: decoded.numSatoshis,
transaction_amount: chainTotal,
swap_fee_sats: swapFee,
chain_fee_sats: minerFee,
preimage: res.preimage,
ephemeral_private_key: res.privKey,
ephemeral_public_key: res.pubkey,
service_url: swapper.getHttpUrl(),
})
return {
swap_operation_id: newSwap.swap_operation_id,
swap_fee_sats: swapFee,
invoice_amount_sats: decoded.numSatoshis,
transaction_amount_sats: amt,
chain_fee_sats: minerFee,
service_fee_sats: serviceFee,
service_url: swapper.getHttpUrl(),
expires_at_block_height: res.createdResponse.timeoutBlockHeight,
paid_at_unix: newSwap.paid_at_unix,
completed_at_unix: newSwap.completed_at_unix,
}
}
async PayAddrWithSwap(appUserId: string, swapOpId: string, address: string, payInvoice: (invoice: string, amt: number) => Promise<void>) {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
this.log("paying address with swap", { appUserId, swapOpId, address })
if (!swapOpId) {
throw new Error("request a swap quote before paying an external address")
}
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(swapOpId, appUserId)
if (!txSwap) {
throw new Error("swap quote not found")
}
const info = await this.lnd.GetInfo()
if (info.blockHeight >= txSwap.timeout_block_height) {
throw new Error("swap timeout")
}
const swapper = this.revSwappers[txSwap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
const keys = this.GetKeys(txSwap.ephemeral_private_key)
const data: TransactionSwapData = {
createdResponse: {
id: txSwap.swap_quote_id,
invoice: txSwap.invoice,
lockupAddress: txSwap.lockup_address,
refundPublicKey: txSwap.refund_public_key,
swapTree: txSwap.swap_tree,
timeoutBlockHeight: txSwap.timeout_block_height,
onchainAmount: txSwap.transaction_amount,
},
info: {
destinationAddress: address,
keys,
chainFee: txSwap.chain_fee_sats,
preimage: Buffer.from(txSwap.preimage, 'hex'),
}
}
// the swap and the invoice payment are linked, swap will not start until the invoice payment is started, and will not complete once the invoice payment is completed
let swapResult = { ok: false, error: "swap never completed" } as { ok: true, txId: string } | { ok: false, error: string }
swapper.SubscribeToTransactionSwap(data, result => {
swapResult = result
})
try {
await this.storage.paymentStorage.SetTransactionSwapPaid(swapOpId)
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
}
}
}

View file

@ -5,9 +5,50 @@ 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";
import { Swaps } from "../lnd/swaps/swaps.js";
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js";
import { TrackedProvider } from "../storage/entity/TrackedProvider.js";
import { NodeInfo } from "../lnd/settings.js";
import { Invoice, Payment, OutputDetail, Transaction, Payment_PaymentStatus, Invoice_InvoiceState } from "../../../proto/lnd/lightning.js";
import { LiquidityProvider } from "./liquidityProvider.js";
/* type TrackedOperation = {
ts: number
amount: number
type: 'user' | 'root'
}
type AssetOperation = {
ts: number
amount: number
tracked?: TrackedOperation
}
type TrackedLndProvider = {
confirmedBalance: number
unconfirmedBalance: number
channelsBalace: number
payments: AssetOperation[]
invoices: AssetOperation[]
incomingTx: AssetOperation[]
outgoingTx: AssetOperation[]
}
type LndAssetProvider = {
pubkey: string
tracked?: TrackedLndProvider
}
type TrackedLiquidityProvider = {
balance: number
payments: AssetOperation[]
invoices: AssetOperation[]
}
type ProviderAssetProvider = {
pubkey: string
tracked?: TrackedLiquidityProvider
} */
const ROOT_OP = Types.TrackedOperationType.ROOT
const USER_OP = Types.TrackedOperationType.USER
export class AdminManager {
settings: SettingsManager
liquidityProvider: LiquidityProvider | null = null
storage: Storage
log = getLogger({ component: "adminManager" })
adminNpub = ""
@ -40,6 +81,10 @@ export class AdminManager {
this.start()
}
attachLiquidityProvider(liquidityProvider: LiquidityProvider) {
this.liquidityProvider = liquidityProvider
}
attachNostrReset(f: () => Promise<void>) {
this.nostrReset = f
}
@ -260,15 +305,64 @@ export class AdminManager {
}
}
async ListAdminSwaps(): Promise<Types.SwapsList> {
return this.swaps.ListSwaps("admin", [], p => undefined, amt => 0)
async ListAdminInvoiceSwaps(): Promise<Types.InvoiceSwapsList> {
return this.swaps.ListInvoiceSwaps("admin")
}
async GetAdminInvoiceSwapQuotes(req: Types.InvoiceSwapRequest): Promise<Types.InvoiceSwapQuoteList> {
const invoice = await this.lnd.NewInvoice(req.amount_sats, "Admin Swap", defaultInvoiceExpiry, { useProvider: false, from: 'system' })
const quotes = await this.swaps.GetInvoiceSwapQuotes("admin", invoice.payRequest)
return { quotes }
}
async PayAdminInvoiceSwap(req: Types.PayAdminInvoiceSwapRequest): Promise<Types.AdminInvoiceSwapResponse> {
const resolvedTxId = await new Promise<string>(res => {
this.swaps.PayInvoiceSwap("admin", req.swap_operation_id, req.sat_per_v_byte, async (addr, amt) => {
const tx = await this.lnd.PayAddress(addr, amt, req.sat_per_v_byte, "", { useProvider: false, from: 'system' })
this.log("paid admin invoice swap", { swapOpId: req.swap_operation_id, txId: tx.txid })
await this.storage.metricsStorage.AddRootOperation("chain_payment", tx.txid, amt, true)
// Fetch the full transaction hex for potential refunds
let lockupTxHex: string | undefined
let chainFeeSats = 0
try {
const txDetails = await this.lnd.GetTx(tx.txid)
chainFeeSats = Number(txDetails.totalFees)
lockupTxHex = txDetails.rawTxHex
} catch (err: any) {
this.log("Warning: Could not fetch transaction hex for refund purposes:", err.message)
}
await this.storage.paymentStorage.SetInvoiceSwapTxId(req.swap_operation_id, tx.txid, chainFeeSats, lockupTxHex)
this.log("saved admin swap txid", { swapOpId: req.swap_operation_id, txId: tx.txid })
res(tx.txid)
return { txId: tx.txid }
})
})
return { tx_id: resolvedTxId }
}
async RefundAdminInvoiceSwap(req: Types.RefundAdminInvoiceSwapRequest): Promise<Types.AdminInvoiceSwapResponse> {
const info = await this.lnd.GetInfo()
const currentHeight = info.blockHeight
const address = await this.lnd.NewAddress(Types.AddressType.WITNESS_PUBKEY_HASH, { useProvider: false, from: 'system' })
const result = await this.swaps.RefundInvoiceSwap(req.swap_operation_id, req.sat_per_v_byte, address.address, currentHeight)
if (result.published) {
return { tx_id: result.txId }
}
await this.lnd.PublishTransaction(result.txHex)
return { tx_id: result.txId }
}
async ListAdminTxSwaps(): Promise<Types.TxSwapsList> {
return this.swaps.ListTxSwaps("admin", [], p => undefined, amt => 0)
}
async GetAdminTransactionSwapQuotes(req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuoteList> {
const quotes = await this.swaps.GetTxSwapQuotes("admin", req.transaction_amount_sats, () => 0)
return { quotes }
}
async PayAdminTransactionSwap(req: Types.PayAdminTransactionSwapRequest): Promise<Types.AdminSwapResponse> {
async PayAdminTransactionSwap(req: Types.PayAdminTransactionSwapRequest): Promise<Types.AdminTxSwapResponse> {
const routingFloor = this.settings.getSettings().lndSettings.routingFeeFloor
const routingLimit = this.settings.getSettings().lndSettings.routingFeeLimitBps / 10000
@ -282,6 +376,222 @@ export class AdminManager {
network_fee: swap.network_fee,
}
}
async GetAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq): Promise<Types.AssetsAndLiabilities> {
const providers = await this.storage.liquidityStorage.GetTrackedProviders()
const lnds: Types.LndAssetProvider[] = []
const liquidityProviders: Types.LiquidityAssetProvider[] = []
for (const provider of providers) {
if (provider.provider_type === 'lnd') {
const lndEntry = await this.GetLndAssetsAndLiabilities(req, provider)
lnds.push(lndEntry)
} else if (provider.provider_type === 'lnPub') {
const liquidityEntry = await this.GetProviderAssetsAndLiabilities(req, provider)
liquidityProviders.push(liquidityEntry)
}
}
const usersBalance = await this.storage.paymentStorage.GetTotalUsersBalance(true)
return {
users_balance: usersBalance,
lnds,
liquidity_providers: liquidityProviders,
}
}
async GetProviderAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq, provider: TrackedProvider): Promise<Types.LiquidityAssetProvider> {
if (!this.liquidityProvider) {
throw new Error("liquidity provider not attached")
}
if (this.liquidityProvider.GetProviderPubkey() !== provider.provider_pubkey) {
return { pubkey: provider.provider_pubkey, tracked: undefined }
}
const providerOps = await this.liquidityProvider.GetOperations(req.limit_providers || 100)
// we only care about invoices cuz they are the only ops we can generate with a provider
const invoices: Types.AssetOperation[] = []
const payments: Types.AssetOperation[] = []
for (const op of providerOps.latestIncomingInvoiceOperations.operations) {
const assetOp = await this.GetProviderInvoiceAssetOperation(op)
invoices.push(assetOp)
}
for (const op of providerOps.latestOutgoingInvoiceOperations.operations) {
const assetOp = await this.GetProviderPaymentAssetOperation(op)
payments.push(assetOp)
}
const balance = await this.liquidityProvider.GetUserState()
return {
pubkey: provider.provider_pubkey,
tracked: {
balance: balance.status === 'OK' ? balance.balance : 0,
payments,
invoices,
}
}
}
async GetProviderInvoiceAssetOperation(op: Types.UserOperation): Promise<Types.AssetOperation> {
const ts = Number(op.paidAtUnix)
const amount = Number(op.amount)
const invoice = op.identifier
const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(invoice)
if (userInvoice) {
const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP }
return { ts, amount, tracked }
}
const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice", invoice)
if (rootOp) {
const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP }
return { ts, amount, tracked }
}
return { ts, amount, tracked: undefined }
}
async GetProviderPaymentAssetOperation(op: Types.UserOperation): Promise<Types.AssetOperation> {
const ts = Number(op.paidAtUnix)
const amount = Number(op.amount)
const invoice = op.identifier
const userInvoice = await this.storage.paymentStorage.GetPaymentOwner(invoice)
if (userInvoice) {
const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP }
return { ts, amount, tracked }
}
const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice_payment", invoice)
if (rootOp) {
const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP }
return { ts, amount, tracked }
}
return { ts, amount, tracked: undefined }
}
async GetLndAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq, provider: TrackedProvider): Promise<Types.LndAssetProvider> {
const info = await this.lnd.GetInfo()
if (provider.provider_pubkey !== info.identityPubkey) {
return { pubkey: provider.provider_pubkey, tracked: undefined }
}
const latestLndPayments = await this.lnd.GetAllPayments(req.limit_payments || 50)
const payments: Types.AssetOperation[] = []
for (const payment of latestLndPayments.payments) {
if (payment.status !== Payment_PaymentStatus.SUCCEEDED) {
continue
}
const assetOp = await this.GetPaymentAssetOperation(payment)
payments.push(assetOp)
}
const invoices: Types.AssetOperation[] = []
const paidInvoices = await this.lnd.GetAllInvoices(req.limit_invoices || 100)
for (const invoiceEntry of paidInvoices.invoices) {
if (invoiceEntry.state !== Invoice_InvoiceState.SETTLED) {
continue
}
const assetOp = await this.GetInvoiceAssetOperation(invoiceEntry)
invoices.push(assetOp)
}
const latestLndTransactions = await this.lnd.GetTransactions(info.blockHeight)
const txOuts: Types.AssetOperation[] = []
const txIns: Types.AssetOperation[] = []
for (const transaction of latestLndTransactions.transactions) {
for (const output of transaction.outputDetails) {
if (output.isOurAddress) {
const assetOp = await this.GetTxOutAssetOperation(transaction, output)
txOuts.push(assetOp)
}
}
// we only produce TXs with a single output
const input = transaction.previousOutpoints.find(p => p.isOurOutput)
if (input) {
const assetOp = await this.GetTxInAssetOperation(transaction)
txIns.push(assetOp)
}
}
const balance = await this.lnd.GetBalance()
const channelsBalance = balance.channelsBalance.reduce((acc, c) => acc + Number(c.localBalanceSats), 0)
return {
pubkey: provider.provider_pubkey,
tracked: {
confirmed_balance: Number(balance.confirmedBalance),
unconfirmed_balance: Number(balance.unconfirmedBalance),
channels_balance: channelsBalance,
payments,
invoices,
incoming_tx: txOuts, // tx outputs, are incoming sats
outgoing_tx: txIns, // tx inputs, are outgoing sats
}
}
}
async GetPaymentAssetOperation(payment: Payment): Promise<Types.AssetOperation> {
const invoice = payment.paymentRequest
const userInvoice = await this.storage.paymentStorage.GetPaymentOwner(invoice)
const ts = Number(payment.creationTimeNs / (BigInt(1000_000_000)))
const amount = Number(payment.valueSat)
if (userInvoice) {
const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP }
return { ts, amount, tracked }
}
const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice_payment", invoice)
if (rootOp) {
const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP }
return { ts, amount, tracked }
}
return { ts, amount, tracked: undefined }
}
async GetInvoiceAssetOperation(invoiceEntry: Invoice): Promise<Types.AssetOperation> {
const invoice = invoiceEntry.paymentRequest
const ts = Number(invoiceEntry.settleDate)
const amount = Number(invoiceEntry.amtPaidSat)
const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(invoice)
if (userInvoice) {
const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP }
return { ts, amount, tracked }
}
const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice", invoice)
if (rootOp) {
const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP }
return { ts, amount, tracked }
}
return { ts, amount, tracked: undefined }
}
async GetTxInAssetOperation(tx: Transaction): Promise<Types.AssetOperation> {
const ts = Number(tx.timeStamp)
const amount = Number(tx.amount)
const userOp = await this.storage.paymentStorage.GetTxHashPaymentOwner(tx.txHash)
if (userOp) {
// user transaction payments are actually deprecated from lnd, but we keep this for consstency
const tracked: Types.TrackedOperation = { ts: userOp.paid_at_unix, amount: userOp.paid_amount, type: USER_OP }
return { ts, amount, tracked }
}
const rootOp = await this.storage.metricsStorage.GetRootOperation("chain_payment", tx.txHash)
if (rootOp) {
const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP }
return { ts, amount, tracked }
}
return { ts, amount, tracked: undefined }
}
async GetTxOutAssetOperation(tx: Transaction, output: OutputDetail): Promise<Types.AssetOperation> {
const ts = Number(tx.timeStamp)
const amount = Number(output.amount)
const outputIndex = Number(output.outputIndex)
const userOp = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner(output.address, tx.txHash)
if (userOp) {
const tracked: Types.TrackedOperation = { ts: userOp.paid_at_unix, amount: userOp.paid_amount, type: USER_OP }
return { ts, amount, tracked }
}
const rootOp = await this.storage.metricsStorage.GetRootAddressTransaction(output.address, tx.txHash, outputIndex)
if (rootOp) {
const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP }
return { ts, amount, tracked }
}
return { ts, amount, tracked: undefined }
}
async BumpTx(req: Types.BumpTx): Promise<void> {
await this.lnd.BumpFee(req.txid, req.output_index, req.sat_per_vbyte)
}
}
const getDataPath = (dataDir: string, dataPath: string) => {

View file

@ -66,7 +66,7 @@ export default class {
const appUser = await this.storage.applicationStorage.GetAppUserFromUser(app, user.user_id)
if (!appUser) {
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
throw new Error("app user not found")
}
const nostrSettings = this.settings.getSettings().nostrRelaySettings
const { max, serviceFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats)
@ -82,7 +82,8 @@ export default class {
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] }),
callback_url: appUser.callback_url,
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl,
topic_id: appUser.topic_id
}
}
@ -131,12 +132,12 @@ export default class {
}
this.log("Found", toDelete.length, "inactive users to delete")
// await this.RemoveUsers(toDelete)
await this.LockUsers(toDelete.map(u => u.userId))
}
async CleanupNeverActiveUsers() {
this.log("Cleaning up never active users")
const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(30)
const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(90)
const toDelete: { userId: string, appUserIds: string[] }[] = []
for (const u of inactiveUsers) {
const user = await this.storage.userStorage.GetUser(u.user_id)
@ -160,13 +161,26 @@ export default class {
}
this.log("Found", toDelete.length, "never active users to delete")
// await this.RemoveUsers(toDelete) TODO: activate deletion
await this.RemoveUsers(toDelete)
}
async LockUsers(toLock: string[]) {
this.log("Locking", toLock.length, "users")
for (const userId of toLock) {
await this.storage.userStorage.BanUser(userId)
}
this.log("Locked users")
}
async RemoveUsers(toDelete: { userId: string, appUserIds: string[] }[]) {
this.log("Deleting", toDelete.length, "inactive users")
for (let i = 0; i < toDelete.length; i++) {
const { userId, appUserIds } = toDelete[i]
const user = await this.storage.userStorage.FindUser(userId)
if (!user || user.balance_sats > 0) {
if (user) this.log("Skipping user", userId, "has balance", user.balance_sats)
continue
}
this.log("Deleting user", userId, "progress", i + 1, "/", toDelete.length)
await this.storage.StartTransaction(async tx => {
for (const appUserId of appUserIds) {
@ -174,13 +188,18 @@ export default class {
await this.storage.offerStorage.DeleteUserOffers(appUserId, tx)
await this.storage.debitStorage.RemoveUserDebitAccess(appUserId, tx)
await this.storage.applicationStorage.RemoveAppUserDevices(appUserId, tx)
}
await this.storage.paymentStorage.RemoveUserInvoices(userId, tx)
await this.storage.productStorage.RemoveUserProducts(userId, tx)
await this.storage.paymentStorage.RemoveUserEphemeralKeys(userId, tx)
await this.storage.paymentStorage.RemoveUserInvoicePayments(userId, tx)
await this.storage.paymentStorage.RemoveUserTransactionPayments(userId, tx)
await this.storage.paymentStorage.RemoveUserToUserPayments(userId, tx)
await this.storage.paymentStorage.RemoveUserReceivingAddresses(userId, tx)
await this.storage.userStorage.DeleteUserAccess(userId, tx)
await this.storage.applicationStorage.RemoveAppUsersAndBaseUsers(appUserIds, userId, tx)
})
}
this.log("Cleaned up inactive users")
}
}
}

View file

@ -169,7 +169,8 @@ export default class {
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] }),
callback_url: u.callback_url,
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl,
topic_id: u.topic_id
},
max_withdrawable: max
@ -227,7 +228,8 @@ export default class {
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] }),
callback_url: user.callback_url,
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl,
topic_id: user.topic_id
},
}
}

View file

@ -72,6 +72,7 @@ export default class {
this.unlocker = unlocker
const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.getSettings().liquiditySettings.liquidityProviderPub, b)
this.liquidityProvider = new LiquidityProvider(() => this.settings.getSettings().liquiditySettings, this.utils, this.invoicePaidCb, updateProviderBalance)
adminManager.attachLiquidityProvider(this.liquidityProvider)
this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider)
const lndGetSettings = () => ({
lndSettings: settings.getSettings().lndSettings,
@ -161,19 +162,23 @@ export default class {
NewBlockHandler = async (height: number, skipMetrics?: boolean) => {
let confirmed: (PendingTx & { confs: number; })[]
let log = getLogger({})
log("NewBlockHandler called", JSON.stringify({ height, skipMetrics }))
this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height)
.catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err))
this.storage.paymentStorage.DeleteExpiredInvoiceSwaps(height)
.catch(err => log(ERROR, "failed to delete expired invoice swaps", err.message || err))
try {
const balanceEvents = await this.paymentManager.GetLndBalance()
if (!skipMetrics) {
await this.metricsManager.NewBlockCb(height, balanceEvents)
}
confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height)
confirmed = await this.paymentManager.CheckNewlyConfirmedTxs()
await this.liquidityManager.onNewBlock()
} catch (err: any) {
log(ERROR, "failed to check transactions after new block", err.message || err)
return
}
log("NewBlockHandler new confirmed transactions", confirmed.length)
await Promise.all(confirmed.map(async c => {
if (c.type === 'outgoing') {
await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs })
@ -208,6 +213,7 @@ export default class {
addressPaidCb: AddressPaidCb = (txOutput, address, amount, used, broadcastHeight) => {
return this.storage.StartTransaction(async tx => {
getLogger({})("addressPaidCb called", JSON.stringify({ txOutput, address, amount, used, broadcastHeight }))
// On-chain payments not supported when bypass is enabled
if (this.liquidityProvider.getSettings().useOnlyLiquidityProvider) {
getLogger({})("addressPaidCb called but USE_ONLY_LIQUIDITY_PROVIDER is enabled, ignoring")
@ -419,13 +425,36 @@ export default class {
if (devices.length === 0 || !app.nostr_public_key || !app.nostr_private_key || !appUser.nostr_public_key) {
return
}
const tokens = devices.map(d => d.firebase_messaging_token)
const ck = nip44.getConversationKey(Buffer.from(app.nostr_private_key, 'hex'), appUser.nostr_public_key)
const j = JSON.stringify(op)
let payloadToEncrypt: Types.PushNotificationPayload;
if (op.inbound) {
payloadToEncrypt = {
data: {
type: Types.PushNotificationPayload_data_type.RECEIVED_OPERATION,
received_operation: op
}
}
} else {
payloadToEncrypt = {
data: {
type: Types.PushNotificationPayload_data_type.SENT_OPERATION,
sent_operation: op
}
}
}
const j = JSON.stringify(payloadToEncrypt)
const encrypted = nip44.encrypt(j, ck)
const encryptedData: { encrypted: string, app_npub_hex: string } = { encrypted, app_npub_hex: app.nostr_public_key }
const envelope: Types.PushNotificationEnvelope = {
topic_id: appUser.topic_id,
app_npub_hex: app.nostr_public_key,
encrypted_payload: encrypted
}
const notification: ShockPushNotification = {
message: JSON.stringify(encryptedData),
message: JSON.stringify(envelope),
body,
title
}

View file

@ -11,7 +11,7 @@ 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"
import { Swaps } from "../lnd/swaps/swaps.js"
export type AppData = {
privateKey: string;
publicKey: string;
@ -79,6 +79,7 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM
await mainHandler.paymentManager.CleanupOldUnpaidInvoices()
await mainHandler.appUserManager.CleanupInactiveUsers()
await mainHandler.appUserManager.CleanupNeverActiveUsers()
await swaps.ResumeInvoiceSwaps()
await mainHandler.paymentManager.watchDog.Start()
return { mainHandler, apps, localProviderClient, wizard, adminManager }
}

View file

@ -277,14 +277,14 @@ export class LiquidityProvider {
return res
}
GetOperations = async () => {
GetOperations = async (max = 200) => {
if (!this.IsReady()) {
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 },
latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { ts: 0, id: 0 },
latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: 200
latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: max
})
if (res.status === 'ERROR') {
this.log("error getting operations", res.reason)

View file

@ -18,7 +18,7 @@ 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 { Swaps } from '../lnd/swaps/swaps.js'
import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js'
import { LndAddress } from '../lnd/lnd.js'
import Metrics from '../metrics/index.js'
@ -201,24 +201,11 @@ export default class {
} else {
log("no missed chain transactions found")
}
await this.reprocessStuckPendingTx(log, currentHeight)
} catch (err: any) {
log(ERROR, "failed to check for missed chain transactions:", err.message || err)
}
}
reprocessStuckPendingTx = async (log: PubLogger, currentHeight: number) => {
const { incoming } = await this.storage.paymentStorage.GetPendingTransactions()
const found = incoming.find(t => t.broadcast_height < currentHeight - 100)
if (found) {
log("found a possibly stuck pending transaction, reprocessing with full transaction history")
// There is a pending transaction more than 100 blocks old, this is likely a transaction
// that has a broadcast height higher than it actually is, so its not getting picked up when being processed
// by calling new block cb with height of 1, we make sure that even if the transaction has a newer height, it will still be processed
await this.newBlockCb(1, true)
}
}
private async getLatestTransactions(log: PubLogger): Promise<{ txs: Transaction[], currentHeight: number, lndPubkey: string, startHeight: number }> {
const lndInfo = await this.lnd.GetInfo()
const lndPubkey = lndInfo.identityPubkey
@ -273,14 +260,14 @@ export default class {
private async processRootAddressOutput(output: OutputDetail, tx: Transaction, addresses: LndAddress[], log: PubLogger): Promise<boolean> {
const addr = addresses.find(a => a.address === output.address)
if (!addr) {
throw new Error(`address ${output.address} not found in list of addresses`)
throw new Error(`root 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)
const existingRootOp = await this.storage.metricsStorage.GetRootAddressTransaction(output.address, tx.txHash, outputIndex)
if (existingRootOp) {
return false
}
@ -302,8 +289,11 @@ export default class {
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', startHeight)
.catch(err => log(ERROR, "failed to process user address output:", err.message || err))
try {
await this.addressPaidCb({ hash: tx.txHash, index: outputIndex }, output.address, amount, 'lnd', startHeight)
} catch (err: any) {
log(ERROR, "failed to process user address output:", err.message || err)
}
return true
}
@ -605,9 +595,15 @@ export default class {
async PayInternalAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
this.log("paying internal address")
let amount = req.amountSats
if (req.swap_operation_id) {
const swap = await this.storage.paymentStorage.GetTransactionSwap(req.swap_operation_id, ctx.app_user_id)
amount = amount > 0 ? amount : swap?.invoice_amount || 0
await this.storage.paymentStorage.DeleteTransactionSwap(req.swap_operation_id)
}
if (amount <= 0) {
throw new Error("invalid tx amount")
}
const { blockHeight } = await this.lnd.GetInfo()
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isManagedUser = ctx.user_id !== app.owner.user_id
@ -634,11 +630,13 @@ export default class {
}
}
async ListSwaps(ctx: Types.UserContext): Promise<Types.SwapsList> {
const payments = await this.storage.paymentStorage.ListSwapPayments(ctx.app_user_id)
async ListTxSwaps(ctx: Types.UserContext): Promise<Types.TxSwapsList> {
console.log("listing tx swaps", { appUserId: ctx.app_user_id })
const payments = await this.storage.paymentStorage.ListTxSwapPayments(ctx.app_user_id)
console.log("payments", payments.length)
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 => {
return this.swaps.ListTxSwaps(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))
@ -973,29 +971,38 @@ export default class {
return { amount: payment.paid_amount, fees: payment.service_fees }
}
async CheckNewlyConfirmedTxs(height: number) {
const pending = await this.storage.paymentStorage.GetPendingTransactions()
let lowestHeight = height
const map: Record<string, PendingTx> = {}
const checkTx = (t: PendingTx) => {
if (t.tx.broadcast_height < lowestHeight) { lowestHeight = t.tx.broadcast_height }
map[t.tx.tx_hash] = t
}
pending.incoming.forEach(t => checkTx({ type: "incoming", tx: t }))
pending.outgoing.forEach(t => checkTx({ type: "outgoing", tx: t }))
const { transactions } = await this.lnd.GetTransactions(lowestHeight)
const newlyConfirmedTxs = transactions.map(tx => {
const { txHash, numConfirmations: confs, amount: amt } = tx
const t = map[txHash]
if (!t || confs === 0) {
return
}
private async getTxConfs(txHash: string): Promise<number> {
try {
const info = await this.lnd.GetTx(txHash)
const { numConfirmations: confs, amount: amt } = info
if (confs > 2 || (amt <= confInTwo && confs > 1) || (amt <= confInOne && confs > 0)) {
return { ...t, confs }
return confs
}
})
return newlyConfirmedTxs.filter(t => t !== undefined) as (PendingTx & { confs: number })[]
} catch (err: any) {
getLogger({})("failed to get tx info", err.message || err)
}
return 0
}
async CheckNewlyConfirmedTxs() {
const pending = await this.storage.paymentStorage.GetPendingTransactions()
let log = getLogger({})
log("CheckNewlyConfirmedTxs ", pending.incoming.length, "incoming", pending.outgoing.length, "outgoing")
const confirmedIncoming: (PendingTx & { confs: number })[] = []
const confirmedOutgoing: (PendingTx & { confs: number })[] = []
for (const tx of pending.incoming) {
const confs = await this.getTxConfs(tx.tx_hash)
if (confs > 0) {
confirmedIncoming.push({ type: "incoming", tx: tx, confs })
}
}
for (const tx of pending.outgoing) {
const confs = await this.getTxConfs(tx.tx_hash)
if (confs > 0) {
confirmedOutgoing.push({ type: "outgoing", tx: tx, confs })
}
}
return confirmedIncoming.concat(confirmedOutgoing)
}
async CleanupOldUnpaidInvoices() {

View file

@ -226,7 +226,7 @@ export default class SanityChecker {
async VerifyEventsLog() {
this.events = await this.storage.eventsLog.GetAllLogs()
this.invoices = (await this.lnd.GetAllPaidInvoices(1000)).invoices
this.invoices = (await this.lnd.GetAllInvoices(1000)).invoices
this.payments = (await this.lnd.GetAllPayments(1000)).payments
this.incrementSources = {}

View file

@ -29,6 +29,7 @@ export class Watchdog {
ready = false
interval: NodeJS.Timer;
lndPubKey: string;
lastHandlerRootOpsAtUnix = 0
constructor(settings: SettingsManager, liquidityManager: LiquidityManager, lnd: LND, storage: Storage, utils: Utils, rugPullTracker: RugPullTracker) {
this.lnd = lnd;
this.settings = settings;
@ -67,7 +68,7 @@ export class Watchdog {
await this.getTracker()
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance()
const { totalExternal } = await this.getAggregatedExternalBalance()
this.initialLndBalance = totalExternal
this.initialUsersBalance = totalUsersBalance
const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix)
@ -76,8 +77,6 @@ export class Watchdog {
const paymentFound = await this.storage.paymentStorage.GetMaxPaymentIndex()
const knownMaxIndex = paymentFound.length > 0 ? Math.max(paymentFound[0].paymentIndex, 0) : 0
this.latestPaymentIndexOffset = await this.lnd.GetLatestPaymentIndex(knownMaxIndex)
const other = { ilnd: this.initialLndBalance, hf: this.accumulatedHtlcFees, iu: this.initialUsersBalance, tu: totalUsersBalance, oext: otherExternal }
//getLogger({ component: 'watchdog_debug2' })(JSON.stringify({ deltaLnd: 0, deltaUsers: 0, totalExternal, latestIndex: this.latestPaymentIndexOffset, other }))
this.interval = setInterval(() => {
if (this.latestCheckStart + (1000 * 58) < Date.now()) {
this.PaymentRequested()
@ -93,7 +92,49 @@ export class Watchdog {
fwEvents.forwardingEvents.forEach((event) => {
this.accumulatedHtlcFees += Number(event.fee)
})
}
handleRootOperations = async () => {
let pendingChange = 0
const pendingChainPayments = await this.storage.metricsStorage.GetPendingChainPayments()
for (const payment of pendingChainPayments) {
try {
const tx = await this.lnd.GetTx(payment.operation_identifier)
if (tx.numConfirmations > 0) {
await this.storage.metricsStorage.SetRootOpConfirmed(payment.serial_id)
continue
}
tx.outputDetails.forEach(o => pendingChange += o.isOurAddress ? Number(o.amount) : 0)
} catch (err: any) {
this.log("Error getting tx for root operation", err.message || err)
}
}
let newReceived = 0
let newSpent = 0
if (this.lastHandlerRootOpsAtUnix === 0) {
this.lastHandlerRootOpsAtUnix = Math.floor(Date.now() / 1000)
return { newReceived, newSpent, pendingChange }
}
const newOps = await this.storage.metricsStorage.GetRootOperations({ from: this.lastHandlerRootOpsAtUnix })
newOps.forEach(o => {
switch (o.operation_type) {
case 'chain_payment':
newSpent += Number(o.operation_amount)
break
case 'invoice_payment':
newSpent += Number(o.operation_amount)
break
case 'chain':
newReceived += Number(o.operation_amount)
break
case 'invoice':
newReceived += Number(o.operation_amount)
break
}
})
return { newReceived, newSpent, pendingChange }
}
getAggregatedExternalBalance = async () => {
@ -101,8 +142,9 @@ export class Watchdog {
const feesPaidForLiquidity = this.liquidityManager.GetPaidFees()
const pb = await this.rugPullTracker.CheckProviderBalance()
const providerBalance = pb.prevBalance || pb.balance
const otherExternal = { pb: providerBalance, f: feesPaidForLiquidity, lnd: totalLndBalance, olnd: othersFromLnd }
return { totalExternal: totalLndBalance + providerBalance + feesPaidForLiquidity, otherExternal }
const { newReceived, newSpent, pendingChange } = await this.handleRootOperations()
const opsTotal = newReceived + pendingChange - newSpent
return { totalExternal: totalLndBalance + providerBalance + feesPaidForLiquidity + opsTotal }
}
checkBalanceUpdate = async (deltaLnd: number, deltaUsers: number) => {
@ -187,7 +229,7 @@ export class Watchdog {
}
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance()
const { totalExternal } = await this.getAggregatedExternalBalance()
this.utils.stateBundler.AddBalancePoint('accumulatedHtlcFees', this.accumulatedHtlcFees)
const deltaLnd = totalExternal - (this.initialLndBalance + this.accumulatedHtlcFees)
const deltaUsers = totalUsersBalance - this.initialUsersBalance
@ -200,8 +242,6 @@ export class Watchdog {
this.log("Payment index advanced from", knownMaxIndex, "to", newLatest, "- updating offset (likely LND restart or external payment)")
this.latestPaymentIndexOffset = newLatest
}
const other = { ilnd: this.initialLndBalance, hf: this.accumulatedHtlcFees, iu: this.initialUsersBalance, tu: totalUsersBalance, km: knownMaxIndex, nl: newLatest, oext: otherExternal }
//getLogger({ component: 'watchdog_debug2' })(JSON.stringify({ deltaLnd, deltaUsers, totalExternal, other }))
const deny = await this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (deny) {
if (historyMismatch) {

View file

@ -414,9 +414,7 @@ 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)
@ -425,8 +423,13 @@ export default class Handler {
const mapRootOpType = (opType: string): Types.OperationType => {
switch (opType) {
case "chain": return Types.OperationType.CHAIN_OP
case "invoice": return Types.OperationType.INVOICE_OP
case "chain_payment":
case "chain":
return Types.OperationType.CHAIN_OP
case "invoice_payment":
case "invoice":
return Types.OperationType.INVOICE_OP
default: throw new Error("Unknown operation type")
}
}

View file

@ -11,28 +11,69 @@ export default class NostrSubprocess {
utils: Utils
awaitingPongs: (() => void)[] = []
log = getLogger({})
latestRestart = 0
private settings: NostrSettings
private eventCallback: EventCallback
private beaconCallback: BeaconCallback
private isShuttingDown = false
constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) {
this.utils = utils
this.settings = settings
this.eventCallback = eventCallback
this.beaconCallback = beaconCallback
this.startSubProcess()
}
private cleanupProcess() {
if (this.childProcess) {
this.childProcess.removeAllListeners()
if (!this.childProcess.killed) {
this.childProcess.kill('SIGTERM')
}
}
}
private startSubProcess() {
this.cleanupProcess()
this.childProcess = fork("./build/src/services/nostr/handler")
this.childProcess.on("error", (error) => {
this.log(ERROR, "nostr subprocess error", error)
})
this.childProcess.on("exit", (code) => {
this.log(ERROR, `nostr subprocess exited with code ${code}`)
if (!code) {
this.childProcess.on("exit", (code, signal) => {
if (this.isShuttingDown) {
this.log("nostr subprocess stopped")
return
}
throw new Error(`nostr subprocess exited with code ${code}`)
if (code === 0) {
this.log("nostr subprocess exited cleanly")
return
}
this.log(ERROR, `nostr subprocess exited with code ${code} and signal ${signal}`)
const now = Date.now()
if (now - this.latestRestart < 5000) {
this.log(ERROR, "nostr subprocess exited too quickly, not restarting")
throw new Error("nostr subprocess crashed repeatedly")
}
this.log("restarting nostr subprocess...")
this.latestRestart = now
setTimeout(() => this.startSubProcess(), 100)
})
this.childProcess.on("message", (message: ChildProcessResponse) => {
switch (message.type) {
case 'ready':
this.sendToChildProcess({ type: 'settings', settings: settings })
this.sendToChildProcess({ type: 'settings', settings: this.settings })
break;
case 'event':
eventCallback(message.event)
this.eventCallback(message.event)
break
case 'processMetrics':
this.utils.tlvStorageFactory.ProcessMetrics(message.metrics, 'nostr')
@ -42,7 +83,7 @@ export default class NostrSubprocess {
this.awaitingPongs = []
break
case 'beacon':
beaconCallback({ content: message.content, pub: message.pub })
this.beaconCallback({ content: message.content, pub: message.pub })
break
default:
console.error("unknown nostr event response", message)
@ -50,11 +91,15 @@ export default class NostrSubprocess {
}
})
}
sendToChildProcess(message: ChildProcessRequest) {
this.childProcess.send(message)
if (this.childProcess && !this.childProcess.killed) {
this.childProcess.send(message)
}
}
Reset(settings: NostrSettings) {
this.settings = settings
this.sendToChildProcess({ type: 'settings', settings })
}
@ -68,7 +113,9 @@ export default class NostrSubprocess {
Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
this.sendToChildProcess({ type: 'send', data, initiator, relays })
}
Stop() {
this.childProcess.kill()
this.isShuttingDown = true
this.cleanupProcess()
}
}

View file

@ -91,6 +91,14 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.CloseChannel(req)
},
BumpTx: async ({ ctx, req }) => {
const err = Types.BumpTxValidate(req, {
txid_CustomCheck: txid => txid !== '',
sat_per_vbyte_CustomCheck: spv => spv > 0
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.BumpTx(req)
},
GetAdminTransactionSwapQuotes: async ({ ctx, req }) => {
const err = Types.TransactionSwapRequestValidate(req, {
transaction_amount_sats_CustomCheck: amt => amt > 0
@ -106,6 +114,36 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.PayAdminTransactionSwap(req)
},
ListAdminTxSwaps: async ({ ctx }) => {
return mainHandler.adminManager.ListAdminTxSwaps()
},
GetAdminInvoiceSwapQuotes: async ({ ctx, req }) => {
const err = Types.InvoiceSwapRequestValidate(req, {
amount_sats_CustomCheck: amt => amt > 0
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.GetAdminInvoiceSwapQuotes(req)
},
RefundAdminInvoiceSwap: async ({ ctx, req }) => {
const err = Types.RefundAdminInvoiceSwapRequestValidate(req, {
swap_operation_id_CustomCheck: id => id !== '',
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.RefundAdminInvoiceSwap(req)
},
ListAdminInvoiceSwaps: async ({ ctx }) => {
return mainHandler.adminManager.ListAdminInvoiceSwaps()
},
PayAdminInvoiceSwap: async ({ ctx, req }) => {
const err = Types.PayAdminInvoiceSwapRequestValidate(req, {
swap_operation_id_CustomCheck: id => id !== '',
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.PayAdminInvoiceSwap(req)
},
GetAssetsAndLiabilities: async ({ ctx, req }) => {
return mainHandler.adminManager.GetAssetsAndLiabilities(req)
},
GetProvidersDisruption: async () => {
return mainHandler.metricsManager.GetProvidersDisruption()
},
@ -145,9 +183,7 @@ 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 !== ""
@ -159,14 +195,14 @@ export default (mainHandler: Main): Types.ServerMethods => {
PayAddress: async ({ ctx, req }) => {
const err = Types.PayAddressRequestValidate(req, {
address_CustomCheck: addr => addr !== '',
amountSats_CustomCheck: amt => amt > 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)
ListTxSwaps: async ({ ctx }) => {
return mainHandler.paymentManager.ListTxSwaps(ctx)
},
GetTransactionSwapQuotes: async ({ ctx, req }) => {
return mainHandler.paymentManager.GetTransactionSwapQuotes(ctx, req)

View file

@ -1,5 +1,5 @@
import crypto from 'crypto';
import { Between, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual, In } from "typeorm"
import { Between, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm"
import { generateSecretKey, getPublicKey } from 'nostr-tools';
import { Application } from "./entity/Application.js"
import UserStorage from './userStorage.js';
@ -72,7 +72,8 @@ export default class {
user: user,
application,
identifier: userIdentifier,
nostr_public_key: nostrPub
nostr_public_key: nostrPub,
topic_id: crypto.randomBytes(32).toString('hex')
}, txId)
})
}
@ -160,10 +161,16 @@ export default class {
this.dbs.Remove<User>('User', baseUser, txId)
}
async RemoveAppUsersAndBaseUsers(appUserIds: string[],baseUser:string, txId?: string) {
await this.dbs.Delete<ApplicationUser>('ApplicationUser', { identifier: In(appUserIds) }, txId)
await this.dbs.Delete<User>('User', { user_id: baseUser }, txId)
async RemoveAppUsersAndBaseUsers(appUserIds: string[], baseUser: string, txId?: string) {
for (const appUserId of appUserIds) {
const appUser = await this.dbs.FindOne<ApplicationUser>('ApplicationUser', { where: { identifier: appUserId } }, txId)
if (appUser) {
await this.dbs.Delete<ApplicationUser>('ApplicationUser', appUser.serial_id, txId)
}
}
const user = await this.userStorage.FindUser(baseUser, txId)
if (!user) return
await this.dbs.Delete<User>('User', user.serial_id, txId)
}

View file

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

View file

@ -8,10 +8,19 @@ type SerializedFindOperator = {
}
export function serializeFindOperator(operator: FindOperator<any>): SerializedFindOperator {
let value: any;
if (Array.isArray(operator['value']) && operator['type'] !== 'between') {
value = operator['value'].map(serializeFindOperator);
} else if ((operator as any).child !== undefined) {
// Not(IsNull()) etc.: TypeORM's .value getter unwraps nested FindOperators, so we'd lose the inner operator. Use .child to serialize the nested operator.
value = serializeFindOperator((operator as any).child);
} else {
value = operator['value'];
}
return {
_type: 'FindOperator',
type: operator['type'],
value: (Array.isArray(operator['value']) && operator['type'] !== 'between') ? operator["value"].map(serializeFindOperator) : operator["value"],
value,
};
}
@ -51,7 +60,8 @@ export function deserializeFindOperator(serialized: SerializedFindOperator): Fin
}
}
export function serializeRequest<T>(r: object): T {
export function serializeRequest<T>(r: object, debug = false): T {
if (debug) console.log("serializeRequest", r)
if (!r || typeof r !== 'object') {
return r;
}
@ -61,23 +71,24 @@ export function serializeRequest<T>(r: object): T {
}
if (Array.isArray(r)) {
return r.map(item => serializeRequest(item)) as any;
return r.map(item => serializeRequest(item, debug)) as any;
}
const result: any = {};
for (const [key, value] of Object.entries(r)) {
result[key] = serializeRequest(value);
result[key] = serializeRequest(value, debug);
}
return result;
}
export function deserializeRequest<T>(r: object): T {
export function deserializeRequest<T>(r: object, debug = false): T {
if (debug) console.log("deserializeRequest", r)
if (!r || typeof r !== 'object') {
return r;
}
if (Array.isArray(r)) {
return r.map(item => deserializeRequest(item)) as any;
return r.map(item => deserializeRequest(item, debug)) as any;
}
if (r && typeof r === 'object' && (r as any)._type === 'FindOperator') {
@ -86,7 +97,7 @@ export function deserializeRequest<T>(r: object): T {
const result: any = {};
for (const [key, value] of Object.entries(r)) {
result[key] = deserializeRequest(value);
result[key] = deserializeRequest(value, debug);
}
return result;
}

View file

@ -29,7 +29,7 @@ export class StorageInterface extends EventEmitter {
private debug: boolean = false;
private utils: Utils
private dbType: 'main' | 'metrics'
private log = getLogger({component: 'StorageInterface'})
private log = getLogger({ component: 'StorageInterface' })
constructor(utils: Utils) {
super();
this.initializeSubprocess();
@ -61,13 +61,13 @@ export class StorageInterface extends EventEmitter {
this.isConnected = false;
});
this.process.on('exit', (code: number) => {
this.log(ERROR, `Storage processor exited with code ${code}`);
this.process.on('exit', (code: number, signal: string) => {
this.log(ERROR, `Storage processor exited with code ${code} and signal ${signal}`);
this.isConnected = false;
if (!code) {
if (code === 0) {
return
}
throw new Error(`Storage processor exited with code ${code}`)
throw new Error(`Storage processor exited with code ${code} and signal ${signal}`)
});
this.isConnected = true;
@ -104,9 +104,10 @@ export class StorageInterface extends EventEmitter {
return this.handleOp<T | null>(findOp)
}
Find<T>(entity: DBNames, q: QueryOptions<T>, txId?: string): Promise<T[]> {
Find<T>(entity: DBNames, q: QueryOptions<T>, txId?: string, debug = false): Promise<T[]> {
if (debug) console.log("Find", { entity })
const opId = Math.random().toString()
const findOp: FindOperation<T> = { type: 'find', entity, opId, q, txId }
const findOp: FindOperation<T> = { type: 'find', entity, opId, q, txId, debug }
return this.handleOp<T[]>(findOp)
}
@ -166,32 +167,33 @@ export class StorageInterface extends EventEmitter {
}
private handleOp<T>(op: IStorageOperation): Promise<T> {
if (this.debug) console.log('handleOp', op)
if (this.debug || op.debug) console.log('handleOp', op)
this.checkConnected()
return new Promise<T>((resolve, reject) => {
const responseHandler = (response: OperationResponse<T>) => {
if (this.debug) console.log('responseHandler', response)
if (this.debug || op.debug) console.log('responseHandler', response)
if (!response.success) {
reject(new Error(response.error));
return
}
if (this.debug || op.debug) console.log("response", response, op)
if (response.type !== op.type) {
reject(new Error('Invalid storage response type'));
return
}
resolve(deserializeResponseData(response.data));
resolve(deserializeResponseData(response.data));
}
this.once(op.opId, responseHandler)
this.process.send(this.serializeOperation(op))
})
}
private serializeOperation(operation: IStorageOperation): IStorageOperation {
private serializeOperation(operation: IStorageOperation, debug = false): IStorageOperation {
const serialized = { ...operation };
if ('q' in serialized) {
(serialized as any).q = serializeRequest((serialized as any).q);
(serialized as any).q = serializeRequest((serialized as any).q, debug);
}
if (this.debug) {
if (this.debug || debug) {
serialized.debug = true
}
return serialized;
@ -205,7 +207,7 @@ export class StorageInterface extends EventEmitter {
public disconnect() {
if (this.process) {
this.process.kill();
this.process.kill(0);
this.isConnected = false;
this.debug = false;
}

View file

@ -26,6 +26,9 @@ export class ApplicationUser {
@Column({ default: "" })
callback_url: string
@Column({ unique: true })
topic_id: string;
@CreateDateColumn()
created_at: Date

View file

@ -0,0 +1,94 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, UpdateDateColumn } from "typeorm";
import { User } from "./User";
@Entity()
export class InvoiceSwap {
@PrimaryGeneratedColumn('uuid')
swap_operation_id: string
@Column()
app_user_id: string
@Column()
swap_quote_id: string
@Column()
swap_tree: string
@Column()
claim_public_key: string
@Column()
payment_hash: 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()
ephemeral_public_key: string
@Column()
address: 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: 0 })
completed_at_unix: number
@Column({ default: 0 })
paid_at_unix: number
@Column({ default: "" })
preimage: string
@Column({ default: "" })
failure_reason: string
@Column({ default: "" })
tx_id: string
@Column({ default: "", type: "text" })
lockup_tx_hex: string
/* @Column({ default: "" })
address_paid: string */
@Column({ default: "" })
service_url: string
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -17,6 +17,9 @@ export class RootOperation {
@Column({ default: 0 })
at_unix: number
@Column({ default: false })
pending: boolean
@CreateDateColumn()
created_at: Date

View file

@ -60,6 +60,12 @@ export class TransactionSwap {
@Column({ default: "" })
tx_id: string
@Column({ default: 0 })
completed_at_unix: number
@Column({ default: 0 })
paid_at_unix: number
@Column({ default: "" })
address_paid: string

View file

@ -10,6 +10,7 @@ import { StorageInterface } from "./db/storageInterface.js";
import { Utils } from "../helpers/utilsWrapper.js";
import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning.js";
import { ChannelEvent } from "./entity/ChannelEvent.js";
export type RootOperationType = 'chain' | 'invoice' | 'chain_payment' | 'invoice_payment'
export default class {
//DB: DataSource | EntityManager
settings: StorageSettings
@ -145,13 +146,27 @@ export default class {
}
}
async AddRootOperation(opType: string, id: string, amount: number, txId?: string) {
return this.dbs.CreateAndSave<RootOperation>('RootOperation', { operation_type: opType, operation_amount: amount, operation_identifier: id, at_unix: Math.floor(Date.now() / 1000) }, txId)
async AddRootOperation(opType: RootOperationType, id: string, amount: number, pending = false, dbTxId?: string) {
return this.dbs.CreateAndSave<RootOperation>('RootOperation', {
operation_type: opType, operation_amount: amount,
operation_identifier: id, at_unix: Math.floor(Date.now() / 1000), pending
}, dbTxId)
}
async GetRootOperation(opType: string, id: string, txId?: string) {
async GetRootOperation(opType: RootOperationType, id: string, txId?: string) {
return this.dbs.FindOne<RootOperation>('RootOperation', { where: { operation_type: opType, operation_identifier: id } }, txId)
}
async GetRootAddressTransaction(address: string, txHash: string, index: number) {
return this.GetRootOperation("chain", `${address}:${txHash}:${index}`)
}
async GetPendingChainPayments() {
return this.dbs.Find<RootOperation>('RootOperation', { where: { operation_type: 'chain_payment', pending: true } })
}
async SetRootOpConfirmed(serialId: number) {
return this.dbs.Update<RootOperation>('RootOperation', serialId, { pending: false })
}
async GetRootOperations({ from, to }: { from?: number, to?: number }, txId?: string) {
const q = getTimeQuery({ from, to })

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class InvoiceSwaps1769529793283 implements MigrationInterface {
name = 'InvoiceSwaps1769529793283'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "invoice_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, "claim_public_key" varchar NOT NULL, "payment_hash" 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, "ephemeral_public_key" varchar NOT NULL, "address" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "preimage" varchar NOT NULL DEFAULT (''), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "invoice_swap"`);
}
}

View file

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class InvoiceSwapsFixes1769805357459 implements MigrationInterface {
name = 'InvoiceSwapsFixes1769805357459'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "temporary_invoice_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, "claim_public_key" varchar NOT NULL, "payment_hash" 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, "ephemeral_public_key" varchar NOT NULL, "address" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "preimage" varchar NOT NULL DEFAULT (''), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "lockup_tx_hex" text NOT NULL DEFAULT (''))`);
await queryRunner.query(`INSERT INTO "temporary_invoice_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at" FROM "invoice_swap"`);
await queryRunner.query(`DROP TABLE "invoice_swap"`);
await queryRunner.query(`ALTER TABLE "temporary_invoice_swap" RENAME TO "invoice_swap"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "invoice_swap" RENAME TO "temporary_invoice_swap"`);
await queryRunner.query(`CREATE TABLE "invoice_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, "claim_public_key" varchar NOT NULL, "payment_hash" 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, "ephemeral_public_key" varchar NOT NULL, "address" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "preimage" varchar NOT NULL DEFAULT (''), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "service_url" 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 "invoice_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at" FROM "temporary_invoice_swap"`);
await queryRunner.query(`DROP TABLE "temporary_invoice_swap"`);
}
}

View file

@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ApplicationUserTopicId1770038768784 implements MigrationInterface {
name = 'ApplicationUserTopicId1770038768784'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_0a0dbb25a73306b037dec82251"`);
await queryRunner.query(`CREATE TABLE "temporary_application_user" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "identifier" varchar NOT NULL, "nostr_public_key" varchar, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "applicationSerialId" integer, "callback_url" varchar NOT NULL DEFAULT (''), "topic_id" varchar NOT NULL, CONSTRAINT "UQ_3175dc397c8285d1e532554dea5" UNIQUE ("nostr_public_key"), CONSTRAINT "REL_0796a381bcc624f52e9a155712" UNIQUE ("userSerialId"), CONSTRAINT "UQ_bd1a42f39fd7b4218bed5cc63d9" UNIQUE ("topic_id"), CONSTRAINT "FK_0796a381bcc624f52e9a155712b" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_1b3bdb6f660cd99533a1e673ef1" FOREIGN KEY ("applicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_application_user"("serial_id", "identifier", "nostr_public_key", "created_at", "updated_at", "userSerialId", "applicationSerialId", "callback_url", "topic_id") SELECT "serial_id", "identifier", "nostr_public_key", "created_at", "updated_at", "userSerialId", "applicationSerialId", "callback_url", lower(hex(randomblob(32))) FROM "application_user"`);
await queryRunner.query(`DROP TABLE "application_user"`);
await queryRunner.query(`ALTER TABLE "temporary_application_user" RENAME TO "application_user"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0a0dbb25a73306b037dec82251" ON "application_user" ("identifier") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_0a0dbb25a73306b037dec82251"`);
await queryRunner.query(`ALTER TABLE "application_user" RENAME TO "temporary_application_user"`);
await queryRunner.query(`CREATE TABLE "application_user" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "identifier" varchar NOT NULL, "nostr_public_key" varchar, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "applicationSerialId" integer, "callback_url" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_3175dc397c8285d1e532554dea5" UNIQUE ("nostr_public_key"), CONSTRAINT "REL_0796a381bcc624f52e9a155712" UNIQUE ("userSerialId"), CONSTRAINT "FK_0796a381bcc624f52e9a155712b" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_1b3bdb6f660cd99533a1e673ef1" FOREIGN KEY ("applicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "application_user"("serial_id", "identifier", "nostr_public_key", "created_at", "updated_at", "userSerialId", "applicationSerialId", "callback_url") SELECT "serial_id", "identifier", "nostr_public_key", "created_at", "updated_at", "userSerialId", "applicationSerialId", "callback_url" FROM "temporary_application_user"`);
await queryRunner.query(`DROP TABLE "temporary_application_user"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0a0dbb25a73306b037dec82251" ON "application_user" ("identifier") `);
}
}

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class SwapTimestamps1771347307798 implements MigrationInterface {
name = 'SwapTimestamps1771347307798'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "temporary_invoice_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, "claim_public_key" varchar NOT NULL, "payment_hash" 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, "ephemeral_public_key" varchar NOT NULL, "address" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "preimage" varchar NOT NULL DEFAULT (''), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "lockup_tx_hex" text NOT NULL DEFAULT (''), "completed_at_unix" integer NOT NULL DEFAULT (0), "paid_at_unix" integer NOT NULL DEFAULT (0))`);
await queryRunner.query(`INSERT INTO "temporary_invoice_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at", "lockup_tx_hex") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at", "lockup_tx_hex" FROM "invoice_swap"`);
await queryRunner.query(`DROP TABLE "invoice_swap"`);
await queryRunner.query(`ALTER TABLE "temporary_invoice_swap" RENAME TO "invoice_swap"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "invoice_swap" RENAME TO "temporary_invoice_swap"`);
await queryRunner.query(`CREATE TABLE "invoice_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, "claim_public_key" varchar NOT NULL, "payment_hash" 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, "ephemeral_public_key" varchar NOT NULL, "address" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "preimage" varchar NOT NULL DEFAULT (''), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "lockup_tx_hex" text NOT NULL DEFAULT (''))`);
await queryRunner.query(`INSERT INTO "invoice_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at", "lockup_tx_hex") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at", "lockup_tx_hex" FROM "temporary_invoice_swap"`);
await queryRunner.query(`DROP TABLE "temporary_invoice_swap"`);
}
}

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class RootOpPending1771524665409 implements MigrationInterface {
name = 'RootOpPending1771524665409'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "temporary_root_operation" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "operation_type" varchar NOT NULL, "operation_amount" integer NOT NULL, "operation_identifier" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "at_unix" integer NOT NULL DEFAULT (0), "pending" boolean NOT NULL DEFAULT (0))`);
await queryRunner.query(`INSERT INTO "temporary_root_operation"("serial_id", "operation_type", "operation_amount", "operation_identifier", "created_at", "updated_at", "at_unix") SELECT "serial_id", "operation_type", "operation_amount", "operation_identifier", "created_at", "updated_at", "at_unix" FROM "root_operation"`);
await queryRunner.query(`DROP TABLE "root_operation"`);
await queryRunner.query(`ALTER TABLE "temporary_root_operation" RENAME TO "root_operation"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "root_operation" RENAME TO "temporary_root_operation"`);
await queryRunner.query(`CREATE TABLE "root_operation" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "operation_type" varchar NOT NULL, "operation_amount" integer NOT NULL, "operation_identifier" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "at_unix" integer NOT NULL DEFAULT (0))`);
await queryRunner.query(`INSERT INTO "root_operation"("serial_id", "operation_type", "operation_amount", "operation_identifier", "created_at", "updated_at", "at_unix") SELECT "serial_id", "operation_type", "operation_amount", "operation_identifier", "created_at", "updated_at", "at_unix" FROM "temporary_root_operation"`);
await queryRunner.query(`DROP TABLE "temporary_root_operation"`);
}
}

View file

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

View file

@ -1,28 +1,21 @@
import { Initial1703170309875 } from './1703170309875-initial.js'
import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js'
import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js'
import { LspOrder1718387847693 } from './1718387847693-lsp_order.js'
import { LiquidityProvider1719335699480 } from './1719335699480-liquidity_provider.js'
import { LndNodeInfo1720187506189 } from './1720187506189-lnd_node_info.js'
import { TrackedProvider1720814323679 } from './1720814323679-tracked_provider.js'
import { CreateInviteTokenTable1721751414878 } from "./1721751414878-create_invite_token_table.js"
import { PaymentIndex1721760297610 } from './1721760297610-payment_index.js'
import { HtlcCount1724266887195 } from './1724266887195-htlc_count.js'
import { BalanceEvents1724860966825 } from './1724860966825-balance_events.js'
import { DebitAccess1726496225078 } from './1726496225078-debit_access.js'
import { DebitAccessFixes1726685229264 } from './1726685229264-debit_access_fixes.js'
import { DebitToPub1727105758354 } from './1727105758354-debit_to_pub.js'
import { UserCbUrl1727112281043 } from './1727112281043-user_cb_url.js'
import { RootOps1732566440447 } from './1732566440447-root_ops.js'
import { UserOffer1733502626042 } from './1733502626042-user_offer.js'
import { RootOpsTime1745428134124 } from './1745428134124-root_ops_time.js'
import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js'
import { ManagementGrant1751307732346 } from './1751307732346-management_grant.js'
import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js'
import { InvoiceCallbackUrls1752425992291 } from './1752425992291-invoice_callback_urls.js'
import { AppUserDevice1753285173175 } from './1753285173175-app_user_device.js'
import { OldSomethingLeftover1753106599604 } from './1753106599604-old_something_leftover.js'
import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_receiving_invoice_idx.js'
import { AppUserDevice1753285173175 } from './1753285173175-app_user_device.js'
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'
@ -32,6 +25,23 @@ 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'
import { InvoiceSwaps1769529793283 } from './1769529793283-invoice_swaps.js'
import { InvoiceSwapsFixes1769805357459 } from './1769805357459-invoice_swaps_fixes.js'
import { ApplicationUserTopicId1770038768784 } from './1770038768784-application_user_topic_id.js'
import { SwapTimestamps1771347307798 } from './1771347307798-swap_timestamps.js'
import { TxSwapTimestamps1771878683383 } from './1771878683383-tx_swap_timestamps.js'
import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js'
import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js'
import { HtlcCount1724266887195 } from './1724266887195-htlc_count.js'
import { BalanceEvents1724860966825 } from './1724860966825-balance_events.js'
import { RootOps1732566440447 } from './1732566440447-root_ops.js'
import { RootOpsTime1745428134124 } from './1745428134124-root_ops_time.js'
import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js'
import { RootOpPending1771524665409 } from './1771524665409-root_op_pending.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
@ -39,9 +49,13 @@ export const allMigrations = [Initial1703170309875, LspOrder1718387847693, Liqui
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036]
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036,
InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798,
TxSwapTimestamps1771878683383]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825,
RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411, RootOpPending1771524665409]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations)
return false

View file

@ -15,6 +15,7 @@ import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.js';
import { TransactionSwap } from './entity/TransactionSwap.js';
import { InvoiceSwap } from './entity/InvoiceSwap.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string }
export const defaultInvoiceExpiry = 60 * 60
export default class {
@ -137,7 +138,15 @@ export default class {
}
async RemoveUserInvoices(userId: string, txId?: string) {
return this.dbs.Delete<UserReceivingInvoice>('UserReceivingInvoice', { user: { user_id: userId } }, txId)
const invoices = await this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', { where: { user: { user_id: userId } } }, txId)
if (invoices.length === 0) {
return 0
}
let deleted = 0
for (const invoice of invoices) {
deleted += await this.dbs.Delete<UserReceivingInvoice>('UserReceivingInvoice', invoice.serial_id, txId)
}
return deleted
}
async GetAddressOwner(address: string, txId?: string): Promise<UserReceivingAddress | null> {
@ -151,6 +160,10 @@ export default class {
return this.dbs.FindOne<UserTransactionPayment>('UserTransactionPayment', { where: { address, tx_hash: txHash } }, txId)
}
async GetTxHashPaymentOwner(txHash: string, txId?: string): Promise<UserTransactionPayment | null> {
return this.dbs.FindOne<UserTransactionPayment>('UserTransactionPayment', { where: { tx_hash: txHash } }, txId)
}
async GetInvoiceOwner(paymentRequest: string, txId?: string): Promise<UserReceivingInvoice | null> {
return this.dbs.FindOne<UserReceivingInvoice>('UserReceivingInvoice', { where: { invoice: paymentRequest } }, txId)
}
@ -317,7 +330,51 @@ export default class {
}
async RemoveUserEphemeralKeys(userId: string, txId?: string) {
return this.dbs.Delete<UserEphemeralKey>('UserEphemeralKey', { user: { user_id: userId } }, txId)
const keys = await this.dbs.Find<UserEphemeralKey>('UserEphemeralKey', { where: { user: { user_id: userId } } }, txId)
if (keys.length === 0) {
return 0
}
let deleted = 0
for (const key of keys) {
deleted += await this.dbs.Delete<UserEphemeralKey>('UserEphemeralKey', key.serial_id, txId)
}
return deleted
}
async RemoveUserReceivingAddresses(userId: string, txId?: string) {
const addresses = await this.dbs.Find<UserReceivingAddress>('UserReceivingAddress', { where: { user: { user_id: userId } } }, txId)
for (const addr of addresses) {
const txs = await this.dbs.Find<AddressReceivingTransaction>('AddressReceivingTransaction', { where: { user_address: { serial_id: addr.serial_id } } }, txId)
for (const tx of txs) {
await this.dbs.Delete<AddressReceivingTransaction>('AddressReceivingTransaction', tx.serial_id, txId)
}
await this.dbs.Delete<UserReceivingAddress>('UserReceivingAddress', addr.serial_id, txId)
}
}
async RemoveUserInvoicePayments(userId: string, txId?: string) {
const payments = await this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { user: { user_id: userId } } }, txId)
for (const p of payments) {
await this.dbs.Delete<UserInvoicePayment>('UserInvoicePayment', p.serial_id, txId)
}
}
async RemoveUserTransactionPayments(userId: string, txId?: string) {
const payments = await this.dbs.Find<UserTransactionPayment>('UserTransactionPayment', { where: { user: { user_id: userId } } }, txId)
for (const p of payments) {
await this.dbs.Delete<UserTransactionPayment>('UserTransactionPayment', p.serial_id, txId)
}
}
async RemoveUserToUserPayments(userId: string, txId?: string) {
const asSender = await this.dbs.Find<UserToUserPayment>('UserToUserPayment', { where: { from_user: { user_id: userId } } }, txId)
const asReceiver = await this.dbs.Find<UserToUserPayment>('UserToUserPayment', { where: { to_user: { user_id: userId } } }, txId)
const seen = new Set<number>()
for (const p of [...asSender, ...asReceiver]) {
if (seen.has(p.serial_id)) continue
seen.add(p.serial_id)
await this.dbs.Delete<UserToUserPayment>('UserToUserPayment', p.serial_id, txId)
}
}
async AddPendingUserToUserPayment(fromUserId: string, toUserId: string, amount: number, fee: number, linkedApplication: Application, txId: string) {
@ -447,8 +504,12 @@ export default class {
}
}
async GetTotalUsersBalance(txId?: string) {
const total = await this.dbs.Sum<User>('User', "balance_sats", {})
async GetTotalUsersBalance(excludeLocked?: boolean, txId?: string) {
const where: { locked?: boolean } = {}
if (excludeLocked) {
where.locked = false
}
const total = await this.dbs.Sum<User>('User', "balance_sats", where, txId)
return total || 0
}
@ -472,20 +533,31 @@ export default class {
return this.dbs.FindOne<TransactionSwap>('TransactionSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId)
}
async FinalizeTransactionSwap(swapOperationId: string, address: string, txId: string) {
async SetTransactionSwapPaid(swapOperationId: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
tx_id: txId,
address_paid: address,
})
paid_at_unix: now,
}, txId)
}
async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string) {
async FinalizeTransactionSwap(swapOperationId: string, address: string, chainTxId: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
tx_id: chainTxId,
address_paid: address,
completed_at_unix: now,
}, txId)
}
async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
failure_reason: failureReason,
address_paid: address,
})
completed_at_unix: now,
}, txId)
}
async DeleteTransactionSwap(swapOperationId: string, txId?: string) {
@ -493,18 +565,18 @@ export default class {
}
async DeleteExpiredTransactionSwaps(currentHeight: number, txId?: string) {
return this.dbs.Delete<TransactionSwap>('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId)
return this.dbs.Delete<TransactionSwap>('TransactionSwap', { timeout_block_height: LessThan(currentHeight), used: false }, txId)
}
async ListPendingTransactionSwaps(appUserId: string, txId?: string) {
return this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId)
}
async ListSwapPayments(userId: string, txId?: string) {
async ListTxSwapPayments(userId: string, txId?: string) {
return this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), user: { user_id: userId } } }, txId)
}
async ListCompletedSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) {
async ListCompletedTxSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) {
const completed = await this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: true, app_user_id: appUserId } }, txId)
// const payments = await this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), } }, txId)
const paymentsMap = new Map<string, UserInvoicePayment>()
@ -515,6 +587,85 @@ export default class {
swap: c, payment: paymentsMap.get(c.swap_operation_id)
}))
}
async AddInvoiceSwap(swap: Partial<InvoiceSwap>) {
return this.dbs.CreateAndSave<InvoiceSwap>('InvoiceSwap', swap)
}
async GetInvoiceSwap(swapOperationId: string, appUserId: string, txId?: string) {
const swap = await this.dbs.FindOne<InvoiceSwap>('InvoiceSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId)
if (!swap || swap.tx_id) {
return null
}
return swap
}
async FinalizeInvoiceSwap(swapOperationId: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, {
used: true,
completed_at_unix: now,
}, txId)
}
async UpdateInvoiceSwap(swapOperationId: string, update: Partial<InvoiceSwap>, txId?: string) {
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, update, txId)
}
async SetInvoiceSwapTxId(swapOperationId: string, chainTxId: string, chainFeeSats: number, lockupTxHex?: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
const update: Partial<InvoiceSwap> = {
tx_id: chainTxId,
paid_at_unix: now,
chain_fee_sats: chainFeeSats,
}
if (lockupTxHex) {
update.lockup_tx_hex = lockupTxHex
}
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, update, txId)
}
async FailInvoiceSwap(swapOperationId: string, failureReason: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, {
used: true,
failure_reason: failureReason,
completed_at_unix: now,
}, txId)
}
async DeleteInvoiceSwap(swapOperationId: string, txId?: string) {
return this.dbs.Delete<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, txId)
}
async DeleteExpiredInvoiceSwaps(currentHeight: number, txId?: string) {
return this.dbs.Delete<InvoiceSwap>('InvoiceSwap', { timeout_block_height: LessThan(currentHeight), used: false, tx_id: "" }, txId)
}
async ListCompletedInvoiceSwaps(appUserId: string, txId?: string) {
return this.dbs.Find<InvoiceSwap>('InvoiceSwap', { where: { used: true, app_user_id: appUserId } }, txId)
}
async ListPendingInvoiceSwaps(appUserId: string, txId?: string) {
return this.dbs.Find<InvoiceSwap>('InvoiceSwap', { where: { used: false, app_user_id: appUserId } }, txId)
}
async ListUnfinishedInvoiceSwaps(txId?: string) {
const swaps = await this.dbs.Find<InvoiceSwap>('InvoiceSwap', { where: { used: false } }, txId)
return swaps.filter(s => !!s.tx_id)
}
async GetRefundableInvoiceSwap(swapOperationId: string, txId?: string) {
const swap = await this.dbs.FindOne<InvoiceSwap>('InvoiceSwap', { where: { swap_operation_id: swapOperationId } }, txId)
if (!swap || !swap.tx_id) {
return null
}
if (swap.used && !swap.failure_reason) {
return null
}
return swap
}
}
const orFail = async <T>(resultPromise: Promise<T | null>) => {

View file

@ -21,6 +21,14 @@ export default class {
}
async RemoveUserProducts(userId: string, txId?: string) {
return this.dbs.Delete<Product>('Product', { owner: { user_id: userId } }, txId)
const products = await this.dbs.Find<Product>('Product', { where: { owner: { user_id: userId } } }, txId)
if (products.length === 0) {
return 0
}
let deleted = 0
for (const product of products) {
deleted += await this.dbs.Delete<Product>('Product', { product_id: product.product_id }, txId)
}
return deleted
}
}

View file

@ -53,13 +53,13 @@ export class TlvStorageFactory extends EventEmitter {
this.isConnected = false;
});
this.process.on('exit', (code: number) => {
this.log(ERROR, `Tlv Storage processor exited with code ${code}`);
this.process.on('exit', (code: number, signal: string) => {
this.log(ERROR, `Tlv Storage processor exited with code ${code} and signal ${signal}`);
this.isConnected = false;
if (!code) {
if (code === 0) {
return
}
throw new Error(`Tlv Storage processor exited with code ${code}`)
throw new Error(`Tlv Storage processor exited with code ${code} and signal ${signal}`)
});
this.isConnected = true;
@ -173,7 +173,7 @@ export class TlvStorageFactory extends EventEmitter {
public disconnect() {
if (this.process) {
this.process.kill();
this.process.kill(0);
this.isConnected = false;
this.debug = false;
}

View file

@ -42,7 +42,7 @@ export default class {
async GetUser(userId: string, txId?: string): Promise<User> {
const user = await this.FindUser(userId, txId)
if (!user) {
throw new Error(`user ${userId} not found`) // TODO: fix logs doxing
throw new Error(`user not found`)
}
return user
}
@ -50,7 +50,7 @@ export default class {
async UnbanUser(userId: string, txId?: string) {
const affected = await this.dbs.Update<User>('User', { user_id: userId }, { locked: false }, txId)
if (!affected) {
throw new Error("unaffected user unlock for " + userId) // TODO: fix logs doxing
throw new Error("unaffected user unlock")
}
}
@ -58,7 +58,7 @@ export default class {
const user = await this.GetUser(userId, txId)
const affected = await this.dbs.Update<User>('User', { user_id: userId }, { balance_sats: 0, locked: true }, txId)
if (!affected) {
throw new Error("unaffected ban user for " + userId) // TODO: fix logs doxing
throw new Error("unaffected ban user")
}
if (user.balance_sats > 0) {
this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: 'ban', amount: user.balance_sats })
@ -80,7 +80,7 @@ export default class {
const affected = await this.dbs.Increment<User>('User', { user_id: userId }, "balance_sats", increment, txId)
if (!affected) {
getLogger({ userId: userId, component: "balanceUpdates" })("user unaffected by increment")
throw new Error("unaffected balance increment for " + userId) // TODO: fix logs doxing
throw new Error("unaffected balance increment")
}
getLogger({ userId: userId, component: "balanceUpdates" })("incremented balance from", user.balance_sats, "sats, by", increment, "sats")
this.eventsLog.LogEvent({ type: 'balance_increment', userId, appId: "", appUserId: "", balance: user.balance_sats, data: reason, amount: increment })
@ -105,7 +105,7 @@ export default class {
const affected = await this.dbs.Decrement<User>('User', { user_id: userId }, "balance_sats", decrement, txId)
if (!affected) {
getLogger({ userId: userId, component: "balanceUpdates" })("user unaffected by decrement")
throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing
throw new Error("unaffected balance decrement")
}
getLogger({ userId: userId, component: "balanceUpdates" })("decremented balance from", user.balance_sats, "sats, by", decrement, "sats")
this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: reason, amount: decrement })
@ -126,4 +126,8 @@ export default class {
const lastSeenAtUnix = now - seconds
return this.dbs.Find<UserAccess>('UserAccess', { where: { last_seen_at_unix: LessThan(lastSeenAtUnix) } })
}
}
async DeleteUserAccess(userId: string, txId?: string) {
return this.dbs.Delete<UserAccess>('UserAccess', { user_id: userId }, txId)
}
}